Files
workspace/code/fms/demo.html
T
2026-09-05 17:25:35 +08:00

511 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ERP 列表页 · 工作台版</title>
<style>
:root {
--primary: #4f7cff;
--primary-2: #8b5cff;
--grad: linear-gradient(135deg, #4f7cff, #8b5cff);
--primary-soft: #eef1ff;
--text: #1d2129;
--text-2: #4e5969;
--text-3: #86909c;
--border: #e5e6eb;
--border-soft: #f2f3f5;
--bg: #f0f2f5;
--surface: #ffffff;
--hover: #f7f8fa;
--hover-row: #f6f8ff;
--selected: #eef2ff;
--success: #00b42a;
--danger: #f53f3f;
--row-h: 52px;
--head-h: 44px;
--radius: 12px;
--radius-sm: 8px;
--shadow: 0 1px 3px rgba(0,0,0,.04), 0 8px 24px rgba(31,35,41,.05);
--shadow-pop: 0 12px 40px rgba(31,35,41,.16);
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font-family: "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text); background: var(--bg); font-size: 13px; -webkit-font-smoothing: antialiased;
}
svg { display: block; }
.icon { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.page { display: flex; flex-direction: column; height: 100vh; padding: 20px; gap: 16px; }
/* 页眉 */
.page-header { flex: none; display: flex; align-items: flex-end; gap: 16px; }
.page-header .tt { font-size: 20px; font-weight: 700; letter-spacing: .3px; }
.page-header .st { color: var(--text-3); font-size: 12px; margin-top: 4px; }
.page-header .spacer { flex: 1; }
.crumb { color: var(--text-3); font-size: 12px; margin-bottom: 4px; }
.crumb b { color: var(--text-2); font-weight: 500; }
/* KPI 概览 */
.kpi-row { flex: none; display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.kpi {
position: relative; background: var(--surface); border: 1px solid var(--border-soft);
border-radius: var(--radius); padding: 16px 18px; cursor: pointer; overflow: hidden;
transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.kpi::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--grad); opacity: 0; transition: opacity .18s; }
.kpi:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
.kpi.active { border-color: var(--primary); background: linear-gradient(180deg,#fbfcff,#fff); }
.kpi.active::before { opacity: 1; }
.kpi .k-top { display: flex; align-items: center; gap: 10px; }
.kpi .k-ico { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; color: #fff; }
.kpi .k-ico svg { width: 18px; height: 18px; }
.kpi .k-label { color: var(--text-3); font-size: 12px; }
.kpi .k-num { font-size: 26px; font-weight: 700; margin-top: 10px; font-variant-numeric: tabular-nums; letter-spacing: .5px; }
.kpi .k-trend { font-size: 11px; margin-top: 4px; color: var(--text-3); }
.kpi .k-trend up { color: var(--success); }
/* 卡片 */
.card { background: var(--surface); border: 1px solid var(--border-soft); border-radius: var(--radius); box-shadow: var(--shadow); }
/* 搜索 */
.search-card { flex: none; padding: 14px 18px; display: flex; align-items: center; gap: 12px; }
.search-card .spacer { flex: 1; }
.quick {
display: inline-flex; align-items: center; height: 36px; min-width: 380px;
border: 1px solid var(--border); border-radius: 10px; background: #fff; overflow: hidden; transition: .18s;
}
.quick:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,124,255,.14); }
.quick .q-field { position: relative; border-right: 1px solid var(--border); }
.quick .q-field select { appearance: none; height: 36px; border: none; background: var(--hover); color: var(--text-2); padding: 0 26px 0 12px; font-size: 13px; cursor: pointer; outline: none; }
.quick .q-field svg { position: absolute; right: 9px; top: 50%; transform: translateY(-50%); width: 12px; height: 12px; color: var(--text-3); pointer-events: none; }
.quick input { flex: 1; min-width: 0; height: 36px; border: none; outline: none; padding: 0 14px; font-size: 13px; background: transparent; }
.quick .q-sub { display: grid; place-items: center; width: 38px; height: 36px; color: var(--primary); cursor: pointer; }
.quick .q-sub:hover { background: var(--primary-soft); }
/* 按钮 */
.btn {
height: 36px; padding: 0 16px; border-radius: 10px; border: 1px solid var(--border);
background: #fff; color: var(--text); font-size: 13px; cursor: pointer; display: inline-flex;
align-items: center; gap: 7px; transition: all .16s; white-space: nowrap; user-select: none; font-weight: 500;
}
.btn:hover { border-color: var(--primary); color: var(--primary); }
.btn:active { transform: translateY(.5px); }
.btn.primary { background: var(--grad); border: none; color: #fff; box-shadow: 0 4px 12px rgba(79,124,255,.3); }
.btn.primary:hover { filter: brightness(1.05); color: #fff; }
.btn.danger:hover { border-color: var(--danger); color: var(--danger); }
.btn:disabled { opacity: .4; cursor: not-allowed; transform: none; }
.btn.icon-btn { width: 36px; padding: 0; justify-content: center; border-color: transparent; background: transparent; color: var(--text-2); }
.btn.icon-btn:hover { background: var(--hover); color: var(--text); border-color: transparent; }
.btn.ghost { border-color: transparent; background: transparent; color: var(--text-2); }
.btn.ghost:hover { background: var(--hover); color: var(--text); }
/* 表格卡 */
.table-card { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
.table-toolbar { flex: none; display: flex; align-items: center; gap: 12px; padding: 14px 18px; border-bottom: 1px solid var(--border-soft); }
.table-toolbar .tb-title { font-weight: 600; font-size: 14px; }
.table-toolbar .tb-count { color: var(--text-3); font-size: 12px; }
.table-toolbar .spacer { flex: 1; }
.table-toolbar .sel-info { color: var(--text-2); font-size: 12px; }
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.seg button { height: 36px; padding: 0 12px; border: none; background: #fff; color: var(--text-2); cursor: pointer; font-size: 12px; display: inline-flex; align-items: center; gap: 5px; }
.seg button + button { border-left: 1px solid var(--border); }
.seg button.on { background: var(--primary-soft); color: var(--primary); font-weight: 600; }
.table-scroll { flex: 1; min-height: 0; overflow: auto; }
table { width: 100%; border-collapse: collapse; }
thead th {
position: sticky; top: 0; z-index: 2; background: #fafbfc; color: var(--text-2);
font-weight: 600; font-size: 12px; text-align: left; white-space: nowrap; height: var(--head-h);
padding: 0 16px; border-bottom: 1px solid var(--border); user-select: none;
}
thead th.sortable { cursor: pointer; transition: color .15s; }
thead th.sortable:hover { color: var(--primary); }
thead th .arrow { color: var(--primary); margin-left: 4px; font-size: 10px; }
thead th.col-check { width: 48px; }
tbody td { height: var(--row-h); padding: 0 16px; border-bottom: 1px solid var(--border-soft); white-space: nowrap; vertical-align: middle; }
tbody tr { transition: background .12s; }
tbody tr:hover { background: var(--hover-row); }
tbody tr.selected { background: var(--selected); }
tbody tr:last-child td { border-bottom: none; }
.avatar { width: 30px; height: 30px; border-radius: 50%; display: inline-grid; place-items: center; color: #fff; font-size: 12px; font-weight: 600; flex: none; }
.user-cell { display: inline-flex; align-items: center; gap: 10px; }
.user-cell .u-name { font-weight: 500; }
.user-cell .u-sub { color: var(--text-3); font-size: 11px; }
.cell-id { color: var(--text-3); font-variant-numeric: tabular-nums; }
.dot { display: inline-flex; align-items: center; gap: 7px; }
.dot::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.dot.on { color: var(--success); }
.dot.off { color: var(--danger); }
.row-actions { display: flex; gap: 12px; opacity: 0; transform: translateX(4px); transition: .15s; }
tbody tr:hover .row-actions { opacity: 1; transform: none; }
.row-actions a { color: var(--primary); cursor: pointer; text-decoration: none; }
.row-actions a.del { color: var(--danger); }
.row-actions a:hover { text-decoration: underline; }
.empty { padding: 70px 0; text-align: center; color: var(--text-3); }
.empty svg { width: 48px; height: 48px; margin: 0 auto 14px; color: #d7dade; }
.empty .e-title { font-size: 14px; color: var(--text-2); }
.table-footer { flex: none; display: flex; align-items: center; gap: 14px; padding: 14px 18px; border-top: 1px solid var(--border-soft); }
.table-footer .total { color: var(--text-2); font-size: 12px; margin-right: auto; }
.pg-size { height: 32px; padding: 0 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; outline: none; background: #fff; cursor: pointer; }
.pager { display: flex; align-items: center; gap: 5px; }
.pg { min-width: 32px; height: 32px; padding: 0 9px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--text); font-size: 13px; cursor: pointer; transition: .15s; }
.pg:hover:not(:disabled):not(.active) { border-color: var(--border); background: var(--hover); }
.pg.active { background: var(--grad); color: #fff; box-shadow: 0 3px 8px rgba(79,124,255,.3); }
.pg:disabled { color: var(--text-3); cursor: not-allowed; }
.pg.dots { cursor: default; }
.table-scroll::-webkit-scrollbar { width: 9px; height: 9px; }
.table-scroll::-webkit-scrollbar-thumb { background: #d4d6d9; border-radius: 5px; border: 2px solid #fff; }
.table-scroll::-webkit-scrollbar-thumb:hover { background: #b8bbbf; }
/* 右侧抽屉:列设置 */
.drawer-mask { position: fixed; inset: 0; background: rgba(31,35,41,.35); opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 60; }
.drawer-mask.show { opacity: 1; pointer-events: auto; }
.drawer { position: absolute; top: 0; right: 0; height: 100%; width: 340px; background: #fff; box-shadow: var(--shadow-pop); transform: translateX(100%); transition: transform .26s cubic-bezier(.4,0,.2,1); display: flex; flex-direction: column; }
.drawer-mask.show .drawer { transform: none; }
.drawer-head { flex: none; display: flex; align-items: center; padding: 18px; border-bottom: 1px solid var(--border-soft); font-weight: 600; font-size: 15px; }
.drawer-head .x { margin-left: auto; cursor: pointer; color: var(--text-3); border-radius: 8px; width: 30px; height: 30px; display: grid; place-items: center; }
.drawer-head .x:hover { background: var(--hover); color: var(--text); }
.drawer-body { flex: 1; min-height: 0; overflow: auto; padding: 8px 14px; }
.col-item { display: flex; align-items: center; gap: 10px; padding: 10px 10px; border-radius: 10px; transition: background .12s; }
.col-item:hover { background: var(--hover); }
.col-item input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--primary); }
.col-item .c-label { flex: 1; }
.col-item .c-move { display: inline-flex; flex-direction: column; }
.col-item .c-move button { width: 22px; height: 16px; border: none; background: transparent; color: var(--text-3); cursor: pointer; display: grid; place-items: center; padding: 0; }
.col-item .c-move button:hover { color: var(--primary); }
.col-item .c-move button:disabled { opacity: .25; cursor: not-allowed; }
.drawer-foot { flex: none; display: flex; gap: 10px; padding: 14px 18px; border-top: 1px solid var(--border-soft); }
.drawer-foot .btn { flex: 1; justify-content: center; }
</style>
</head>
<body>
<div class="page">
<!-- 页眉 -->
<div class="page-header">
<div>
<div class="crumb">系统管理 / <b>用户管理</b></div>
<div class="tt">用户管理</div>
<div class="st">集中维护账号、角色与部门归属关系</div>
</div>
<div class="spacer"></div>
<button class="btn primary" id="addBtn">
<svg class="icon" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
新建用户
</button>
</div>
<!-- KPI 概览 -->
<div class="kpi-row" id="kpiRow"></div>
<!-- 搜索 -->
<div class="card search-card">
<div class="quick">
<div class="q-field">
<select id="qField"></select>
<svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<input id="qInput" placeholder="搜索用户名、姓名、部门、角色…" />
<div class="q-sub" id="qBtn" title="搜索">
<svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</div>
</div>
<button class="btn ghost" id="advToggle">
<svg class="icon" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="7" y1="12" x2="17" y2="12"/><line x1="10" y1="18" x2="14" y2="18"/></svg>
高级筛选
</button>
<div class="spacer"></div>
<button class="btn icon-btn" id="moreBtn" title="更多">
<svg class="icon" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>
</button>
</div>
<!-- 高级筛选 -->
<div class="card" id="advPanel" hidden style="padding:16px 18px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px 18px;">
<div style="display:flex;flex-direction:column;gap:5px;"><label style="font-size:12px;color:var(--text-2)">姓名</label><input id="fName" placeholder="模糊匹配" style="height:34px;padding:0 10px;border:1px solid var(--border);border-radius:8px;outline:none" onfocus="this.style.borderColor='var(--primary)'" onblur="this.style.borderColor='var(--border)'"></div>
<div style="display:flex;flex-direction:column;gap:5px;"><label style="font-size:12px;color:var(--text-2)">部门</label><select id="fDept" style="height:34px;padding:0 10px;border:1px solid var(--border);border-radius:8px;outline:none;background:#fff"></select></div>
<div style="display:flex;flex-direction:column;gap:5px;"><label style="font-size:12px;color:var(--text-2)">角色</label><select id="fRole" style="height:34px;padding:0 10px;border:1px solid var(--border);border-radius:8px;outline:none;background:#fff"></select></div>
<div style="display:flex;flex-direction:column;gap:5px;"><label style="font-size:12px;color:var(--text-2)">状态</label><select id="fStatus" style="height:34px;padding:0 10px;border:1px solid var(--border);border-radius:8px;outline:none;background:#fff"><option value="">全部</option><option value="enabled">启用</option><option value="disabled">禁用</option></select></div>
<div style="display:flex;flex-direction:column;gap:5px;"><label style="font-size:12px;color:var(--text-2)">创建时间</label><div style="display:flex;align-items:center;gap:6px"><input type="date" id="fStart" style="flex:1;height:34px;padding:0 8px;border:1px solid var(--border);border-radius:8px;outline:none"><span style="color:var(--text-3)">~</span><input type="date" id="fEnd" style="flex:1;height:34px;padding:0 8px;border:1px solid var(--border);border-radius:8px;outline:none"></div></div>
</div>
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:16px;">
<button class="btn primary" id="advSearch">查询</button>
<button class="btn" id="advReset">重置</button>
</div>
</div>
<!-- 表格卡 -->
<div class="card table-card">
<div class="table-toolbar">
<span class="tb-title">用户列表</span>
<span class="tb-count" id="tbCount"></span>
<div class="spacer"></div>
<span class="sel-info" id="selInfo">未选择</span>
<button class="btn danger" id="delBtn" disabled>
<svg class="icon" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg>
删除
</button>
<button class="btn icon-btn" id="refreshBtn" title="刷新">
<svg class="icon" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.5 9a9 9 0 0 1 14.8-3.4L23 10M1 14l4.7 4.4A9 9 0 0 0 20.5 15"/></svg>
</button>
<div class="seg" id="densitySeg">
<button data-d="comfort" class="on">舒适</button>
<button data-d="compact">紧凑</button>
</div>
<button class="btn icon-btn" id="colBtn" title="列设置">
<svg class="icon" viewBox="0 0 24 24"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>
</button>
</div>
<div class="table-scroll">
<table id="grid"></table>
<div class="empty" id="empty" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="9" x2="9" y2="20"/></svg>
<div class="e-title">没有符合条件的用户</div>
<div style="font-size:12px;margin-top:4px">试试调整筛选条件或清空搜索</div>
</div>
</div>
<div class="table-footer">
<span class="total" id="totalText"></span>
<select class="pg-size" id="pageSize">
<option value="10">10 条/页</option>
<option value="20">20 条/页</option>
<option value="50">50 条/页</option>
</select>
<div class="pager" id="pages"></div>
</div>
</div>
</div>
<!-- 列设置抽屉 -->
<div class="drawer-mask" id="colDrawer">
<div class="drawer">
<div class="drawer-head">列设置<span class="x" id="colClose">✕</span></div>
<div class="drawer-body" id="colList"></div>
<div class="drawer-foot">
<button class="btn" id="colReset">恢复默认</button>
<button class="btn primary" id="colDone">完成</button>
</div>
</div>
</div>
<script>
(function () {
const COLUMNS = [
{ key: 'username', label: '用户', sortable: true },
{ key: 'name', label: '姓名', sortable: true },
{ key: 'dept', label: '部门', sortable: true },
{ key: 'role', label: '角色', sortable: false },
{ key: 'status', label: '状态', sortable: true },
{ key: 'created', label: '创建时间', sortable: true },
];
const depts = ['研发部', '财务部', '人事部', '销售部', '采购部'];
const roles = ['管理员', '普通用户', '审核员', '访客'];
const AVATAR_COLORS = ['#4f7cff','#8b5cff','#00b42a','#ff7d00','#f53f3f','#14c9c9','#f76560','#3491fa'];
function pad(n){ return n<10?'0'+n:n; }
function randDate(){ const m=1+Math.floor(Math.random()*6), d=1+Math.floor(Math.random()*27); return `2026-${pad(m)}-${pad(d)}`; }
const DATA = Array.from({ length: 53 }, (_, i) => {
const n = i + 1;
return { id: 1000+n, username: 'user'+(1000+n), name: '用户'+n, dept: depts[i%depts.length], role: roles[i%roles.length], status: i%4===0?'disabled':'enabled', created: randDate() };
});
const state = {
keyword: '', searchField: 'username',
filters: { name:'', dept:'', role:'', status:'', start:'', end:'' },
sortField: 'id', sortOrder: 'asc',
page: 1, pageSize: 10,
selected: new Set(),
visibleCols: new Set(COLUMNS.map(c => c.key)),
colOrder: COLUMNS.map(c => c.key),
density: 'comfort',
activeKpi: 'all',
};
const $ = id => document.getElementById(id);
const grid = $('grid'), empty = $('empty'), pages = $('pages'), totalText = $('totalText');
const qField = $('qField');
['username','name','dept','role'].forEach(k => { const c = COLUMNS.find(x=>x.key===k); qField.appendChild(new Option(c.label, k)); });
const fDept = $('fDept'); fDept.appendChild(new Option('全部','')); depts.forEach(d => fDept.appendChild(new Option(d, d)));
const fRole = $('fRole'); fRole.appendChild(new Option('全部','')); roles.forEach(r => fRole.appendChild(new Option(r, r)));
function avatarColor(seed){ let h=0; for(const ch of String(seed)) h=(h*31+ch.charCodeAt(0))>>>0; return AVATAR_COLORS[h % AVATAR_COLORS.length]; }
function getRows() {
let rows = DATA.slice();
if (state.activeKpi === 'enabled') rows = rows.filter(r => r.status === 'enabled');
else if (state.activeKpi === 'disabled') rows = rows.filter(r => r.status === 'disabled');
else if (state.activeKpi === 'admin') rows = rows.filter(r => r.role === '管理员');
if (state.keyword) { const f = state.searchField, kw = state.keyword.toLowerCase(); rows = rows.filter(r => String(r[f]).toLowerCase().includes(kw)); }
const ft = state.filters;
if (ft.name) rows = rows.filter(r => r.name.includes(ft.name));
if (ft.dept) rows = rows.filter(r => r.dept === ft.dept);
if (ft.role) rows = rows.filter(r => r.role === ft.role);
if (ft.status) rows = rows.filter(r => r.status === ft.status);
if (ft.start) rows = rows.filter(r => r.created >= ft.start);
if (ft.end) rows = rows.filter(r => r.created <= ft.end);
const sf = state.sortField, so = state.sortOrder;
rows.sort((a,b) => a[sf]<b[sf] ? (so==='asc'?-1:1) : a[sf]>b[sf] ? (so==='asc'?1:-1) : 0);
return rows;
}
function renderKpi() {
const total = DATA.length;
const enabled = DATA.filter(r=>r.status==='enabled').length;
const disabled = total - enabled;
const admin = DATA.filter(r=>r.role==='管理员').length;
const cards = [
{ key:'all', label:'用户总数', num:total, color:'var(--grad)', ico:'<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/>', trend:'较上月 ↑ 6.2%' },
{ key:'enabled', label:'已启用', num:enabled, color:'linear-gradient(135deg,#00b42a,#3ad07a)', ico:'<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>', trend:'占比 '+Math.round(enabled/total*100)+'%' },
{ key:'disabled', label:'已禁用', num:disabled, color:'linear-gradient(135deg,#f53f3f,#ff8a8a)', ico:'<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>', trend:'占比 '+Math.round(disabled/total*100)+'%' },
{ key:'admin', label:'管理员', num:admin, color:'linear-gradient(135deg,#ff7d00,#ffb15c)', ico:'<path d="M12 2l3 7h7l-5.5 4 2 7L12 17l-6.5 3 2-7L2 9h7z"/>', trend:'需重点关注' },
];
$('kpiRow').innerHTML = cards.map(c => `
<div class="kpi ${state.activeKpi===c.key?'active':''}" data-kpi="${c.key}">
<div class="k-top">
<div class="k-ico" style="background:${c.color}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${c.ico}</svg></div>
<div class="k-label">${c.label}</div>
</div>
<div class="k-num">${c.num}</div>
<div class="k-trend">${c.trend}</div>
</div>`).join('');
$('kpiRow').querySelectorAll('.kpi').forEach(el => el.onclick = () => {
state.activeKpi = state.activeKpi === el.dataset.kpi ? 'all' : el.dataset.kpi;
state.page = 1; renderKpi(); render();
});
}
function render() {
const rows = getRows();
const total = rows.length;
const pageCount = Math.max(1, Math.ceil(total / state.pageSize));
if (state.page > pageCount) state.page = pageCount;
const start = (state.page - 1) * state.pageSize;
const pageRows = rows.slice(start, start + state.pageSize);
const cols = state.colOrder.filter(k => state.visibleCols.has(k)).map(k => COLUMNS.find(c => c.key === k));
const allChecked = pageRows.length > 0 && pageRows.every(r => state.selected.has(r.id));
let html = '<thead><tr><th class="col-check"><input type="checkbox" id="checkAll"'+(allChecked?' checked':'')+'></th>';
cols.forEach(c => {
const arrow = c.sortable && state.sortField === c.key ? `<span class="arrow">${state.sortOrder==='asc'?'▲':'▼'}</span>` : '';
html += `<th class="${c.sortable?'sortable':''}" data-key="${c.key}">${c.label}${arrow}</th>`;
});
html += '<th>操作</th></tr></thead><tbody>';
pageRows.forEach(r => {
const checked = state.selected.has(r.id) ? ' checked' : '';
html += `<tr class="${state.selected.has(r.id)?'selected':''}"><td><input type="checkbox" class="rowCheck" data-id="${r.id}"${checked}></td>`;
cols.forEach(c => {
let v;
if (c.key === 'username') v = `<span class="user-cell"><span class="avatar" style="background:${avatarColor(r.username)}">${r.username.slice(0,1).toUpperCase()}</span><span><span class="u-name">${r.username}</span><br><span class="u-sub">ID ${r.id}</span></span></span>`;
else if (c.key === 'status') v = `<span class="dot ${r.status==='enabled'?'on':'off'}">${r.status==='enabled'?'启用':'禁用'}</span>`;
else v = c.key === 'id' ? `<span class="cell-id">${r[c.key]}</span>` : r[c.key];
html += `<td>${v}</td>`;
});
html += `<td><div class="row-actions"><a data-act="view" data-id="${r.id}">查看</a><a data-act="edit" data-id="${r.id}">编辑</a><a class="del" data-act="del" data-id="${r.id}">删除</a></div></td></tr>`;
});
html += '</tbody>';
grid.innerHTML = html;
empty.hidden = pageRows.length > 0;
$('tbCount').textContent = `(${total})`;
totalText.textContent = `共 ${total} 条`;
renderPages(pageCount);
updateSelInfo();
grid.querySelectorAll('th.sortable').forEach(th => th.onclick = () => {
const k = th.dataset.key;
if (state.sortField === k) state.sortOrder = state.sortOrder==='asc'?'desc':'asc';
else { state.sortField = k; state.sortOrder = 'asc'; }
render();
});
const ca = $('checkAll');
if (ca) ca.onchange = () => { pageRows.forEach(r => ca.checked ? state.selected.add(r.id) : state.selected.delete(r.id)); render(); };
grid.querySelectorAll('.rowCheck').forEach(cb => cb.onchange = () => { cb.checked ? state.selected.add(+cb.dataset.id) : state.selected.delete(+cb.dataset.id); render(); });
grid.querySelectorAll('.row-actions a').forEach(a => a.onclick = () => {
const id = +a.dataset.id, act = a.dataset.act;
if (act === 'del') { const i = DATA.findIndex(r=>r.id===id); if (i>=0) DATA.splice(i,1); state.selected.delete(id); renderKpi(); render(); }
else alert(`${act==='view'?'查看':'编辑'} 用户 ID=${id}(demo)`);
});
}
function renderPages(pageCount) {
const cur = state.page;
let html = `<button class="pg" data-pg="${cur-1}" ${cur===1?'disabled':''}>‹</button>`;
const win = [];
for (let p=1; p<=pageCount; p++) if (p===1 || p===pageCount || Math.abs(p-cur)<=1) win.push(p);
let last = 0;
win.forEach(p => { if (p-last>1) html += '<span class="pg dots">…</span>'; html += `<button class="pg ${p===cur?'active':''}" data-pg="${p}">${p}</button>`; last = p; });
html += `<button class="pg" data-pg="${cur+1}" ${cur===pageCount?'disabled':''}>›</button>`;
pages.innerHTML = html;
pages.querySelectorAll('.pg[data-pg]').forEach(b => b.onclick = () => { const p=+b.dataset.pg; if (p>=1 && p<=pageCount){ state.page=p; render(); } });
}
function updateSelInfo() {
const n = state.selected.size;
$('selInfo').textContent = n ? `已选 ${n} 项` : '未选择';
$('delBtn').disabled = n === 0;
}
function doQuick(){ state.keyword = $('qInput').value.trim(); state.page = 1; render(); }
$('qBtn').onclick = doQuick;
$('qInput').onkeydown = e => { if (e.key === 'Enter') doQuick(); };
qField.onchange = () => state.searchField = qField.value;
$('advToggle').onclick = () => { const p = $('advPanel'); p.hidden = !p.hidden; };
$('advSearch').onclick = () => { state.filters = { name:$('fName').value.trim(), dept:$('fDept').value, role:$('fRole').value, status:$('fStatus').value, start:$('fStart').value, end:$('fEnd').value }; state.page=1; render(); };
$('advReset').onclick = () => { ['fName','fStart','fEnd'].forEach(id=>$(id).value=''); $('fDept').value=''; $('fRole').value=''; $('fStatus').value=''; state.filters={name:'',dept:'',role:'',status:'',start:'',end:''}; state.page=1; render(); };
$('moreBtn').onclick = () => alert('更多:导入 / 导出 / 打印 …(demo)');
$('refreshBtn').onclick = render;
$('addBtn').onclick = () => { const id = DATA.length ? Math.min(...DATA.map(r=>r.id))-1 : 1000; DATA.unshift({ id, username:'user'+id, name:'新用户', dept:depts[0], role:roles[1], status:'enabled', created:'2026-09-05' }); state.page=1; renderKpi(); render(); };
$('delBtn').onclick = () => { for (let i=DATA.length-1;i>=0;i--) if (state.selected.has(DATA[i].id)) DATA.splice(i,1); state.selected.clear(); renderKpi(); render(); };
$('pageSize').onchange = () => { state.pageSize = +$('pageSize').value; state.page=1; render(); };
$('densitySeg').querySelectorAll('button').forEach(b => b.onclick = () => {
$('densitySeg').querySelectorAll('button').forEach(x=>x.classList.remove('on'));
b.classList.add('on');
state.density = b.dataset.d;
document.documentElement.style.setProperty('--row-h', state.density==='compact' ? '38px' : '52px');
render();
});
// 列设置抽屉
const colDrawer = $('colDrawer');
function renderColList() {
$('colList').innerHTML = state.colOrder.map((k, idx) => {
const c = COLUMNS.find(x=>x.key===k);
return `<div class="col-item" data-k="${k}">
<input type="checkbox" class="colChk" ${state.visibleCols.has(k)?'checked':''}>
<span class="c-label">${c.label}</span>
<span class="c-move">
<button class="up" ${idx===0?'disabled':''} title="上移">▲</button>
<button class="down" ${idx===state.colOrder.length-1?'disabled':''} title="下移">▼</button>
</span></div>`;
}).join('');
$('colList').querySelectorAll('.col-item').forEach(item => {
const k = item.dataset.k;
item.querySelector('.up').onclick = () => { const i = state.colOrder.indexOf(k); if (i>0){ [state.colOrder[i-1],state.colOrder[i]]=[state.colOrder[i],state.colOrder[i-1]]; renderColList(); } };
item.querySelector('.down').onclick = () => { const i = state.colOrder.indexOf(k); if (i<state.colOrder.length-1){ [state.colOrder[i+1],state.colOrder[i]]=[state.colOrder[i],state.colOrder[i+1]]; renderColList(); } };
});
}
$('colBtn').onclick = () => { renderColList(); colDrawer.classList.add('show'); };
$('colClose').onclick = () => colDrawer.classList.remove('show');
$('colDone').onclick = () => { state.visibleCols = new Set([...$('colList').querySelectorAll('.colChk')].filter(c=>c.checked).map(c=>c.closest('.col-item').dataset.k)); colDrawer.classList.remove('show'); render(); };
$('colReset').onclick = () => { state.colOrder = COLUMNS.map(c=>c.key); state.visibleCols = new Set(state.colOrder); renderColList(); render(); };
colDrawer.onclick = e => { if (e.target === colDrawer) colDrawer.classList.remove('show'); };
renderKpi();
render();
})();
</script>
</body>
</html>