openxiangda-cli 2.0.0-alpha.86 → 2.0.0-alpha.90

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 (78) hide show
  1. package/README.md +2 -2
  2. package/bin/run.js +1 -0
  3. package/dist/commands/skill.d.ts +26 -0
  4. package/dist/commands/skill.d.ts.map +1 -0
  5. package/dist/commands/skill.js +43 -0
  6. package/dist/commands/skill.js.map +1 -0
  7. package/dist/create-workspace.d.ts.map +1 -1
  8. package/dist/create-workspace.js +3 -3
  9. package/dist/create-workspace.js.map +1 -1
  10. package/dist/toolchain-capsule.d.ts.map +1 -1
  11. package/dist/toolchain-capsule.js +4 -8
  12. package/dist/toolchain-capsule.js.map +1 -1
  13. package/package.json +6 -7
  14. package/template/AGENTS.md +2 -1
  15. package/template/README.md +5 -5
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +14 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/server/test/smoke.test.ts +3 -1
  20. package/template/apps/web/e2e/client-fixture.ts +5 -0
  21. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  22. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  23. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  24. package/template/apps/web/e2e/resources.spec.ts +20 -181
  25. package/template/apps/web/field-protocol.e2e.html +3 -0
  26. package/template/apps/web/package.json +2 -5
  27. package/template/apps/web/src/main.tsx +12 -60
  28. package/template/apps/web/test/contracts.test.ts +52 -104
  29. package/template/openxiangda.config.ts +2 -2
  30. package/template/package.json +1 -3
  31. package/template/packages/contracts/src/generated.ts +27 -0
  32. package/template/scripts/verify-template-budget.mjs +2 -2
  33. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  34. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  35. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  36. package/template/apps/web/src/Shell.tsx +0 -445
  37. package/template/apps/web/src/appearance.tsx +0 -147
  38. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  39. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  40. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  41. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  42. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  43. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  44. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  45. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  46. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  47. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  48. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  49. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  50. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  51. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  52. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  53. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  54. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  55. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  56. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  57. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  58. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  59. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  60. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  61. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  62. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  63. package/template/apps/web/src/data-provider.ts +0 -72
  64. package/template/apps/web/src/platform-client.ts +0 -708
  65. package/template/apps/web/src/runtime-meta.ts +0 -75
  66. package/template/apps/web/src/runtime.tsx +0 -227
  67. package/template/apps/web/src/styles.css +0 -1941
  68. package/template/apps/web/test/address-field.test.ts +0 -66
  69. package/template/apps/web/test/cascade-value.test.ts +0 -68
  70. package/template/apps/web/test/directory-value.test.ts +0 -55
  71. package/template/apps/web/test/field-bounds.test.ts +0 -17
  72. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  73. package/template/apps/web/test/location-field.test.ts +0 -83
  74. package/template/apps/web/test/resource-import.test.ts +0 -143
  75. package/template/apps/web/test/resource-query.test.ts +0 -144
  76. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  77. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  78. package/template/apps/web/test/subtable-value.test.ts +0 -144
