@locode2/harness 0.1.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +27 -0
  2. package/THIRD_PARTY_LICENSES.txt +32769 -0
  3. package/THIRD_PARTY_NOTICES.md +13 -0
  4. package/apps/host/dist/document-parser-worker.js +215 -0
  5. package/apps/host/dist/frontend-runtime-worker.js +1755 -0
  6. package/apps/host/dist/index.js +23380 -0
  7. package/apps/web/dist/assets/LocalMonacoEditor-CXkKwxiP.css +1 -0
  8. package/apps/web/dist/assets/LocalMonacoEditor-WKAHQmzG.js +940 -0
  9. package/apps/web/dist/assets/LogsView-BFFFSFB-.js +1 -0
  10. package/apps/web/dist/assets/PluginsView-DM_ZnENS.js +1 -0
  11. package/apps/web/dist/assets/SettingsView-Cb5Q4ile.js +1 -0
  12. package/apps/web/dist/assets/abap-DLDM7-KI.js +1 -0
  13. package/apps/web/dist/assets/apex-DNDY2TF8.js +1 -0
  14. package/apps/web/dist/assets/azcli-Y6nb8tq_.js +1 -0
  15. package/apps/web/dist/assets/bat-BwHxbl9M.js +1 -0
  16. package/apps/web/dist/assets/bicep-CFznDFnq.js +2 -0
  17. package/apps/web/dist/assets/cameligo-Bf6VGUru.js +1 -0
  18. package/apps/web/dist/assets/clojure-Dnu-v4kV.js +1 -0
  19. package/apps/web/dist/assets/codicon-Brq4_Ui5.ttf +0 -0
  20. package/apps/web/dist/assets/coffee-Bd8akH9Z.js +1 -0
  21. package/apps/web/dist/assets/cpp-BbWJElDN.js +1 -0
  22. package/apps/web/dist/assets/csharp-Co3qMtFm.js +1 -0
  23. package/apps/web/dist/assets/csp-D-4FJmMZ.js +1 -0
  24. package/apps/web/dist/assets/css-DdJfP1eB.js +3 -0
  25. package/apps/web/dist/assets/css.worker-11mhRRLC.js +97 -0
  26. package/apps/web/dist/assets/cssMode-L5f5LAEn.js +1 -0
  27. package/apps/web/dist/assets/cypher-cTPe9QuQ.js +1 -0
  28. package/apps/web/dist/assets/dart-BOtBlQCF.js +1 -0
  29. package/apps/web/dist/assets/dockerfile-BG73LgW2.js +1 -0
  30. package/apps/web/dist/assets/ecl-BEgZUVRK.js +1 -0
  31. package/apps/web/dist/assets/editor.worker-DysRFSaV.js +30 -0
  32. package/apps/web/dist/assets/elixir-BkW5O-1t.js +1 -0
  33. package/apps/web/dist/assets/flow9-BeJ5waoc.js +1 -0
  34. package/apps/web/dist/assets/freemarker2-UOwftS75.js +3 -0
  35. package/apps/web/dist/assets/fsharp-PahG7c26.js +1 -0
  36. package/apps/web/dist/assets/go-acbASCJo.js +1 -0
  37. package/apps/web/dist/assets/graphql-BxJiqAUM.js +1 -0
  38. package/apps/web/dist/assets/handlebars-C_E41ffr.js +1 -0
  39. package/apps/web/dist/assets/hcl-DtV1sZF8.js +1 -0
  40. package/apps/web/dist/assets/html-HsrixH2x.js +1 -0
  41. package/apps/web/dist/assets/html.worker-C8Tw8ncf.js +474 -0
  42. package/apps/web/dist/assets/htmlMode-DipGmEOi.js +1 -0
  43. package/apps/web/dist/assets/index-COUF63cT.js +213 -0
  44. package/apps/web/dist/assets/index-SNSmF2-c.css +1 -0
  45. package/apps/web/dist/assets/ini-Kd9XrMLS.js +1 -0
  46. package/apps/web/dist/assets/java-CXBNlu9o.js +1 -0
  47. package/apps/web/dist/assets/javascript-D36WEqF9.js +1 -0
  48. package/apps/web/dist/assets/json.worker-BzkYZgJ7.js +62 -0
  49. package/apps/web/dist/assets/jsonMode-D9BrIats.js +7 -0
  50. package/apps/web/dist/assets/julia-cl7-CwDS.js +1 -0
  51. package/apps/web/dist/assets/kotlin-s7OhZKlX.js +1 -0
  52. package/apps/web/dist/assets/less-9HpZscsL.js +2 -0
  53. package/apps/web/dist/assets/lexon-OrD6JF1K.js +1 -0
  54. package/apps/web/dist/assets/liquid-DES89Tjc.js +1 -0
  55. package/apps/web/dist/assets/lock-keyhole-jo-FJcGq.js +1 -0
  56. package/apps/web/dist/assets/lspLanguageFeatures-YKkgcn4q.js +4 -0
  57. package/apps/web/dist/assets/lua-Cyyb5UIc.js +1 -0
  58. package/apps/web/dist/assets/m3-B8OfTtLu.js +1 -0
  59. package/apps/web/dist/assets/markdown-BFxVWTOG.js +1 -0
  60. package/apps/web/dist/assets/mdx-D1Bfax8e.js +1 -0
  61. package/apps/web/dist/assets/mips-CiqrrVzr.js +1 -0
  62. package/apps/web/dist/assets/msdax-DmeGPVcC.js +1 -0
  63. package/apps/web/dist/assets/mysql-C_tMU-Nz.js +1 -0
  64. package/apps/web/dist/assets/objective-c-BDtDVThU.js +1 -0
  65. package/apps/web/dist/assets/pascal-vHIfCaH5.js +1 -0
  66. package/apps/web/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
  67. package/apps/web/dist/assets/perl-Ub6l9XKa.js +1 -0
  68. package/apps/web/dist/assets/pgsql-BlNEE0v7.js +1 -0
  69. package/apps/web/dist/assets/php-BBUBE1dy.js +1 -0
  70. package/apps/web/dist/assets/pla-DSh2-awV.js +1 -0
  71. package/apps/web/dist/assets/postiats-CocnycG-.js +1 -0
  72. package/apps/web/dist/assets/powerquery-tScXyioY.js +1 -0
  73. package/apps/web/dist/assets/powershell-COWaemsV.js +1 -0
  74. package/apps/web/dist/assets/protobuf-Brw8urJB.js +2 -0
  75. package/apps/web/dist/assets/pug-8SOpv6rk.js +1 -0
  76. package/apps/web/dist/assets/python-DKhPhY0N.js +1 -0
  77. package/apps/web/dist/assets/qsharp-Bw9ernYp.js +1 -0
  78. package/apps/web/dist/assets/r-j7ic8hl3.js +1 -0
  79. package/apps/web/dist/assets/razor-Cha8qrI6.js +1 -0
  80. package/apps/web/dist/assets/redis-Bu5POkcn.js +1 -0
  81. package/apps/web/dist/assets/redshift-Bs9aos_-.js +1 -0
  82. package/apps/web/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
  83. package/apps/web/dist/assets/ruby-zBfavPgS.js +1 -0
  84. package/apps/web/dist/assets/rust-BzKRNQWT.js +1 -0
  85. package/apps/web/dist/assets/sb-BBc9UKZt.js +1 -0
  86. package/apps/web/dist/assets/scala-D9hQfWCl.js +1 -0
  87. package/apps/web/dist/assets/scheme-BPhDTwHR.js +1 -0
  88. package/apps/web/dist/assets/scss-CBJaRo0y.js +3 -0
  89. package/apps/web/dist/assets/shell-DiJ1NA_G.js +1 -0
  90. package/apps/web/dist/assets/solidity-Db0IVjzk.js +1 -0
  91. package/apps/web/dist/assets/sophia-CnS9iZB_.js +1 -0
  92. package/apps/web/dist/assets/sparql-CJmd_6j2.js +1 -0
  93. package/apps/web/dist/assets/sql-ClhHkBeG.js +1 -0
  94. package/apps/web/dist/assets/st-CHwy0fLd.js +1 -0
  95. package/apps/web/dist/assets/swift-CnmFD0ga.js +1 -0
  96. package/apps/web/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
  97. package/apps/web/dist/assets/tcl-Dm6ycUr_.js +1 -0
  98. package/apps/web/dist/assets/ts.worker-Bupg6llt.js +67722 -0
  99. package/apps/web/dist/assets/tsMode-CEhssXv8.js +11 -0
  100. package/apps/web/dist/assets/twig-Csy3S7wG.js +1 -0
  101. package/apps/web/dist/assets/typescript-DWqmw58R.js +1 -0
  102. package/apps/web/dist/assets/typespec-Btyra-wh.js +1 -0
  103. package/apps/web/dist/assets/vb-Db0cS2oM.js +1 -0
  104. package/apps/web/dist/assets/wgsl-DumH7NcR.js +298 -0
  105. package/apps/web/dist/assets/workers-BmZvNFRq.js +1 -0
  106. package/apps/web/dist/assets/xml-WQTAFV9b.js +1 -0
  107. package/apps/web/dist/assets/yaml-Ke2Lf79d.js +1 -0
  108. package/apps/web/dist/index.html +15 -0
  109. package/bin/launch.mjs +71 -0
  110. package/bin/locode-mini.mjs +12 -0
  111. package/bin/options.mjs +49 -0
  112. package/bin/restore-assets.mjs +40 -0
  113. package/bundled-plugins/fast-frontend/.gitignore +2 -0
  114. package/bundled-plugins/fast-frontend/README.md +29 -0
  115. package/bundled-plugins/fast-frontend/harness.plugin.json +123 -0
  116. package/bundled-plugins/fast-frontend/runtimes/antd-admin/PUBLIC_CONTRACT.md +139 -0
  117. package/bundled-plugins/fast-frontend/runtimes/antd-admin/contracts/app.schema.json +24 -0
  118. package/bundled-plugins/fast-frontend/runtimes/antd-admin/contracts/route.schema.json +52 -0
  119. package/bundled-plugins/fast-frontend/runtimes/antd-admin/examples/app.json +9 -0
  120. package/bundled-plugins/fast-frontend/runtimes/antd-admin/examples/route.json +8 -0
  121. package/bundled-plugins/fast-frontend/runtimes/antd-admin/index.html +17 -0
  122. package/bundled-plugins/fast-frontend/runtimes/antd-admin/package.json +31 -0
  123. package/bundled-plugins/fast-frontend/runtimes/antd-admin/scripts/smoke-build.mjs +40 -0
  124. package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/inspector/index.ts +612 -0
  125. package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/main.tsx +17 -0
  126. package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/sdk/index.ts +1028 -0
  127. package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/shell/RuntimeApp.tsx +380 -0
  128. package/bundled-plugins/fast-frontend/runtimes/antd-admin/src/shell/runtime.css +246 -0
  129. package/bundled-plugins/fast-frontend/runtimes/antd-admin/tsconfig.json +20 -0
  130. package/bundled-plugins/fast-frontend/runtimes/antd-admin/types/runtime.consumer-smoke.ts +70 -0
  131. package/bundled-plugins/fast-frontend/runtimes/antd-admin/types/runtime.d.ts +227 -0
  132. package/bundled-plugins/fast-frontend/templates/antd-admin/.gitignore +9 -0
  133. package/bundled-plugins/fast-frontend/templates/antd-admin/.harness/template.json +10 -0
  134. package/bundled-plugins/fast-frontend/templates/antd-admin/index.html +13 -0
  135. package/bundled-plugins/fast-frontend/templates/antd-admin/package.json +34 -0
  136. package/bundled-plugins/fast-frontend/templates/antd-admin/pnpm-lock.yaml +1451 -0
  137. package/bundled-plugins/fast-frontend/templates/antd-admin/pnpm-workspace.yaml +4 -0
  138. package/bundled-plugins/fast-frontend/templates/antd-admin/public/generated/.gitkeep +1 -0
  139. package/bundled-plugins/fast-frontend/templates/antd-admin/scripts/validate-routes.mjs +153 -0
  140. package/bundled-plugins/fast-frontend/templates/antd-admin/src/app/App.tsx +27 -0
  141. package/bundled-plugins/fast-frontend/templates/antd-admin/src/app/NotFoundPage.tsx +15 -0
  142. package/bundled-plugins/fast-frontend/templates/antd-admin/src/features/.gitkeep +1 -0
  143. package/bundled-plugins/fast-frontend/templates/antd-admin/src/layouts/MainLayout.tsx +77 -0
  144. package/bundled-plugins/fast-frontend/templates/antd-admin/src/main.tsx +14 -0
  145. package/bundled-plugins/fast-frontend/templates/antd-admin/src/mocks/dashboard.ts +48 -0
  146. package/bundled-plugins/fast-frontend/templates/antd-admin/src/pages/dashboard/page.tsx +151 -0
  147. package/bundled-plugins/fast-frontend/templates/antd-admin/src/pages/dashboard/route.ts +9 -0
  148. package/bundled-plugins/fast-frontend/templates/antd-admin/src/routes/definition.ts +21 -0
  149. package/bundled-plugins/fast-frontend/templates/antd-admin/src/routes/menu.tsx +44 -0
  150. package/bundled-plugins/fast-frontend/templates/antd-admin/src/routes/registry.ts +48 -0
  151. package/bundled-plugins/fast-frontend/templates/antd-admin/src/routes/router.tsx +34 -0
  152. package/bundled-plugins/fast-frontend/templates/antd-admin/src/shared/components/business/.gitkeep +1 -0
  153. package/bundled-plugins/fast-frontend/templates/antd-admin/src/shared/components/business/BusinessProviders.tsx +9 -0
  154. package/bundled-plugins/fast-frontend/templates/antd-admin/src/styles/global.css +180 -0
  155. package/bundled-plugins/fast-frontend/templates/antd-admin/src/vite-env.d.ts +1 -0
  156. package/bundled-plugins/fast-frontend/templates/antd-admin/tsconfig.app.json +24 -0
  157. package/bundled-plugins/fast-frontend/templates/antd-admin/tsconfig.json +7 -0
  158. package/bundled-plugins/fast-frontend/templates/antd-admin/tsconfig.node.json +17 -0
  159. package/bundled-plugins/fast-frontend/templates/antd-admin/vite.config.ts +39 -0
  160. package/npm-shrinkwrap.json +9737 -0
  161. package/package.json +80 -0
  162. package/release-integrity.json +168 -0
