Commit 4416c52fbdae4a3b8e8580f78200841e452c4d82

Authored by zichun
1 parent 1cb8cf53

chore(prototype): add UI prototype files and reference screenshots

prototype/XLY-ERP.html 0 → 100644
  1 +<!doctype html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="utf-8" />
  5 +<title>XLY-ERP · 印刷制造管理平台</title>
  6 +<meta name="viewport" content="width=device-width, initial-scale=1" />
  7 +<link rel="preconnect" href="https://fonts.googleapis.com" />
  8 +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  9 +<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
  10 +<style>
  11 + :root {
  12 + --accent: #3a8ee0;
  13 + --accent-strong: #2776c6;
  14 + --accent-soft: #eaf4fc;
  15 + --selected: #d4e8f7;
  16 + --bg-app: #f3f5f8;
  17 + --bg-card: #ffffff;
  18 + --bg-input: #fafbfc;
  19 + --bg-input-focus: #ffffff;
  20 + --bg-topbar: #262d3a;
  21 + --bg-toolbar-dark: #2c3340;
  22 + --bg-tab-strip: #e8ecf2;
  23 + --bg-tab-active: #ffffff;
  24 + --bg-row-zebra: #fafbfc;
  25 + --bg-row-hover: #eaf4fc;
  26 + --bg-disabled: #eef0f3;
  27 + --border: #dce1e8;
  28 + --border-strong: #c2cad6;
  29 + --border-input: #d6dce4;
  30 + --text: #2a3142;
  31 + --text-muted: #6b7280;
  32 + --text-faint: #9aa3b2;
  33 + --text-on-dark: #e9ecf2;
  34 + --text-on-dark-muted: #98a1b3;
  35 + --required: #e74c3c;
  36 + --success: #27a567;
  37 + --warning: #d98e1f;
  38 + --danger: #d04141;
  39 + --row-h: 26px;
  40 + --tb-h: 28px;
  41 + --input-h: 26px;
  42 + }
  43 + * { box-sizing: border-box; }
  44 + html, body { height: 100%; margin: 0; }
  45 + body {
  46 + font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", -apple-system, "Segoe UI", sans-serif;
  47 + font-size: 13px;
  48 + color: var(--text);
  49 + background: var(--bg-app);
  50 + -webkit-font-smoothing: antialiased;
  51 + overflow: hidden;
  52 + }
  53 + #root { height: 100vh; width: 100vw; }
  54 + button { font-family: inherit; font-size: inherit; color: inherit; }
  55 + input, select, textarea { font-family: inherit; font-size: inherit; color: inherit; }
  56 + input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); background: var(--bg-input-focus); }
  57 + ::-webkit-scrollbar { width: 10px; height: 10px; }
  58 + ::-webkit-scrollbar-thumb { background: #c5cdd9; border-radius: 5px; border: 2px solid var(--bg-app); }
  59 + ::-webkit-scrollbar-thumb:hover { background: #aab3c2; }
  60 + ::-webkit-scrollbar-track { background: transparent; }
  61 + .mono { font-family: "JetBrains Mono", Menlo, Consolas, monospace; }
  62 +</style>
  63 +<script type="application/json" id="boot-config">{"name":"XLY-ERP"}</script>
  64 +</head>
  65 +<body>
  66 +<div id="root"></div>
  67 +
  68 +<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
  69 +<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
  70 +<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
  71 +
  72 +<script type="text/babel" src="src/icons.jsx"></script>
  73 +<script type="text/babel" src="src/data.jsx"></script>
  74 +<script type="text/babel" src="src/primitives.jsx"></script>
  75 +<script type="text/babel" src="src/login.jsx"></script>
  76 +<script type="text/babel" src="src/sidebar.jsx"></script>
  77 +<script type="text/babel" src="src/tabs.jsx"></script>
  78 +<script type="text/babel" src="src/screen-module.jsx"></script>
  79 +<script type="text/babel" src="src/screen-userlist.jsx"></script>
  80 +<script type="text/babel" src="src/screen-userdetail.jsx"></script>
  81 +<script type="text/babel" src="src/screen-home.jsx"></script>
  82 +<script type="text/babel" src="src/meganav.jsx"></script>
  83 +<script type="text/babel" src="src/workspace.jsx"></script>
  84 +<script type="text/babel" src="src/app.jsx"></script>
  85 +</body>
  86 +</html>
... ...
prototype/src/app.jsx 0 → 100644
  1 +// Top-level app: login → workspace.
  2 +
  3 +const App = () => {
  4 + const [session, setSession] = React.useState(null);
  5 + if (!session) return <Login onLogin={setSession} />;
  6 + return <Workspace session={session} onLogout={() => setSession(null)} />;
  7 +};
  8 +
  9 +ReactDOM.createRoot(document.getElementById("root")).render(<App />);
... ...
prototype/src/data.jsx 0 → 100644
  1 +// Seed data — plausible printing-industry ERP tenant.
  2 +// Original company name; not derived from the reference product.
  3 +
  4 +const COMPANIES = [
  5 + { id: "std", name: "标准版 (Standard Edition) / 8s" },
  6 + { id: "ent", name: "企业版 (Enterprise Edition) / 8s" },
  7 + { id: "trial", name: "试用版 (Trial Edition) / 30d" },
  8 +];
  9 +
  10 +// Sidebar tree — printing-industry processes
  11 +const NAV_TREE = [
  12 + { id: "home", label: "首页", icon: "home", leaf: true },
  13 + {
  14 + id: "kpi", label: "KPI 流程作业单", icon: "doc",
  15 + children: [
  16 + {
  17 + id: "quote", label: "估价管理流程",
  18 + children: [
  19 + { id: "quote-01", label: "01/04 【新增】新报价单", leaf: true, badge: "估价" },
  20 + { id: "quote-02", label: "02/04 审核报价单->客户确认...", leaf: true },
  21 + { id: "quote-03", label: "03/04 客户确认->二次确认", leaf: true },
  22 + { id: "quote-04", label: "04/04 报价单->销售订单", leaf: true },
  23 + { id: "quote-05", label: "04/04 报价单->转拼版单", leaf: true },
  24 + { id: "quote-06", label: "04/07 主管审核报价单", leaf: true },
  25 + { id: "quote-07", label: "05/07 业务确认报价单", leaf: true },
  26 + ],
  27 + },
  28 + { id: "order", label: "订单生产流程" },
  29 + { id: "panel", label: "自动拼版流程" },
  30 + { id: "ship", label: "销售送货流程" },
  31 + { id: "purch", label: "物料采购流程" },
  32 + { id: "issue", label: "物料领用流程" },
  33 + { id: "outproc", label: "发外加工流程" },
  34 + { id: "qc", label: "质量管理流程" },
  35 + { id: "fin", label: "财务收付款流程" },
  36 + ],
  37 + },
  38 + { id: "crm", label: "CRM 管理", icon: "folder" },
  39 + { id: "plm", label: "PLM 管理", icon: "folder" },
  40 + { id: "prod", label: "产品管理", icon: "folder" },
  41 + { id: "sales", label: "销售管理", icon: "folder" },
  42 + { id: "mfg", label: "生产管理", icon: "folder" },
  43 + { id: "exec", label: "生产执行", icon: "folder" },
  44 + { id: "mold", label: "模具管理", icon: "folder" },
  45 + {
  46 + id: "sys", label: "系统管理", icon: "settings",
  47 + children: [
  48 + { id: "roles", label: "角色管理", leaf: true },
  49 + { id: "menucfg", label: "菜单配置", leaf: true },
  50 + { id: "log", label: "操作日志", leaf: true },
  51 + ],
  52 + },
  53 +];
  54 +
  55 +// Permission-group rows (left column of permission tab)
  56 +const PERMISSION_GROUPS = [
  57 + "权限分类", "默认显示(必选)", "禁止查看价格", "客服报单",
  58 + "报价组员工", "物控组员工", "供应链 PMC", "允许查看订单价格",
  59 + "储运员工", "外接供应商", "品质组员工", "技术中心员工",
  60 + "机修组员工", "生产排计划员工", "外发组员工",
  61 + "模切车间", "装订车间", "粘接工车间", "品质部管理", "精品车间",
  62 + "人事组", "统计组", "机修主管", "样品开发员工", "设计开发", "总经办",
  63 + "财务部", "销售员", "采购员", "仓库管理员",
  64 +];
  65 +
  66 +const DEPARTMENTS = [
  67 + "工艺技术", "印刷车间", "机修", "机务部", "财务部",
  68 + "装订车间", "总经办公室", "总务部", "供应链", "质量管理部",
  69 + "模切车间", "计划组", "样品开发", "设计部", "仓库",
  70 +];
  71 +
  72 +const USER_TYPES = ["超级管理员", "高级管理员", "普通用户", "外部用户", "只读用户"];
  73 +const LANGUAGES = ["中文", "英文", "繁体"];
  74 +
  75 +// Plausible Chinese-name pinyin pairs
  76 +const NAME_POOL = [
  77 + ["管广飞", "ggf"], ["李斌", "lib"], ["系统管理员", "admin"], ["朱财喜", "zhucx"],
  78 + ["林杰华", "ljh"], ["汪鑫", "wx"], ["钱昉", "qianb"], ["张冠飞", "zgf"],
  79 + ["孟威", "mengw"], ["杭仁萍", "hangrp"], ["王月", "wy"], ["王宽明", "wkm"],
  80 + ["潘强", "pq"], ["耿广东", "ggd"], ["余涛", "yt"], ["梁赵军", "lzj"],
  81 + ["曹佳怡", "cjy"], ["陈思琪", "csq"], ["张红英", "zhy"], ["吕欣彦", "lxy"],
  82 + ["陈雪婷", "cxt"], ["路鑫", "luxin"], ["陆鑫·储运部", "ZY0006"],
  83 + ["朱晓兵", "zhuxb"], ["孟丽花", "menglh"], ["彭敏", "pengm"], ["顾鹏", "gp"],
  84 + ["田雨", "ty"], ["黄文豪", "hwh"], ["邓佳", "dj"], ["孙浩然", "shr"],
  85 + ["徐瑞", "xr"], ["许云", "xy"], ["何晨曦", "hcx"], ["林婉君", "lwj"],
  86 + ["杨柳", "yl"], ["蒋婷", "jt"],
  87 +];
  88 +
  89 +const seededRandom = (seed) => {
  90 + let s = seed;
  91 + return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
  92 +};
  93 +
  94 +const pad = (n) => String(n).padStart(2, "0");
  95 +const dt = (y, m, d, hh, mm, ss) =>
  96 + `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`;
  97 +
  98 +const buildUsers = () => {
  99 + const rand = seededRandom(7);
  100 + return NAME_POOL.map((nm, i) => {
  101 + const r = rand();
  102 + return {
  103 + id: i + 1,
  104 + seq: i + 1,
  105 + employee: nm[0],
  106 + empNo: nm[1],
  107 + account: nm[1],
  108 + department: DEPARTMENTS[Math.floor(rand() * DEPARTMENTS.length)],
  109 + type: USER_TYPES[Math.floor(rand() * USER_TYPES.length)],
  110 + language: r < 0.15 ? "英文" : "中文",
  111 + disabled: r < 0.08,
  112 + lastLogin: dt(2026, 1 + Math.floor(rand() * 4), 1 + Math.floor(rand() * 27),
  113 + 8 + Math.floor(rand() * 10), Math.floor(rand() * 60), Math.floor(rand() * 60)),
  114 + createdBy: ["超级管理员", "机仁萍", "李丹", "YFZ", "LJH", "孟琰"][Math.floor(rand() * 6)],
  115 + createdAt: dt(2023 + Math.floor(rand() * 3), 1 + Math.floor(rand() * 12),
  116 + 1 + Math.floor(rand() * 27), 8 + Math.floor(rand() * 10),
  117 + Math.floor(rand() * 60), Math.floor(rand() * 60)),
  118 + // Default: a small subset of permission groups checked
  119 + permissions: PERMISSION_GROUPS.reduce((acc, g) => {
  120 + acc[g] = rand() < 0.18; return acc;
  121 + }, {}),
  122 + tabPerms: { customer: rand() < 0.4, supplier: rand() < 0.3, staff: rand() < 0.3, process: rand() < 0.3, driver: rand() < 0.2 },
  123 + };
  124 + });
  125 +};
  126 +
  127 +// Mega-nav grid: top-level sections (left rail) → category columns → leaf items
  128 +const MEGA_NAV = [
  129 + { id: "sales-mgmt", icon: "folder", label: "销售管理" },
  130 + { id: "dcs", icon: "folder", label: "DCS 系统" },
  131 + { id: "prod-mgmt", icon: "folder", label: "产品管理" },
  132 + { id: "prod-ops", icon: "folder", label: "生产运营" },
  133 + { id: "prod-exec", icon: "folder", label: "生产执行" },
  134 + { id: "mold", icon: "folder", label: "模具管理" },
  135 + { id: "purch", icon: "folder", label: "采购管理" },
  136 + { id: "matwh", icon: "folder", label: "材料库存" },
  137 + { id: "fgwh", icon: "folder", label: "成品库存" },
  138 + { id: "outsrc", icon: "folder", label: "外协管理" },
  139 + { id: "logistics", icon: "folder", label: "物流管理" },
  140 + { id: "qc", icon: "folder", label: "质量管理" },
  141 + { id: "fin", icon: "folder", label: "财务管理" },
  142 + { id: "cost-pro", icon: "folder", label: "成本管理(专)" },
  143 + { id: "cost", icon: "folder", label: "成本管理" },
  144 + { id: "equip", icon: "folder", label: "设备管理" },
  145 + { id: "hr", icon: "folder", label: "人事行政" },
  146 + { id: "oa", icon: "folder", label: "OA 系统" },
  147 + { id: "base", icon: "folder", label: "基础设置" },
  148 + { id: "sys", icon: "settings", label: "系统设置", active: true },
  149 +];
  150 +
  151 +const MEGA_COLUMNS = {
  152 + "sys": [
  153 + { title: "期初设置", items: [
  154 + { label: "客户期初" }, { label: "供应商期初" }, { label: "材料期初" }, { label: "产品期初" }, { label: "数据导入" }, { label: "离线导出下载" },
  155 + ]},
  156 + { title: "用户管理", items: [
  157 + { label: "用户列表", screen: "userlist", featured: true },
  158 + { label: "系统权限" }, { label: "系统权限程查表" }, { label: "权限组" },
  159 + ]},
  160 + { title: "系统参数", items: [
  161 + { label: "系统参数" }, { label: "财务结准" }, { label: "系统常量配置" },
  162 + ]},
  163 + { title: "计算方案", items: [
  164 + { label: "方案列表" }, { label: "计算参数" },
  165 + ]},
  166 + { title: "日志", items: [
  167 + { label: "个性化模块" }, { label: "操作日志" }, { label: "异常清除KPI任务表" }, { label: "MYSQL监听器" },
  168 + ]},
  169 + { title: "开发平台", items: [
  170 + { label: "自定义开发范例" }, { label: "系统功能模块设置" }, { label: "ERC流程清单" }, { label: "功能模块界面设置" }, { label: "模拟收付款业务处理" },
  171 + ]},
  172 + { title: "API 对接管理", items: [
  173 + { label: "调用第三方接口(TOKEN配置)" }, { label: "调用第三方接口(接口定义)" }, { label: "被第三方调用(生成token)" }, { label: "数据同步" }, { label: "被第三方调用(API定义)" },
  174 + ]},
  175 + { title: "系统模块", items: [
  176 + { label: "系统模块配置", screen: "module", featured: true },
  177 + { label: "菜单配置" }, { label: "模块字段配置" },
  178 + ]},
  179 + ],
  180 + // Fallback for other sections — show a placeholder column
  181 +};
  182 +
  183 +window.XLY = {
  184 + COMPANIES,
  185 + NAV_TREE,
  186 + MEGA_NAV,
  187 + MEGA_COLUMNS,
  188 + PERMISSION_GROUPS,
  189 + DEPARTMENTS,
  190 + USER_TYPES,
  191 + LANGUAGES,
  192 + buildUsers,
  193 +};
... ...
prototype/src/icons.jsx 0 → 100644
  1 +// Tiny inline SVG icons. All sized via currentColor.
  2 +const Ic = {};
  3 +
  4 +const make = (name, paths, vb = "0 0 16 16") => {
  5 + Ic[name] = ({ size = 14, color, style, ...rest }) => (
  6 + <svg
  7 + width={size}
  8 + height={size}
  9 + viewBox={vb}
  10 + fill="none"
  11 + stroke={color || "currentColor"}
  12 + strokeWidth="1.4"
  13 + strokeLinecap="round"
  14 + strokeLinejoin="round"
  15 + style={{ flex: "none", display: "inline-block", verticalAlign: "-2px", ...style }}
  16 + {...rest}
  17 + >
  18 + {paths}
  19 + </svg>
  20 + );
  21 +};
  22 +
  23 +make("plus", <><path d="M8 3v10M3 8h10" /></>);
  24 +make("edit", <><path d="M11.5 2.5l2 2-8 8H3.5v-2l8-8z" /></>);
  25 +make("trash", <><path d="M3 4.5h10M6 4.5V3a1 1 0 011-1h2a1 1 0 011 1v1.5M5 4.5v8a1 1 0 001 1h4a1 1 0 001-1v-8" /></>);
  26 +make("save", <><path d="M3 3h8l2 2v8H3V3z" /><path d="M5 3v3h5V3M5 13v-4h6v4" /></>);
  27 +make("cancel", <><circle cx="8" cy="8" r="5.5" /><path d="M5.5 5.5l5 5M10.5 5.5l-5 5" /></>);
  28 +make("refresh", <><path d="M13 4v3h-3M3 12V9h3" /><path d="M3.5 7a4.5 4.5 0 018-1.5M12.5 9a4.5 4.5 0 01-8 1.5" /></>);
  29 +make("export", <><path d="M8 2v8M5 6.5L8 9.5l3-3M3 11v2h10v-2" /></>);
  30 +make("import", <><path d="M8 10V2M5 5.5L8 2.5l3 3M3 11v2h10v-2" /></>);
  31 +make("search", <><circle cx="7" cy="7" r="4" /><path d="M10 10l3 3" /></>);
  32 +make("close", <><path d="M4 4l8 8M12 4l-8 8" /></>);
  33 +make("chevronDown", <><path d="M4 6l4 4 4-4" /></>);
  34 +make("chevronRight", <><path d="M6 4l4 4-4 4" /></>);
  35 +make("chevronLeft", <><path d="M10 4l-4 4 4 4" /></>);
  36 +make("triangle", <><path d="M4 5l4 5 4-5z" fill="currentColor" stroke="none" /></>);
  37 +make("triangleR", <><path d="M5 4l5 4-5 4z" fill="currentColor" stroke="none" /></>);
  38 +make("user", <><circle cx="8" cy="6" r="2.5" /><path d="M3 13.5c.5-2.5 2.5-4 5-4s4.5 1.5 5 4" /></>);
  39 +make("lock", <><rect x="3.5" y="7" width="9" height="6" rx="1" /><path d="M5.5 7V5a2.5 2.5 0 015 0v2" /></>);
  40 +make("building", <><rect x="3" y="3" width="10" height="10" /><path d="M5.5 5.5h1m3 0h1m-5 2h1m3 0h1m-5 2h1m3 0h1M7 13v-2h2v2" /></>);
  41 +make("home", <><path d="M2.5 8L8 3l5.5 5M4 8v5h8V8" /></>);
  42 +make("menu", <><path d="M3 4h10M3 8h10M3 12h10" /></>);
  43 +make("bell", <><path d="M8 2.5a3.5 3.5 0 013.5 3.5v2.5l1 2H3.5l1-2V6A3.5 3.5 0 018 2.5zM6.5 12.5a1.5 1.5 0 003 0" /></>);
  44 +make("settings", <><circle cx="8" cy="8" r="2" /><path d="M8 1.5l.7 1.6 1.7-.4.4 1.7 1.6.7-.7 1.6.7 1.6-1.6.7-.4 1.7-1.7-.4L8 13.5l-.7-1.6-1.7.4-.4-1.7-1.6-.7.7-1.6-.7-1.6 1.6-.7.4-1.7 1.7.4z" /></>);
  45 +make("function", <><path d="M3 8h2l1-3 2 6 1-3h4" /></>);
  46 +make("key", <><circle cx="5" cy="8" r="2.5" /><path d="M7.5 8H13l-1 2M11 8v2" /></>);
  47 +make("redo", <><path d="M13 4v3h-3" /><path d="M13 7a5 5 0 10-1 5" /></>);
  48 +make("undo", <><path d="M3 4v3h3" /><path d="M3 7a5 5 0 111 5" /></>);
  49 +make("clipboard", <><rect x="4" y="3" width="8" height="11" rx="1" /><path d="M6 3V2h4v1" /></>);
  50 +make("doc", <><path d="M4 2h5l3 3v9H4V2z" /><path d="M9 2v3h3" /></>);
  51 +make("folder", <><path d="M2 4.5h4l1.5 1.5H14v6.5H2v-8z" /></>);
  52 +make("expand", <><path d="M3 6V3h3M13 6V3h-3M3 10v3h3M13 10v3h-3" /></>);
  53 +make("dot", <><circle cx="8" cy="8" r="1.5" fill="currentColor" stroke="none" /></>);
  54 +make("filter", <><path d="M3 4h10l-4 4.5V13l-2-1.5V8.5L3 4z" /></>);
  55 +make("sortAsc", <><path d="M5 12V4M5 4l-2 2M5 4l2 2M9 5h4M9 8h3M9 11h2" /></>);
  56 +
  57 +// Brand mark — overlapping registration squares (original geometric mark)
  58 +Ic.Brand = ({ size = 28, accent = "#3a8ee0" }) => (
  59 + <svg width={size} height={size} viewBox="0 0 32 32" fill="none">
  60 + <rect x="3" y="3" width="16" height="16" stroke="#fff" strokeWidth="2" />
  61 + <rect x="13" y="13" width="16" height="16" stroke={accent} strokeWidth="2" />
  62 + <rect x="13" y="13" width="6" height="6" fill="#fff" />
  63 + </svg>
  64 +);
  65 +
  66 +window.Ic = Ic;
... ...
prototype/src/login.jsx 0 → 100644
  1 +// Login screen. Original logo (geometric registration-mark), original product name.
  2 +
  3 +const Login = ({ onLogin }) => {
  4 + const [user, setUser] = React.useState("admin");
  5 + const [pass, setPass] = React.useState("••••••••");
  6 + const [company, setCompany] = React.useState("std");
  7 + const [companyOpen, setCompanyOpen] = React.useState(false);
  8 + const [submitting, setSubmitting] = React.useState(false);
  9 +
  10 + const submit = (e) => {
  11 + e && e.preventDefault();
  12 + if (!user || !pass) return;
  13 + setSubmitting(true);
  14 + setTimeout(() => onLogin({ user, company: XLY.COMPANIES.find(c => c.id === company)?.name }), 480);
  15 + };
  16 +
  17 + return (
  18 + <div style={{
  19 + position: "fixed", inset: 0,
  20 + background: "radial-gradient(ellipse at center, #424b60 0%, #2e3645 70%, #262d3a 100%)",
  21 + display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
  22 + fontFamily: "inherit",
  23 + }}>
  24 + <div style={{ width: 320, marginTop: -40 }}>
  25 + {/* Logo + wordmark */}
  26 + <div style={{ display: "flex", flexDirection: "column", alignItems: "center", marginBottom: 28 }}>
  27 + <div style={{ position: "relative", marginBottom: 10 }}>
  28 + <svg width="76" height="76" viewBox="0 0 76 76" fill="none">
  29 + {/* Three overlapping registration squares — print-press metaphor */}
  30 + <rect x="8" y="8" width="34" height="34" stroke="rgba(255,255,255,0.95)" strokeWidth="2" />
  31 + <rect x="22" y="22" width="34" height="34" stroke="rgba(255,255,255,0.85)" strokeWidth="2" />
  32 + <rect x="36" y="36" width="32" height="32" stroke="rgba(255,255,255,0.7)" strokeWidth="2" />
  33 + {/* registration cross */}
  34 + <circle cx="38" cy="38" r="4" stroke="#fff" strokeWidth="1.5" />
  35 + <path d="M38 32v12M32 38h12" stroke="#fff" strokeWidth="1" />
  36 + </svg>
  37 + </div>
  38 + <div style={{ color: "#fff", fontSize: 22, fontWeight: 600, letterSpacing: 4, marginBottom: 2 }}>
  39 + XLY-ERP
  40 + </div>
  41 + <div style={{ color: "rgba(255,255,255,0.55)", fontSize: 11, letterSpacing: 2 }}>
  42 + 印刷制造管理平台
  43 + </div>
  44 + </div>
  45 +
  46 + <form onSubmit={submit}>
  47 + {/* User */}
  48 + <div style={loginStyles.fieldWrap}>
  49 + <span style={loginStyles.fieldIcon}><Ic.user size={14} color="rgba(0,0,0,0.55)" /></span>
  50 + <span style={loginStyles.fieldDivider} />
  51 + <input
  52 + value={user}
  53 + onChange={(e) => setUser(e.target.value)}
  54 + placeholder="请输入用户名"
  55 + style={loginStyles.input}
  56 + />
  57 + </div>
  58 + {/* Password */}
  59 + <div style={loginStyles.fieldWrap}>
  60 + <span style={loginStyles.fieldIcon}><Ic.lock size={14} color="rgba(0,0,0,0.55)" /></span>
  61 + <span style={loginStyles.fieldDivider} />
  62 + <input
  63 + type="password"
  64 + value={pass}
  65 + onChange={(e) => setPass(e.target.value)}
  66 + placeholder="请输入密码"
  67 + style={loginStyles.input}
  68 + />
  69 + </div>
  70 + {/* Company picker */}
  71 + <div style={{ position: "relative", marginBottom: 14 }}>
  72 + <button
  73 + type="button"
  74 + onClick={() => setCompanyOpen((v) => !v)}
  75 + style={{
  76 + ...loginStyles.fieldWrap,
  77 + width: "100%",
  78 + cursor: "pointer", textAlign: "left",
  79 + paddingRight: 12,
  80 + }}
  81 + >
  82 + <span style={{ flex: 1, color: company ? "#1a2332" : "rgba(0,0,0,0.5)", fontSize: 13, paddingLeft: 12 }}>
  83 + {company ? XLY.COMPANIES.find(c => c.id === company)?.name : "请选择公司名"}
  84 + </span>
  85 + <span style={{ color: "rgba(0,0,0,0.5)", display: "inline-flex", paddingRight: 4 }}>
  86 + <Ic.chevronDown size={14} style={{ transform: companyOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s" }} />
  87 + </span>
  88 + </button>
  89 + {companyOpen ? (
  90 + <div style={{
  91 + position: "absolute", top: "calc(100% + 2px)", left: 0, right: 0,
  92 + background: "rgba(60,68,82,0.98)", border: "1px solid rgba(255,255,255,0.1)",
  93 + zIndex: 10,
  94 + }}>
  95 + {XLY.COMPANIES.map((c, i) => {
  96 + const sel = c.id === company;
  97 + return (
  98 + <div
  99 + key={c.id}
  100 + onClick={() => { setCompany(c.id); setCompanyOpen(false); }}
  101 + style={{
  102 + padding: "9px 12px", color: "#fff", fontSize: 13,
  103 + cursor: "pointer",
  104 + background: sel ? "var(--accent)" : "transparent",
  105 + borderTop: i === 0 ? "none" : "1px solid rgba(255,255,255,0.04)",
  106 + }}
  107 + onMouseEnter={(e) => { if (!sel) e.currentTarget.style.background = "rgba(255,255,255,0.06)"; }}
  108 + onMouseLeave={(e) => { if (!sel) e.currentTarget.style.background = "transparent"; }}
  109 + >
  110 + {c.name}
  111 + </div>
  112 + );
  113 + })}
  114 + </div>
  115 + ) : null}
  116 + </div>
  117 + {/* Submit */}
  118 + <button
  119 + type="submit"
  120 + disabled={submitting}
  121 + style={{
  122 + width: "100%", height: 38,
  123 + background: submitting ? "#3478b8" : "var(--accent)",
  124 + border: "none", color: "#fff", fontSize: 14, letterSpacing: 8,
  125 + cursor: submitting ? "wait" : "pointer",
  126 + fontFamily: "inherit",
  127 + }}
  128 + onMouseEnter={(e) => { if (!submitting) e.currentTarget.style.background = "var(--accent-strong)"; }}
  129 + onMouseLeave={(e) => { if (!submitting) e.currentTarget.style.background = "var(--accent)"; }}
  130 + >
  131 + {submitting ? "登 录 中…" : "登 录"}
  132 + </button>
  133 + </form>
  134 + </div>
  135 +
  136 + {/* Footer */}
  137 + <div style={{
  138 + position: "absolute", bottom: 24, left: 0, right: 0, textAlign: "center",
  139 + color: "rgba(255,255,255,0.3)", fontSize: 11, letterSpacing: 1,
  140 + }}>
  141 + XLY 软件 · 印刷制造管理平台 · 版权所有 © 2017–2026
  142 + </div>
  143 + </div>
  144 + );
  145 +};
  146 +
  147 +const loginStyles = {
  148 + fieldWrap: {
  149 + display: "flex", alignItems: "center", height: 38,
  150 + background: "rgba(255,255,255,0.95)",
  151 + border: "1px solid rgba(255,255,255,0.15)",
  152 + marginBottom: 10,
  153 + },
  154 + fieldIcon: { width: 36, display: "inline-flex", alignItems: "center", justifyContent: "center", color: "rgba(0,0,0,0.5)" },
  155 + fieldDivider: { width: 1, height: 18, background: "rgba(0,0,0,0.12)" },
  156 + input: {
  157 + flex: 1, height: "100%", border: "none", background: "transparent",
  158 + paddingLeft: 10, paddingRight: 10, fontSize: 13, color: "#1a2332",
  159 + outline: "none", fontFamily: "inherit",
  160 + },
  161 +};
  162 +
  163 +window.Login = Login;
... ...
prototype/src/meganav.jsx 0 → 100644
  1 +// Full-viewport "全部导航" mega-menu, modeled after the reference screenshot.
  2 +const MegaNav = ({ onClose, onOpen }) => {
  3 + const [activeSection, setActiveSection] = React.useState("sys");
  4 + const cols = XLY.MEGA_COLUMNS[activeSection];
  5 +
  6 + return (
  7 + <div style={{
  8 + position: "fixed", inset: 0, zIndex: 100,
  9 + background: "#1a2030", color: "var(--text-on-dark)",
  10 + display: "flex", flexDirection: "column",
  11 + }}>
  12 + {/* Header */}
  13 + <div style={{
  14 + height: 36, flex: "none", display: "flex", alignItems: "center",
  15 + padding: "0 10px", background: "#262d3a", borderBottom: "1px solid #1a1f2a",
  16 + }}>
  17 + <button
  18 + onClick={onClose}
  19 + style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 28, padding: "0 10px", background: "rgba(58,142,224,0.18)", color: "#fff", border: "1px solid rgba(58,142,224,0.4)", cursor: "pointer", fontSize: 12 }}
  20 + >
  21 + <Ic.menu size={14} /> 全部导航
  22 + </button>
  23 + <button onClick={onClose} style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 28, padding: "0 10px", background: "transparent", color: "var(--text-on-dark)", border: "none", cursor: "pointer", fontSize: 12, marginLeft: 4 }}>
  24 + <Ic.home size={14} /> 主页
  25 + </button>
  26 + <div style={{ flex: 1 }} />
  27 + <button onClick={onClose} title="关闭" style={{ width: 28, height: 28, background: "transparent", color: "var(--text-on-dark)", border: "none", cursor: "pointer" }}>
  28 + <Ic.close size={14} />
  29 + </button>
  30 + </div>
  31 +
  32 + {/* Body */}
  33 + <div style={{ flex: 1, display: "flex", overflow: "hidden", minHeight: 0 }}>
  34 + {/* Left rail */}
  35 + <div style={{
  36 + width: 150, flex: "none", borderRight: "1px solid rgba(255,255,255,0.08)",
  37 + overflow: "auto", padding: "8px 0",
  38 + }}>
  39 + {XLY.MEGA_NAV.map((s) => {
  40 + const active = s.id === activeSection;
  41 + return (
  42 + <div
  43 + key={s.id}
  44 + onClick={() => setActiveSection(s.id)}
  45 + style={{
  46 + display: "flex", alignItems: "center", gap: 8,
  47 + padding: "8px 14px", cursor: "pointer", fontSize: 12,
  48 + background: active ? "var(--accent)" : "transparent",
  49 + color: active ? "#fff" : "var(--text-on-dark)",
  50 + borderLeft: active ? "3px solid #fff" : "3px solid transparent",
  51 + }}
  52 + onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = "rgba(255,255,255,0.06)"; }}
  53 + onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = "transparent"; }}
  54 + >
  55 + <Ic.folder size={13} />
  56 + <span>{s.label}</span>
  57 + </div>
  58 + );
  59 + })}
  60 + </div>
  61 +
  62 + {/* Columns */}
  63 + <div style={{ flex: 1, overflow: "auto", padding: "16px 24px" }}>
  64 + {cols ? (
  65 + <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: "0 24px" }}>
  66 + {cols.map((col) => (
  67 + <div key={col.title} style={{ marginBottom: 24 }}>
  68 + <div style={{ fontSize: 13, fontWeight: 500, color: "#fff", marginBottom: 10, paddingBottom: 6, borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
  69 + {col.title}
  70 + </div>
  71 + <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
  72 + {col.items.map((it, i) => {
  73 + const clickable = !!it.screen;
  74 + return (
  75 + <div
  76 + key={i}
  77 + onClick={clickable ? () => { onOpen(it.screen, it.label); onClose(); } : undefined}
  78 + style={{
  79 + fontSize: 12,
  80 + color: it.featured ? "var(--accent)" : "var(--text-on-dark-muted)",
  81 + cursor: clickable ? "pointer" : "default",
  82 + display: "inline-flex", alignItems: "center", gap: 4,
  83 + padding: "1px 0",
  84 + }}
  85 + onMouseEnter={(e) => { if (clickable) e.currentTarget.style.color = "#fff"; }}
  86 + onMouseLeave={(e) => { if (clickable) e.currentTarget.style.color = it.featured ? "var(--accent)" : "var(--text-on-dark-muted)"; }}
  87 + >
  88 + {it.label}
  89 + {it.featured ? <span style={{ color: "var(--warning)", fontSize: 10 }}>★</span> : null}
  90 + </div>
  91 + );
  92 + })}
  93 + </div>
  94 + </div>
  95 + ))}
  96 + </div>
  97 + ) : (
  98 + <div style={{ color: "var(--text-on-dark-muted)", fontSize: 13, padding: 40 }}>
  99 + {XLY.MEGA_NAV.find((s) => s.id === activeSection)?.label} · 模块开发中
  100 + </div>
  101 + )}
  102 + </div>
  103 + </div>
  104 + </div>
  105 + );
  106 +};
  107 +
  108 +window.MegaNav = MegaNav;
