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.
- package/README.md +2 -2
- package/bin/run.js +1 -0
- package/dist/commands/skill.d.ts +26 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +43 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +3 -3
- package/dist/create-workspace.js.map +1 -1
- package/dist/toolchain-capsule.d.ts.map +1 -1
- package/dist/toolchain-capsule.js +4 -8
- package/dist/toolchain-capsule.js.map +1 -1
- package/package.json +6 -7
- package/template/AGENTS.md +2 -1
- package/template/README.md +3 -3
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -1
- package/template/apps/server/src/main.ts +1 -1
- package/template/apps/web/e2e/client-fixture.ts +5 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
- package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
- package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
- package/template/apps/web/e2e/resources.spec.ts +20 -181
- package/template/apps/web/field-protocol.e2e.html +3 -0
- package/template/apps/web/package.json +2 -5
- package/template/apps/web/src/main.tsx +12 -60
- package/template/apps/web/test/contracts.test.ts +52 -104
- package/template/openxiangda.config.ts +1 -1
- package/template/package.json +1 -3
- package/template/packages/contracts/src/generated.ts +1 -0
- package/template/scripts/verify-template-budget.mjs +2 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
- package/template/apps/web/src/FilePreviewPage.tsx +0 -324
- package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
- package/template/apps/web/src/Shell.tsx +0 -445
- package/template/apps/web/src/appearance.tsx +0 -147
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
- package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
- package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
- package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
- package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
- package/template/apps/web/src/components/resource/resource-import.ts +0 -205
- package/template/apps/web/src/data-provider.ts +0 -72
- package/template/apps/web/src/platform-client.ts +0 -708
- package/template/apps/web/src/runtime-meta.ts +0 -75
- package/template/apps/web/src/runtime.tsx +0 -227
- package/template/apps/web/src/styles.css +0 -1941
- package/template/apps/web/test/address-field.test.ts +0 -66
- package/template/apps/web/test/cascade-value.test.ts +0 -68
- package/template/apps/web/test/directory-value.test.ts +0 -55
- package/template/apps/web/test/field-bounds.test.ts +0 -17
- package/template/apps/web/test/field-form-codec.test.ts +0 -85
- package/template/apps/web/test/location-field.test.ts +0 -83
- package/template/apps/web/test/resource-import.test.ts +0 -143
- package/template/apps/web/test/resource-query.test.ts +0 -144
- package/template/apps/web/test/rich-json-field.test.ts +0 -53
- package/template/apps/web/test/signature-serial-field.test.ts +0 -40
- package/template/apps/web/test/subtable-value.test.ts +0 -144
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { DataResourceSurface } from 'openxiangda-contracts/browser';
|
|
2
|
-
|
|
3
|
-
export interface GeneratedResourceDefinition {
|
|
4
|
-
code: string;
|
|
5
|
-
name: string;
|
|
6
|
-
capabilities: {
|
|
7
|
-
read: string;
|
|
8
|
-
create: string;
|
|
9
|
-
update: string;
|
|
10
|
-
delete: string;
|
|
11
|
-
};
|
|
12
|
-
surface: DataResourceSurface;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type GeneratedResourceDefinitions = Record<
|
|
16
|
-
string,
|
|
17
|
-
GeneratedResourceDefinition
|
|
18
|
-
>;
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import type { DataResourceSurface, DataTransactionOperation } from 'openxiangda-contracts/browser';
|
|
2
|
-
|
|
3
|
-
const MAX_IMPORT_BYTES = 5 * 1024 * 1024;
|
|
4
|
-
const MAX_IMPORT_ROWS = 100;
|
|
5
|
-
|
|
6
|
-
export interface ResourceImportRow {
|
|
7
|
-
rowNumber: number;
|
|
8
|
-
data: Record<string, unknown>;
|
|
9
|
-
errors: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ResourceImportPreview {
|
|
13
|
-
fileName: string;
|
|
14
|
-
rows: ResourceImportRow[];
|
|
15
|
-
errors: string[];
|
|
16
|
-
operations: DataTransactionOperation[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function parseResourceImportFile(
|
|
20
|
-
file: File,
|
|
21
|
-
resourceCode: string,
|
|
22
|
-
surface: DataResourceSurface,
|
|
23
|
-
writableFieldCodes: string[]
|
|
24
|
-
): Promise<ResourceImportPreview> {
|
|
25
|
-
if (file.size > MAX_IMPORT_BYTES) {
|
|
26
|
-
return failure(file.name, '导入文件不能超过 5MB');
|
|
27
|
-
}
|
|
28
|
-
if (!/\.(?:csv|xls|xlsx)$/i.test(file.name)) {
|
|
29
|
-
return failure(file.name, '仅支持 CSV、XLS 或 XLSX 文件');
|
|
30
|
-
}
|
|
31
|
-
const XLSX = await import('xlsx');
|
|
32
|
-
const workbook = XLSX.read(await file.arrayBuffer(), {
|
|
33
|
-
cellDates: true,
|
|
34
|
-
type: 'array',
|
|
35
|
-
});
|
|
36
|
-
const sheetName = workbook.SheetNames[0];
|
|
37
|
-
if (!sheetName) return failure(file.name, '文件中没有可读取的工作表');
|
|
38
|
-
const sheet = workbook.Sheets[sheetName];
|
|
39
|
-
if (!sheet) return failure(file.name, '文件中的首个工作表不可读取');
|
|
40
|
-
const matrix = XLSX.utils.sheet_to_json<unknown[]>(sheet, {
|
|
41
|
-
defval: '',
|
|
42
|
-
header: 1,
|
|
43
|
-
raw: true,
|
|
44
|
-
});
|
|
45
|
-
return parseResourceImportMatrix(file.name, matrix, resourceCode, surface, writableFieldCodes);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function parseResourceImportMatrix(
|
|
49
|
-
fileName: string,
|
|
50
|
-
matrix: unknown[][],
|
|
51
|
-
resourceCode: string,
|
|
52
|
-
surface: DataResourceSurface,
|
|
53
|
-
writableFieldCodes: string[]
|
|
54
|
-
): ResourceImportPreview {
|
|
55
|
-
if (!matrix.length) return failure(fileName, '导入文件为空');
|
|
56
|
-
const headerRow = matrix[0];
|
|
57
|
-
if (!headerRow) return failure(fileName, '导入文件缺少表头');
|
|
58
|
-
const writable = new Set(writableFieldCodes);
|
|
59
|
-
const fieldEntries = Object.entries(surface.fields).filter(([, field]) => !field.system);
|
|
60
|
-
const aliases = new Map<string, string[]>();
|
|
61
|
-
for (const [code, field] of fieldEntries) {
|
|
62
|
-
for (const alias of [code, field.label]) {
|
|
63
|
-
const key = String(alias || '').trim();
|
|
64
|
-
if (!key) continue;
|
|
65
|
-
aliases.set(key, [...(aliases.get(key) || []), code]);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const headers = headerRow.map((value) => String(value ?? '').trim());
|
|
69
|
-
const errors: string[] = [];
|
|
70
|
-
const mapped = headers.map((header, index) => {
|
|
71
|
-
if (!header) return null;
|
|
72
|
-
const matches = [...new Set(aliases.get(header) || [])];
|
|
73
|
-
if (matches.length === 0) {
|
|
74
|
-
errors.push(`第 ${index + 1} 列“${header}”不是已声明字段`);
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
if (matches.length > 1) {
|
|
78
|
-
errors.push(`第 ${index + 1} 列“${header}”对应多个字段,请改用字段代码`);
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
const code = matches[0];
|
|
82
|
-
if (!code) return null;
|
|
83
|
-
const field = surface.fields[code];
|
|
84
|
-
if (!field) return null;
|
|
85
|
-
if (['file', 'image', 'signature'].includes(field.type)) {
|
|
86
|
-
errors.push(`文件字段“${field.label}”必须通过平台上传组件录入`);
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
if (!writable.has(code)) {
|
|
90
|
-
errors.push(`字段“${field.label}”不可在新增时写入`);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
return code;
|
|
94
|
-
});
|
|
95
|
-
const duplicateCodes = mapped.filter((code, index): code is string => Boolean(code) && mapped.indexOf(code) !== index);
|
|
96
|
-
for (const code of [...new Set(duplicateCodes)]) {
|
|
97
|
-
const field = surface.fields[code];
|
|
98
|
-
if (field) errors.push(`字段“${field.label}”在表头中重复出现`);
|
|
99
|
-
}
|
|
100
|
-
const sourceRows = matrix
|
|
101
|
-
.slice(1)
|
|
102
|
-
.filter((row) => row.some((value) => value !== undefined && value !== null && String(value).trim() !== ''));
|
|
103
|
-
headers.forEach((header, index) => {
|
|
104
|
-
if (!header && sourceRows.some((row) => row[index] !== undefined && row[index] !== null && String(row[index]).trim() !== '')) {
|
|
105
|
-
errors.push(`第 ${index + 1} 列存在数据但缺少表头`);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
if (!sourceRows.length) errors.push('导入文件没有数据行');
|
|
109
|
-
if (sourceRows.length > MAX_IMPORT_ROWS) {
|
|
110
|
-
errors.push(`单次最多导入 ${MAX_IMPORT_ROWS} 行,当前为 ${sourceRows.length} 行`);
|
|
111
|
-
}
|
|
112
|
-
const rows = sourceRows.slice(0, MAX_IMPORT_ROWS).map((source, rowIndex) => {
|
|
113
|
-
const data: Record<string, unknown> = {};
|
|
114
|
-
const rowErrors: string[] = [];
|
|
115
|
-
mapped.forEach((code, columnIndex) => {
|
|
116
|
-
if (!code) return;
|
|
117
|
-
const field = surface.fields[code];
|
|
118
|
-
if (!field) return;
|
|
119
|
-
try {
|
|
120
|
-
const value = importValue(field, source[columnIndex]);
|
|
121
|
-
if (value !== undefined) data[code] = value;
|
|
122
|
-
} catch (error) {
|
|
123
|
-
rowErrors.push(`${field.label}:${error instanceof Error ? error.message : String(error)}`);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
for (const [code, field] of fieldEntries) {
|
|
127
|
-
if (writable.has(code) && field.requiredHint && (data[code] === undefined || data[code] === null || data[code] === '')) {
|
|
128
|
-
rowErrors.push(`${field.label}为必填项`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return { rowNumber: rowIndex + 2, data, errors: rowErrors };
|
|
132
|
-
});
|
|
133
|
-
const rowErrors = rows.flatMap((row) => row.errors.map((error) => `第 ${row.rowNumber} 行:${error}`));
|
|
134
|
-
return {
|
|
135
|
-
fileName,
|
|
136
|
-
rows,
|
|
137
|
-
errors: [...errors, ...rowErrors],
|
|
138
|
-
operations:
|
|
139
|
-
errors.length || rowErrors.length
|
|
140
|
-
? []
|
|
141
|
-
: rows.map((row) => ({
|
|
142
|
-
operation: 'create' as const,
|
|
143
|
-
resourceCode,
|
|
144
|
-
data: row.data,
|
|
145
|
-
})),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function importValue(field: DataResourceSurface['fields'][string], input: unknown): unknown {
|
|
150
|
-
if (input === undefined || input === null || input === '') return undefined;
|
|
151
|
-
if (field.widget === 'number' || field.widget === 'money' || field.widget === 'percent') {
|
|
152
|
-
const value = Number(input);
|
|
153
|
-
if (!Number.isFinite(value)) throw new Error('必须是数字');
|
|
154
|
-
return value;
|
|
155
|
-
}
|
|
156
|
-
if (field.type === 'boolean') {
|
|
157
|
-
if (input === true || input === 1 || /^(?:是|true|yes|1)$/i.test(String(input).trim())) return true;
|
|
158
|
-
if (input === false || input === 0 || /^(?:否|false|no|0)$/i.test(String(input).trim())) return false;
|
|
159
|
-
throw new Error('请填写是或否');
|
|
160
|
-
}
|
|
161
|
-
if (field.widget === 'date' || field.widget === 'datetime') {
|
|
162
|
-
const text = String(input).trim();
|
|
163
|
-
if (field.widget === 'date' && /^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
164
|
-
const dateParts = text.split('-');
|
|
165
|
-
const year = Number(dateParts[0]);
|
|
166
|
-
const month = Number(dateParts[1]);
|
|
167
|
-
const day = Number(dateParts[2]);
|
|
168
|
-
const exact = new Date(Date.UTC(year, month - 1, day));
|
|
169
|
-
if (exact.getUTCFullYear() !== year || exact.getUTCMonth() !== month - 1 || exact.getUTCDate() !== day) {
|
|
170
|
-
throw new Error('日期格式无效');
|
|
171
|
-
}
|
|
172
|
-
return text;
|
|
173
|
-
}
|
|
174
|
-
const date = input instanceof Date ? input : new Date(String(input));
|
|
175
|
-
if (Number.isNaN(date.getTime())) throw new Error('日期格式无效');
|
|
176
|
-
return field.widget === 'date'
|
|
177
|
-
? `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
178
|
-
: date.toISOString();
|
|
179
|
-
}
|
|
180
|
-
if (field.widget === 'json') {
|
|
181
|
-
if (typeof input === 'object') return input;
|
|
182
|
-
try {
|
|
183
|
-
return JSON.parse(String(input));
|
|
184
|
-
} catch {
|
|
185
|
-
throw new Error('必须是有效 JSON');
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const multiple = field.type.endsWith('.multiple');
|
|
189
|
-
const values = multiple
|
|
190
|
-
? (Array.isArray(input) ? input : String(input).split(/[、,,;;\n]+/)).map((value) => String(value).trim()).filter(Boolean)
|
|
191
|
-
: [String(input).trim()];
|
|
192
|
-
if (field.options?.length) {
|
|
193
|
-
const resolved = values.map((value) => {
|
|
194
|
-
const option = field.options?.find((item) => item.value === value || item.label === value);
|
|
195
|
-
if (!option) throw new Error(`“${value}”不在可选项中`);
|
|
196
|
-
return { ...option };
|
|
197
|
-
});
|
|
198
|
-
return multiple ? resolved : resolved[0];
|
|
199
|
-
}
|
|
200
|
-
return multiple ? values : values[0];
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function failure(fileName: string, message: string): ResourceImportPreview {
|
|
204
|
-
return { fileName, rows: [], errors: [message], operations: [] };
|
|
205
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type { DataProvider } from '@refinedev/core';
|
|
2
|
-
import { createNativeResourceClient, type GenericResourceQuery } from './platform-client';
|
|
3
|
-
import { applicationCode } from './runtime-meta';
|
|
4
|
-
import { resourceDefinitions } from '../../../packages/contracts/src/generated.js';
|
|
5
|
-
import type { GeneratedResourceDefinitions } from './components/resource/generated-resource-definition';
|
|
6
|
-
|
|
7
|
-
const definitions = resourceDefinitions as unknown as GeneratedResourceDefinitions;
|
|
8
|
-
type ListParams = Parameters<NonNullable<DataProvider['getList']>>[0];
|
|
9
|
-
type OneParams = Parameters<NonNullable<DataProvider['getOne']>>[0];
|
|
10
|
-
type CreateParams = Parameters<NonNullable<DataProvider['create']>>[0];
|
|
11
|
-
type UpdateParams = Parameters<NonNullable<DataProvider['update']>>[0];
|
|
12
|
-
type DeleteParams = Parameters<NonNullable<DataProvider['deleteOne']>>[0];
|
|
13
|
-
|
|
14
|
-
export function createApplicationProvider(
|
|
15
|
-
declaredDefinitions: GeneratedResourceDefinitions
|
|
16
|
-
) {
|
|
17
|
-
const clientFor = (resource: string) => {
|
|
18
|
-
const definition = declaredDefinitions[resource];
|
|
19
|
-
if (!definition) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
`OPENXIANGDA_GENERATED_RESOURCE_NOT_DECLARED:${resource}`
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
return createNativeResourceClient(resource, definition.surface);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
getApiUrl: () =>
|
|
29
|
-
`/service/openxiangda-api/v2/applications/${applicationCode()}/native/data`,
|
|
30
|
-
getList: async ({ resource, pagination, sorters, meta }: ListParams) => {
|
|
31
|
-
const query = (meta?.query || {}) as Partial<GenericResourceQuery>;
|
|
32
|
-
const sorter = sorters?.[0];
|
|
33
|
-
const result = await clientFor(resource).list({
|
|
34
|
-
page: pagination?.currentPage || 1,
|
|
35
|
-
pageSize: pagination?.pageSize || 20,
|
|
36
|
-
...query,
|
|
37
|
-
...(sorter
|
|
38
|
-
? {
|
|
39
|
-
sort: {
|
|
40
|
-
field: String(sorter.field),
|
|
41
|
-
order: (sorter.order || 'asc') as 'asc' | 'desc',
|
|
42
|
-
},
|
|
43
|
-
}
|
|
44
|
-
: {}),
|
|
45
|
-
});
|
|
46
|
-
return { data: result.rows, total: result.total };
|
|
47
|
-
},
|
|
48
|
-
getOne: async ({ resource, id }: OneParams) => ({
|
|
49
|
-
data: await clientFor(resource).get(String(id)),
|
|
50
|
-
}),
|
|
51
|
-
create: async ({ resource, variables }: CreateParams) => ({
|
|
52
|
-
data: await clientFor(resource).create(
|
|
53
|
-
variables as Record<string, unknown>
|
|
54
|
-
),
|
|
55
|
-
}),
|
|
56
|
-
update: async ({ resource, id, variables, meta }: UpdateParams) => ({
|
|
57
|
-
data: await clientFor(resource).update(
|
|
58
|
-
String(id),
|
|
59
|
-
Number(meta?.expectedRevision),
|
|
60
|
-
variables as Record<string, unknown>
|
|
61
|
-
),
|
|
62
|
-
}),
|
|
63
|
-
deleteOne: async ({ resource, id, meta }: DeleteParams) => ({
|
|
64
|
-
data: await clientFor(resource).remove(
|
|
65
|
-
String(id),
|
|
66
|
-
Number(meta?.expectedRevision)
|
|
67
|
-
),
|
|
68
|
-
}),
|
|
69
|
-
} as unknown as DataProvider;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const applicationProvider = createApplicationProvider(definitions);
|