20260906214757
This commit is contained in:
1 parent
3b03eccdb5
commit
78825797e7
4 files changed
+950
-707
No files matched your search
@@ -0,0 +1,63 @@
|
||||
# 2026-09-06
|
||||
|
||||
## fms-vue dashboard 列表页布局重构:改为分层白卡 + 灰缝
|
||||
|
||||
背景:`src/views/dashboard/index.vue` 原为「单张白卡 + 内部 4 条 border-bottom 硬切分区」,
|
||||
违反 `layouts/DefaultLayout.vue` 注释里定下的约定(分层白卡 + 卡间留灰缝,不用 border 画分隔线)。
|
||||
另外 search-area / preview-bar / list-footer 三处同刷 `--fms-table-header-bg`,底色分层失效。
|
||||
|
||||
改动:
|
||||
- 页面根 `.dashboard-page` 去掉白卡样式,改为 `flex column + gap: 8px`(灰缝)承载两张卡
|
||||
- 卡片 1 `.filter-card`:筛选 / 操作区(方案 A 搜索条 + 高级搜索面板;方案 B 操作栏 + 浮层)
|
||||
- 卡片 2 `.table-card`:操作栏(仅方案 A)+ 表格 + 分页,`flex:1 + min-height:320px` 内部滚动;
|
||||
min-height 兜底,高级搜索展开挤压空间时由 `.fms-content` 纵向滚动,避免表格被压扁
|
||||
- 删除常驻 40px 的 `preview-bar` 调试条;两种布局方案改由 URL query 控制:`?layout=popover`
|
||||
(layoutMode 由 ref 改为可写 computed,读写路由 query)
|
||||
- 删除全部 `border-bottom` 分隔线与 3 处 `--fms-table-header-bg` 灰底
|
||||
- 留白改由各区块自身承担(search-bar / action-bar / advanced-panel),卡片不再设 padding,
|
||||
避免方案 B 下操作栏 padding 与卡片 padding 叠加
|
||||
- 表格区 padding 14px → 16px,与其它区块对齐
|
||||
|
||||
验证:oxlint 0 warning 0 error;用 `vue/compiler-sfc` 跑 parse + compileScript + compileTemplate 通过。
|
||||
|
||||
未处理(本次用户只要求改布局结构,已在回复中列出):
|
||||
- 分页器与表格未联动:dataSource 恒 1w 行全量渲染,切页无反应
|
||||
- `min-width: 1040px` + `overflow: hidden`,窄屏内容被裁且无法横向滚动
|
||||
- quick-search 固定 320px + space-between,中间大片留白
|
||||
- 方案 A / B 两套模板重复 quick-search 与 advanced-grid 代码块,未抽组件
|
||||
|
||||
## dashboard 列表页布局:第二轮讨论 + 清空重建
|
||||
|
||||
定位拍板:dashboard = **列表布局样板**(不是工作台),后续 `FmsModuleListPage` 跟进它的结构;
|
||||
本轮只动 dashboard,业务页下一轮再说。
|
||||
|
||||
纵向预算(1366×768,浏览器可视高约 640px,实测值:顶栏 40 / 页签 40 / 控制高 28 /
|
||||
表格行高 30 / 表头 34):扣完顶栏+页签+padding,**页面可用 544px**。其中分页 44 +
|
||||
操作栏 48 + gap 8 必须留,表格保底 8 行需 274 → **筛选区(含标签行)总预算仅 170px**。
|
||||
|
||||
关键结论(推翻了「平铺 vs grid 是主要矛盾」的第一轮判断):
|
||||
- 平铺式与 grid 式只差约 1 行(40px),是次要矛盾;真正的分水岭是
|
||||
**挤压 vs 浮层,差 140px(约 4 行表格)**
|
||||
- 用户确认填条件时**需要同时看表格** → 方案 B(popover 浮层)出局
|
||||
- 因此设计目标改为:展开态下表格仍保证 7~8 行可见、分页条不被滚走
|
||||
|
||||
三条硬约束:
|
||||
1. **列密度决定行数**——平铺式真正的价值不是「少点击」,而是同样宽度塞更多字段、
|
||||
从而同样高度少换行。现状 grid `minmax(240px,1fr)` 在 1040px 下只有 **3 列**,
|
||||
12 个条件要 4 行超预算;按字段类型给宽度(select 120 / text 160 / date-range 235)
|
||||
可排到 **5 列**、12 项压进 3 行
|
||||
2. **展开后不另起底部操作条**(demo.html 的 `.expanded-actions` 占 48px,近 1/3 预算);
|
||||
查询/重置/展开按钮贴首行右侧
|
||||
3. **溢出时滚动的是筛选区,不是整页**——现状 `.table-card` 的 `min-height:320px`
|
||||
把挤压转成 `.fms-content` 整页滚动,分页条被滚走,违背「边调边看」前提。
|
||||
原则:**筛选区可以委屈,表格和分页不能**
|
||||
|
||||
遗留待定:「常用条件」从哪来(查询字段来自 `s_field_query` 后台配置,前端写死不了),
|
||||
候选:按 `b_xh` 取前 N / 配置加常用标记 / 记住用户上次展开的条件。
|
||||
|
||||
改动:按用户要求**删除 dashboard 中除表格外的全部代码**——查询区(queryItems /
|
||||
quick-search / 高级搜索 / 已选标签)、操作栏、分页、A/B 两套布局与 `?layout=popover`
|
||||
URL 切换、相关 import 与样式全部移除,只保留 columns / dataSource / rowClassName +
|
||||
一张白卡承载 FmsTable。文件 591 → 139 行。
|
||||
验证:oxlint 0 warning 0 error;`vue/compiler-sfc` parse + compileScript +
|
||||
compileTemplate + compileStyle 均通过。
|
||||
+26
-497
@@ -1,509 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<!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>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>ERP 查询构建器 Demo</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; }
|
||||
:root{--border:#e5e7eb;--border-strong:#cfd6e2;--text:#20242b;--muted:#7b8491;--primary:#1677ff;--primary-soft:#edf5ff;--bg:#f4f6f8;--danger:#e5484d;--shadow:0 16px 44px rgba(30,41,59,.18)}
|
||||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}button,input,select{font:inherit}button{cursor:pointer}
|
||||
.page{padding:24px;max-width:1500px;margin:auto}.header{height:42px;display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.title{font-size:20px;font-weight:650}.subtitle{color:var(--muted);font-size:12px;margin-top:4px}
|
||||
.btn{height:34px;padding:0 13px;background:#fff;border:1px solid var(--border-strong);border-radius:6px;color:#303640;display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.btn:hover{background:#f8fafc;border-color:#aeb9c8}.btn.primary{background:var(--primary);border-color:var(--primary);color:#fff}.btn.primary:hover{background:#0f69e8}.btn.link-btn{border:0;background:transparent;color:var(--primary);padding:0 4px}.btn.danger{color:var(--danger)}
|
||||
.search-panel,.table-panel{background:#fff;border:1px solid var(--border);border-radius:8px}.search-panel{padding:14px 16px;margin-bottom:14px}.search-line{display:flex;gap:8px;align-items:center}.search{position:relative;flex:1;max-width:680px}.search input{width:100%;height:36px;border:1px solid var(--border-strong);border-radius:6px;padding:0 12px 0 35px;outline:none}.search input:focus{border-color:var(--primary);box-shadow:0 0 0 2px #1677ff16}.search .icon{position:absolute;left:12px;top:7px;color:#8c95a2;font-size:18px}.badge{padding:2px 7px;border-radius:10px;background:#dcecff;color:var(--primary);font-size:12px}.query-summary{display:flex;align-items:center;gap:9px;margin-top:10px;min-height:28px}.summary-label{font-size:12px;color:var(--muted);white-space:nowrap}.conditions{display:flex;flex-wrap:wrap;gap:7px;min-width:0}.tag{height:28px;display:flex;align-items:center;gap:5px;border:1px solid #cfe0f8;background:#f7faff;border-radius:5px;padding:0 8px;color:#394454;white-space:nowrap}.tag .label{color:#6f7b89}.tag .x{border:0;background:none;color:#8993a0;padding:0 2px;font-size:16px;line-height:1}.table-head{height:48px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #eef0f3}.table-title{font-weight:600}.table-actions{display:flex;align-items:center;gap:10px}.muted{color:var(--muted);font-size:12px}table{width:100%;border-collapse:collapse}th,td{height:48px;padding:0 15px;text-align:left;border-bottom:1px solid #f0f1f3;white-space:nowrap}th{background:#fafbfc;color:#626a76;font-weight:500}tr:hover td{background:#fafcff}.th-label{display:inline-flex;align-items:center;gap:5px}.th-filter{border:0;background:transparent;color:#9aa3ae;padding:3px 2px;font-size:13px}.th-filter:hover{color:var(--primary)}.status{padding:3px 8px;border-radius:4px;background:#fff6df;color:#ad7200;font-size:12px}.link{color:var(--primary);margin-right:14px;cursor:pointer}
|
||||
.overlay{display:none;position:fixed;inset:0;background:rgba(16,24,40,.32);z-index:20}.overlay.show{display:block}.drawer{position:absolute;right:0;top:0;width:min(820px,100%);height:100%;background:#fff;box-shadow:var(--shadow);display:flex;flex-direction:column}.drawer-head{height:64px;padding:0 22px;border-bottom:1px solid #edf0f3;display:flex;align-items:center;justify-content:space-between}.drawer-title{font-size:16px;font-weight:650}.drawer-title small{display:block;color:var(--muted);font-size:12px;font-weight:400;margin-top:4px}.close{border:0;background:none;color:#89919d;font-size:24px;line-height:1}.drawer-body{padding:18px 22px;overflow:auto;flex:1}.logic-bar{display:flex;align-items:center;gap:8px;padding:10px 12px;background:#f7f9fc;border:1px solid #e8edf3;border-radius:7px;margin-bottom:14px}.logic-bar label{color:#667180;font-size:12px}.logic-bar select{height:30px;border:1px solid var(--border-strong);background:#fff;border-radius:5px;padding:0 28px 0 9px;outline:none}.logic-hint{margin-left:auto;color:var(--muted);font-size:12px}
|
||||
.builder{display:flex;flex-direction:column;gap:9px}.condition-row{display:grid;grid-template-columns:minmax(185px,1.15fr) 130px minmax(150px,1fr) 28px;gap:8px;align-items:center}.condition-row select,.condition-row input,.field-picker{height:36px;border:1px solid var(--border-strong);border-radius:6px;background:#fff;padding:0 10px;outline:none;min-width:0}.condition-row select:focus,.condition-row input:focus,.field-picker:focus{border-color:var(--primary);box-shadow:0 0 0 2px #1677ff12}.field-picker{width:100%;font-weight:500;color:#303946;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;position:relative;padding-right:28px}.field-picker:after{content:'⌄';position:absolute;right:10px;color:#8792a0}.value-wrap{min-width:0}.value-input{width:100%}.row-values{display:flex;align-items:center;gap:7px}.row-values input{flex:1}.remove-row{height:30px;width:28px;border:0;background:transparent;color:#9aa3ae;font-size:19px;border-radius:4px}.remove-row:hover{background:#fff0f0;color:var(--danger)}.condition-group{border:1px solid #d8e5f5;border-left:3px solid #78aaf1;border-radius:7px;background:#fbfdff;padding:11px 12px}.group-head{display:flex;align-items:center;gap:8px;margin-bottom:9px;color:#607084;font-size:12px}.group-head strong{font-weight:600;color:#40536b}.group-head select{height:28px;border:1px solid var(--border-strong);border-radius:5px;background:#fff;padding:0 24px 0 8px}.group-head .remove-group{margin-left:auto;border:0;background:transparent;color:#8e98a5}.group-rows{display:flex;flex-direction:column;gap:8px}.group-add{margin-top:9px;border:0;background:transparent;color:var(--primary);padding:2px 0}.add-bar{display:flex;align-items:center;gap:16px;margin-top:14px;padding-top:14px;border-top:1px dashed #e0e5eb}.add-bar .btn{height:32px}.field-guide{margin-top:18px;padding:13px 14px;background:#fafbfc;border:1px solid #edf0f3;border-radius:7px}.field-guide-title{font-size:12px;color:#667180;margin-bottom:9px}.quick-fields{display:flex;flex-wrap:wrap;gap:7px}.quick-field{height:28px;border:1px solid #d8e0ea;background:#fff;border-radius:5px;color:#435164;padding:0 9px;font-size:12px}.quick-field:hover{border-color:var(--primary);color:var(--primary);background:var(--primary-soft)}.drawer-foot{padding:13px 22px;border-top:1px solid #edf0f3;text-align:right}.drawer-foot button{margin-left:8px}
|
||||
.field-pop{display:none;position:fixed;z-index:40;width:300px;max-height:min(520px,calc(100vh - 24px));background:#fff;border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow);padding:10px}.field-pop.show{display:block}.field-search{width:100%;height:32px;border:1px solid var(--border-strong);border-radius:5px;padding:0 9px;outline:none;margin-bottom:8px}.field-options{max-height:min(455px,calc(100vh - 100px));overflow:auto}.field-section{font-size:11px;color:#8b95a3;padding:7px 7px 4px}.field-option{display:flex;align-items:center;gap:7px;width:100%;height:31px;border:0;background:#fff;text-align:left;padding:0 7px;border-radius:4px;color:#3c4755}.field-option:hover{background:#f3f7fc;color:var(--primary)}.field-option .path{color:#98a2ae;font-size:11px;margin-left:auto}.toast{position:fixed;left:50%;top:25px;transform:translateX(-50%);background:#20242b;color:#fff;padding:9px 16px;border-radius:6px;display:none;z-index:50}.toast.show{display:block}
|
||||
@media(max-width:760px){.page{padding:12px}.search-line{flex-wrap:wrap}.search{max-width:none;flex-basis:100%}.drawer{width:100%}.condition-row{grid-template-columns:1fr 1fr}.condition-row .value-wrap{grid-column:1 / span 2}.condition-row .remove-row{grid-column:2;justify-self:end;margin-top:-38px}.logic-hint{display:none}.query-summary{align-items:flex-start}.summary-label{padding-top:6px}}
|
||||
</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 class="header"><div><div class="title">费用列表</div><div class="subtitle">费用单与账单、发票、收付款状态统一查询</div></div><button class="btn primary">+ 新增</button></div>
|
||||
<div class="search-panel"><div class="search-line"><div class="search"><span class="icon">⌕</span><input id="keyword" placeholder="搜索费用单号、客户、摘要…"></div><button class="btn" id="filterBtn">高级查询 <span class="badge" id="filterBadge">4</span></button><button class="btn primary" id="searchBtn">查询</button><button class="btn" id="resetBtn">重置</button></div><div class="query-summary"><span class="summary-label">当前查询</span><div class="conditions" id="conditions"></div><button class="btn link-btn" id="editQuery">编辑</button></div></div>
|
||||
<div class="table-panel"><div class="table-head"><span class="table-title">费用数据</span><div class="table-actions"><span class="muted" id="resultText">已应用 4 个条件 · 共 18 条</span><button class="btn">保存视图</button></div></div><table><thead><tr><th width="45">☐</th><th><span class="th-label">费用单号 <button class="th-filter" data-field="expense.no" title="按此列添加条件">⌄</button></span></th><th><span class="th-label">客户 <button class="th-filter" data-field="expense.customer" title="按此列添加条件">⌄</button></span></th><th><span class="th-label">费用类型 <button class="th-filter" data-field="expense.type" title="按此列添加条件">⌄</button></span></th><th><span class="th-label">金额 <button class="th-filter" data-field="expense.amount" title="按此列添加条件">⌄</button></span></th><th><span class="th-label">状态 <button class="th-filter" data-field="expense.status" title="按此列添加条件">⌄</button></span></th><th><span class="th-label">创建时间 <button class="th-filter" data-field="expense.createdAt" title="按此列添加条件">⌄</button></span></th><th>操作</th></tr></thead><tbody><tr><td>□</td><td>FY20260906001</td><td>上海东方物流有限公司</td><td>海运费</td><td>¥12,800.00</td><td><span class="status">待审核</span></td><td>2026-09-06</td><td><span class="link">审核</span><span class="link">详情</span></td></tr><tr><td>□</td><td>FY20260906002</td><td>深圳华南供应链</td><td>港杂费</td><td>¥8,620.00</td><td><span class="status">待审核</span></td><td>2026-09-05</td><td><span class="link">审核</span><span class="link">详情</span></td></tr><tr><td>□</td><td>FY20260906003</td><td>宁波海联国际</td><td>拖车费</td><td>¥4,200.00</td><td><span class="status">已审核</span></td><td>2026-09-05</td><td><span class="link">详情</span></td></tr><tr><td>□</td><td>FY20260906004</td><td>上海东方物流有限公司</td><td>报关费</td><td>¥16,300.00</td><td><span class="status">待审核</span></td><td>2026-09-04</td><td><span class="link">审核</span><span class="link">详情</span></td></tr></tbody></table></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>
|
||||
|
||||
<div class="overlay" id="overlay"><div class="drawer"><div class="drawer-head"><div class="drawer-title">高级查询<small>当前列表列是快捷字段,也可以查询未展示的关联业务字段</small></div><button class="close" id="closeBtn">×</button></div><div class="drawer-body"><div class="logic-bar"><label>满足</label><select id="rootLogic"><option>全部条件(AND)</option><option>任一条件(OR)</option></select><span class="logic-hint">普通列条件与关联单据条件会组合成同一个查询</span></div><div class="builder" id="builder"></div><div class="add-bar"><button class="btn" id="addCondition">+ 添加条件</button><button class="btn" id="addGroup">+ 添加条件组</button></div><div class="field-guide"><div class="field-guide-title">快捷字段 · 点击即可添加</div><div class="quick-fields"><button class="quick-field" data-quick="expense.status">当前费用单 / 状态</button><button class="quick-field" data-quick="expense.amount">当前费用单 / 金额</button><button class="quick-field" data-quick="invoice.status">关联发票 / 发票状态</button><button class="quick-field" data-quick="payment.unpaidAmount">关联收付款 / 未付款金额</button></div></div></div><div class="drawer-foot"><button class="btn" id="drawerReset">重置条件</button><button class="btn" id="cancelBtn">取消</button><button class="btn primary" id="applyBtn">应用查询</button></div></div></div>
|
||||
<div class="field-pop" id="fieldPop"><input class="field-search" id="fieldSearch" placeholder="搜索字段,例如:发票"><div class="field-options" id="fieldOptions"></div></div><div class="toast" id="toast"></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();
|
||||
(function(){
|
||||
var catalog=[{group:'当前费用单',items:[['expense.no','费用单号'],['expense.customer','客户'],['expense.type','费用类型'],['expense.amount','金额'],['expense.status','状态'],['expense.createdAt','创建时间'],['expense.creator','创建人']]},{group:'关联账单',items:[['bill.exists','账单是否存在'],['bill.status','账单状态'],['bill.amount','账单金额'],['bill.createdAt','账单日期']]},{group:'关联发票',items:[['invoice.exists','发票是否存在'],['invoice.status','发票状态'],['invoice.no','发票号码'],['invoice.amount','发票金额'],['invoice.createdAt','开票日期']]},{group:'关联收付款',items:[['payment.exists','收付款是否存在'],['payment.status','收付款状态'],['payment.unpaidAmount','未付款金额'],['payment.paidAt','付款日期']]}];var fieldMap={};catalog.forEach(function(g){g.items.forEach(function(i){fieldMap[i[0]]={label:i[1],group:g.group};});});var operators={text:[['contains','包含'],['eq','等于'],['ne','不等于'],['isNull','为空']],enum:[['eq','等于'],['ne','不等于'],['in','属于']],number:[['gt','大于'],['ge','大于等于'],['lt','小于'],['le','小于等于'],['between','介于']],date:[['eq','等于'],['ge','不早于'],['le','不晚于'],['between','介于']],exists:[['eq','是']]};var types={no:'text',customer:'text',type:'enum',amount:'number',status:'enum',createdAt:'date',creator:'text',exists:'exists',unpaidAmount:'number',paidAt:'date'};var applied=[{field:'expense.status',op:'eq',value:'待审核'},{field:'expense.amount',op:'gt',value:'10000'},{field:'invoice.status',op:'eq',value:'未开具'},{field:'payment.unpaidAmount',op:'gt',value:'0'}];var draft=[];var overlay=document.getElementById('overlay'),builder=document.getElementById('builder'),conditions=document.getElementById('conditions'),badge=document.getElementById('filterBadge'),resultText=document.getElementById('resultText'),fieldPop=document.getElementById('fieldPop'),fieldOptions=document.getElementById('fieldOptions'),fieldSearch=document.getElementById('fieldSearch'),toast=document.getElementById('toast');
|
||||
function typeFor(field){return types[field.split('.').pop()]||'text'}function valueOptions(field){var k=field.split('.').pop();if(k==='status')return ['待审核','已审核','已驳回','已生成','未开具','已开具','未付款','部分付款','已付款'];if(k==='type')return ['海运费','港杂费','拖车费','报关费'];return []}function inputHtml(row){var type=typeFor(row.field),opts=valueOptions(row.field);if(type==='enum'||type==='exists')return '<select class="value-input"><option value="">请选择</option>'+opts.map(function(o){return '<option>'+o+'</option>'}).join('')+'</select>';if(type==='date')return '<input class="value-input" type="date">';if(row.op==='between')return '<div class="row-values"><input class="value-input" placeholder="最小值"><span>至</span><input class="value-input" placeholder="最大值"></div>';return '<input class="value-input" placeholder="输入值">'}
|
||||
function renderBuilder(){builder.innerHTML='';draft.forEach(function(item,index){if(item.kind==='group'){builder.appendChild(makeGroup(item,index))}else builder.appendChild(makeRow(item,index,false))})}function makeFieldButton(row,index,inGroup){var b=document.createElement('button');b.type='button';b.className='field-picker';b.textContent=fieldMap[row.field].group+' / '+fieldMap[row.field].label;b.onclick=function(){openFieldPop(b,row,index,inGroup)};return b}function makeRow(row,index,inGroup){var wrap=document.createElement('div');wrap.className='condition-row';var fs=makeFieldButton(row,index,inGroup);var os=document.createElement('select');os.innerHTML=operators[typeFor(row.field)].map(function(o){return '<option value="'+o[0]+'">'+o[1]+'('+o[0]+')</option>'}).join('');os.value=row.op;os.onchange=function(){row.op=os.value;renderBuilder()};var vw=document.createElement('div');vw.className='value-wrap';vw.innerHTML=inputHtml(row);var inputs=vw.querySelectorAll('.value-input');inputs.forEach(function(inp,i){if(row.value&&inputs.length===1)inp.value=row.value;if(Array.isArray(row.value))inp.value=row.value[i]||'';inp.oninput=function(){row.value=inputs.length===1?inp.value:Array.from(inputs).map(function(x){return x.value})}});var rm=document.createElement('button');rm.className='remove-row';rm.textContent='×';rm.title='删除条件';rm.onclick=function(){if(inGroup)draft.forEach(function(x){if(x.kind==='group'){var n=x.children.indexOf(row);if(n>-1)x.children.splice(n,1)}});else draft.splice(index,1);renderBuilder()};wrap.appendChild(fs);wrap.appendChild(os);wrap.appendChild(vw);wrap.appendChild(rm);return wrap}
|
||||
function makeGroup(group,index){var box=document.createElement('div');box.className='condition-group';var head=document.createElement('div');head.className='group-head';head.innerHTML='<strong>条件组</strong><span>满足</span>';var logic=document.createElement('select');logic.innerHTML='<option value="and">全部条件(AND)</option><option value="or">任一条件(OR)</option>';logic.value=group.logic||'and';logic.onchange=function(){group.logic=logic.value};head.appendChild(logic);var remove=document.createElement('button');remove.className='remove-group';remove.textContent='删除条件组';remove.onclick=function(){draft.splice(index,1);renderBuilder()};head.appendChild(remove);box.appendChild(head);var rows=document.createElement('div');rows.className='group-rows';group.children.forEach(function(row){rows.appendChild(makeRow(row,index,true))});box.appendChild(rows);var add=document.createElement('button');add.className='group-add';add.textContent='+ 在组内添加条件';add.onclick=function(){group.children.push({field:'invoice.status',op:'eq',value:''});renderBuilder()};box.appendChild(add);return box}
|
||||
function openFieldPop(anchor,target){var r=anchor.getBoundingClientRect();fieldPop.style.left=Math.min(r.left,window.innerWidth-320)+'px';fieldPop.style.top=Math.min(r.bottom+5,window.innerHeight-390)+'px';fieldPop.classList.add('show');fieldPop._target=target;fieldSearch.value='';renderFieldOptions('');fieldSearch.focus()}function renderFieldOptions(query){var q=query.toLowerCase();fieldOptions.innerHTML='';catalog.forEach(function(g){var items=g.items.filter(function(i){return !q||i[1].toLowerCase().includes(q)||g.group.toLowerCase().includes(q)});if(!items.length)return;var s=document.createElement('div');s.className='field-section';s.textContent=g.group;fieldOptions.appendChild(s);items.forEach(function(i){var b=document.createElement('button');b.className='field-option';b.innerHTML=i[1]+'<span class="path">'+g.group.replace('当前费用单','当前')+'</span>';b.onclick=function(){var target=fieldPop._target;target.field=i[0];target.op=operators[typeFor(i[0])][0][0];target.value='';fieldPop.classList.remove('show');renderBuilder()};fieldOptions.appendChild(b)})})}
|
||||
function cloneRows(rows){return rows.map(function(r){if(r.kind==='group')return {kind:'group',logic:r.logic,children:cloneRows(r.children)};return {field:r.field,op:r.op,value:Array.isArray(r.value)?r.value.slice():r.value}})}function operatorLabel(op){return {eq:'=',ne:'≠',gt:'>',ge:'≥',lt:'<',le:'≤',contains:'包含',between:'介于',isNull:'为空'}[op]||op}function displayValue(r){if(r.op==='isNull')return '';if(Array.isArray(r.value))return (r.value[0]||'不限')+' 至 '+(r.value[1]||'不限');return r.value||'未设置'}function countConditions(rows){return rows.reduce(function(n,r){return n+(r.kind==='group'?countConditions(r.children):1)},0)}function renderSummary(){conditions.innerHTML='';applied.forEach(function(r){var tag=document.createElement('div');tag.className='tag';if(r.kind==='group'){tag.innerHTML='<span class="label">条件组</span><span>'+countConditions(r.children)+' 个条件 · '+String(r.logic||'and').toUpperCase()+'</span>'}else{tag.innerHTML='<span class="label">'+fieldMap[r.field].label+'</span><span>'+operatorLabel(r.op)+' '+displayValue(r)+'</span>'}var x=document.createElement('button');x.className='x';x.textContent='×';x.onclick=function(){applied.splice(applied.indexOf(r),1);renderSummary()};tag.appendChild(x);conditions.appendChild(tag)});var count=countConditions(applied);badge.textContent=count;badge.style.display=count?'inline-block':'none';resultText.textContent='已应用 '+count+' 个条件 · 共 '+(count?18:128)+' 条'}function validRow(r){return r.op==='isNull'||(r.value!==''&&!(Array.isArray(r.value)&&!r.value[0]&&!r.value[1]))}function normalizeRows(rows){var out=[];rows.forEach(function(r){if(r.kind==='group'){var children=normalizeRows(r.children);if(children.length)out.push({kind:'group',logic:r.logic||'and',children:children})}else if(validRow(r))out.push(r)});return cloneRows(out)}function showDrawer(){draft=cloneRows(applied.length?applied:[{field:'expense.status',op:'eq',value:''}]);renderBuilder();overlay.classList.add('show')}function hideDrawer(){overlay.classList.remove('show');fieldPop.classList.remove('show')}
|
||||
document.getElementById('filterBtn').onclick=showDrawer;document.getElementById('editQuery').onclick=showDrawer;document.getElementById('closeBtn').onclick=hideDrawer;document.getElementById('cancelBtn').onclick=hideDrawer;overlay.onclick=function(e){if(e.target===overlay)hideDrawer()};document.getElementById('addCondition').onclick=function(){draft.push({field:'expense.status',op:'eq',value:''});renderBuilder()};document.getElementById('addGroup').onclick=function(){draft.push({kind:'group',logic:'and',children:[{field:'invoice.status',op:'eq',value:''}]});renderBuilder()};document.querySelectorAll('.quick-field').forEach(function(b){b.onclick=function(){draft.push({field:b.dataset.quick,op:operators[typeFor(b.dataset.quick)][0][0],value:''});renderBuilder()}});document.querySelectorAll('.th-filter').forEach(function(b){b.onclick=function(){draft=cloneRows(applied);draft.push({field:b.dataset.field,op:operators[typeFor(b.dataset.field)][0][0],value:''});renderBuilder();overlay.classList.add('show')}});document.getElementById('drawerReset').onclick=function(){draft=[];renderBuilder()};document.getElementById('applyBtn').onclick=function(){applied=normalizeRows(draft);renderSummary();hideDrawer();showToast('查询条件已应用')};document.getElementById('resetBtn').onclick=function(){document.getElementById('keyword').value='';applied=[];renderSummary();showToast('查询条件已重置')};document.getElementById('searchBtn').onclick=function(){var k=document.getElementById('keyword').value.trim();if(k){resultText.textContent='搜索“'+k+'” · 已应用 '+countConditions(applied)+' 个条件';showToast('正在查询:'+k)}else showToast('请输入关键词,或使用高级查询')};document.getElementById('keyword').onkeydown=function(e){if(e.key==='Enter')document.getElementById('searchBtn').click()};fieldSearch.oninput=function(){renderFieldOptions(fieldSearch.value)};document.addEventListener('click',function(e){if(fieldPop.classList.contains('show')&&!fieldPop.contains(e.target)&&!e.target.classList.contains('field-picker'))fieldPop.classList.remove('show')});function showToast(t){toast.textContent=t;toast.classList.add('show');clearTimeout(window.__t);window.__t=setTimeout(function(){toast.classList.remove('show')},1500)}renderSummary();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user