... ...
prototype/src/primitives.jsx 0 → 100644
  1 +// Form primitives shared across screens. Tight, dense, faithful enterprise look.
  2 +
  3 +const fieldStyles = {
  4 + row: { display: "flex", alignItems: "center", gap: 0, minWidth: 0 },
  5 + label: {
  6 + width: 88, flex: "none", textAlign: "right", paddingRight: 8,
  7 + color: "var(--text)", fontSize: 12, lineHeight: "26px",
  8 + whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
  9 + },
  10 + labelReq: { color: "var(--required)" },
  11 + control: {
  12 + flex: 1, minWidth: 0, height: "var(--input-h)",
  13 + border: "1px solid var(--border-input)",
  14 + background: "var(--bg-input)",
  15 + padding: "0 6px", fontSize: 12, color: "var(--text)",
  16 + borderRadius: 0,
  17 + },
  18 + controlDisabled: { background: "var(--bg-disabled)", color: "var(--text-muted)" },
  19 + controlReq: { background: "#fff8e1" }, // matches reference: required cells get a yellow tint
  20 +};
  21 +
  22 +const Field = ({ label, required, children, labelWidth, span = 1, valign = "center" }) => (
  23 + <div style={{ display: "flex", alignItems: valign === "top" ? "flex-start" : "center", gridColumn: `span ${span}`, minWidth: 0 }}>
  24 + <div
  25 + style={{
  26 + ...fieldStyles.label,
  27 + width: labelWidth || fieldStyles.label.width,
  28 + ...(required ? { color: "var(--required)" } : {}),
  29 + }}
  30 + title={label}
  31 + >
  32 + {required ? <span style={{ marginRight: 2 }}>*</span> : null}
  33 + {label}:
  34 + </div>
  35 + {children}
  36 + </div>
  37 +);
  38 +
  39 +const Input = ({ value, onChange, disabled, required, placeholder, mono, style }) => (
  40 + <input
  41 + type="text"
  42 + value={value ?? ""}
  43 + onChange={onChange ? (e) => onChange(e.target.value) : undefined}
  44 + disabled={disabled}
  45 + placeholder={placeholder}
  46 + style={{
  47 + ...fieldStyles.control,
  48 + ...(disabled ? fieldStyles.controlDisabled : {}),
  49 + ...(required && !disabled ? fieldStyles.controlReq : {}),
  50 + ...(mono ? { fontFamily: '"JetBrains Mono", Menlo, Consolas, monospace', fontSize: 11 } : {}),
  51 + ...style,
  52 + }}
  53 + />
  54 +);
  55 +
  56 +const Select = ({ value, onChange, options, disabled, required, style }) => (
  57 + <div style={{ position: "relative", flex: 1, minWidth: 0 }}>
  58 + <select
  59 + value={value ?? ""}
  60 + onChange={onChange ? (e) => onChange(e.target.value) : undefined}
  61 + disabled={disabled}
  62 + style={{
  63 + ...fieldStyles.control,
  64 + ...(disabled ? fieldStyles.controlDisabled : {}),
  65 + ...(required && !disabled ? fieldStyles.controlReq : {}),
  66 + appearance: "none", paddingRight: 22, width: "100%",
  67 + ...style,
  68 + }}
  69 + >
  70 + <option value="" />
  71 + {options.map((o) => {
  72 + const v = typeof o === "string" ? o : o.value;
  73 + const l = typeof o === "string" ? o : o.label;
  74 + return <option key={v} value={v}>{l}</option>;
  75 + })}
  76 + </select>
  77 + <div style={{ position: "absolute", right: 6, top: "50%", transform: "translateY(-50%)", color: "var(--text-faint)", pointerEvents: "none" }}>
  78 + <Ic.chevronDown size={10} />
  79 + </div>
  80 + </div>
  81 +);
  82 +
  83 +const Checkbox = ({ checked, onChange, disabled, label, size = 13 }) => (
  84 + <label style={{ display: "inline-flex", alignItems: "center", gap: 4, cursor: disabled ? "not-allowed" : "pointer", userSelect: "none", color: "var(--text)", fontSize: 12 }}>
  85 + <span
  86 + onClick={disabled ? undefined : () => onChange && onChange(!checked)}
  87 + style={{
  88 + width: size, height: size, border: "1px solid var(--border-strong)",
  89 + background: checked ? "var(--accent)" : "#fff",
  90 + borderColor: checked ? "var(--accent)" : "var(--border-strong)",
  91 + display: "inline-flex", alignItems: "center", justifyContent: "center",
  92 + cursor: disabled ? "not-allowed" : "pointer", flex: "none",
  93 + opacity: disabled ? 0.6 : 1,
  94 + }}
  95 + >
  96 + {checked ? (
  97 + <svg width={size - 4} height={size - 4} viewBox="0 0 10 10" fill="none">
  98 + <path d="M2 5l2 2 4-4" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
  99 + </svg>
  100 + ) : null}
  101 + </span>
  102 + {label != null ? <span>{label}</span> : null}
  103 + </label>
  104 +);
  105 +
  106 +// Top-bar style (light blue) toolbar button — used in module config screen
  107 +const ToolbarBtnLight = ({ children, icon, onClick, disabled, primary, danger, success }) => {
  108 + const [hover, setHover] = React.useState(false);
  109 + let bg = "#fff", border = "var(--border-input)", color = "var(--text)";
  110 + if (primary) { bg = hover ? "#3a9ae8" : "#5cabe8"; border = "#3a8ad6"; color = "#fff"; }
  111 + else if (danger) { bg = hover ? "#e85a5a" : "#f07070"; border = "#d04141"; color = "#fff"; }
  112 + else if (success) { bg = hover ? "#38b777" : "#4cc488"; border = "#27a567"; color = "#fff"; }
  113 + else if (hover && !disabled) { bg = "var(--accent-soft)"; border = "var(--accent)"; }
  114 + return (
  115 + <button
  116 + onClick={disabled ? undefined : onClick}
  117 + onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
  118 + disabled={disabled}
  119 + style={{
  120 + height: "var(--tb-h)", padding: "0 10px",
  121 + display: "inline-flex", alignItems: "center", gap: 4,
  122 + background: disabled ? "var(--bg-disabled)" : bg,
  123 + border: `1px solid ${disabled ? "var(--border)" : border}`,
  124 + color: disabled ? "var(--text-faint)" : color,
  125 + cursor: disabled ? "not-allowed" : "pointer",
  126 + fontSize: 12, fontWeight: primary || danger || success ? 500 : 400,
  127 + borderRadius: 2, whiteSpace: "nowrap",
  128 + }}
  129 + >
  130 + {icon ? <span style={{ display: "inline-flex" }}>{icon}</span> : null}
  131 + {children}
  132 + </button>
  133 + );
  134 +};
  135 +
  136 +// Dark-band toolbar button (used in user detail, matches the dark sub-toolbar in references)
  137 +const ToolbarBtnDark = ({ children, icon, onClick, disabled, danger }) => {
  138 + const [hover, setHover] = React.useState(false);
  139 + return (
  140 + <button
  141 + onClick={disabled ? undefined : onClick}
  142 + onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
  143 + disabled={disabled}
  144 + style={{
  145 + height: 26, padding: "0 10px",
  146 + display: "inline-flex", alignItems: "center", gap: 4,
  147 + background: hover && !disabled ? "rgba(255,255,255,0.08)" : "transparent",
  148 + border: "1px solid transparent",
  149 + color: disabled ? "var(--text-on-dark-muted)" : danger && hover ? "#ff8b8b" : "var(--text-on-dark)",
  150 + cursor: disabled ? "not-allowed" : "pointer",
  151 + fontSize: 12, borderRadius: 2, whiteSpace: "nowrap",
  152 + }}
  153 + >
  154 + {icon ? <span style={{ display: "inline-flex" }}>{icon}</span> : null}
  155 + {children}
  156 + </button>
  157 + );
  158 +};
  159 +
  160 +const Divider = ({ vertical }) =>
  161 + vertical
  162 + ? <span style={{ width: 1, height: 18, background: "rgba(255,255,255,0.12)", margin: "0 4px" }} />
  163 + : <div style={{ height: 1, background: "var(--border)", margin: "4px 0" }} />;
  164 +
  165 +Object.assign(window, {
  166 + Field, Input, Select, Checkbox, ToolbarBtnLight, ToolbarBtnDark, Divider, fieldStyles,
  167 +});
