openxiangda-cli 2.0.0-alpha.67 → 2.0.0-alpha.69
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.
- package/package.json +4 -4
- package/template/AGENTS.md +12 -7
- package/template/README.md +7 -7
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/resources.spec.ts +11 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/scripts/check.mjs +18 -13
- package/template/apps/web/src/FilePreviewPage.tsx +3 -3
- package/template/apps/web/src/Shell.tsx +323 -33
- package/template/apps/web/src/appearance.tsx +147 -0
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +645 -109
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +319 -0
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +13 -52
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +1 -1
- package/template/apps/web/src/components/resource/resource-import.ts +205 -0
- package/template/apps/web/src/main.tsx +22 -21
- package/template/apps/web/src/platform-client.ts +195 -40
- package/template/apps/web/src/styles.css +455 -220
- package/template/apps/web/test/contracts.test.ts +52 -5
- package/template/apps/web/test/resource-import.test.ts +142 -0
- package/template/openxiangda.config.ts +2 -1
- package/template/package.json +4 -3
- package/template/scripts/verify-template-budget.mjs +5 -0
|
@@ -40,13 +40,60 @@ test('standard surfaces keep authoritative directory/resource selectors and mobi
|
|
|
40
40
|
const client = readFileSync(new URL('../src/platform-client.ts', import.meta.url), 'utf8');
|
|
41
41
|
assert.match(client, /declaredFields\.has\(requestedSort\.field\)/);
|
|
42
42
|
assert.match(client, /if \(!declaredFields\.has\(fieldCode\)\) continue/);
|
|
43
|
+
assert.match(client, /\/api\/auth\/logout/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('admin appearance is a bounded Ant Design theme with compact route chrome', () => {
|
|
47
|
+
const appearance = readFileSync(new URL('../src/appearance.tsx', import.meta.url), 'utf8');
|
|
48
|
+
const main = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8');
|
|
49
|
+
const shell = readFileSync(new URL('../src/Shell.tsx', import.meta.url), 'utf8');
|
|
50
|
+
assert.match(appearance, /'system' \| 'light' \| 'dark'/);
|
|
51
|
+
assert.match(appearance, /antdTheme\.darkAlgorithm/);
|
|
52
|
+
assert.match(appearance, /antdTheme\.defaultAlgorithm/);
|
|
53
|
+
assert.match(appearance, /ConfigProvider/);
|
|
54
|
+
assert.match(appearance, /openxiangda\.admin\.appearance/);
|
|
55
|
+
assert.match(main, /<AppearanceProvider>/);
|
|
56
|
+
assert.match(main, /oxa-global-request-loading/);
|
|
57
|
+
assert.match(shell, /openxiangda\.admin\.route-history/);
|
|
58
|
+
assert.match(shell, /MAX_HISTORY_ITEMS = 8/);
|
|
59
|
+
assert.match(shell, /<Segmented/);
|
|
60
|
+
assert.match(shell, /跟随系统/);
|
|
61
|
+
assert.match(shell, /退出登录/);
|
|
62
|
+
assert.match(shell, /<Header className="oxa-topbar">\s*<Breadcrumb/);
|
|
63
|
+
assert.doesNotMatch(shell, /oxa-topbar-page/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('standard desktop CRUD exposes compact filters, table tools, and lazy audit history', () => {
|
|
67
|
+
const pages = readFileSync(new URL('../src/components/resource/StandardResourcePages.tsx', import.meta.url), 'utf8');
|
|
68
|
+
const crud = readFileSync(new URL('../src/components/resource/GeneratedResourceCrud.tsx', import.meta.url), 'utf8');
|
|
69
|
+
assert.match(pages, /className="oxa-list-surface"/);
|
|
70
|
+
assert.doesNotMatch(pages, /className="oxa-page-heading"/);
|
|
71
|
+
assert.match(crud, /filtersExpanded/);
|
|
72
|
+
assert.match(crud, /更多筛选/);
|
|
73
|
+
assert.match(crud, /columnMenu/);
|
|
74
|
+
assert.match(crud, /densityMenu/);
|
|
75
|
+
assert.match(crud, /列设置/);
|
|
76
|
+
assert.match(crud, /密度/);
|
|
77
|
+
assert.match(crud, /ResourceImportButton/);
|
|
78
|
+
assert.match(crud, /ResourceBatchActions/);
|
|
79
|
+
const batch = readFileSync(new URL('../src/components/resource/ResourceBatchActions.tsx', import.meta.url), 'utf8');
|
|
80
|
+
assert.match(batch, /transactNativeData/);
|
|
81
|
+
assert.match(batch, /批量修改/);
|
|
82
|
+
assert.match(batch, /批量删除/);
|
|
83
|
+
assert.match(batch, /确认导入/);
|
|
84
|
+
assert.match(crud, /!auditExpanded/);
|
|
85
|
+
assert.match(crud, /<Collapse/);
|
|
86
|
+
assert.match(crud, /查看创建、更新和删除的字段变化/);
|
|
43
87
|
});
|
|
44
88
|
|
|
45
89
|
test('runtime metadata stays mount-scoped', () => {
|
|
46
|
-
const mount = readRuntimeMount(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
90
|
+
const mount = readRuntimeMount(
|
|
91
|
+
(name) =>
|
|
92
|
+
({
|
|
93
|
+
'openxiangda-runtime-base': '/view/example-app/',
|
|
94
|
+
'openxiangda-app-code': 'example-app',
|
|
95
|
+
'openxiangda-environment': 'preproduction',
|
|
96
|
+
}[name])
|
|
97
|
+
);
|
|
51
98
|
assert.equal(resolveApplicationBasename(mount), '/view/example-app');
|
|
52
99
|
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import type { DataResourceSurface } from 'openxiangda-contracts/browser';
|
|
4
|
+
import { parseResourceImportMatrix } from '../src/components/resource/resource-import';
|
|
5
|
+
|
|
6
|
+
const surface = {
|
|
7
|
+
generated: true,
|
|
8
|
+
fields: {
|
|
9
|
+
visitorName: { label: '访客姓名', widget: 'text', requiredHint: true },
|
|
10
|
+
visitorCount: { label: '来访人数', widget: 'number' },
|
|
11
|
+
approved: { label: '是否通过', widget: 'boolean' },
|
|
12
|
+
status: {
|
|
13
|
+
label: '状态',
|
|
14
|
+
widget: 'select',
|
|
15
|
+
options: [
|
|
16
|
+
{ label: '待审核', value: 'pending' },
|
|
17
|
+
{ label: '已通过', value: 'approved' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
visitDate: { label: '来访日期', widget: 'date' },
|
|
21
|
+
attachments: { label: '附件', widget: 'file' },
|
|
22
|
+
},
|
|
23
|
+
} as unknown as DataResourceSurface;
|
|
24
|
+
|
|
25
|
+
test('maps exact labels and codes into one canonical create transaction', () => {
|
|
26
|
+
const preview = parseResourceImportMatrix(
|
|
27
|
+
'访客.xlsx',
|
|
28
|
+
[
|
|
29
|
+
['访客姓名', 'visitorCount', '是否通过', '状态'],
|
|
30
|
+
['张三', 2, '是', '待审核'],
|
|
31
|
+
['李四', '3', '否', 'approved'],
|
|
32
|
+
],
|
|
33
|
+
'visitor-reservations',
|
|
34
|
+
surface,
|
|
35
|
+
['visitorName', 'visitorCount', 'approved', 'status']
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
assert.deepEqual(preview.errors, []);
|
|
39
|
+
assert.deepEqual(preview.operations, [
|
|
40
|
+
{
|
|
41
|
+
operation: 'create',
|
|
42
|
+
resourceCode: 'visitor-reservations',
|
|
43
|
+
data: {
|
|
44
|
+
visitorName: '张三',
|
|
45
|
+
visitorCount: 2,
|
|
46
|
+
approved: true,
|
|
47
|
+
status: 'pending',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
operation: 'create',
|
|
52
|
+
resourceCode: 'visitor-reservations',
|
|
53
|
+
data: {
|
|
54
|
+
visitorName: '李四',
|
|
55
|
+
visitorCount: 3,
|
|
56
|
+
approved: false,
|
|
57
|
+
status: 'approved',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('preserves exact dates and rejects impossible calendar dates', () => {
|
|
64
|
+
const valid = parseResourceImportMatrix(
|
|
65
|
+
'日期.csv',
|
|
66
|
+
[
|
|
67
|
+
['访客姓名', '来访日期'],
|
|
68
|
+
['张三', '2026-08-23'],
|
|
69
|
+
],
|
|
70
|
+
'visitor-reservations',
|
|
71
|
+
surface,
|
|
72
|
+
['visitorName', 'visitDate']
|
|
73
|
+
);
|
|
74
|
+
assert.deepEqual(valid.errors, []);
|
|
75
|
+
assert.deepEqual(valid.operations[0], {
|
|
76
|
+
operation: 'create',
|
|
77
|
+
resourceCode: 'visitor-reservations',
|
|
78
|
+
data: { visitorName: '张三', visitDate: '2026-08-23' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const invalid = parseResourceImportMatrix(
|
|
82
|
+
'错误日期.csv',
|
|
83
|
+
[
|
|
84
|
+
['访客姓名', '来访日期'],
|
|
85
|
+
['张三', '2026-02-30'],
|
|
86
|
+
],
|
|
87
|
+
'visitor-reservations',
|
|
88
|
+
surface,
|
|
89
|
+
['visitorName', 'visitDate']
|
|
90
|
+
);
|
|
91
|
+
assert.equal(invalid.operations.length, 0);
|
|
92
|
+
assert.ok(invalid.errors.some((error) => error.includes('日期格式无效')));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('rejects values in a column without a header', () => {
|
|
96
|
+
const preview = parseResourceImportMatrix(
|
|
97
|
+
'空表头.csv',
|
|
98
|
+
[
|
|
99
|
+
['访客姓名', ''],
|
|
100
|
+
['张三', '未声明的数据'],
|
|
101
|
+
],
|
|
102
|
+
'visitor-reservations',
|
|
103
|
+
surface,
|
|
104
|
+
['visitorName']
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
assert.equal(preview.operations.length, 0);
|
|
108
|
+
assert.ok(preview.errors.some((error) => error.includes('缺少表头')));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('rejects unknown, managed-file and invalid cells before submission', () => {
|
|
112
|
+
const preview = parseResourceImportMatrix(
|
|
113
|
+
'错误.csv',
|
|
114
|
+
[
|
|
115
|
+
['访客姓名', '是否通过', '附件', '任意列'],
|
|
116
|
+
['', '不确定', 'https://untrusted.example/file', 'value'],
|
|
117
|
+
],
|
|
118
|
+
'visitor-reservations',
|
|
119
|
+
surface,
|
|
120
|
+
['visitorName', 'approved', 'attachments']
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
assert.equal(preview.operations.length, 0);
|
|
124
|
+
assert.ok(preview.errors.some((error) => error.includes('不是已声明字段')));
|
|
125
|
+
assert.ok(preview.errors.some((error) => error.includes('必须通过平台上传组件')));
|
|
126
|
+
assert.ok(preview.errors.some((error) => error.includes('请填写是或否')));
|
|
127
|
+
assert.ok(preview.errors.some((error) => error.includes('访客姓名为必填项')));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('keeps the atomic import boundary at one hundred rows', () => {
|
|
131
|
+
const preview = parseResourceImportMatrix(
|
|
132
|
+
'过大.csv',
|
|
133
|
+
[['访客姓名'], ...Array.from({ length: 101 }, (_, index) => [`访客 ${index + 1}`])],
|
|
134
|
+
'visitor-reservations',
|
|
135
|
+
surface,
|
|
136
|
+
['visitorName']
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
assert.equal(preview.rows.length, 100);
|
|
140
|
+
assert.equal(preview.operations.length, 0);
|
|
141
|
+
assert.ok(preview.errors.some((error) => error.includes('单次最多导入 100 行')));
|
|
142
|
+
});
|
package/template/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"node": ">=22.12"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"openxiangda": "openxiangda",
|
|
10
11
|
"prepare": "pnpm workspace:build",
|
|
11
12
|
"workspace:build": "pnpm --filter @app/contracts build",
|
|
12
13
|
"predev": "pnpm workspace:build",
|
|
@@ -21,9 +22,9 @@
|
|
|
21
22
|
"build": "pnpm --recursive build"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"openxiangda-cli": "2.0.0-alpha.
|
|
25
|
-
"openxiangda-contracts": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
25
|
+
"openxiangda-cli": "2.0.0-alpha.69",
|
|
26
|
+
"openxiangda-contracts": "2.0.0-alpha.32",
|
|
27
|
+
"openxiangda-devkit-core": "2.0.0-alpha.42",
|
|
27
28
|
"typescript": "5.9.3"
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -31,6 +31,11 @@ const productionFiles = files.filter(file => {
|
|
|
31
31
|
);
|
|
32
32
|
});
|
|
33
33
|
const source = productionFiles.map(file => readFileSync(file, 'utf8')).join('\n');
|
|
34
|
+
const agents = readFileSync(resolve(root, 'AGENTS.md'), 'utf8');
|
|
35
|
+
if (!agents.includes('pnpm openxiangda')) throw new Error('TEMPLATE_AGENT_PINNED_CLI_MISSING');
|
|
36
|
+
if (agents.includes('RoleSession') && !agents.includes('Do not select RoleSession')) {
|
|
37
|
+
throw new Error('TEMPLATE_AGENT_ROLE_SESSION_PATH_ACTIVE');
|
|
38
|
+
}
|
|
34
39
|
for (const marker of ['@umijs/', 'openxiangda-' + 'admin', 'openxiangda-' + 'user', 'instrument_query', 'instrument_save', 'function.invoke', 'departmentScopeKey', 'instrumentScopeKey', 'adminUserIds', 'college-' + 'am', 'user-' + 'wang', 'dept-' + 'am-test']) {
|
|
35
40
|
if (source.includes(marker)) throw new Error(`TEMPLATE_FORBIDDEN_MARKER:${marker}`);
|
|
36
41
|
}
|