@@ -1,324 +0,0 @@
1
- import {
2
- CloseOutlined,
3
- DownloadOutlined,
4
- FileExcelOutlined,
5
- FileImageOutlined,
6
- FilePdfOutlined,
7
- FileWordOutlined,
8
- PaperClipOutlined,
9
- } from '@ant-design/icons';
10
- import { Alert, Button, Empty, Result, Segmented, Spin, Typography } from 'antd';
11
- import type { DataFilePreview } from 'openxiangda-contracts/browser';
12
- import { useEffect, useMemo, useRef, useState } from 'react';
13
- import { useParams } from 'react-router-dom';
14
- import {
15
- downloadDataFile,
16
- fetchDataFileBlob,
17
- loadDataFilePreview,
18
- } from './platform-client';
19
- import { formatManagedFileSize } from './components/platform-fields/AttachmentFileList';
20
-
21
- const MAX_SPREADSHEET_ROWS = 5_000;
22
- const MAX_SPREADSHEET_COLUMNS = 200;
23
-
24
- function DocxDocument({ blob }: { blob: Blob }) {
25
- const styleRef = useRef<HTMLDivElement>(null);
26
- const bodyRef = useRef<HTMLDivElement>(null);
27
- const [error, setError] = useState('');
28
- useEffect(() => {
29
- let active = true;
30
- const body = bodyRef.current;
31
- const style = styleRef.current;
32
- if (!body || !style) return;
33
- body.replaceChildren();
34
- style.replaceChildren();
35
- setError('');
36
- void import('docx-preview')
37
- .then(({ renderAsync }) =>
38
- renderAsync(blob, body, style, {
39
- className: 'oxa-docx-document',
40
- inWrapper: true,
41
- ignoreFonts: false,
42
- ignoreHeight: false,
43
- ignoreWidth: false,
44
- renderChanges: false,
45
- renderHeaders: true,
46
- renderFooters: true,
47
- })
48
- )
49
- .catch(reason => {
50
- if (active) {
51
- setError(reason instanceof Error ? reason.message : String(reason));
52
- }
53
- });
54
- return () => {
55
- active = false;
56
- body.replaceChildren();
57
- style.replaceChildren();
58
- };
59
- }, [blob]);
60
- if (error) {
61
- return <Alert title="Word 文档解析失败" description={error} type="error" showIcon />;
62
- }
63
- return (
64
- <div className="oxa-docx-preview">
65
- <div ref={styleRef} />
66
- <div ref={bodyRef} />
67
- </div>
68
- );
69
- }
70
-
71
- function SpreadsheetDocument({ blob }: { blob: Blob }) {
72
- const [sheetName, setSheetName] = useState('');
73
- const [sheets, setSheets] = useState<Record<string, unknown[][]>>({});
74
- const [error, setError] = useState('');
75
- useEffect(() => {
76
- let active = true;
77
- setError('');
78
- setSheets({});
79
- void Promise.all([blob.arrayBuffer(), import('xlsx')])
80
- .then(([buffer, XLSX]) => {
81
- const workbook = XLSX.read(buffer, {
82
- type: 'array',
83
- cellFormula: false,
84
- cellHTML: false,
85
- cellText: true,
86
- });
87
- if (!active) return;
88
- setSheets(
89
- Object.fromEntries(
90
- workbook.SheetNames.slice(0, 50).map(name => [
91
- name,
92
- (
93
- XLSX.utils.sheet_to_json<unknown[]>(workbook.Sheets[name]!, {
94
- header: 1,
95
- raw: false,
96
- blankrows: false,
97
- defval: '',
98
- }) as unknown[][]
99
- )
100
- .slice(0, MAX_SPREADSHEET_ROWS)
101
- .map(row => row.slice(0, MAX_SPREADSHEET_COLUMNS)),
102
- ])
103
- )
104
- );
105
- setSheetName(workbook.SheetNames[0] || '');
106
- })
107
- .catch(reason => {
108
- if (active) {
109
- setError(reason instanceof Error ? reason.message : String(reason));
110
- }
111
- });
112
- return () => {
113
- active = false;
114
- };
115
- }, [blob]);
116
- const rows = sheets[sheetName] || [];
117
- if (error) {
118
- return <Alert title="Excel 文件解析失败" description={error} type="error" showIcon />;
119
- }
120
- const sheetNames = Object.keys(sheets);
121
- if (!sheetNames.length) return <Spin description="正在解析 Excel 文件" />;
122
- return (
123
- <div className="oxa-spreadsheet-preview">
124
- {sheetNames.length > 1 && (
125
- <Segmented
126
- block
127
- onChange={value => setSheetName(String(value))}
128
- options={sheetNames}
129
- value={sheetName}
130
- />
131
- )}
132
- <div className="oxa-spreadsheet-scroll">
133
- {rows.length ? (
134
- <table>
135
- <tbody>
136
- {rows.map((row, rowIndex) => (
137
- <tr key={rowIndex}>
138
- <th>{rowIndex + 1}</th>
139
- {row.map((cell, columnIndex) => (
140
- <td key={columnIndex}>{String(cell ?? '')}</td>
141
- ))}
142
- </tr>
143
- ))}
144
- </tbody>
145
- </table>
146
- ) : (
147
- <Empty description="当前工作表没有可显示的数据" />
148
- )}
149
- </div>
150
- {(rows.length >= MAX_SPREADSHEET_ROWS ||
151
- rows.some(row => row.length >= MAX_SPREADSHEET_COLUMNS)) && (
152
- <Alert
153
- banner
154
- title={`为保证浏览器稳定,仅展示前 ${MAX_SPREADSHEET_ROWS} 行、${MAX_SPREADSHEET_COLUMNS} 列`}
155
- type="warning"
156
- />
157
- )}
158
- </div>
159
- );
160
- }
161
-
162
- function PreviewBody({ preview, blob }: { preview: DataFilePreview; blob?: Blob }) {
163
- const objectUrl = useMemo(() => (blob ? URL.createObjectURL(blob) : ''), [blob]);
164
- useEffect(
165
- () => () => {
166
- if (objectUrl) URL.revokeObjectURL(objectUrl);
167
- },
168
- [objectUrl]
169
- );
170
- if (!preview.canPreview || preview.renderMode === 'download') {
171
- return (
172
- <Empty
173
- description={
174
- <div>
175
- <Typography.Text strong>当前文件无法在线预览</Typography.Text>
176
- <br />
177
- <Typography.Text type="secondary">
178
- {preview.unsupportedReason || '请下载后使用本地应用打开'}
179
- </Typography.Text>
180
- </div>
181
- }
182
- />
183
- );
184
- }
185
- if (!blob) return <Spin description="正在读取文件内容" />;
186
- if (preview.previewType === 'image') {
187
- return <img alt={preview.file.name} className="oxa-preview-image" src={objectUrl} />;
188
- }
189
- if (preview.renderMode === 'pdfjs') {
190
- return (
191
- <iframe
192
- className="oxa-preview-frame"
193
- src={objectUrl}
194
- title={`${preview.file.name} PDF 预览`}
195
- />
196
- );
197
- }
198
- if (preview.renderMode === 'docx-html') return <DocxDocument blob={blob} />;
199
- if (
200
- preview.renderMode === 'excel-client' ||
201
- preview.renderMode === 'excel-basic'
202
- ) {
203
- return <SpreadsheetDocument blob={blob} />;
204
- }
205
- return (
206
- <Empty description={preview.unsupportedReason || '当前文件没有可用的预览器'} />
207
- );
208
- }
209
-
210
- function previewIcon(preview?: DataFilePreview) {
211
- if (preview?.previewType === 'image') return <FileImageOutlined />;
212
- if (preview?.previewType === 'pdf') return <FilePdfOutlined />;
213
- if (preview?.previewType === 'spreadsheet') return <FileExcelOutlined />;
214
- if (preview?.previewType === 'office') return <FileWordOutlined />;
215
- return <PaperClipOutlined />;
216
- }
217
-
218
- export function FilePreviewPage() {
219
- const { resourceCode = '', fileId = '' } = useParams();
220
- const [preview, setPreview] = useState<DataFilePreview>();
221
- const [blob, setBlob] = useState<Blob>();
222
- const [error, setError] = useState('');
223
- const [downloading, setDownloading] = useState(false);
224
- useEffect(() => {
225
- let active = true;
226
- setPreview(undefined);
227
- setBlob(undefined);
228
- setError('');
229
- if (!resourceCode || !fileId) {
230
- setError('OPENXIANGDA_NATIVE_DATA_FILE_PREVIEW_PATH_INVALID');
231
- return () => {
232
- active = false;
233
- };
234
- }
235
- void loadDataFilePreview(resourceCode, fileId)
236
- .then(async metadata => {
237
- if (!active) return;
238
- setPreview(metadata);
239
- document.title = `${metadata.file.name} - 文件预览`;
240
- if (metadata.canPreview && metadata.renderMode !== 'download') {
241
- const content = await fetchDataFileBlob(resourceCode, fileId);
242
- if (active) setBlob(content);
243
- }
244
- })
245
- .catch(reason => {
246
- if (active) setError(reason instanceof Error ? reason.message : String(reason));
247
- });
248
- return () => {
249
- active = false;
250
- };
251
- }, [fileId, resourceCode]);
252
-
253
- const download = async () => {
254
- if (!resourceCode || !fileId) return;
255
- setDownloading(true);
256
- try {
257
- await downloadDataFile(
258
- resourceCode,
259
- fileId,
260
- preview?.file.name || 'attachment'
261
- );
262
- } catch (reason) {
263
- setError(reason instanceof Error ? reason.message : String(reason));
264
- } finally {
265
- setDownloading(false);
266
- }
267
- };
268
- return (
269
- <div className="oxa-file-preview-page">
270
- <header className="oxa-file-preview-toolbar">
271
- <div className="oxa-file-preview-title">
272
- {previewIcon(preview)}
273
- <div>
274
- <strong>{preview?.file.name || '文件预览'}</strong>
275
- <span>
276
- {preview
277
- ? `${preview.extension.toUpperCase() || '文件'} · ${formatManagedFileSize(preview.file.size)} · 只读预览`
278
- : '正在加载预览信息'}
279
- </span>
280
- </div>
281
- </div>
282
- <div>
283
- {resourceCode && fileId && (
284
- <Button
285
- icon={<DownloadOutlined />}
286
- loading={downloading}
287
- onClick={() => void download()}
288
- >
289
- 下载文件
290
- </Button>
291
- )}
292
- <Button icon={<CloseOutlined />} onClick={() => window.close()}>
293
- 关闭
294
- </Button>
295
- </div>
296
- </header>
297
- <main className="oxa-file-preview-body">
298
- {error ? (
299
- <Result
300
- status="error"
301
- title="文件预览加载失败"
302
- subTitle={error}
303
- extra={
304
- resourceCode && fileId ? (
305
- <Button
306
- icon={<DownloadOutlined />}
307
- loading={downloading}
308
- onClick={() => void download()}
309
- type="primary"
310
- >
311
- 下载文件
312
- </Button>
313
- ) : undefined
314
- }
315
- />
316
- ) : preview ? (
317
- <PreviewBody blob={blob} preview={preview} />
318
- ) : (
319
- <Spin description="正在确认文件权限与预览能力" />
320
- )}
321
- </main>
322
- </div>
323
- );
324
- }
@@ -1,128 +0,0 @@
1
- import { Select, Typography } from 'antd';
2
- import { useEffect, useState } from 'react';
3
- import type {
4
- RoleSubjectChoice,
5
- RoleSubjectChoicePage,
6
- } from 'openxiangda-contracts/browser';
7
- import { loadRoleSubjects } from './platform-client';
8
-
9
- function mergeSubjects(
10
- current: RoleSubjectChoice[],
11
- incoming: RoleSubjectChoice[]
12
- ) {
13
- const merged = new Map(current.map(item => [item.subjectKey, item]));
14
- for (const item of incoming) merged.set(item.subjectKey, item);
15
- return [...merged.values()];
16
- }
17
-
18
- export function RoleSubjectSelect({
19
- disabled,
20
- initialPage,
21
- onChange,
22
- value,
23
- }: {
24
- disabled?: boolean;
25
- initialPage: RoleSubjectChoicePage;
26
- onChange: (value: string) => void;
27
- value?: string;
28
- }) {
29
- const [keyword, setKeyword] = useState('');
30
- const [options, setOptions] = useState(initialPage.items);
31
- const [cursor, setCursor] = useState(initialPage.nextCursor);
32
- const [loading, setLoading] = useState(false);
33
- const [loadError, setLoadError] = useState('');
34
-
35
- useEffect(() => {
36
- let active = true;
37
- const timer = window.setTimeout(() => {
38
- setLoading(true);
39
- setLoadError('');
40
- void loadRoleSubjects({ keyword, limit: 20 })
41
- .then(page => {
42
- if (!active) return;
43
- setOptions(current =>
44
- mergeSubjects(
45
- current.filter(item => item.subjectKey === value),
46
- page.items
47
- )
48
- );
49
- setCursor(page.nextCursor);
50
- })
51
- .catch(reason => {
52
- if (!active) return;
53
- setLoadError(
54
- reason instanceof Error ? reason.message : '应用角色读取失败'
55
- );
56
- })
57
- .finally(() => {
58
- if (active) setLoading(false);
59
- });
60
- }, keyword ? 250 : 0);
61
- return () => {
62
- active = false;
63
- window.clearTimeout(timer);
64
- };
65
- }, [keyword, value]);
66
-
67
- const loadMore = async () => {
68
- if (!cursor || loading) return;
69
- setLoading(true);
70
- setLoadError('');
71
- try {
72
- const page = await loadRoleSubjects({ keyword, cursor, limit: 20 });
73
- setOptions(current => mergeSubjects(current, page.items));
74
- setCursor(page.nextCursor);
75
- } catch (reason) {
76
- setLoadError(
77
- reason instanceof Error ? reason.message : '更多应用角色读取失败'
78
- );
79
- } finally {
80
- setLoading(false);
81
- }
82
- };
83
-
84
- return (
85
- <Select
86
- aria-label="选择应用角色"
87
- className="oxa-role-switch-select"
88
- disabled={disabled}
89
- filterOption={false}
90
- loading={loading}
91
- notFoundContent={
92
- loadError ? (
93
- <Typography.Text type="danger">应用角色读取失败,请重试</Typography.Text>
94
- ) : undefined
95
- }
96
- onChange={onChange}
97
- onPopupScroll={event => {
98
- const target = event.currentTarget;
99
- if (
100
- target.scrollTop + target.clientHeight >=
101
- target.scrollHeight - 12
102
- ) {
103
- void loadMore();
104
- }
105
- }}
106
- onSearch={setKeyword}
107
- optionRender={option => {
108
- const subject = options.find(item => item.subjectKey === option.value);
109
- return (
110
- <div className="oxa-role-switch-option">
111
- <strong>{subject?.role.name || option.label}</strong>
112
- {subject?.role.description && (
113
- <span>{subject.role.description}</span>
114
- )}
115
- </div>
116
- );
117
- }}
118
- options={options.map(subject => ({
119
- label: subject.role.name,
120
- value: subject.subjectKey,
121
- }))}
122
- placeholder="选择当前应用角色"
123
- showSearch
124
- status={loadError ? 'error' : undefined}
125
- value={value}
126
- />
127
- );
128
- }