... ...
prototype/src/screen-home.jsx 0 → 100644
  1 +// Simple home — gives the workspace something useful when first opened.
  2 +
  3 +const Home = ({ user, onOpenScreen }) => {
  4 + const cards = [
  5 + { id: "userlist", title: "用户列表", desc: "管理系统账号、角色与权限分组", icon: <Ic.user size={18} /> },
  6 + { id: "module", title: "系统模块配置", desc: "配置 KPI 流程作业单与业务模块", icon: <Ic.settings size={18} /> },
  7 + ];
  8 + const stats = [
  9 + { label: "今日待办", value: 12, tone: "var(--accent)" },
  10 + { label: "进行中报价", value: 47, tone: "var(--warning)" },
  11 + { label: "本月订单", value: 286, tone: "var(--success)" },
  12 + { label: "待审核", value: 8, tone: "var(--danger)" },
  13 + ];
  14 + return (
  15 + <div style={{ padding: 16, height: "100%", overflow: "auto", background: "var(--bg-app)" }}>
  16 + <div style={{ background: "#fff", border: "1px solid var(--border)", padding: 16, marginBottom: 12 }}>
  17 + <div style={{ fontSize: 16, fontWeight: 500, marginBottom: 4 }}>欢迎回来,{user || "admin"}</div>
  18 + <div style={{ fontSize: 12, color: "var(--text-muted)" }}>
  19 + XLY-ERP 印刷制造管理平台 · {new Date().toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric", weekday: "long" })}
  20 + </div>
  21 + </div>
  22 + <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 8, marginBottom: 12 }}>
  23 + {stats.map((s) => (
  24 + <div key={s.label} style={{ background: "#fff", border: "1px solid var(--border)", padding: "12px 14px" }}>
  25 + <div style={{ fontSize: 11, color: "var(--text-muted)", marginBottom: 4 }}>{s.label}</div>
  26 + <div style={{ fontSize: 22, fontWeight: 600, color: s.tone, fontFamily: '"JetBrains Mono", Menlo, monospace' }}>
  27 + {s.value}
  28 + </div>
  29 + </div>
  30 + ))}
  31 + </div>
  32 + <div style={{ background: "#fff", border: "1px solid var(--border)" }}>
  33 + <div style={{ padding: "8px 12px", borderBottom: "1px solid var(--border)", fontSize: 12, fontWeight: 500, background: "var(--bg-row-zebra)" }}>
  34 + 快捷入口
  35 + </div>
  36 + <div style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 1, background: "var(--border)" }}>
  37 + {cards.map((c) => (
  38 + <div
  39 + key={c.id}
  40 + onClick={() => onOpenScreen(c.id, c.title)}
  41 + style={{
  42 + background: "#fff", padding: 14,
  43 + cursor: "pointer", display: "flex", alignItems: "center", gap: 10,
  44 + }}
  45 + onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-row-hover)"}
  46 + onMouseLeave={(e) => e.currentTarget.style.background = "#fff"}
  47 + >
  48 + <span style={{ width: 32, height: 32, background: "var(--accent-soft)", color: "var(--accent-strong)", display: "inline-flex", alignItems: "center", justifyContent: "center" }}>{c.icon}</span>
  49 + <div>
  50 + <div style={{ fontSize: 13, fontWeight: 500 }}>{c.title}</div>
  51 + <div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 2 }}>{c.desc}</div>
  52 + </div>
  53 + </div>
  54 + ))}
  55 + </div>
  56 + </div>
  57 + </div>
  58 + );
  59 +};
  60 +
  61 +window.Home = Home;
