Commit 4416c52fbdae4a3b8e8580f78200841e452c4d82
1 parent
1cb8cf53
chore(prototype): add UI prototype files and reference screenshots
Showing
22 changed files
with
1912 additions
and
0 deletions
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