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,338 +0,0 @@
1
- import {
2
- CloseOutlined,
3
- EnvironmentOutlined,
4
- } from '@ant-design/icons';
5
- import {
6
- Alert,
7
- Button,
8
- Cascader,
9
- Drawer,
10
- Input,
11
- Select,
12
- Space,
13
- Spin,
14
- Typography,
15
- } from 'antd';
16
- import type {
17
- LabeledValue,
18
- StableAddressValue,
19
- } from 'openxiangda-contracts/browser';
20
- import { useCallback, useEffect, useMemo, useState } from 'react';
21
- import {
22
- loadChinaDivisions,
23
- type ChinaDivisionListItem,
24
- } from '../../platform-client';
25
- import {
26
- ADDRESS_LEVELS,
27
- addressControlValue,
28
- addressDisplay,
29
- addressPath,
30
- addressStoredValue,
31
- type AddressLevel,
32
- } from './address-value';
33
-
34
- interface AddressOption extends LabeledValue {
35
- hasChildren: boolean;
36
- isLeaf: boolean;
37
- children?: AddressOption[];
38
- }
39
-
40
- const LEVEL_LABELS: Record<AddressLevel, string> = {
41
- province: '省份',
42
- city: '城市',
43
- district: '区县',
44
- street: '街道',
45
- };
46
-
47
- function divisionOption(item: ChinaDivisionListItem): AddressOption {
48
- return {
49
- label: item.name,
50
- value: item.adcode,
51
- hasChildren: item.hasChildren,
52
- isLeaf: !item.hasChildren,
53
- };
54
- }
55
-
56
- function snapshotBranch(path: LabeledValue[], index = 0): AddressOption[] {
57
- const item = path[index];
58
- if (!item) return [];
59
- const children = snapshotBranch(path, index + 1);
60
- return [{
61
- ...item,
62
- hasChildren: children.length > 0,
63
- isLeaf: children.length === 0,
64
- ...(children.length ? { children } : {}),
65
- }];
66
- }
67
-
68
- function mergeStoredPath(
69
- options: AddressOption[],
70
- path: LabeledValue[],
71
- index = 0
72
- ): AddressOption[] {
73
- const stored = path[index];
74
- if (!stored) return options;
75
- let matched = false;
76
- const next = options.map(option => {
77
- if (option.value !== stored.value) return option;
78
- matched = true;
79
- const children = mergeStoredPath(option.children || [], path, index + 1);
80
- return {
81
- ...option,
82
- label: stored.label || option.label,
83
- ...(children.length ? { children, isLeaf: false } : {}),
84
- };
85
- });
86
- return matched ? next : [...next, ...snapshotBranch(path, index)];
87
- }
88
-
89
- function replaceChildren(
90
- options: AddressOption[],
91
- path: string[],
92
- children: AddressOption[],
93
- index = 0
94
- ): AddressOption[] {
95
- const current = path[index];
96
- return options.map(option => {
97
- if (option.value !== current) return option;
98
- if (index === path.length - 1) {
99
- return {
100
- ...option,
101
- children,
102
- hasChildren: children.length > 0,
103
- isLeaf: children.length === 0,
104
- };
105
- }
106
- return {
107
- ...option,
108
- children: replaceChildren(option.children || [], path, children, index + 1),
109
- };
110
- });
111
- }
112
-
113
- function optionSnapshot(option: AddressOption): LabeledValue {
114
- return { label: option.label, value: option.value };
115
- }
116
-
117
- export function AddressValueDisplay({ value }: { value: StableAddressValue }) {
118
- const parts = addressPath(value);
119
- return (
120
- <span className="oxa-address-value">
121
- <Typography.Text>{addressDisplay(value) || '-'}</Typography.Text>
122
- {parts.length > 0 && (
123
- <Typography.Text type="secondary">
124
- {parts.map(item => item.label).join(' / ')}
125
- </Typography.Text>
126
- )}
127
- </span>
128
- );
129
- }
130
-
131
- export function AddressField({
132
- value,
133
- onChange,
134
- disabled = false,
135
- mobile = false,
136
- detailEnabled = true,
137
- }: {
138
- value?: StableAddressValue;
139
- onChange?: (value: StableAddressValue | undefined) => void;
140
- disabled?: boolean;
141
- mobile?: boolean;
142
- detailEnabled?: boolean;
143
- }) {
144
- const storedPath = useMemo(() => addressPath(value), [value]);
145
- const [options, setOptions] = useState<AddressOption[]>(() =>
146
- snapshotBranch(storedPath)
147
- );
148
- const [loading, setLoading] = useState(false);
149
- const [error, setError] = useState('');
150
- const [mobileOpen, setMobileOpen] = useState(false);
151
- const [draft, setDraft] = useState<StableAddressValue | undefined>(value);
152
- const [mobileOptions, setMobileOptions] = useState<AddressOption[][]>([]);
153
-
154
- const loadOptions = useCallback(async (parentAdcode?: string) =>
155
- (await loadChinaDivisions(parentAdcode)).map(divisionOption), []);
156
-
157
- useEffect(() => {
158
- let active = true;
159
- setLoading(true);
160
- loadOptions()
161
- .then(items => {
162
- if (active) setOptions(mergeStoredPath(items, storedPath));
163
- })
164
- .catch(reason => {
165
- if (active) setError(reason instanceof Error ? reason.message : String(reason));
166
- })
167
- .finally(() => {
168
- if (active) setLoading(false);
169
- });
170
- return () => {
171
- active = false;
172
- };
173
- }, [loadOptions, storedPath]);
174
-
175
- const loadChildren = async (selected: AddressOption[]) => {
176
- const target = selected[selected.length - 1];
177
- if (!target) return;
178
- setError('');
179
- try {
180
- const children = await loadOptions(target.value);
181
- setOptions(current => replaceChildren(
182
- current,
183
- selected.map(item => item.value),
184
- children
185
- ));
186
- } catch (reason) {
187
- setError(reason instanceof Error ? reason.message : String(reason));
188
- }
189
- };
190
-
191
- const hydrateMobile = async (stored?: StableAddressValue) => {
192
- setLoading(true);
193
- setError('');
194
- try {
195
- const path = addressPath(stored);
196
- const lists: AddressOption[][] = [];
197
- lists[0] = await loadOptions();
198
- for (let index = 0; index < path.length && index < ADDRESS_LEVELS.length - 1; index += 1) {
199
- lists[index + 1] = await loadOptions(path[index]?.value);
200
- }
201
- setMobileOptions(lists);
202
- } catch (reason) {
203
- setError(reason instanceof Error ? reason.message : String(reason));
204
- } finally {
205
- setLoading(false);
206
- }
207
- };
208
-
209
- const openMobile = () => {
210
- setDraft(value);
211
- setMobileOpen(true);
212
- void hydrateMobile(value);
213
- };
214
-
215
- const selectMobileLevel = async (index: number, selectedValue?: string) => {
216
- const currentPath = addressPath(draft).slice(0, index);
217
- const selected = mobileOptions[index]?.find(item => item.value === selectedValue);
218
- const nextPath = selected ? [...currentPath, optionSnapshot(selected)] : currentPath;
219
- const next = addressStoredValue(nextPath, draft?.detail);
220
- setDraft(next);
221
- setMobileOptions(current => current.slice(0, index + 1));
222
- if (selected?.hasChildren && index < ADDRESS_LEVELS.length - 1) {
223
- try {
224
- const children = await loadOptions(selected.value);
225
- setMobileOptions(current => {
226
- const lists = current.slice(0, index + 1);
227
- lists[index + 1] = children;
228
- return lists;
229
- });
230
- } catch (reason) {
231
- setError(reason instanceof Error ? reason.message : String(reason));
232
- }
233
- }
234
- };
235
-
236
- const detailInput = (current?: StableAddressValue, commit?: (next?: StableAddressValue) => void) =>
237
- detailEnabled ? (
238
- <Input
239
- disabled={disabled || !addressPath(current).length}
240
- maxLength={4000}
241
- onChange={event => commit?.(
242
- addressStoredValue(addressPath(current), event.target.value)
243
- )}
244
- placeholder="请输入详细门牌地址"
245
- value={current?.detail || ''}
246
- />
247
- ) : null;
248
-
249
- if (mobile) {
250
- return (
251
- <div className="oxa-address-field oxa-mobile-address-field">
252
- <Button
253
- block
254
- disabled={disabled}
255
- icon={<EnvironmentOutlined />}
256
- onClick={openMobile}
257
- >
258
- {addressDisplay(value) || '请选择行政区地址'}
259
- </Button>
260
- {value && !disabled && (
261
- <Button
262
- aria-label="清除地址"
263
- icon={<CloseOutlined />}
264
- onClick={() => onChange?.(undefined)}
265
- type="text"
266
- />
267
- )}
268
- <Drawer
269
- destroyOnHidden
270
- onClose={() => setMobileOpen(false)}
271
- open={mobileOpen}
272
- placement="bottom"
273
- size="85vh"
274
- title="选择行政区地址"
275
- >
276
- {loading ? <Spin /> : (
277
- <div className="oxa-address-mobile-levels">
278
- {ADDRESS_LEVELS.map((level, index) => {
279
- const selected = addressPath(draft)[index]?.value;
280
- const choices = mobileOptions[index] || [];
281
- if (index > 0 && !choices.length && !selected) return null;
282
- return (
283
- <Select
284
- allowClear
285
- key={level}
286
- onChange={next => void selectMobileLevel(index, next)}
287
- options={choices}
288
- placeholder={`请选择${LEVEL_LABELS[level]}`}
289
- value={selected}
290
- />
291
- );
292
- })}
293
- {detailInput(draft, setDraft)}
294
- </div>
295
- )}
296
- {error && <Alert showIcon title={error} type="error" />}
297
- <Space className="oxa-address-mobile-actions">
298
- <Button onClick={() => setMobileOpen(false)}>取消</Button>
299
- <Button
300
- disabled={!addressPath(draft).length}
301
- onClick={() => {
302
- onChange?.(draft);
303
- setMobileOpen(false);
304
- }}
305
- type="primary"
306
- >
307
- 确定
308
- </Button>
309
- </Space>
310
- </Drawer>
311
- </div>
312
- );
313
- }
314
-
315
- return (
316
- <div className="oxa-address-field">
317
- <Cascader
318
- allowClear
319
- changeOnSelect
320
- disabled={disabled}
321
- loadData={selected => void loadChildren(selected as AddressOption[])}
322
- loading={loading}
323
- onChange={(_path, selected) => onChange?.(
324
- addressStoredValue(
325
- (selected as AddressOption[]).map(optionSnapshot),
326
- value?.detail
327
- )
328
- )}
329
- options={options}
330
- placeholder="请选择行政区地址"
331
- showSearch={false}
332
- value={addressControlValue(value)}
333
- />
334
- {detailInput(value, onChange)}
335
- {error && <Alert showIcon title={error} type="error" />}
336
- </div>
337
- );
338
- }
@@ -1,245 +0,0 @@
1
- import {
2
- CloseOutlined,
3
- DownloadOutlined,
4
- EyeOutlined,
5
- FileExcelOutlined,
6
- FileImageOutlined,
7
- FilePdfOutlined,
8
- FileTextOutlined,
9
- FileWordOutlined,
10
- PaperClipOutlined,
11
- } from '@ant-design/icons';
12
- import { App, Button, Image, Space } from 'antd';
13
- import type { DataFileRef } from 'openxiangda-contracts/browser';
14
- import { useEffect, useMemo, useState } from 'react';
15
- import {
16
- downloadDataFile,
17
- fetchDataFileBlob,
18
- loadDataFilePreview,
19
- } from '../../platform-client';
20
- import { attachmentPreviewPath } from '../../runtime-meta';
21
-
22
- interface PreviewImage {
23
- id: string;
24
- name: string;
25
- src: string;
26
- }
27
-
28
- const IMAGE_EXTENSIONS = new Set([
29
- 'avif',
30
- 'bmp',
31
- 'gif',
32
- 'ico',
33
- 'jpeg',
34
- 'jpg',
35
- 'png',
36
- 'svg',
37
- 'webp',
38
- ]);
39
-
40
- function extension(name: string) {
41
- return name.toLowerCase().match(/\.([a-z0-9]+)$/)?.[1] || '';
42
- }
43
-
44
- function isImage(file: DataFileRef) {
45
- return (
46
- file.contentType.toLowerCase().startsWith('image/') ||
47
- IMAGE_EXTENSIONS.has(extension(file.name))
48
- );
49
- }
50
-
51
- function fileIcon(file: DataFileRef) {
52
- const suffix = extension(file.name);
53
- if (isImage(file)) return <FileImageOutlined />;
54
- if (suffix === 'pdf') return <FilePdfOutlined />;
55
- if (suffix === 'doc' || suffix === 'docx') return <FileWordOutlined />;
56
- if (suffix === 'xls' || suffix === 'xlsx') return <FileExcelOutlined />;
57
- if (suffix === 'txt' || suffix === 'csv') return <FileTextOutlined />;
58
- return <PaperClipOutlined />;
59
- }
60
-
61
- export function formatManagedFileSize(value: number) {
62
- if (value < 1024) return `${value} B`;
63
- if (value < 1024 * 1024) return `${Math.ceil(value / 1024)} KB`;
64
- return `${(value / 1024 / 1024).toFixed(1)} MB`;
65
- }
66
-
67
- function revokeImages(images: PreviewImage[]) {
68
- images.forEach(image => URL.revokeObjectURL(image.src));
69
- }
70
-
71
- function openIsolatedWindow(url: string) {
72
- const target = window.open('about:blank', '_blank');
73
- if (!target) return false;
74
- target.opener = null;
75
- target.location.replace(url);
76
- return true;
77
- }
78
-
79
- export function AttachmentFileList({
80
- files,
81
- resourceCode = 'resources',
82
- removable = false,
83
- onRemove,
84
- }: {
85
- files: DataFileRef[];
86
- resourceCode?: string;
87
- removable?: boolean;
88
- onRemove?: (file: DataFileRef) => void;
89
- }) {
90
- const { message } = App.useApp();
91
- const [openingId, setOpeningId] = useState('');
92
- const [downloadingId, setDownloadingId] = useState('');
93
- const [images, setImages] = useState<PreviewImage[]>([]);
94
- const [imageOpen, setImageOpen] = useState(false);
95
- const [currentImage, setCurrentImage] = useState(0);
96
- const imageFiles = useMemo(() => files.filter(isImage), [files]);
97
-
98
- useEffect(() => () => revokeImages(images), [images]);
99
-
100
- const closeImages = () => {
101
- setImageOpen(false);
102
- setImages(current => {
103
- revokeImages(current);
104
- return [];
105
- });
106
- };
107
-
108
- const previewImage = async (file: DataFileRef) => {
109
- setOpeningId(file.id);
110
- try {
111
- const settled = await Promise.allSettled(
112
- imageFiles.map(async candidate => {
113
- const preview = await loadDataFilePreview(resourceCode, candidate.id);
114
- if (!preview.canPreview || preview.previewType !== 'image') {
115
- throw new Error(
116
- preview.unsupportedReason || `${candidate.name} 暂不支持图片预览`
117
- );
118
- }
119
- const blob = await fetchDataFileBlob(resourceCode, candidate.id);
120
- return {
121
- id: candidate.id,
122
- name: candidate.name,
123
- src: URL.createObjectURL(blob),
124
- };
125
- })
126
- );
127
- const resolved = settled.flatMap(result =>
128
- result.status === 'fulfilled' ? [result.value] : []
129
- );
130
- const current = resolved.findIndex(image => image.id === file.id);
131
- if (current < 0) {
132
- const failure = settled.find(
133
- result => result.status === 'rejected'
134
- ) as PromiseRejectedResult | undefined;
135
- throw failure?.reason || new Error('图片预览加载失败');
136
- }
137
- setImages(previous => {
138
- revokeImages(previous);
139
- return resolved;
140
- });
141
- setCurrentImage(current);
142
- setImageOpen(true);
143
- } catch (error) {
144
- message.error(error instanceof Error ? error.message : String(error));
145
- } finally {
146
- setOpeningId('');
147
- }
148
- };
149
-
150
- const openPreview = (file: DataFileRef) => {
151
- if (isImage(file)) {
152
- void previewImage(file);
153
- return;
154
- }
155
- if (!openIsolatedWindow(attachmentPreviewPath(resourceCode, file.id))) {
156
- void message.warning('浏览器阻止了预览窗口,请允许弹出窗口后重试');
157
- }
158
- };
159
-
160
- const download = async (file: DataFileRef) => {
161
- setDownloadingId(file.id);
162
- try {
163
- await downloadDataFile(resourceCode, file.id, file.name);
164
- } catch (error) {
165
- message.error(error instanceof Error ? error.message : String(error));
166
- } finally {
167
- setDownloadingId('');
168
- }
169
- };
170
-
171
- return (
172
- <>
173
- <div className="oxa-file-list">
174
- {files.map(file => (
175
- <div className="oxa-file-item" key={file.id}>
176
- <span className="oxa-file-kind">
177
- {file.thumbnailUrl ? (
178
- <img
179
- alt=""
180
- className="oxa-file-thumbnail"
181
- height={file.height}
182
- src={file.thumbnailUrl}
183
- width={file.width}
184
- />
185
- ) : (
186
- fileIcon(file)
187
- )}
188
- </span>
189
- <button
190
- className="oxa-file-meta oxa-file-name-button"
191
- disabled={openingId === file.id}
192
- onClick={() => openPreview(file)}
193
- type="button"
194
- >
195
- <strong>{file.name}</strong>
196
- <small>{formatManagedFileSize(file.size)} · 已上传</small>
197
- </button>
198
- <Space size={2}>
199
- <Button
200
- aria-label={`预览${file.name}`}
201
- icon={<EyeOutlined />}
202
- loading={openingId === file.id}
203
- onClick={() => openPreview(file)}
204
- size="small"
205
- type="text"
206
- />
207
- <Button
208
- aria-label={`下载${file.name}`}
209
- icon={<DownloadOutlined />}
210
- loading={downloadingId === file.id}
211
- onClick={() => void download(file)}
212
- size="small"
213
- type="text"
214
- />
215
- {removable && (
216
- <Button
217
- aria-label={`移除${file.name}`}
218
- danger
219
- icon={<CloseOutlined />}
220
- onClick={() => onRemove?.(file)}
221
- size="small"
222
- type="text"
223
- />
224
- )}
225
- </Space>
226
- </div>
227
- ))}
228
- </div>
229
- <Image.PreviewGroup
230
- items={images.map(image => ({ src: image.src, alt: image.name }))}
231
- preview={{
232
- open: imageOpen,
233
- current: currentImage,
234
- countRender: (current, total) => `${current}/${total}`,
235
- onChange: setCurrentImage,
236
- onOpenChange: open => {
237
- if (!open) closeImages();
238
- },
239
- }}
240
- >
241
- <span aria-hidden="true" style={{ display: 'none' }} />
242
- </Image.PreviewGroup>
243
- </>
244
- );
245
- }
@@ -1,49 +0,0 @@
1
- import { Cascader } from 'antd';
2
- import type { DataFieldSurface, LabeledValue } from 'openxiangda-contracts/browser';
3
- import {
4
- cascadeControlValue,
5
- cascadeStoredValue,
6
- type CascadeStoredValue,
7
- } from './cascade-value';
8
-
9
- export function CascadeField({
10
- field,
11
- value,
12
- onChange,
13
- disabled,
14
- }: {
15
- field: DataFieldSurface;
16
- value?: CascadeStoredValue;
17
- onChange?: (value: LabeledValue[] | LabeledValue[][] | undefined) => void;
18
- disabled?: boolean;
19
- }) {
20
- const multiple = field.type === 'cascade.multiple';
21
- return (
22
- <Cascader
23
- allowClear
24
- disabled={disabled}
25
- maxTagCount="responsive"
26
- multiple={multiple}
27
- onChange={(next: unknown) => onChange?.(
28
- cascadeStoredValue(field.options || [], next, multiple)
29
- )}
30
- options={field.options}
31
- placeholder={`请选择${field.label}`}
32
- showSearch
33
- value={cascadeControlValue(value, multiple) as never}
34
- />
35
- );
36
- }
37
-
38
- export function CascadeValueDisplay({
39
- value,
40
- multiple,
41
- }: {
42
- value: CascadeStoredValue;
43
- multiple: boolean;
44
- }) {
45
- const paths = multiple
46
- ? value as LabeledValue[][]
47
- : [value as LabeledValue[]];
48
- return <>{paths.map(path => path.map(item => item.label).join(' / ')).join(';') || '-'}</>;
49
- }