... ...
prototype/src/screen-module.jsx 0 → 100644
  1 +// Module configuration screen — KPI process tree on left, dense form on right.
  2 +// Form fields mirror the structural pattern in 模块 1.png but with original copy.
  3 +
  4 +const moduleSections = [
  5 + {
  6 + title: "估价管理流程", processes: [
  7 + { id: "01", code: "01/04", label: "【新增】新报价单", active: true },
  8 + { id: "02", code: "02/04", label: "审核报价单->客户确认->二次确认" },
  9 + { id: "03", code: "03/04", label: "客户确认->二次确认" },
  10 + { id: "04", code: "04/04", label: "报价单->销售订单" },
  11 + { id: "05", code: "04/04", label: "报价单->转拼版单" },
  12 + { id: "06", code: "04/07", label: "主管审核报价单" },
  13 + { id: "07", code: "05/07", label: "业务确认报价单" },
  14 + ],
  15 + },
  16 + { title: "订单生产流程", collapsed: true },
  17 + { title: "自动拼版流程", collapsed: true },
  18 + { title: "销售送货流程", collapsed: true },
  19 + { title: "物料采购流程", collapsed: true },
  20 + { title: "物料领用流程", collapsed: true },
  21 + { title: "发外加工流程", collapsed: true },
  22 + { title: "质量管理流程", collapsed: true },
  23 + { title: "财务收付款流程", collapsed: true },
  24 +];
  25 +
  26 +const ModuleScreen = () => {
  27 + const [mode, setMode] = React.useState("view"); // 'view' | 'edit'
  28 + const [activeProcess] = React.useState("01");
  29 +
  30 + const initial = {
  31 + displayCategory: "印刷业务",
  32 + deptEn: "Pricing Personnel",
  33 + deptCn: "报价员",
  34 + permissionVisible: true,
  35 + seq: "1",
  36 + saveProc: "Sp_Check_sQtt",
  37 + storageUrl: "/indexPage/quotationPackTl...",
  38 + showOpt: false,
  39 + nodes: "-1752556899000998290E",
  40 + deptId: "",
  41 + flowId: "20250303094458291296...",
  42 + uniqueId: "10125124011501607650...",
  43 + flowEnName: "",
  44 + flowReqName: "",
  45 + titleCn: "01/04【新增】新报价单",
  46 + mobileLogo: "",
  47 + nameEn: "Quotation Document",
  48 + nameNotice: "常规产品报价单据",
  49 + showCategory: "待处理",
  50 + deptManager: "报价人员",
  51 + storeProc: "Sp_Calc_sQtt",
  52 + quickShow: "",
  53 + addId: "",
  54 + modType: "quotation/quotation/quotati",
  55 + calcProc: "Sp_Quotation_CalcDataPac",
  56 + saveAfterProc: "Sp_beforeSave_sQtt",
  57 + parent: "1752562484000281572E",
  58 + };
  59 +
  60 + const [form, setForm] = React.useState(initial);
  61 + const set = (k, v) => setForm((s) => ({ ...s, [k]: v }));
  62 +
  63 + const startEdit = () => setMode("edit");
  64 + const save = () => { setMode("view"); };
  65 + const cancel = () => { setForm(initial); setMode("view"); };
  66 +
  67 + const disabled = mode === "view";
  68 +
  69 + return (
  70 + <div style={{ display: "flex", flexDirection: "column", height: "100%", background: "var(--bg-app)" }}>
  71 + {/* Light blue toolbar */}
  72 + <div style={{
  73 + display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", rowGap: 6,
  74 + padding: "6px 8px", background: "#eaf1f7",
  75 + borderBottom: "1px solid var(--border)", flex: "none",
  76 + }}>
  77 + <ToolbarBtnLight icon={<Ic.plus size={12} />} primary disabled={mode === "edit"}>添加节点</ToolbarBtnLight>
  78 + <ToolbarBtnLight icon={<Ic.plus size={12} />} primary disabled={mode === "edit"}>添加子节点</ToolbarBtnLight>
  79 + <ToolbarBtnLight icon={<Ic.edit size={12} />} primary disabled={mode === "edit"} onClick={startEdit}>修 改</ToolbarBtnLight>
  80 + <ToolbarBtnLight icon={<Ic.clipboard size={12} />} primary disabled={mode === "edit"}>复制节点</ToolbarBtnLight>
  81 + <ToolbarBtnLight icon={<Ic.save size={12} />} success disabled={mode !== "edit"} onClick={save}>保 存</ToolbarBtnLight>
  82 + <ToolbarBtnLight icon={<Ic.cancel size={12} />} disabled={mode !== "edit"} onClick={cancel}>取 消</ToolbarBtnLight>
  83 + <ToolbarBtnLight icon={<Ic.trash size={12} />} danger disabled={mode === "edit"}>删除节点</ToolbarBtnLight>
  84 + <div style={{ flex: 1 }} />
  85 + <ToolbarBtnLight icon={<Ic.settings size={12} />} primary>生成监听器单</ToolbarBtnLight>
  86 + </div>
  87 +
  88 + {/* Body: form only (tree lives in main sidebar) */}
  89 + <div style={{ flex: 1, display: "flex", overflow: "hidden", minHeight: 0 }}>
  90 + {/* Form panel */}
  91 + <div style={{ flex: 1, overflow: "auto", padding: 12, minWidth: 0 }}>
  92 + <div style={{
  93 + display: "grid",
  94 + gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
  95 + rowGap: 8, columnGap: 14,
  96 + background: "#fff", padding: "14px 14px 16px",
  97 + border: "1px solid var(--border)",
  98 + }}>
  99 + <Field label="显示类别(默认)" required>
  100 + <Select value={form.displayCategory} onChange={(v) => set("displayCategory", v)} disabled={disabled} required options={["印刷业务", "包装业务", "物流业务", "数码印刷"]} />
  101 + </Field>
  102 + <Field label="所有节点ID">
  103 + <Input value={form.nodes} disabled mono />
  104 + </Field>
  105 + <Field label="存储过程(算)">
  106 + <Input value={form.storeProc} onChange={(v) => set("storeProc", v)} disabled={disabled} mono />
  107 + </Field>
  108 + <Field label="模块类型">
  109 + <Input value={form.modType} onChange={(v) => set("modType", v)} disabled={disabled} mono />
  110 + </Field>
  111 +
  112 + <Field label="管理部门英文">
  113 + <Input value={form.deptEn} onChange={(v) => set("deptEn", v)} disabled={disabled} />
  114 + </Field>
  115 + <Field label="管理部门ID">
  116 + <Input value={form.deptId} onChange={(v) => set("deptId", v)} disabled={disabled} mono />
  117 + </Field>
  118 + <Field label="快速下单显示">
  119 + <Select value={form.quickShow} onChange={(v) => set("quickShow", v)} disabled={disabled} options={["显示", "隐藏"]} />
  120 + </Field>
  121 + <Field label="加载ID">
  122 + <Input value={form.addId} onChange={(v) => set("addId", v)} disabled={disabled} mono />
  123 + </Field>
  124 +
  125 + <Field label="权限是否显示" required>
  126 + <div style={{ flex: 1, height: "var(--input-h)", display: "flex", alignItems: "center", paddingLeft: 4, background: !disabled ? "#fff8e1" : "transparent", border: "1px solid var(--border-input)" }}>
  127 + <Checkbox checked={form.permissionVisible} onChange={(v) => set("permissionVisible", v)} disabled={disabled} />
  128 + </div>
  129 + </Field>
  130 + <Field label="流程ID">
  131 + <Input value={form.flowId} disabled mono />
  132 + </Field>
  133 + <Field label="流程名称">
  134 + <Select value={form.flowEnName} onChange={(v) => set("flowEnName", v)} disabled={disabled} options={["主流程", "子流程"]} />
  135 + </Field>
  136 + <Field label="流程英文名称">
  137 + <Input value={form.flowReqName} onChange={(v) => set("flowReqName", v)} disabled={disabled} />
  138 + </Field>
  139 +
  140 + <Field label="序号" required>
  141 + <Input value={form.seq} onChange={(v) => set("seq", v)} disabled={disabled} required />
  142 + </Field>
  143 + <Field label="唯一ID">
  144 + <Input value={form.uniqueId} disabled mono />
  145 + </Field>
  146 + <Field label="计算过程名">
  147 + <Input value={form.calcProc} onChange={(v) => set("calcProc", v)} disabled={disabled} mono />
  148 + </Field>
  149 + <Field label="保存前校验过...">
  150 + <Input value={form.saveAfterProc} onChange={(v) => set("saveAfterProc", v)} disabled={disabled} mono />
  151 + </Field>
  152 +
  153 + <Field label="保存过程名">
  154 + <Input value={form.saveProc} onChange={(v) => set("saveProc", v)} disabled={disabled} mono />
  155 + </Field>
  156 + <Field label="界面名中文">
  157 + <Input value={form.titleCn} onChange={(v) => set("titleCn", v)} disabled={disabled} />
  158 + </Field>
  159 + <Field label="删除过程名">
  160 + <Input value={form.deleteProc || ""} onChange={(v) => set("deleteProc", v)} disabled={disabled} mono />
  161 + </Field>
  162 + <Field label="父级">
  163 + <Input value={form.parent} disabled mono />
  164 + </Field>
  165 +
  166 + <Field label="存储页面URL路...">
  167 + <Input value={form.storageUrl} onChange={(v) => set("storageUrl", v)} disabled={disabled} mono />
  168 + </Field>
  169 + <Field label="手机端LOGO">
  170 + <Input value={form.mobileLogo} onChange={(v) => set("mobileLogo", v)} disabled={disabled} />
  171 + </Field>
  172 + <Field label="界面名英文">
  173 + <Input value={form.nameEn} onChange={(v) => set("nameEn", v)} disabled={disabled} />
  174 + </Field>
  175 + <Field label="界面名备注">
  176 + <Input value={form.nameNotice} onChange={(v) => set("nameNotice", v)} disabled={disabled} />
  177 + </Field>
  178 +
  179 + <Field label="是否显示">
  180 + <div style={{ flex: 1, height: "var(--input-h)", display: "flex", alignItems: "center", paddingLeft: 4, border: "1px solid var(--border-input)", background: disabled ? "var(--bg-disabled)" : "var(--bg-input)" }}>
  181 + <Checkbox checked={form.showOpt} onChange={(v) => set("showOpt", v)} disabled={disabled} />
  182 + </div>
  183 + </Field>
  184 + <Field label="是否消息通知">
  185 + <Select value={form.notice || ""} onChange={(v) => set("notice", v)} disabled={disabled} options={["开启", "关闭"]} />
  186 + </Field>
  187 + <Field label="显示类型">
  188 + <Select value={form.showCategory} onChange={(v) => set("showCategory", v)} disabled={disabled} options={["待处理", "已处理", "全部"]} />
  189 + </Field>
  190 + <Field label="管理部门">
  191 + <Select value={form.deptManager} onChange={(v) => set("deptManager", v)} disabled={disabled} options={["报价人员", "业务员", "主管", "财务"]} />
  192 + </Field>
  193 +
  194 + <div style={{ gridColumn: "1 / -1", display: "flex", gap: 8, marginTop: 4, paddingTop: 8, borderTop: "1px dashed var(--border)" }}>
  195 + <ToolbarBtnLight icon={<Ic.import size={12} />} disabled={disabled}>上传</ToolbarBtnLight>
  196 + <span style={{ fontSize: 11, color: "var(--text-faint)", lineHeight: "26px" }}>
  197 + 关联文件(PDF / 图片,≤ 5 MB)
  198 + </span>
  199 + </div>
  200 + </div>
  201 +
  202 + <div style={{ marginTop: 8, padding: "6px 12px", background: "#fff", border: "1px solid var(--border)", fontSize: 11, color: "var(--text-muted)", display: "flex", justifyContent: "space-between" }}>
  203 + <span>当前节点:{activeProcess} 【新增】新报价单</span>
  204 + <span>状态:<span style={{ color: mode === "edit" ? "var(--warning)" : "var(--success)", fontWeight: 500 }}>{mode === "edit" ? "编辑中" : "只读"}</span></span>
  205 + </div>
  206 + </div>
  207 + </div>
  208 + </div>
  209 + );
  210 +};
  211 +
  212 +window.ModuleScreen = ModuleScreen;
