openxiangda-cli 2.0.0-alpha.89 → 2.0.0-alpha.91

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 (77) 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 +3 -3
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +1 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/web/e2e/client-fixture.ts +5 -0
  20. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  21. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  22. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  23. package/template/apps/web/e2e/resources.spec.ts +20 -181
  24. package/template/apps/web/field-protocol.e2e.html +3 -0
  25. package/template/apps/web/package.json +2 -5
  26. package/template/apps/web/src/main.tsx +12 -60
  27. package/template/apps/web/test/contracts.test.ts +52 -104
  28. package/template/openxiangda.config.ts +1 -1
  29. package/template/package.json +1 -3
  30. package/template/packages/contracts/src/generated.ts +1 -0
  31. package/template/scripts/verify-template-budget.mjs +2 -2
  32. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  33. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  34. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  35. package/template/apps/web/src/Shell.tsx +0 -445
  36. package/template/apps/web/src/appearance.tsx +0 -147
  37. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  38. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  39. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  40. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  41. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  42. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  43. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  44. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  45. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  46. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  47. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  48. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  49. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  50. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  51. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  52. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  53. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  54. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  55. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  56. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  57. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  58. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  59. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  60. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  61. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  62. package/template/apps/web/src/data-provider.ts +0 -72
  63. package/template/apps/web/src/platform-client.ts +0 -708
  64. package/template/apps/web/src/runtime-meta.ts +0 -75
  65. package/template/apps/web/src/runtime.tsx +0 -227
  66. package/template/apps/web/src/styles.css +0 -1941
  67. package/template/apps/web/test/address-field.test.ts +0 -66
  68. package/template/apps/web/test/cascade-value.test.ts +0 -68
  69. package/template/apps/web/test/directory-value.test.ts +0 -55
  70. package/template/apps/web/test/field-bounds.test.ts +0 -17
  71. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  72. package/template/apps/web/test/location-field.test.ts +0 -83
  73. package/template/apps/web/test/resource-import.test.ts +0 -143
  74. package/template/apps/web/test/resource-query.test.ts +0 -144
  75. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  76. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  77. 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
- }