@@ -0,0 +1,4 @@
1
+ packages:
2
+ - .
3
+
4
+ onlyBuiltDependencies: []
@@ -0,0 +1,153 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { dirname, join, relative, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import ts from "typescript";
5
+
6
+ const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
7
+ const pagesRoot = join(projectRoot, "src", "pages");
8
+ const allowedKeys = new Set([
9
+ "id",
10
+ "path",
11
+ "title",
12
+ "icon",
13
+ "order",
14
+ "parentId",
15
+ "hidden",
16
+ ]);
17
+ const allowedIcons = new Set([
18
+ "AppstoreOutlined",
19
+ "DashboardOutlined",
20
+ "FileTextOutlined",
21
+ "FormOutlined",
22
+ "SettingOutlined",
23
+ "TeamOutlined",
24
+ ]);
25
+
26
+ async function collectRouteFiles(directory) {
27
+ const entries = await readdir(directory, { withFileTypes: true });
28
+ const files = [];
29
+ for (const entry of entries) {
30
+ const entryPath = join(directory, entry.name);
31
+ if (entry.isDirectory()) files.push(...await collectRouteFiles(entryPath));
32
+ else if (entry.isFile() && entry.name === "route.ts") files.push(entryPath);
33
+ }
34
+ return files;
35
+ }
36
+
37
+ function displayPath(path) {
38
+ return relative(projectRoot, path).split(sep).join("/");
39
+ }
40
+
41
+ function assert(condition, message) {
42
+ if (!condition) throw new Error(message);
43
+ }
44
+
45
+ function unwrapExpression(expression) {
46
+ let current = expression;
47
+ while (
48
+ ts.isParenthesizedExpression(current) ||
49
+ ts.isAsExpression(current) ||
50
+ ts.isSatisfiesExpression(current)
51
+ ) {
52
+ current = current.expression;
53
+ }
54
+ return current;
55
+ }
56
+
57
+ function propertyName(property, file) {
58
+ const name = property.name;
59
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
60
+ return name.text;
61
+ }
62
+ throw new Error(`${displayPath(file)} 只能使用静态属性名`);
63
+ }
64
+
65
+ function literalValue(expression, file, key) {
66
+ const value = unwrapExpression(expression);
67
+ if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return value.text;
68
+ if (ts.isNumericLiteral(value)) return Number(value.text);
69
+ if (value.kind === ts.SyntaxKind.TrueKeyword) return true;
70
+ if (value.kind === ts.SyntaxKind.FalseKeyword) return false;
71
+ throw new Error(`${displayPath(file)} 的 ${key} 必须是静态字符串、数字或布尔字面量`);
72
+ }
73
+
74
+ function parseRouteSource(source, file) {
75
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
76
+ const exportAssignment = sourceFile.statements.find(
77
+ (statement) => ts.isExportAssignment(statement) && !statement.isExportEquals,
78
+ );
79
+ assert(exportAssignment !== undefined, `${displayPath(file)} 必须 default export definePageRoute({...})`);
80
+
81
+ let expression = unwrapExpression(exportAssignment.expression);
82
+ if (ts.isCallExpression(expression)) {
83
+ assert(
84
+ ts.isIdentifier(expression.expression) && expression.expression.text === "definePageRoute",
85
+ `${displayPath(file)} 的默认导出必须调用 definePageRoute`,
86
+ );
87
+ assert(expression.arguments.length === 1, `${displayPath(file)} 的 definePageRoute 只接受一个参数`);
88
+ expression = unwrapExpression(expression.arguments[0]);
89
+ }
90
+ assert(ts.isObjectLiteralExpression(expression), `${displayPath(file)} 必须导出静态路由对象`);
91
+
92
+ const route = {};
93
+ for (const property of expression.properties) {
94
+ assert(ts.isPropertyAssignment(property), `${displayPath(file)} 不允许展开、方法或计算属性`);
95
+ const key = propertyName(property, file);
96
+ assert(allowedKeys.has(key), `${displayPath(file)} 包含未知字段:${key}`);
97
+ route[key] = literalValue(property.initializer, file, key);
98
+ }
99
+ return route;
100
+ }
101
+
102
+ function validateRoute(route, file) {
103
+ const label = displayPath(file);
104
+ assert(typeof route.id === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/.test(route.id), `${label} 的 id 无效`);
105
+ assert(typeof route.path === "string" && /^\/(?:[^\s?#]*)$/.test(route.path), `${label} 的 path 必须是以 / 开头且不含空白、查询或片段的路径`);
106
+ assert(typeof route.title === "string" && route.title.trim().length > 0, `${label} 的 title 不能为空`);
107
+ if (route.icon !== undefined) assert(typeof route.icon === "string" && allowedIcons.has(route.icon), `${label} 的 icon 不受支持:${String(route.icon)}`);
108
+ if (route.order !== undefined) assert(Number.isFinite(route.order), `${label} 的 order 必须是有限数字`);
109
+ if (route.parentId !== undefined) assert(typeof route.parentId === "string" && route.parentId.length > 0, `${label} 的 parentId 无效`);
110
+ if (route.hidden !== undefined) assert(typeof route.hidden === "boolean", `${label} 的 hidden 必须是布尔值`);
111
+ return route;
112
+ }
113
+
114
+ function validateParentGraph(routes) {
115
+ const byId = new Map(routes.map(({ route }) => [route.id, route]));
116
+ for (const { route, file } of routes) {
117
+ if (route.parentId === undefined) continue;
118
+ assert(route.parentId !== route.id, `${displayPath(file)} 不能把自身设为父级`);
119
+ assert(byId.has(route.parentId), `${displayPath(file)} 引用了不存在的父级:${route.parentId}`);
120
+
121
+ const visited = new Set([route.id]);
122
+ let current = route;
123
+ while (current.parentId !== undefined) {
124
+ assert(!visited.has(current.parentId), `${displayPath(file)} 的父级关系存在循环`);
125
+ visited.add(current.parentId);
126
+ current = byId.get(current.parentId);
127
+ assert(current !== undefined, `${displayPath(file)} 引用了不存在的父级`);
128
+ }
129
+ }
130
+ }
131
+
132
+ const routeFiles = await collectRouteFiles(pagesRoot);
133
+ assert(routeFiles.length > 0, "src/pages 中至少需要一个 route.ts");
134
+
135
+ const routes = [];
136
+ for (const file of routeFiles) {
137
+ const route = validateRoute(parseRouteSource(await readFile(file, "utf8"), file), file);
138
+ const pageFile = join(dirname(file), "page.tsx");
139
+ assert((await stat(pageFile).catch(() => undefined))?.isFile() === true, `${displayPath(file)} 缺少同目录 page.tsx`);
140
+ routes.push({ route, file });
141
+ }
142
+
143
+ const ids = new Map();
144
+ const paths = new Map();
145
+ for (const { route, file } of routes) {
146
+ assert(!ids.has(route.id), `${displayPath(file)} 与 ${ids.get(route.id)} 使用了重复 id:${route.id}`);
147
+ assert(!paths.has(route.path), `${displayPath(file)} 与 ${paths.get(route.path)} 使用了重复 path:${route.path}`);
148
+ ids.set(route.id, displayPath(file));
149
+ paths.set(route.path, displayPath(file));
150
+ }
151
+ validateParentGraph(routes);
152
+
153
+ console.log(`路由校验通过:${routes.length} 个页面`);
@@ -0,0 +1,27 @@
1
+ import { App as AntApp, ConfigProvider, theme } from "antd";
2
+ import zhCN from "antd/locale/zh_CN";
3
+ import { RouterProvider } from "react-router-dom";
4
+
5
+ import { router } from "../routes/router";
6
+
7
+ export function App() {
8
+ return (
9
+ <ConfigProvider
10
+ locale={zhCN}
11
+ theme={{
12
+ algorithm: theme.defaultAlgorithm,
13
+ token: {
14
+ borderRadius: 10,
15
+ colorPrimary: "#635bff",
16
+ colorBgLayout: "#f5f7fb",
17
+ fontFamily:
18
+ "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
19
+ },
20
+ }}
21
+ >
22
+ <AntApp>
23
+ <RouterProvider router={router} />
24
+ </AntApp>
25
+ </ConfigProvider>
26
+ );
27
+ }
@@ -0,0 +1,15 @@
1
+ import { Button, Result } from "antd";
2
+ import { useNavigate } from "react-router-dom";
3
+
4
+ export function NotFoundPage() {
5
+ const navigate = useNavigate();
6
+
7
+ return (
8
+ <Result
9
+ status="404"
10
+ title="404"
11
+ subTitle="抱歉,你访问的页面不存在或已经被移动。"
12
+ extra={<Button type="primary" onClick={() => navigate("/")}>返回工作台</Button>}
13
+ />
14
+ );
15
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ BellOutlined,
3
+ MenuFoldOutlined,
4
+ MenuUnfoldOutlined,
5
+ ProductOutlined,
6
+ } from "@ant-design/icons";
7
+ import { Avatar, Breadcrumb, Button, Flex, Layout, Menu, Space, Typography, theme } from "antd";
8
+ import { useMemo, useState } from "react";
9
+ import { Outlet, useLocation, useNavigate } from "react-router-dom";
10
+
11
+ import { menuItems } from "../routes/menu";
12
+ import { findPageByLocation } from "../routes/registry";
13
+
14
+ const { Content, Header, Sider } = Layout;
15
+
16
+ export function MainLayout() {
17
+ const [collapsed, setCollapsed] = useState(false);
18
+ const location = useLocation();
19
+ const navigate = useNavigate();
20
+ const {
21
+ token: { colorBgContainer, colorBorderSecondary },
22
+ } = theme.useToken();
23
+ const activePage = useMemo(() => findPageByLocation(location.pathname), [location.pathname]);
24
+
25
+ return (
26
+ <Layout className="app-shell">
27
+ <Sider
28
+ breakpoint="lg"
29
+ className="app-sider"
30
+ collapsed={collapsed}
31
+ collapsedWidth={72}
32
+ trigger={null}
33
+ width={244}
34
+ onBreakpoint={setCollapsed}
35
+ >
36
+ <Flex align="center" className="brand" gap={12}>
37
+ <span className="brand-mark"><ProductOutlined /></span>
38
+ {!collapsed && <span className="brand-name">业务工作台</span>}
39
+ </Flex>
40
+ <Menu
41
+ items={menuItems}
42
+ mode="inline"
43
+ selectedKeys={[activePage?.path ?? location.pathname]}
44
+ onClick={({ key }) => navigate(key)}
45
+ />
46
+ </Sider>
47
+
48
+ <Layout>
49
+ <Header
50
+ className="app-header"
51
+ style={{ background: colorBgContainer, borderColor: colorBorderSecondary }}
52
+ >
53
+ <Flex align="center" justify="space-between">
54
+ <Space size={12}>
55
+ <Button
56
+ aria-label={collapsed ? "展开导航" : "收起导航"}
57
+ icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
58
+ type="text"
59
+ onClick={() => setCollapsed((value) => !value)}
60
+ />
61
+ <Breadcrumb items={[{ title: "首页" }, { title: activePage?.title ?? "页面" }]} />
62
+ </Space>
63
+ <Space size={14}>
64
+ <Button aria-label="通知" icon={<BellOutlined />} type="text" />
65
+ <Avatar className="user-avatar">管</Avatar>
66
+ <Typography.Text className="user-name">管理员</Typography.Text>
67
+ </Space>
68
+ </Flex>
69
+ </Header>
70
+
71
+ <Content className="app-content">
72
+ <Outlet />
73
+ </Content>
74
+ </Layout>
75
+ </Layout>
76
+ );
77
+ }
@@ -0,0 +1,14 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ import { App } from "./app/App";
5
+ import "./styles/global.css";
6
+
7
+ const root = document.getElementById("root");
8
+ if (root === null) throw new Error("缺少 #root 挂载节点");
9
+
10
+ createRoot(root).render(
11
+ <StrictMode>
12
+ <App />
13
+ </StrictMode>,
14
+ );
@@ -0,0 +1,48 @@
1
+ export interface Metric {
2
+ readonly key: string;
3
+ readonly label: string;
4
+ readonly value: number;
5
+ readonly suffix?: string;
6
+ readonly trend: number;
7
+ readonly note: string;
8
+ }
9
+
10
+ export interface WorkItem {
11
+ readonly id: string;
12
+ readonly title: string;
13
+ readonly owner: string;
14
+ readonly status: "待处理" | "进行中" | "已完成";
15
+ readonly dueAt: string;
16
+ }
17
+
18
+ export interface DashboardOverview {
19
+ readonly metrics: readonly Metric[];
20
+ readonly workItems: readonly WorkItem[];
21
+ readonly completionRate: number;
22
+ }
23
+
24
+ const overview: DashboardOverview = {
25
+ metrics: [
26
+ { key: "visits", label: "今日访问", value: 8236, trend: 12.8, note: "较昨日" },
27
+ { key: "orders", label: "新增订单", value: 386, trend: 8.2, note: "较昨日" },
28
+ { key: "revenue", label: "本月营收", value: 128.6, suffix: "万元", trend: 18.4, note: "较上月" },
29
+ { key: "conversion", label: "转化率", value: 6.42, suffix: "%", trend: -0.6, note: "较上周" },
30
+ ],
31
+ completionRate: 76,
32
+ workItems: [
33
+ { id: "WK-1048", title: "确认华东区域季度目标", owner: "李敏", status: "进行中", dueAt: "今天 17:00" },
34
+ { id: "WK-1047", title: "审核重点客户折扣方案", owner: "王岩", status: "待处理", dueAt: "明天 10:30" },
35
+ { id: "WK-1042", title: "发布新版运营周报", owner: "陈晨", status: "已完成", dueAt: "昨天 16:20" },
36
+ { id: "WK-1039", title: "补充供应商风险材料", owner: "赵宁", status: "待处理", dueAt: "8 月 30 日" },
37
+ ],
38
+ };
39
+
40
+ export function loadDashboardOverview(signal?: AbortSignal): Promise<DashboardOverview> {
41
+ return new Promise((resolve, reject) => {
42
+ const timer = window.setTimeout(() => resolve(overview), 180);
43
+ signal?.addEventListener("abort", () => {
44
+ window.clearTimeout(timer);
45
+ reject(new DOMException("请求已取消", "AbortError"));
46
+ }, { once: true });
47
+ });
48
+ }
@@ -0,0 +1,151 @@
1
+ import {
2
+ ArrowDownOutlined,
3
+ ArrowRightOutlined,
4
+ ArrowUpOutlined,
5
+ CheckCircleFilled,
6
+ ClockCircleFilled,
7
+ SyncOutlined,
8
+ } from "@ant-design/icons";
9
+ import {
10
+ Alert,
11
+ Button,
12
+ Card,
13
+ Col,
14
+ Flex,
15
+ Progress,
16
+ Row,
17
+ Skeleton,
18
+ Space,
19
+ Statistic,
20
+ Table,
21
+ Tag,
22
+ Typography,
23
+ type TableColumnsType,
24
+ } from "antd";
25
+ import { useEffect, useState } from "react";
26
+
27
+ import {
28
+ loadDashboardOverview,
29
+ type DashboardOverview,
30
+ type WorkItem,
31
+ } from "../../mocks/dashboard";
32
+
33
+ const statusPresentation = {
34
+ 待处理: { color: "gold", icon: <ClockCircleFilled /> },
35
+ 进行中: { color: "processing", icon: <SyncOutlined spin /> },
36
+ 已完成: { color: "success", icon: <CheckCircleFilled /> },
37
+ } as const;
38
+
39
+ const columns: TableColumnsType<WorkItem> = [
40
+ {
41
+ title: "事项",
42
+ dataIndex: "title",
43
+ render: (title: string, record) => (
44
+ <Space orientation="vertical" size={2}>
45
+ <Typography.Text strong>{title}</Typography.Text>
46
+ <Typography.Text type="secondary">{record.id}</Typography.Text>
47
+ </Space>
48
+ ),
49
+ },
50
+ { title: "负责人", dataIndex: "owner", width: 110 },
51
+ {
52
+ title: "状态",
53
+ dataIndex: "status",
54
+ width: 120,
55
+ render: (status: WorkItem["status"]) => {
56
+ const presentation = statusPresentation[status];
57
+ return <Tag color={presentation.color} icon={presentation.icon}>{status}</Tag>;
58
+ },
59
+ },
60
+ { title: "截止时间", dataIndex: "dueAt", width: 140 },
61
+ ];
62
+
63
+ export default function DashboardPage() {
64
+ const [data, setData] = useState<DashboardOverview>();
65
+ const [error, setError] = useState<string>();
66
+
67
+ useEffect(() => {
68
+ const controller = new AbortController();
69
+ void loadDashboardOverview(controller.signal)
70
+ .then(setData)
71
+ .catch((reason: unknown) => {
72
+ if (!(reason instanceof DOMException && reason.name === "AbortError")) {
73
+ setError(reason instanceof Error ? reason.message : "数据加载失败");
74
+ }
75
+ });
76
+ return () => controller.abort();
77
+ }, []);
78
+
79
+ return (
80
+ <main className="dashboard-page">
81
+ <Flex align="flex-end" className="page-heading" justify="space-between" wrap>
82
+ <div>
83
+ <Typography.Title level={2}>下午好,管理员</Typography.Title>
84
+ <Typography.Text type="secondary">这里是今天的业务进展和待办事项。</Typography.Text>
85
+ </div>
86
+ <Button type="primary">新建业务单</Button>
87
+ </Flex>
88
+
89
+ {error === undefined ? null : <Alert message="无法加载工作台" description={error} showIcon type="error" />}
90
+
91
+ <Row gutter={[16, 16]}>
92
+ {(data?.metrics ?? [null, null, null, null]).map((metric, index) => (
93
+ <Col key={metric?.key ?? index} lg={6} md={12} xs={24}>
94
+ <Card className="metric-card" variant="borderless">
95
+ {metric === null ? <Skeleton active paragraph={false} /> : (
96
+ <>
97
+ <Statistic
98
+ title={metric.label}
99
+ value={metric.value}
100
+ {...(metric.suffix === undefined ? {} : { suffix: metric.suffix })}
101
+ {...(metric.suffix === "%" ? { precision: 2 } : {})}
102
+ />
103
+ <Space className={metric.trend >= 0 ? "trend-positive" : "trend-negative"} size={4}>
104
+ {metric.trend >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
105
+ <span>{Math.abs(metric.trend)}%</span>
106
+ <Typography.Text type="secondary">{metric.note}</Typography.Text>
107
+ </Space>
108
+ </>
109
+ )}
110
+ </Card>
111
+ </Col>
112
+ ))}
113
+ </Row>
114
+
115
+ <Row gutter={[16, 16]}>
116
+ <Col lg={16} xs={24}>
117
+ <Card
118
+ className="work-card"
119
+ title="重点待办"
120
+ extra={<Button icon={<ArrowRightOutlined />} iconPlacement="end" type="link">查看全部</Button>}
121
+ >
122
+ <Table<WorkItem>
123
+ columns={columns}
124
+ dataSource={data?.workItems}
125
+ loading={data === undefined && error === undefined}
126
+ pagination={false}
127
+ rowKey="id"
128
+ scroll={{ x: 620 }}
129
+ />
130
+ </Card>
131
+ </Col>
132
+ <Col lg={8} xs={24}>
133
+ <Card className="progress-card" title="本月目标" variant="borderless">
134
+ <Flex align="center" className="progress-content" vertical>
135
+ <Progress
136
+ percent={data?.completionRate ?? 0}
137
+ size={168}
138
+ strokeColor={{ "0%": "#8178ff", "100%": "#4e7cff" }}
139
+ type="dashboard"
140
+ />
141
+ <Typography.Title level={4}>稳步推进中</Typography.Title>
142
+ <Typography.Paragraph type="secondary">
143
+ 当前进度领先计划 4.2%,继续关注重点客户的交付节奏。
144
+ </Typography.Paragraph>
145
+ </Flex>
146
+ </Card>
147
+ </Col>
148
+ </Row>
149
+ </main>
150
+ );
151
+ }
@@ -0,0 +1,9 @@
1
+ import { definePageRoute } from "../../routes/definition";
2
+
3
+ export default definePageRoute({
4
+ id: "dashboard",
5
+ path: "/",
6
+ title: "工作台",
7
+ icon: "DashboardOutlined",
8
+ order: 10,
9
+ });
@@ -0,0 +1,21 @@
1
+ export type RouteIconName =
2
+ | "AppstoreOutlined"
3
+ | "DashboardOutlined"
4
+ | "FileTextOutlined"
5
+ | "FormOutlined"
6
+ | "SettingOutlined"
7
+ | "TeamOutlined";
8
+
9
+ export interface PageRouteDefinition {
10
+ readonly id: string;
11
+ readonly path: string;
12
+ readonly title: string;
13
+ readonly icon?: RouteIconName;
14
+ readonly order?: number;
15
+ readonly parentId?: string;
16
+ readonly hidden?: boolean;
17
+ }
18
+
19
+ export function definePageRoute<const T extends PageRouteDefinition>(definition: T): T {
20
+ return definition;
21
+ }
@@ -0,0 +1,44 @@
1
+ import {
2
+ AppstoreOutlined,
3
+ DashboardOutlined,
4
+ FileTextOutlined,
5
+ FormOutlined,
6
+ SettingOutlined,
7
+ TeamOutlined,
8
+ } from "@ant-design/icons";
9
+ import type { MenuProps } from "antd";
10
+ import type { ReactNode } from "react";
11
+
12
+ import type { RouteIconName } from "./definition";
13
+ import { registeredPages, type RegisteredPage } from "./registry";
14
+
15
+ type MenuItem = Required<MenuProps>["items"][number];
16
+
17
+ const icons: Record<RouteIconName, ReactNode> = {
18
+ AppstoreOutlined: <AppstoreOutlined />,
19
+ DashboardOutlined: <DashboardOutlined />,
20
+ FileTextOutlined: <FileTextOutlined />,
21
+ FormOutlined: <FormOutlined />,
22
+ SettingOutlined: <SettingOutlined />,
23
+ TeamOutlined: <TeamOutlined />,
24
+ };
25
+
26
+ function childrenOf(parent: RegisteredPage): MenuItem[] {
27
+ return registeredPages
28
+ .filter((page) => !page.hidden && page.parentId === parent.id)
29
+ .map(toMenuItem);
30
+ }
31
+
32
+ function toMenuItem(page: RegisteredPage): MenuItem {
33
+ const children = childrenOf(page);
34
+ return {
35
+ key: page.path,
36
+ label: page.title,
37
+ ...(page.icon === undefined ? {} : { icon: icons[page.icon] }),
38
+ ...(children.length === 0 ? {} : { children }),
39
+ };
40
+ }
41
+
42
+ export const menuItems: NonNullable<MenuProps["items"]> = registeredPages
43
+ .filter((page) => !page.hidden && page.parentId === undefined)
44
+ .map(toMenuItem);
@@ -0,0 +1,48 @@
1
+ import { lazy, type ComponentType, type LazyExoticComponent } from "react";
2
+
3
+ import type { PageRouteDefinition } from "./definition";
4
+
5
+ export interface RegisteredPage extends PageRouteDefinition {
6
+ readonly Component: LazyExoticComponent<ComponentType>;
7
+ readonly sourceDirectory: string;
8
+ }
9
+
10
+ interface PageModule {
11
+ readonly default: ComponentType;
12
+ }
13
+
14
+ const routeModules = import.meta.glob<PageRouteDefinition>("../pages/**/route.ts", {
15
+ eager: true,
16
+ import: "default",
17
+ });
18
+ const pageModules = import.meta.glob<PageModule>("../pages/**/page.tsx");
19
+
20
+ function pageFileFor(routeFile: string): string {
21
+ return `${routeFile.slice(0, -"route.ts".length)}page.tsx`;
22
+ }
23
+
24
+ function comparePages(left: RegisteredPage, right: RegisteredPage): number {
25
+ const byOrder = (left.order ?? 0) - (right.order ?? 0);
26
+ return byOrder === 0 ? left.title.localeCompare(right.title, "zh-CN") : byOrder;
27
+ }
28
+
29
+ export const registeredPages: readonly RegisteredPage[] = Object.entries(routeModules)
30
+ .map(([routeFile, definition]) => {
31
+ const pageFile = pageFileFor(routeFile);
32
+ const loadPage = pageModules[pageFile];
33
+ if (loadPage === undefined) {
34
+ throw new Error(`${routeFile} 缺少同目录 page.tsx,请先运行 pnpm run validate:routes`);
35
+ }
36
+ return {
37
+ ...definition,
38
+ Component: lazy(loadPage),
39
+ sourceDirectory: routeFile.slice(0, -"route.ts".length),
40
+ };
41
+ })
42
+ .sort(comparePages);
43
+
44
+ export function findPageByLocation(pathname: string): RegisteredPage | undefined {
45
+ return [...registeredPages]
46
+ .sort((left, right) => right.path.length - left.path.length)
47
+ .find(({ path }) => path === "/" ? pathname === "/" : pathname === path || pathname.startsWith(`${path}/`));
48
+ }
@@ -0,0 +1,34 @@
1
+ import type { RouteObject } from "react-router-dom";
2
+ import { createBrowserRouter } from "react-router-dom";
3
+ import { Suspense } from "react";
4
+
5
+ import { NotFoundPage } from "../app/NotFoundPage";
6
+ import { MainLayout } from "../layouts/MainLayout";
7
+ import { BusinessProviders } from "../shared/components/business/BusinessProviders";
8
+ import { registeredPages, type RegisteredPage } from "./registry";
9
+
10
+ function pageRoute(page: RegisteredPage): RouteObject {
11
+ const Page = page.Component;
12
+ const element = (
13
+ <Suspense fallback={<div className="page-loading" role="status">正在加载页面…</div>}>
14
+ <Page />
15
+ </Suspense>
16
+ );
17
+ if (page.path === "/") return { index: true, element };
18
+ return { path: page.path.replace(/^\//, ""), element };
19
+ }
20
+
21
+ export const router = createBrowserRouter([
22
+ {
23
+ path: "/",
24
+ element: (
25
+ <BusinessProviders>
26
+ <MainLayout />
27
+ </BusinessProviders>
28
+ ),
29
+ children: [
30
+ ...registeredPages.map(pageRoute),
31
+ { path: "*", element: <NotFoundPage /> },
32
+ ],
33
+ },
34
+ ]);
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * Register application-wide business Context providers here.
5
+ * The protected router mounts this component around every business route.
6
+ */
7
+ export function BusinessProviders({ children }: { readonly children: ReactNode }) {
8
+ return <>{children}</>;
9
+ }