... ...
prototype/src/screen-userdetail.jsx 0 → 100644
  1 +// User detail — header form + permission tabs (with checkbox grid).
  2 +// Modes: view (read-only) | edit | new
  3 +
  4 +const PERM_TABS = [
  5 + { id: "groups", label: "权限组" },
  6 + { id: "customer", label: "客户查看权限" },
  7 + { id: "supplier", label: "供应商查看权限" },
  8 + { id: "staff", label: "人员查看权限" },
  9 + { id: "process", label: "工序查看权限" },
  10 + { id: "driver", label: "司机查看权限" },
  11 +];
  12 +
  13 +const UserDetail = ({ user: initial, mode: initialMode, onSave, onDelete, onClose }) => {
  14 + const [mode, setMode] = React.useState(initialMode || "view");
  15 + const [user, setUser] = React.useState(initial);
  16 + const [tab, setTab] = React.useState("groups");
  17 + const [snapshot, setSnapshot] = React.useState(initial);
  18 +
  19 + React.useEffect(() => {
  20 + setUser(initial);
  21 + setSnapshot(initial);
  22 + setMode(initialMode || "view");
  23 + }, [initial, initialMode]);
  24 +
  25 + const set = (k, v) => setUser((s) => ({ ...s, [k]: v }));
  26 + const setPerm = (g, v) => setUser((s) => ({ ...s, permissions: { ...s.permissions, [g]: v } }));
  27 + const setTabPerm = (k, v) => setUser((s) => ({ ...s, tabPerms: { ...s.tabPerms, [k]: v } }));
  28 +
  29 + const start = (m) => { setSnapshot(user); setMode(m); };
  30 + const save = () => { onSave && onSave(user); setSnapshot(user); setMode("view"); };
  31 + const cancel = () => { setUser(snapshot); setMode("view"); };
  32 +
  33 + const disabled = mode === "view";
  34 + const isNew = mode === "new";
  35 +
  36 + const checked = Object.values(user.permissions || {}).filter(Boolean).length;
  37 +
  38 + return (
  39 + <div style={{ display: "flex", flexDirection: "column", height: "100%", background: "var(--bg-app)" }}>
  40 + {/* Dark sub-toolbar */}
  41 + <div style={{
  42 + display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap", rowGap: 4,
  43 + padding: "4px 8px", background: "var(--bg-toolbar-dark)",
  44 + flex: "none", borderBottom: "1px solid #1a1f2a",
  45 + }}>
  46 + <ToolbarBtnDark icon={<Ic.plus size={12} />} onClick={() => start("new")} disabled={mode !== "view"}>新增</ToolbarBtnDark>
  47 + <ToolbarBtnDark icon={<Ic.edit size={12} />} onClick={() => start("edit")} disabled={mode !== "view"}>修改</ToolbarBtnDark>
  48 + <ToolbarBtnDark icon={<Ic.trash size={12} />} danger disabled={mode !== "view"} onClick={onDelete}>删除</ToolbarBtnDark>
  49 + <ToolbarBtnDark icon={<Ic.save size={12} />} onClick={save} disabled={mode === "view"}>保存</ToolbarBtnDark>
  50 + <ToolbarBtnDark icon={<Ic.cancel size={12} />} onClick={cancel} disabled={mode === "view"}>取消</ToolbarBtnDark>
  51 + <span style={{ width: 1, height: 16, background: "rgba(255,255,255,0.12)", margin: "0 4px" }} />
  52 + <ToolbarBtnDark icon={<Ic.function size={12} />}>功能</ToolbarBtnDark>
  53 + <ToolbarBtnDark icon={<Ic.clipboard size={12} />}>作废</ToolbarBtnDark>
  54 + <ToolbarBtnDark icon={<Ic.key size={12} />}>重置密码</ToolbarBtnDark>
  55 + <ToolbarBtnDark icon={<Ic.undo size={12} />}>取消作废</ToolbarBtnDark>
  56 + <div style={{ flex: 1 }} />
  57 + <span style={{ fontSize: 11, color: "var(--text-on-dark-muted)", marginRight: 8 }}>
  58 + 已选权限:<span style={{ color: "#fff", fontWeight: 500 }}>{checked}</span> / {XLY.PERMISSION_GROUPS.length - 1}
  59 + </span>
  60 + <span style={{
  61 + padding: "2px 8px", fontSize: 11,
  62 + background: mode === "view" ? "rgba(39,165,103,0.18)" : mode === "new" ? "rgba(58,142,224,0.22)" : "rgba(217,142,31,0.22)",
  63 + color: mode === "view" ? "#7be0a8" : mode === "new" ? "#9cc8f0" : "#ffc77a",
  64 + border: `1px solid ${mode === "view" ? "rgba(39,165,103,0.4)" : mode === "new" ? "rgba(58,142,224,0.4)" : "rgba(217,142,31,0.4)"}`,
  65 + }}>
  66 + {mode === "view" ? "只读" : mode === "new" ? "新增中" : "编辑中"}
  67 + </span>
  68 + <button style={{ marginLeft: 8, width: 22, height: 22, border: "none", background: "transparent", color: "var(--text-on-dark-muted)", cursor: "pointer" }}>
  69 + <Ic.settings size={13} />
  70 + </button>
  71 + </div>
  72 +
  73 + {/* Header form (3 cols × 3 rows) */}
  74 + <div style={{
  75 + background: "#fff", padding: "10px 12px",
  76 + borderBottom: "1px solid var(--border)", flex: "none",
  77 + display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))",
  78 + rowGap: 6, columnGap: 16,
  79 + }}>
  80 + <Field label="创建时间">
  81 + <Input value={user.createdAt} disabled mono />
  82 + </Field>
  83 + <Field label="制单人" required>
  84 + <Input value={user.createdBy} onChange={(v) => set("createdBy", v)} disabled={disabled} required />
  85 + </Field>
  86 + <Field label="员工名" required>
  87 + <Input value={user.employee} onChange={(v) => set("employee", v)} disabled={disabled} required />
  88 + </Field>
  89 +
  90 + <Field label="用户名" required>
  91 + <Input value={user.account} onChange={(v) => set("account", v)} disabled={disabled} required />
  92 + </Field>
  93 + <Field label="类型" required>
  94 + <Select value={user.type} onChange={(v) => set("type", v)} disabled={disabled} required options={XLY.USER_TYPES} />
  95 + </Field>
  96 + <Field label="用户号" required>
  97 + <Input value={user.empNo} onChange={(v) => set("empNo", v)} disabled={disabled} required mono />
  98 + </Field>
  99 +
  100 + <Field label="部门">
  101 + <Select value={user.department} onChange={(v) => set("department", v)} disabled={disabled} options={XLY.DEPARTMENTS} />
  102 + </Field>
  103 + <Field label="语言" required>
  104 + <Select value={user.language} onChange={(v) => set("language", v)} disabled={disabled} required options={XLY.LANGUAGES} />
  105 + </Field>
  106 + <Field label="单据修改权限">
  107 + <Select value={user.editPerm || ""} onChange={(v) => set("editPerm", v)} disabled={disabled} options={["全部允许", "仅本人单据", "仅查看"]} />
  108 + </Field>
  109 + </div>
  110 +
  111 + {/* Permission tabs */}
  112 + <div style={{ display: "flex", alignItems: "stretch", background: "#fff", borderBottom: "1px solid var(--border)", flex: "none" }}>
  113 + {PERM_TABS.map((t) => {
  114 + const active = t.id === tab;
  115 + return (
  116 + <div
  117 + key={t.id}
  118 + onClick={() => setTab(t.id)}
  119 + style={{
  120 + padding: "6px 14px", fontSize: 12, cursor: "pointer",
  121 + color: active ? "var(--accent-strong)" : "var(--text)",
  122 + background: active ? "var(--accent-soft)" : "transparent",
  123 + borderTop: active ? "2px solid var(--accent)" : "2px solid transparent",
  124 + borderRight: "1px solid var(--border)",
  125 + fontWeight: active ? 500 : 400,
  126 + }}
  127 + onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = "var(--bg-row-hover)"; }}
  128 + onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = "transparent"; }}
  129 + >
  130 + {t.label}
  131 + </div>
  132 + );
  133 + })}
  134 + </div>
  135 +
  136 + {/* Tab content */}
  137 + <div style={{ flex: 1, overflow: "auto", background: "#fff" }}>
  138 + {tab === "groups" ? (
  139 + <PermissionGrid user={user} setPerm={setPerm} disabled={disabled} />
  140 + ) : (
  141 + <ScopeTab tabKey={tab} user={user} setTabPerm={setTabPerm} disabled={disabled} />
  142 + )}
  143 + </div>
  144 +
  145 + {/* Floating help */}
  146 + <div style={{ position: "absolute", right: 0, top: "50%", transform: "translateY(-50%)" }}>
  147 + <button style={{
  148 + width: 18, height: 40, background: "var(--accent)", color: "#fff", border: "none",
  149 + borderTopLeftRadius: 4, borderBottomLeftRadius: 4, cursor: "pointer", fontSize: 11,
  150 + writingMode: "vertical-rl",
  151 + }}>
  152 + 帮助
  153 + </button>
  154 + </div>
  155 + </div>
  156 + );
  157 +};
  158 +
  159 +const PermissionGrid = ({ user, setPerm, disabled }) => {
  160 + const [filter, setFilter] = React.useState("");
  161 + const [hovered, setHovered] = React.useState(null);
  162 + return (
  163 + <div>
  164 + <table style={{ borderCollapse: "collapse", width: "100%", fontSize: 12, tableLayout: "fixed" }}>
  165 + <colgroup>
  166 + <col style={{ width: 40 }} />
  167 + <col />
  168 + </colgroup>
  169 + <thead>
  170 + <tr style={{ background: "var(--bg-row-zebra)", borderBottom: "1px solid var(--border)" }}>
  171 + <th style={{ padding: "5px 8px", borderRight: "1px solid var(--border)", textAlign: "center" }}>
  172 + <Checkbox
  173 + checked={Object.values(user.permissions).every(Boolean)}
  174 + onChange={(v) => {
  175 + XLY.PERMISSION_GROUPS.forEach((g) => { if (g !== "权限分类") setPerm(g, v); });
  176 + }}
  177 + disabled={disabled}
  178 + />
  179 + </th>
  180 + <th style={{ padding: "5px 8px", textAlign: "left", fontWeight: 500 }}>
  181 + <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
  182 + 权限分类
  183 + <input
  184 + value={filter} onChange={(e) => setFilter(e.target.value)}
  185 + placeholder="过滤..."
  186 + style={{ height: 20, padding: "0 6px", border: "1px solid var(--border-input)", background: "#fff", fontSize: 11, width: 120, fontFamily: "inherit" }}
  187 + />
  188 + </span>
  189 + </th>
  190 + </tr>
  191 + </thead>
  192 + <tbody>
  193 + {XLY.PERMISSION_GROUPS.filter((g) => g !== "权限分类" && (!filter || g.includes(filter))).map((g, i) => {
  194 + const isHover = hovered === g;
  195 + return (
  196 + <tr
  197 + key={g}
  198 + onMouseEnter={() => setHovered(g)} onMouseLeave={() => setHovered(null)}
  199 + onClick={disabled ? undefined : () => setPerm(g, !user.permissions[g])}
  200 + style={{
  201 + background: isHover && !disabled ? "var(--bg-row-hover)"
  202 + : user.permissions[g] ? "var(--accent-soft)"
  203 + : i % 2 === 0 ? "#fff" : "var(--bg-row-zebra)",
  204 + cursor: disabled ? "default" : "pointer",
  205 + height: 24,
  206 + }}
  207 + >
  208 + <td style={{ padding: "3px 0", textAlign: "center", borderRight: "1px solid var(--border)", borderBottom: "1px solid #f0f2f5" }}>
  209 + <Checkbox
  210 + checked={!!user.permissions[g]}
  211 + onChange={(v) => setPerm(g, v)}
  212 + disabled={disabled}
  213 + />
  214 + </td>
  215 + <td style={{ padding: "3px 8px", borderBottom: "1px solid #f0f2f5", color: user.permissions[g] ? "var(--accent-strong)" : "var(--text)", fontWeight: user.permissions[g] ? 500 : 400 }}>
  216 + {g}
  217 + </td>
  218 + </tr>
  219 + );
  220 + })}
  221 + </tbody>
  222 + </table>
  223 + </div>
  224 + );
  225 +};
  226 +
  227 +const ScopeTab = ({ tabKey, user, setTabPerm, disabled }) => {
  228 + const map = {
  229 + customer: { label: "客户", items: ["上海印行包装", "锐尚文创", "京华彩印", "广印纸品", "万象图文", "联合包装", "鼎盛印刷", "九洲胶印"] },
  230 + supplier: { label: "供应商", items: ["华东油墨", "正信纸业", "宝洁化工", "日新油墨", "三鼎纸业", "鸿丰胶辊", "永利印材"] },
  231 + staff: { label: "人员", items: ["管广飞", "李斌", "孟威", "王宽明", "潘强", "杨柳"] },
  232 + process: { label: "工序", items: ["印前", "印刷", "覆膜", "模切", "装订", "胶装", "丝网", "烫金", "包装"] },
  233 + driver: { label: "司机", items: ["陈师傅", "李师傅", "王师傅", "钱师傅", "赵师傅"] },
  234 + };
  235 + const cfg = map[tabKey];
  236 + const [filter, setFilter] = React.useState("");
  237 + const enabled = !!user.tabPerms[tabKey];
  238 +
  239 + return (
  240 + <div style={{ padding: 12 }}>
  241 + <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 10, paddingBottom: 8, borderBottom: "1px solid var(--border)" }}>
  242 + <Checkbox
  243 + checked={enabled}
  244 + onChange={(v) => setTabPerm(tabKey, v)}
  245 + disabled={disabled}
  246 + label={`启用${cfg.label}查看权限`}
  247 + />
  248 + <input
  249 + value={filter} onChange={(e) => setFilter(e.target.value)}
  250 + placeholder={`筛选${cfg.label}...`}
  251 + style={{ height: 24, padding: "0 8px", border: "1px solid var(--border-input)", background: "var(--bg-input)", fontSize: 12, width: 200, fontFamily: "inherit" }}
  252 + />
  253 + <span style={{ flex: 1 }} />
  254 + <span style={{ fontSize: 11, color: "var(--text-muted)" }}>
  255 + 共 {cfg.items.length} 个{cfg.label}
  256 + </span>
  257 + </div>
  258 + <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 6 }}>
  259 + {cfg.items.filter((x) => !filter || x.includes(filter)).map((it) => (
  260 + <label key={it} style={{
  261 + display: "flex", alignItems: "center", gap: 6,
  262 + padding: "5px 8px", border: "1px solid var(--border)",
  263 + background: enabled ? "#fff" : "var(--bg-disabled)",
  264 + cursor: disabled || !enabled ? "default" : "pointer", fontSize: 12,
  265 + }}>
  266 + <Checkbox disabled={disabled || !enabled} />
  267 + <span style={{ color: enabled ? "var(--text)" : "var(--text-faint)" }}>{it}</span>
  268 + </label>
  269 + ))}
  270 + </div>
  271 + </div>
  272 + );
  273 +};
  274 +
  275 +window.UserDetail = UserDetail;
