207 lines
14 KiB
HTML
207 lines
14 KiB
HTML
<!--
|
||
WF_<US-id>_v1.0.html — wireframe low-fi tự chứa, sinh bởi skill ba-3-specification (activity wf).
|
||
|
||
QUY TẮC SINH FILE NÀY:
|
||
· Mỗi màn hình SCR-xx → một <section class="screen" id="SCR-xx">, theo đúng bảng WF §3.x.1 (vùng) và §3.x.2 (thành phần).
|
||
· Mỗi vùng Zn → <div class="zone" data-zone="Zn"> có nhãn vùng.
|
||
· Mỗi thành phần C-id → một phần tử có data-c="Cnn" và class theo kiểu trình bày (.btn-primary, .btn-secondary, .input, .select, .table, .card, .badge, .link, .label).
|
||
Tập data-c phải KHỚP HAI CHIỀU với bảng thành phần SRS §2.3.1 — đây là thứ ba-traceability kiểm.
|
||
· Trạng thái (WF §3.x.3) → các khối data-state="default|loading|empty-none|empty-filter|error|submitting" trong cùng vùng; thanh công cụ ở đầu file bật/tắt.
|
||
· Thành phần CHỈ CÓ Ở PROTOTYPE, chưa có trong SRS (WF §5 loại Tồn tại) → vẽ đúng chỗ prototype đặt nó, bọc <span class="proto-only" data-ref="WF §5 #n">…</span>,
|
||
KHÔNG gán data-c. Khung đỏ đứt để PO thấy ngay đang thiếu quyết định gì; PO quyết xong ⇒ hoặc gán C-id (SRS bổ sung) hoặc xoá khỏi WF.
|
||
· Bố cục nhiều cột (vd danh sách + sidebar tổng kết) → bọc các vùng trong <div class="cols">; ở khung nhìn mobile lớp này tự xếp chồng 1 cột (đúng WF §3.x.4). Không tự viết grid riêng.
|
||
· Không tô màu thương hiệu, không font riêng — đây là bố cục, không phải visual. Màu duy nhất được dùng: thang xám + một màu nhấn cho nút chính.
|
||
· Text hiển thị lấy nguyên văn từ SRS §4.2 (khoá i18n ghi ở data-key).
|
||
· Sửa nội dung mẫu bên dưới; giữ nguyên <style> và <script> để mọi WF trong dự án trông và thao tác giống nhau.
|
||
-->
|
||
<!doctype html>
|
||
<html lang="vi">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>WF — <US-id> — <Tên US></title>
|
||
<style>
|
||
:root{--ink:#222;--mute:#777;--line:#bbb;--fill:#eee;--fill2:#f7f7f7;--accent:#3b6ea5;--danger:#b23a3a;--bg:#fafafa}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;font:14px/1.45 system-ui,Segoe UI,Roboto,sans-serif;color:var(--ink);background:var(--bg)}
|
||
header.toolbar{position:sticky;top:0;z-index:9;background:#fff;border-bottom:1px solid var(--line);padding:8px 16px;display:flex;gap:16px;align-items:center;flex-wrap:wrap}
|
||
header.toolbar h1{font-size:15px;margin:0 16px 0 0}
|
||
header.toolbar label{font-size:12px;color:var(--mute)}
|
||
header.toolbar select{font:inherit}
|
||
.legend{font-size:12px;color:var(--mute);margin-left:auto}
|
||
main{padding:24px 16px;display:grid;gap:40px;justify-items:center}
|
||
.screen{width:100%;max-width:1200px;background:#fff;border:1px solid var(--line);border-radius:6px;overflow:hidden}
|
||
.screen>h2{margin:0;padding:8px 12px;font-size:13px;font-weight:600;background:var(--fill2);border-bottom:1px dashed var(--line);color:var(--mute)}
|
||
.screen>h2 small{font-weight:400;margin-left:8px}
|
||
.body{padding:12px;display:grid;gap:12px}
|
||
.zone{border:1px dashed var(--line);border-radius:4px;padding:10px;position:relative;background:#fff;min-height:44px}
|
||
.zone::before{content:attr(data-zone) " · " attr(data-name);position:absolute;top:-8px;left:8px;background:#fff;padding:0 4px;font-size:10px;color:var(--mute);letter-spacing:.02em}
|
||
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||
.row.right{justify-content:flex-end}
|
||
.row.between{justify-content:space-between}
|
||
[data-c]{position:relative}
|
||
[data-c]::after{content:attr(data-c);position:absolute;top:-9px;right:-4px;font-size:9px;background:var(--ink);color:#fff;padding:0 4px;border-radius:3px;line-height:14px;opacity:.85}
|
||
.label{font-weight:600;font-size:16px}
|
||
.btn{display:inline-block;padding:6px 14px;border:1px solid var(--ink);border-radius:4px;background:#fff;cursor:default;white-space:nowrap}
|
||
.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
||
.btn-secondary{background:#fff}
|
||
.btn-danger{border-color:var(--danger);color:var(--danger)}
|
||
.btn[aria-disabled="true"]{opacity:.45;border-style:dashed}
|
||
.link{color:var(--accent);text-decoration:underline}
|
||
.input,.select{display:inline-block;min-width:160px;padding:6px 8px;border:1px solid var(--line);border-radius:4px;background:#fff;color:var(--mute)}
|
||
.select::after{content:" ▾";float:right}
|
||
.field{display:grid;gap:4px}
|
||
.field>span{font-size:12px}
|
||
.field>span .req{color:var(--danger)}
|
||
.hint{font-size:12px;color:var(--mute)}
|
||
.table{width:100%;border-collapse:collapse}
|
||
.table th,.table td{border:1px solid var(--line);padding:6px 8px;text-align:left;font-size:13px}
|
||
.table th{background:var(--fill2)}
|
||
.card{border:1px solid var(--line);border-radius:4px;padding:10px;background:#fff}
|
||
.badge{display:inline-block;padding:0 8px;border:1px solid var(--line);border-radius:10px;font-size:12px;background:var(--fill2)}
|
||
.img{background:repeating-linear-gradient(45deg,var(--fill),var(--fill) 6px,var(--fill2) 6px,var(--fill2) 12px);border:1px solid var(--line);min-height:80px;display:grid;place-items:center;color:var(--mute);font-size:12px}
|
||
.skeleton{height:14px;background:linear-gradient(90deg,var(--fill),var(--fill2),var(--fill));border-radius:3px;margin:8px 0}
|
||
.state{padding:24px;text-align:center;color:var(--mute);border:1px dashed var(--line);border-radius:4px;display:grid;gap:10px;justify-items:center}
|
||
.state.error{border-color:var(--danger);color:var(--danger)}
|
||
.state .illus{width:80px;height:60px}
|
||
.spinner{display:inline-block;width:14px;height:14px;border:2px solid #fff;border-top-color:transparent;border-radius:50%;vertical-align:-2px;margin-right:6px}
|
||
.proto-only{display:inline-block;border:1.5px dashed var(--danger);border-radius:4px;padding:4px 8px;background:#fdf3f3;color:var(--danger);position:relative}
|
||
.proto-only::before{content:"chỉ ở prototype — " attr(data-ref);font-size:10px;border:1px solid var(--danger);border-radius:3px;padding:0 4px;margin-right:6px;vertical-align:1px}
|
||
.modal{max-width:520px;margin:0 auto;border:1px solid var(--ink);border-radius:6px;background:#fff;box-shadow:0 0 0 2000px rgba(0,0,0,.25)}
|
||
.modal .body{padding:16px}
|
||
[data-state]{display:none}
|
||
[data-state="default"]{display:block}
|
||
body[data-view="mobile"] .screen{max-width:390px}
|
||
body[data-view="mobile"] .m-hide{display:none!important}
|
||
body[data-view="mobile"] .body,body[data-view="mobile"] .cols{grid-template-columns:1fr!important;display:grid}
|
||
.cols{display:grid;grid-template-columns:2fr 1fr;gap:12px;align-items:start}
|
||
body[data-view="mobile"] .m-stack{flex-direction:column;align-items:stretch}
|
||
body[data-view="desktop"] .d-hide{display:none!important}
|
||
body[data-view="mobile"] .table{display:none}
|
||
body[data-view="mobile"] .cards{display:grid!important;gap:8px}
|
||
.cards{display:none}
|
||
footer.note{padding:16px;font-size:12px;color:var(--mute);text-align:center}
|
||
</style>
|
||
</head>
|
||
<body data-view="desktop">
|
||
|
||
<header class="toolbar">
|
||
<h1>WF — <US-id> · <Tên US></h1>
|
||
<label>Khung nhìn
|
||
<select id="view"><option value="desktop">Desktop ≥1024</option><option value="mobile">Mobile <768</option></select>
|
||
</label>
|
||
<label>Trạng thái
|
||
<select id="state">
|
||
<option value="default">Mặc định (có dữ liệu)</option>
|
||
<option value="loading">Đang tải</option>
|
||
<option value="empty-none">Rỗng — chưa có dữ liệu nào</option>
|
||
<option value="empty-filter">Rỗng — bộ lọc không khớp</option>
|
||
<option value="error">Lỗi tải dữ liệu</option>
|
||
<option value="submitting">Đang gửi</option>
|
||
</select>
|
||
</label>
|
||
<span class="legend">Nhãn đen = C-id trong SRS §2.3.1 · khung đứt xám = vùng Zn trong WF §3.x.1 · <b style="color:var(--danger)">khung đứt đỏ</b> = chỉ có ở prototype, chưa có trong SRS (WF §5) · hành vi xem SRS, không suy từ hình</span>
|
||
</header>
|
||
|
||
<main>
|
||
|
||
<!-- ============================ SCR-01 (mẫu: màn hình danh sách) ============================ -->
|
||
<section class="screen" id="SCR-01">
|
||
<h2>SCR-01 · <Tên màn hình> <small>Danh sách · vào từ Menu · vai trò: ROLE-01, ROLE-02</small></h2>
|
||
<div class="body">
|
||
|
||
<div class="zone" data-zone="Z1" data-name="Header màn hình">
|
||
<div class="row between">
|
||
<span class="label" data-c="C01" data-key="scr01.title">Tiêu đề màn hình</span>
|
||
<span class="btn btn-primary" data-c="C02" data-key="btn.create">Tạo mới</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="zone" data-zone="Z2" data-name="Thanh lọc">
|
||
<div class="row m-stack">
|
||
<span class="input" data-c="C03" data-key="scr01.search.ph">Nhập mã hoặc tên…</span>
|
||
<span class="select m-hide" data-c="C04" data-key="scr01.filter.status">Trạng thái: Tất cả</span>
|
||
<span class="btn btn-secondary d-hide" data-key="btn.filter">Lọc</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="zone" data-zone="Z3" data-name="Dữ liệu">
|
||
<div data-state="default">
|
||
<table class="table" data-c="C05">
|
||
<thead><tr><th>Mã</th><th>Tên</th><th>Trạng thái</th><th>Ngày tạo</th><th></th></tr></thead>
|
||
<tbody>
|
||
<tr><td>STR-001</td><td>Cửa hàng A</td><td><span class="badge">Nháp</span></td><td>30/08/2026</td><td><span class="link">Chi tiết</span></td></tr>
|
||
<tr><td>STR-002</td><td>Cửa hàng B</td><td><span class="badge">Đã duyệt</span></td><td>29/08/2026</td><td><span class="link">Chi tiết</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="cards" data-c="C05">
|
||
<div class="card"><div class="row between"><b>STR-001</b><span class="badge">Nháp</span></div><div>Cửa hàng A</div><div class="hint">30/08/2026</div></div>
|
||
<div class="card"><div class="row between"><b>STR-002</b><span class="badge">Đã duyệt</span></div><div>Cửa hàng B</div><div class="hint">29/08/2026</div></div>
|
||
</div>
|
||
</div>
|
||
<div data-state="loading"><div class="skeleton"></div><div class="skeleton" style="width:85%"></div><div class="skeleton" style="width:92%"></div></div>
|
||
<div data-state="empty-none"><div class="state"><div class="img illus">minh hoạ</div><span data-key="empty.none">Chưa có bản ghi nào. Tạo bản ghi đầu tiên?</span><span class="btn btn-primary" data-key="btn.create">Tạo mới</span></div></div>
|
||
<div data-state="empty-filter"><div class="state"><span data-key="empty.filter">Không tìm thấy kết quả phù hợp với bộ lọc.</span><span class="btn btn-secondary" data-key="btn.clearFilter">Xoá lọc</span></div></div>
|
||
<div data-state="error"><div class="state error"><span data-key="error.load">Không tải được dữ liệu.</span><span class="btn btn-secondary" data-key="btn.retry">Thử lại</span></div></div>
|
||
</div>
|
||
|
||
<div class="zone" data-zone="Z4" data-name="Phân trang">
|
||
<div class="row between">
|
||
<span class="proto-only" data-ref="WF §5 #1">Nút "Xuất Excel" — prototype có, BACKLOG không có US ⇒ PO quyết</span>
|
||
<span class="row" data-c="C06"><span class="btn btn-secondary">‹</span><span>1 / 7</span><span class="btn btn-secondary">›</span><span class="select m-hide">20 dòng</span></span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================ SCR-03 (mẫu: form trong modal) ============================ -->
|
||
<section class="screen" id="SCR-03">
|
||
<h2>SCR-03 · <Tên form> <small>Modal · mở từ C02 · vai trò: ROLE-01, ROLE-02</small></h2>
|
||
<div class="body">
|
||
<div class="modal">
|
||
<div class="body">
|
||
<div class="zone" data-zone="Z1" data-name="Tiêu đề modal">
|
||
<div class="row between"><span class="label" data-c="C01" data-key="scr03.title">Tạo mới</span><span class="link" data-c="C08" data-key="btn.close">✕</span></div>
|
||
</div>
|
||
<div class="zone" data-zone="Z2" data-name="Form">
|
||
<div class="body">
|
||
<label class="field"><span data-key="f01.label">Mã <b class="req">*</b></span><span class="input" data-c="F01" data-key="f01.ph">3–20 ký tự, A–Z 0–9 -</span><span class="hint" data-key="f01.hint">Không trùng với mã đã có</span></label>
|
||
<label class="field"><span data-key="f02.label">Loại <b class="req">*</b></span><span class="select" data-c="F02">Thường</span></label>
|
||
<label class="field"><span data-key="f03.label">Tên</span><span class="input" data-c="F03" data-key="f03.ph">Tên hiển thị</span></label>
|
||
</div>
|
||
</div>
|
||
<div class="zone" data-zone="Z3" data-name="Hành động">
|
||
<div class="row right">
|
||
<span class="btn btn-secondary" data-c="C07" data-key="btn.cancel">Huỷ</span>
|
||
<span data-state="default" style="display:inline"><span class="btn btn-primary" data-c="C09" data-key="btn.save">Lưu</span></span>
|
||
<span data-state="submitting"><span class="btn btn-primary" data-c="C09" aria-disabled="true"><i class="spinner"></i>Đang lưu…</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="hint">Field F01–F03 tham chiếu bảng field SRS §2.3.3. Thất bại ⇒ giữ nguyên dữ liệu đã nhập (AC-09), lỗi hiện dưới field theo UICONV §4.</p>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<footer class="note">Wireframe low-fi · bố cục và trạng thái · không phải thiết kế visual · nguồn hành vi: SRS_<US-id> · nguồn quy ước: UICONV_<PROJECT></footer>
|
||
|
||
<script>
|
||
(function(){
|
||
var view=document.getElementById('view'),state=document.getElementById('state');
|
||
function apply(){
|
||
document.body.setAttribute('data-view',view.value);
|
||
var s=state.value;
|
||
document.querySelectorAll('[data-state]').forEach(function(el){
|
||
var on=el.getAttribute('data-state')===s || (s==='submitting'&&el.getAttribute('data-state')==='default'&&!el.closest('.zone[data-name="Hành động"]'));
|
||
el.style.display=on?(el.tagName==='SPAN'?'inline':'block'):'none';
|
||
});
|
||
}
|
||
view.addEventListener('change',apply);state.addEventListener('change',apply);apply();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|