20260905172534

This commit is contained in:
oneao committed 2026-09-05 17:25:35 +08:00
1 parent 1b01a31252
commit 7b00eeb88c
711 files changed
+2849 -959

No files matched your search

+510
View File
@@ -0,0 +1,510 @@
<!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>