... ...
prototype/src/screen-userlist.jsx 0 → 100644
  1 +// User list — searchable/filterable table.
  2 +
  3 +const UserList = ({ users, onOpenUser, onCreateUser }) => {
  4 + const [scope, setScope] = React.useState("all");
  5 + const [field, setField] = React.useState("empName");
  6 + const [matchMode, setMatchMode] = React.useState("contains");
  7 + const [query, setQuery] = React.useState("");
  8 + const [selected, setSelected] = React.useState(null);
  9 + const [checkedRows, setCheckedRows] = React.useState({});
  10 + const [sort, setSort] = React.useState({ key: null, dir: "asc" });
  11 +
  12 + const toggleRow = (id) => setCheckedRows((s) => ({ ...s, [id]: !s[id] }));
  13 + const checkedCount = Object.values(checkedRows).filter(Boolean).length;
  14 +
  15 + const filtered = React.useMemo(() => {
  16 + let rows = users;
  17 + if (scope === "active") rows = rows.filter((u) => !u.disabled);
  18 + if (scope === "disabled") rows = rows.filter((u) => u.disabled);
  19 + if (query) {
  20 + const q = query.toLowerCase();
  21 + const cmp = (a, b) => {
  22 + if (matchMode === "exact") return a === b;
  23 + if (matchMode === "starts") return a.startsWith(b);
  24 + return a.includes(b);
  25 + };
  26 + rows = rows.filter((u) => {
  27 + const v = String(u[field === "empName" ? "employee" : field === "account" ? "account" : field === "empNo" ? "empNo" : field === "department" ? "department" : "employee"] || "").toLowerCase();
  28 + return cmp(v, q);
  29 + });
  30 + }
  31 + if (sort.key) {
  32 + const dir = sort.dir === "asc" ? 1 : -1;
  33 + rows = [...rows].sort((a, b) => String(a[sort.key]).localeCompare(String(b[sort.key]), "zh") * dir);
  34 + }
  35 + return rows;
  36 + }, [users, scope, query, field, matchMode, sort]);
  37 +
  38 + const toggleSort = (k) => setSort((s) => ({ key: k, dir: s.key === k && s.dir === "asc" ? "desc" : "asc" }));
  39 +
  40 + return (
  41 + <div style={{ display: "flex", flexDirection: "column", height: "100%", background: "var(--bg-app)" }}>
  42 + {/* Filter toolbar */}
  43 + <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 8px", background: "#eaf1f7", borderBottom: "1px solid var(--border)", flex: "none" }}>
  44 + <ToolbarBtnLight icon={<Ic.refresh size={12} />}>刷新</ToolbarBtnLight>
  45 + <ToolbarBtnLight icon={<Ic.plus size={12} />} primary onClick={onCreateUser}>新增</ToolbarBtnLight>
  46 + <ToolbarBtnLight icon={<Ic.export size={12} />}>导出 Excel</ToolbarBtnLight>
  47 + <span style={{ width: 1, height: 18, background: "var(--border)", margin: "0 2px" }} />
  48 + <select value={scope} onChange={(e) => setScope(e.target.value)} style={selectStyle}>
  49 + <option value="all">全部用户</option>
  50 + <option value="active">启用用户</option>
  51 + <option value="disabled">禁用用户</option>
  52 + </select>
  53 + <select value={field} onChange={(e) => setField(e.target.value)} style={selectStyle}>
  54 + <option value="empName">员工名</option>
  55 + <option value="account">用户名</option>
  56 + <option value="empNo">用户号</option>
  57 + <option value="department">部门</option>
  58 + </select>
  59 + <select value={matchMode} onChange={(e) => setMatchMode(e.target.value)} style={selectStyle}>
  60 + <option value="contains">包含</option>
  61 + <option value="starts">开头是</option>
  62 + <option value="exact">完全匹配</option>
  63 + </select>
  64 + <input
  65 + value={query}
  66 + onChange={(e) => setQuery(e.target.value)}
  67 + placeholder=""
  68 + style={{ height: 26, width: 180, border: "1px solid var(--border-input)", padding: "0 6px", fontSize: 12, background: "var(--bg-input)" }}
  69 + />
  70 + <ToolbarBtnLight icon={<Ic.search size={12} />} primary>查 找</ToolbarBtnLight>
  71 + <ToolbarBtnLight onClick={() => { setQuery(""); setScope("all"); }}>清 空</ToolbarBtnLight>
  72 + <div style={{ flex: 1 }} />
  73 + <span style={{ fontSize: 11, color: "var(--text-muted)" }}>
  74 + {checkedCount > 0 ? <>已选 <span style={{ color: "var(--accent-strong)", fontWeight: 500 }}>{checkedCount}</span> 条 / </> : null}
  75 + 共 {users.length} 条记录
  76 + </span>
  77 + </div>
  78 +
  79 + {/* Table */}
  80 + <div style={{ flex: 1, overflow: "auto", background: "#fff" }}>
  81 + <table style={{ borderCollapse: "collapse", width: "100%", fontSize: 12, tableLayout: "fixed" }}>
  82 + <colgroup>
  83 + <col style={{ width: 32 }} />
  84 + <col style={{ width: 50 }} />
  85 + {colDefs.map((c) => <col key={c.key} style={{ width: c.w }} />)}
  86 + </colgroup>
  87 + <thead>
  88 + <tr style={{ background: "var(--bg-row-zebra)", borderBottom: "1px solid var(--border)" }}>
  89 + <th style={thStyle}>
  90 + <Checkbox
  91 + checked={filtered.length > 0 && filtered.every((u) => checkedRows[u.id])}
  92 + onChange={(v) => {
  93 + const next = { ...checkedRows };
  94 + filtered.forEach((u) => { next[u.id] = v; });
  95 + setCheckedRows(next);
  96 + }}
  97 + />
  98 + </th>
  99 + <th style={thStyle} onClick={() => toggleSort("seq")}>序号</th>
  100 + {colDefs.map((c) => (
  101 + <th key={c.key} style={{ ...thStyle, cursor: "pointer" }} onClick={() => toggleSort(c.key)}>
  102 + <span style={{ display: "inline-flex", alignItems: "center", gap: 3 }}>
  103 + {c.label}
  104 + {sort.key === c.key ? (
  105 + <span style={{ color: "var(--accent)" }}>
  106 + {sort.dir === "asc" ? <Ic.triangle size={8} /> : <Ic.triangle size={8} style={{ transform: "rotate(180deg)" }} />}
  107 + </span>
  108 + ) : (
  109 + <span style={{ color: "var(--text-faint)", opacity: 0.4 }}><Ic.triangle size={8} /></span>
  110 + )}
  111 + </span>
  112 + </th>
  113 + ))}
  114 + </tr>
  115 + </thead>
  116 + <tbody>
  117 + {filtered.map((u, idx) => {
  118 + const isSel = selected === u.id;
  119 + return (
  120 + <tr
  121 + key={u.id}
  122 + onClick={() => setSelected(u.id)}
  123 + onDoubleClick={() => onOpenUser(u)}
  124 + style={{
  125 + background: isSel ? "var(--selected)" : idx % 2 === 0 ? "#fff" : "var(--bg-row-zebra)",
  126 + cursor: "pointer", height: 24,
  127 + color: u.disabled ? "var(--text-faint)" : "var(--text)",
  128 + }}
  129 + onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = "var(--bg-row-hover)"; }}
  130 + onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = idx % 2 === 0 ? "#fff" : "var(--bg-row-zebra)"; }}
  131 + >
  132 + <td style={tdStyle} onClick={(e) => e.stopPropagation()}>
  133 + <Checkbox checked={!!checkedRows[u.id]} onChange={() => toggleRow(u.id)} />
  134 + </td>
  135 + <td style={{ ...tdStyle, fontFamily: '"JetBrains Mono", Menlo, monospace', color: "var(--text-muted)" }}>{u.seq}</td>
  136 + <td style={tdStyle}>{u.employee}</td>
  137 + <td style={tdStyle}>{u.empNo}</td>
  138 + <td style={{ ...tdStyle, fontFamily: '"JetBrains Mono", Menlo, monospace' }}>{u.account}</td>
  139 + <td style={tdStyle}>{u.department}</td>
  140 + <td style={tdStyle}>{u.type}</td>
  141 + <td style={tdStyle}>{u.language}</td>
  142 + <td style={tdStyle}>
  143 + <Checkbox checked={u.disabled} disabled />
  144 + </td>
  145 + <td style={{ ...tdStyle, fontFamily: '"JetBrains Mono", Menlo, monospace', fontSize: 11 }}>{u.lastLogin}</td>
  146 + <td style={tdStyle}>{u.createdBy}</td>
  147 + <td style={{ ...tdStyle, fontFamily: '"JetBrains Mono", Menlo, monospace', fontSize: 11 }}>{u.createdAt}</td>
  148 + </tr>
  149 + );
  150 + })}
  151 + </tbody>
  152 + </table>
  153 + {filtered.length === 0 ? (
  154 + <div style={{ padding: 32, textAlign: "center", color: "var(--text-faint)", fontSize: 12 }}>
  155 + 没有匹配的记录
  156 + </div>
  157 + ) : null}
  158 + </div>
  159 +
  160 + {/* Footer */}
  161 + <div style={{
  162 + display: "flex", alignItems: "center", justifyContent: "space-between",
  163 + padding: "6px 12px", background: "#fff", borderTop: "1px solid var(--border)", flex: "none",
  164 + fontSize: 11, color: "var(--text-muted)",
  165 + }}>
  166 + <span>当前显示:共 {filtered.length} 条单据 共 {filtered.length} 条记录</span>
  167 + <div style={{ display: "flex", gap: 4, alignItems: "center" }}>
  168 + <button style={pgBtn} disabled><Ic.chevronLeft size={10} /></button>
  169 + <span style={{ background: "var(--accent)", color: "#fff", padding: "2px 8px", fontSize: 11 }}>1</span>
  170 + <button style={pgBtn} disabled><Ic.chevronRight size={10} /></button>
  171 + <select style={{ ...selectStyle, height: 22, marginLeft: 8 }}>
  172 + <option>10000 条/页</option>
  173 + <option>500 条/页</option>
  174 + <option>100 条/页</option>
  175 + </select>
  176 + </div>
  177 + </div>
  178 + </div>
  179 + );
  180 +};
  181 +
  182 +const colDefs = [
  183 + { key: "employee", label: "员工名", w: 100 },
  184 + { key: "empNo", label: "员工号", w: 90 },
  185 + { key: "account", label: "用户号", w: 90 },
  186 + { key: "department", label: "部门", w: 110 },
  187 + { key: "type", label: "用户类型", w: 110 },
  188 + { key: "language", label: "语言", w: 60 },
  189 + { key: "disabled", label: "作废", w: 50 },
  190 + { key: "lastLogin", label: "登录日期", w: 145 },
  191 + { key: "createdBy", label: "制单人", w: 90 },
  192 + { key: "createdAt", label: "制单日期", w: 145 },
  193 +];
  194 +
  195 +const selectStyle = {
  196 + height: 26, padding: "0 6px", border: "1px solid var(--border-input)",
  197 + background: "#fff", fontSize: 12, color: "var(--text)", fontFamily: "inherit",
  198 +};
  199 +
  200 +const thStyle = {
  201 + padding: "5px 8px", textAlign: "left", fontWeight: 500,
  202 + borderRight: "1px solid var(--border)", borderBottom: "1px solid var(--border)",
  203 + whiteSpace: "nowrap",
  204 +};
  205 +
  206 +const tdStyle = {
  207 + padding: "3px 8px", borderRight: "1px solid var(--border)", borderBottom: "1px solid #f0f2f5",
  208 + whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
  209 +};
  210 +
  211 +const pgBtn = {
  212 + width: 22, height: 22, border: "1px solid var(--border-input)", background: "#fff", cursor: "pointer",
  213 + display: "inline-flex", alignItems: "center", justifyContent: "center",
  214 +};
  215 +
  216 +window.UserList = UserList;
... ...
prototype/src/sidebar.jsx 0 → 100644
  1 +// Sidebar — search + collapsible tree, dense Windows-explorer style.
  2 +
  3 +const Sidebar = ({ tree, activeNodeId, expanded, setExpanded, onNodeClick, query, setQuery }) => {
  4 + return (
  5 + <div style={{
  6 + width: "100%", height: "100%", display: "flex", flexDirection: "column",
  7 + background: "#fff", borderRight: "1px solid var(--border)",
  8 + fontSize: 12, color: "var(--text)",
  9 + }}>
  10 + {/* Search bar */}
  11 + <div style={{
  12 + padding: 6, borderBottom: "1px solid var(--border)",
  13 + display: "flex", alignItems: "center", background: "#fff", flex: "none",
  14 + }}>
  15 + <div style={{ position: "relative", flex: 1 }}>
  16 + <input
  17 + value={query}
  18 + onChange={(e) => setQuery(e.target.value)}
  19 + placeholder="请输入您想要搜索的关键字"
  20 + style={{
  21 + width: "100%", height: 26, paddingLeft: 8, paddingRight: 26,
  22 + border: "1px solid var(--border-input)", background: "var(--bg-input)",
  23 + fontSize: 12, color: "var(--text)",
  24 + }}
  25 + />
  26 + <div style={{ position: "absolute", right: 6, top: "50%", transform: "translateY(-50%)", color: "var(--text-faint)" }}>
  27 + <Ic.search size={12} />
  28 + </div>
  29 + </div>
  30 + </div>
  31 + {/* Tree */}
  32 + <div style={{ flex: 1, overflow: "auto", padding: "4px 0" }}>
  33 + {tree.map((node) => (
  34 + <TreeNode
  35 + key={node.id}
  36 + node={node}
  37 + depth={0}
  38 + activeNodeId={activeNodeId}
  39 + expanded={expanded}
  40 + setExpanded={setExpanded}
  41 + onNodeClick={onNodeClick}
  42 + query={query}
  43 + />
  44 + ))}
  45 + </div>
  46 + </div>
  47 + );
  48 +};
  49 +
  50 +const matches = (node, q) => {
  51 + if (!q) return true;
  52 + const t = q.toLowerCase();
  53 + if ((node.label || "").toLowerCase().includes(t)) return true;
  54 + if (node.children) return node.children.some((c) => matches(c, q));
  55 + return false;
  56 +};
  57 +
  58 +const TreeNode = ({ node, depth, activeNodeId, expanded, setExpanded, onNodeClick, query }) => {
  59 + const hasChildren = node.children && node.children.length > 0;
  60 + const isExpanded = expanded[node.id] || (query && matches(node, query) && hasChildren);
  61 + const isActive = activeNodeId === node.id;
  62 + const visible = matches(node, query);
  63 + if (!visible) return null;
  64 +
  65 + const click = () => {
  66 + if (hasChildren) {
  67 + setExpanded((s) => ({ ...s, [node.id]: !isExpanded }));
  68 + }
  69 + if (node.leaf || !hasChildren) {
  70 + onNodeClick(node);
  71 + }
  72 + };
  73 +
  74 + return (
  75 + <div>
  76 + <div
  77 + onClick={click}
  78 + style={{
  79 + display: "flex", alignItems: "center",
  80 + height: 24, paddingLeft: 6 + depth * 14, paddingRight: 8,
  81 + cursor: "pointer",
  82 + background: isActive ? "var(--selected)" : "transparent",
  83 + color: isActive ? "var(--accent-strong)" : node.badge ? "var(--accent-strong)" : "var(--text)",
  84 + fontWeight: isActive || node.badge ? 500 : 400,
  85 + whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
  86 + }}
  87 + onMouseEnter={(e) => { if (!isActive) e.currentTarget.style.background = "var(--bg-row-hover)"; }}
  88 + onMouseLeave={(e) => { if (!isActive) e.currentTarget.style.background = "transparent"; }}
  89 + >
  90 + {hasChildren ? (
  91 + <span style={{ width: 12, color: "var(--text-faint)", display: "inline-flex" }}>
  92 + {isExpanded ? <Ic.triangle size={9} /> : <Ic.triangleR size={9} />}
  93 + </span>
  94 + ) : (
  95 + <span style={{ width: 12, display: "inline-flex", justifyContent: "center", color: "var(--text-faint)" }}>
  96 + <Ic.dot size={6} />
  97 + </span>
  98 + )}
  99 + <span style={{ marginLeft: 4, overflow: "hidden", textOverflow: "ellipsis", flex: 1 }}>
  100 + {node.label}
  101 + </span>
  102 + </div>
  103 + {isExpanded && hasChildren ? (
  104 + <div>
  105 + {node.children.map((child) => (
  106 + <TreeNode
  107 + key={child.id}
  108 + node={child}
  109 + depth={depth + 1}
  110 + activeNodeId={activeNodeId}
  111 + expanded={expanded}
  112 + setExpanded={setExpanded}
  113 + onNodeClick={onNodeClick}
  114 + query={query}
  115 + />
  116 + ))}
  117 + </div>
  118 + ) : null}
  119 + </div>
  120 + );
  121 +};
  122 +
  123 +window.Sidebar = Sidebar;
... ...
prototype/src/tabs.jsx 0 → 100644
  1 +// Top-bar tab strip — IDE-style, multi-tab open/close/switch.
  2 +
  3 +const TabStrip = ({ tabs, activeTabId, onSelect, onClose }) => {
  4 + return (
  5 + <div style={{
  6 + display: "flex", alignItems: "stretch", height: 30,
  7 + background: "var(--bg-tab-strip)",
  8 + borderBottom: "1px solid var(--border)",
  9 + paddingLeft: 8, gap: 0,
  10 + overflow: "hidden",
  11 + }}>
  12 + {tabs.map((t) => {
  13 + const active = t.id === activeTabId;
  14 + return <Tab key={t.id} tab={t} active={active} onSelect={onSelect} onClose={onClose} />;
  15 + })}
  16 + </div>
  17 + );
  18 +};
  19 +
  20 +const Tab = ({ tab, active, onSelect, onClose }) => {
  21 + const [hover, setHover] = React.useState(false);
  22 + return (
  23 + <div
  24 + onClick={() => onSelect(tab.id)}
  25 + onMouseEnter={() => setHover(true)}
  26 + onMouseLeave={() => setHover(false)}
  27 + style={{
  28 + position: "relative",
  29 + display: "flex", alignItems: "center", gap: 6,
  30 + height: "100%", padding: "0 12px",
  31 + background: active ? "var(--bg-tab-active)" : hover ? "rgba(255,255,255,0.5)" : "transparent",
  32 + borderTop: active ? "2px solid var(--accent)" : "2px solid transparent",
  33 + borderLeft: "1px solid var(--border)",
  34 + borderRight: "1px solid var(--border)",
  35 + marginRight: -1,
  36 + cursor: "pointer", fontSize: 12,
  37 + color: active ? "var(--text)" : "var(--text-muted)",
  38 + fontWeight: active ? 500 : 400,
  39 + whiteSpace: "nowrap",
  40 + }}
  41 + >
  42 + <span>{tab.label}</span>
  43 + {tab.closable !== false ? (
  44 + <span
  45 + onClick={(e) => { e.stopPropagation(); onClose(tab.id); }}
  46 + style={{
  47 + display: "inline-flex", alignItems: "center", justifyContent: "center",
  48 + width: 14, height: 14, color: "var(--text-faint)",
  49 + borderRadius: 2,
  50 + }}
  51 + onMouseEnter={(e) => {
  52 + e.currentTarget.style.background = "var(--danger)";
  53 + e.currentTarget.style.color = "#fff";
  54 + }}
  55 + onMouseLeave={(e) => {
  56 + e.currentTarget.style.background = "transparent";
  57 + e.currentTarget.style.color = "var(--text-faint)";
  58 + }}
  59 + >
  60 + <Ic.close size={10} />
  61 + </span>
  62 + ) : null}
  63 + </div>
  64 + );
  65 +};
  66 +
  67 +window.TabStrip = TabStrip;
... ...
prototype/src/workspace.jsx 0 → 100644
  1 +// Workspace shell: top bar + sidebar + tabs + screen routing.
  2 +
  3 +const Workspace = ({ session, onLogout }) => {
  4 + const [users, setUsers] = React.useState(() => XLY.buildUsers());
  5 + const [expanded, setExpanded] = React.useState({ kpi: true, quote: true, sys: true });
  6 + const [searchQ, setSearchQ] = React.useState("");
  7 + const [activeNodeId, setActiveNodeId] = React.useState("home");
  8 + const [sidebarOpen, setSidebarOpen] = React.useState(true);
  9 +
  10 + const [tabs, setTabs] = React.useState([
  11 + { id: "home", label: "主页", screen: "home", icon: "home", closable: false },
  12 + ]);
  13 + const [activeTabId, setActiveTabId] = React.useState("home");
  14 + const [megaOpen, setMegaOpen] = React.useState(false);
  15 +
  16 + const openTab = (tab) => {
  17 + setTabs((ts) => ts.find((t) => t.id === tab.id) ? ts : [...ts, tab]);
  18 + setActiveTabId(tab.id);
  19 + };
  20 + const closeTab = (id) => {
  21 + setTabs((ts) => {
  22 + const i = ts.findIndex((t) => t.id === id);
  23 + const next = ts.filter((t) => t.id !== id);
  24 + if (id === activeTabId) {
  25 + const fallback = next[Math.max(0, i - 1)] || next[0];
  26 + if (fallback) setActiveTabId(fallback.id);
  27 + }
  28 + return next;
  29 + });
  30 + };
  31 +
  32 + const onNodeClick = (node) => {
  33 + setActiveNodeId(node.id);
  34 + if (node.screen === "userlist") openTab({ id: "userlist", label: "用户列表", screen: "userlist" });
  35 + else if (node.screen === "module") openTab({ id: "module", label: "系统模块配置", screen: "module" });
  36 + else if (node.id === "home") openTab({ id: "home", label: "主页", screen: "home", closable: false });
  37 + else if (node.id === "quote-01") openTab({ id: "module", label: "系统模块配置", screen: "module" });
  38 + else openTab({ id: node.id, label: node.label, screen: "stub", stubLabel: node.label });
  39 + };
  40 +
  41 + const openUserDetail = (user, mode = "view") => {
  42 + const id = `user-${user.id}`;
  43 + const label = `用户信息单据 · ${user.employee}`;
  44 + setTabs((ts) => {
  45 + if (ts.find((t) => t.id === id)) return ts;
  46 + return [...ts, { id, label, screen: "userdetail", userId: user.id, mode }];
  47 + });
  48 + setActiveTabId(id);
  49 + };
  50 + const createUser = () => {
  51 + const newU = {
  52 + id: `new-${Date.now()}`, seq: users.length + 1, employee: "", empNo: "", account: "",
  53 + department: "", type: "普通用户", language: "中文", disabled: false,
  54 + lastLogin: "", createdBy: session.user, createdAt: new Date().toISOString().slice(0, 19).replace("T", " "),
  55 + permissions: XLY.PERMISSION_GROUPS.reduce((a, g) => (a[g] = false, a), {}),
  56 + tabPerms: {},
  57 + };
  58 + setUsers((us) => [...us, newU]);
  59 + openUserDetail(newU, "new");
  60 + };
  61 +
  62 + const saveUser = (u) => setUsers((us) => us.map((x) => x.id === u.id ? u : x));
  63 + const activeTab = tabs.find((t) => t.id === activeTabId);
  64 +
  65 + return (
  66 + <div style={{ height: "100vh", width: "100vw", display: "flex", flexDirection: "column", background: "var(--bg-app)" }}>
  67 + {/* Top bar */}
  68 + <div style={{
  69 + height: 36, flex: "none",
  70 + background: "var(--bg-topbar)", color: "var(--text-on-dark)",
  71 + display: "flex", alignItems: "center", padding: "0 10px",
  72 + borderBottom: "1px solid #1a1f2a",
  73 + }}>
  74 + <button onClick={() => setMegaOpen(true)} style={topBtn} title="全部导航">
  75 + <Ic.menu size={14} /> 全部导航
  76 + </button>
  77 + <button onClick={() => { setActiveTabId("home"); setSidebarOpen(true); }} style={topBtn} title="主页">
  78 + <Ic.home size={14} /> 主页
  79 + </button>
  80 + <div style={{ flex: 1, minWidth: 0, display: "flex", alignItems: "center", gap: 8, paddingLeft: 12, overflow: "hidden" }}>
  81 + <Ic.Brand size={22} />
  82 + <span style={{ color: "#fff", fontSize: 13, fontWeight: 500, letterSpacing: 1, whiteSpace: "nowrap", flex: "none" }}>XLY-ERP</span>
  83 + <span style={{ color: "var(--text-on-dark-muted)", fontSize: 11, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 }}>· {session.company}</span>
  84 + </div>
  85 + <button style={topBtn}><Ic.search size={14} /></button>
  86 + <button style={topBtn}>
  87 + <span style={{ position: "relative", display: "inline-flex" }}>
  88 + <Ic.bell size={14} />
  89 + <span style={{ position: "absolute", top: -2, right: -3, width: 6, height: 6, background: "var(--danger)", borderRadius: 3 }} />
  90 + </span>
  91 + </button>
  92 + <button style={topBtn}>
  93 + <Ic.building size={14} /> {session.user}(超级管理员)
  94 + <Ic.chevronDown size={10} style={{ marginLeft: 2 }} />
  95 + </button>
  96 + <button onClick={onLogout} style={topBtn} title="登出">
  97 + <span style={{ fontSize: 14, lineHeight: 1 }}>···</span>
  98 + </button>
  99 + </div>
  100 +
  101 + {/* Tab strip */}
  102 + <TabStrip tabs={tabs} activeTabId={activeTabId} onSelect={setActiveTabId} onClose={closeTab} />
  103 +
  104 + {/* Body */}
  105 + <div style={{ flex: 1, display: "flex", minHeight: 0, overflow: "hidden" }}>
  106 + {sidebarOpen && activeTabId === "home" ? (
  107 + <div style={{ width: 230, flex: "none", height: "100%" }}>
  108 + <Sidebar
  109 + tree={XLY.NAV_TREE}
  110 + activeNodeId={activeNodeId}
  111 + expanded={expanded} setExpanded={setExpanded}
  112 + onNodeClick={onNodeClick}
  113 + query={searchQ} setQuery={setSearchQ}
  114 + />
  115 + </div>
  116 + ) : null}
  117 + <div style={{ flex: 1, position: "relative", minWidth: 0 }}>
  118 + {activeTab ? <ScreenRouter tab={activeTab} users={users} onOpenUser={openUserDetail} onCreateUser={createUser} onSaveUser={saveUser} session={session} onOpenScreen={(s, label) => openTab({ id: s, label, screen: s })} /> : null}
  119 + </div>
  120 + </div>
  121 +
  122 + {/* Status bar */}
  123 + <div style={{
  124 + height: 22, flex: "none", background: "#fff", borderTop: "1px solid var(--border)",
  125 + display: "flex", alignItems: "center", justifyContent: "space-between",
  126 + padding: "0 12px", fontSize: 11, color: "var(--text-muted)",
  127 + }}>
  128 + <span>就绪 · 当前用户 {session.user} · {session.company}</span>
  129 + <span>XLY-ERP v8.6.2 · © 2017–2026 XLY 软件股份</span>
  130 + </div>
  131 + {megaOpen ? (
  132 + <MegaNav
  133 + onClose={() => setMegaOpen(false)}
  134 + onOpen={(screen, label) => openTab({ id: screen, label, screen })}
  135 + />
  136 + ) : null}
  137 + </div>
  138 + );
  139 +};
  140 +
  141 +const ScreenRouter = ({ tab, users, onOpenUser, onCreateUser, onSaveUser, session, onOpenScreen }) => {
  142 + if (tab.screen === "home") return <Home user={session.user} onOpenScreen={onOpenScreen} />;
  143 + if (tab.screen === "userlist") return <UserList users={users} onOpenUser={onOpenUser} onCreateUser={onCreateUser} />;
  144 + if (tab.screen === "module") return <ModuleScreen />;
  145 + if (tab.screen === "userdetail") {
  146 + const u = users.find((x) => x.id === tab.userId);
  147 + if (!u) return <Empty>用户不存在</Empty>;
  148 + return <UserDetail user={u} mode={tab.mode} onSave={onSaveUser} />;
  149 + }
  150 + return <Empty>{tab.stubLabel || tab.label} · 模块开发中</Empty>;
  151 +};
  152 +
  153 +const Empty = ({ children }) => (
  154 + <div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text-faint)", fontSize: 13, background: "var(--bg-app)" }}>
  155 + {children}
  156 + </div>
  157 +);
  158 +
  159 +const topBtn = {
  160 + display: "inline-flex", alignItems: "center", gap: 5,
  161 + height: 28, padding: "0 10px", background: "transparent",
  162 + color: "var(--text-on-dark)", border: "none", cursor: "pointer",
  163 + fontSize: 12,
  164 +};
  165 +
  166 +window.Workspace = Workspace;
... ...
prototype/uploads/pasted-1777540186759-0.png 0 → 100644

351 KB

prototype/uploads/模块 1.png 0 → 100644

406 KB

prototype/uploads/用户2.png 0 → 100644

201 KB

prototype/uploads/用户修改 1.png 0 → 100644

214 KB

prototype/uploads/用户修改 2.png 0 → 100644

216 KB

prototype/uploads/用户查询.png 0 → 100644

574 KB

prototype/uploads/登录 1.png 0 → 100644

85 KB

prototype/uploads/登录 2.png 0 → 100644

94.5 KB