openxiangda-cli 2.0.0-alpha.61 → 2.0.0-alpha.62

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 (38) hide show
  1. package/dist/create-workspace.d.ts.map +1 -1
  2. package/dist/create-workspace.js +1 -0
  3. package/dist/create-workspace.js.map +1 -1
  4. package/package.json +4 -4
  5. package/template/AGENTS.md +1 -1
  6. package/template/README.md +7 -7
  7. package/template/apps/server/package.json +1 -1
  8. package/template/apps/server/src/app.module.ts +1 -3
  9. package/template/apps/server/test/smoke.test.ts +6 -23
  10. package/template/apps/web/e2e/resources.spec.ts +20 -0
  11. package/template/apps/web/index.html +1 -1
  12. package/template/apps/web/package.json +1 -1
  13. package/template/apps/web/src/AuthoritativeSelector.tsx +38 -29
  14. package/template/apps/web/src/Shell.tsx +39 -211
  15. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
  16. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +26 -5
  17. package/template/apps/web/src/components/resource/SurfaceFields.tsx +32 -1
  18. package/template/apps/web/src/data-provider.ts +23 -102
  19. package/template/apps/web/src/main.tsx +22 -82
  20. package/template/apps/web/src/platform-client.ts +71 -415
  21. package/template/apps/web/src/runtime-meta.ts +1 -1
  22. package/template/apps/web/src/styles.css +3 -3
  23. package/template/apps/web/test/contracts.test.ts +31 -769
  24. package/template/openxiangda.config.ts +14 -81
  25. package/template/package.json +3 -3
  26. package/template/packages/contracts/src/generated.ts +7 -804
  27. package/template/apps/server/src/instrument-context.controller.ts +0 -25
  28. package/template/apps/web/e2e/instruments.spec.ts +0 -1338
  29. package/template/apps/web/src/CollegePage.tsx +0 -181
  30. package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
  31. package/template/apps/web/src/InstrumentForm.tsx +0 -380
  32. package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
  33. package/template/apps/web/src/InstrumentListPage.tsx +0 -457
  34. package/template/apps/web/src/fields.ts +0 -18
  35. package/template/apps/web/src/instrument.ts +0 -56
  36. package/template/platform/data/colleges.ts +0 -21
  37. package/template/platform/data/instrument-surface.ts +0 -125
  38. package/template/platform/data/instruments.ts +0 -83
@@ -1,457 +0,0 @@
1
- import {
2
- DeleteOutlined,
3
- EditOutlined,
4
- EyeOutlined,
5
- SearchOutlined,
6
- } from '@ant-design/icons';
7
- import { useDelete, useList } from '@refinedev/core';
8
- import {
9
- App,
10
- Button,
11
- Card,
12
- Empty,
13
- Input,
14
- Popconfirm,
15
- Select,
16
- Space,
17
- Spin,
18
- Table,
19
- Tag,
20
- Typography,
21
- type TableColumnsType,
22
- } from 'antd';
23
- import { useMemo, useState } from 'react';
24
- import { useNavigate } from 'react-router-dom';
25
- import {
26
- AuthoritativeSelector,
27
- ResolvedValueText,
28
- } from './AuthoritativeSelector';
29
- import {
30
- listSurfaceFields,
31
- SurfaceFilterControl,
32
- SurfaceFieldValue,
33
- type SurfaceField,
34
- } from './components/resource/SurfaceFields';
35
- import {
36
- MobileResourceListPage,
37
- ResourceListPage,
38
- } from './components/resource/StandardResourcePages';
39
- import {
40
- INSTRUMENT_CAPABILITIES,
41
- type InstrumentListQuery,
42
- type InstrumentRecord,
43
- } from './instrument';
44
- import { instrumentSurface } from '../../../platform/data/instrument-surface.js';
45
- import { useRuntime } from './runtime';
46
-
47
- const defaultSort = instrumentSurface.list?.defaultSort || {
48
- field: 'instrumentCode',
49
- order: 'asc' as const,
50
- };
51
- const listFields = listSurfaceFields(instrumentSurface.fields);
52
- const searchableFields = (instrumentSurface.list?.searchableFields || []).map(
53
- key => ({ label: instrumentSurface.fields[key]?.label || key, value: key })
54
- );
55
- const listWidths: Record<string, number> = {
56
- image: 84,
57
- chineseName: 210,
58
- specModel: 160,
59
- instrumentCode: 140,
60
- assetCode: 140,
61
- collegeId: 140,
62
- instrumentAdminIds: 130,
63
- instrumentStatus: 100,
64
- openToOutside: 90,
65
- };
66
-
67
- export function InstrumentListPage({ variant = 'desktop' }: { variant?: 'desktop' | 'mobile' }) {
68
- return variant === 'mobile' ? <MobileInstrumentListPage /> : <DesktopInstrumentListPage />;
69
- }
70
-
71
- function DesktopInstrumentListPage() {
72
- const navigate = useNavigate();
73
- const { hasCapability } = useRuntime();
74
- const { message } = App.useApp();
75
- const [page, setPage] = useState(1);
76
- const [pageSize, setPageSize] = useState(
77
- instrumentSurface.list?.defaultPageSize || 20
78
- );
79
- const [draft, setDraft] = useState<Partial<InstrumentListQuery>>({});
80
- const [query, setQuery] = useState<Partial<InstrumentListQuery>>({});
81
- const [sort, setSort] = useState<{
82
- field: string;
83
- order: 'asc' | 'desc';
84
- }>({ field: defaultSort.field, order: defaultSort.order || 'asc' });
85
- const list = useList<InstrumentRecord>({
86
- resource: 'instruments',
87
- pagination: { currentPage: page, pageSize },
88
- sorters: [sort],
89
- meta: { query },
90
- });
91
- const remove = useDelete();
92
- const data = (list.result.data || []) as InstrumentRecord[];
93
- const total = list.result.total || 0;
94
- const canUpdate = hasCapability(INSTRUMENT_CAPABILITIES.update);
95
- const canDelete = hasCapability(INSTRUMENT_CAPABILITIES.delete);
96
- const columns = useMemo<TableColumnsType<InstrumentRecord>>(() => {
97
- const standardColumns = listFields.map(field => ({
98
- title: field.label,
99
- dataIndex: field.key,
100
- key: field.key,
101
- width: listWidths[field.key] || 140,
102
- sorter: Boolean(field.sortable),
103
- render: (value: unknown, record: InstrumentRecord) =>
104
- renderListValue(field, value, record, navigate),
105
- }));
106
- return [
107
- ...standardColumns,
108
- {
109
- title: '更新时间',
110
- dataIndex: 'updated_at',
111
- width: 160,
112
- render: value => (value ? new Date(value).toLocaleString() : '-'),
113
- },
114
- {
115
- title: '操作',
116
- fixed: 'right',
117
- width: 180,
118
- render: (_, record) => (
119
- <Space>
120
- <Button
121
- aria-label="查看"
122
- icon={<EyeOutlined />}
123
- size="small"
124
- onClick={() => navigate(`/instruments/${record.id}`)}
125
- />
126
- {canUpdate && (
127
- <Button
128
- aria-label="编辑"
129
- icon={<EditOutlined />}
130
- size="small"
131
- onClick={() => navigate(`/instruments/${record.id}/edit`)}
132
- />
133
- )}
134
- {canDelete && (
135
- <Popconfirm
136
- title="确定删除这台仪器?"
137
- description="删除后不可恢复"
138
- onConfirm={() =>
139
- remove.mutate(
140
- {
141
- resource: 'instruments',
142
- id: record.id,
143
- meta: { expectedRevision: record.revision },
144
- },
145
- {
146
- onSuccess: () => {
147
- message.success('已删除');
148
- void list.query.refetch();
149
- },
150
- }
151
- )
152
- }
153
- >
154
- <Button
155
- danger
156
- aria-label="删除"
157
- icon={<DeleteOutlined />}
158
- size="small"
159
- />
160
- </Popconfirm>
161
- )}
162
- </Space>
163
- ),
164
- },
165
- ];
166
- }, [canDelete, canUpdate, list.query, message, navigate, remove]);
167
- return (
168
- <ResourceListPage
169
- createCapability={INSTRUMENT_CAPABILITIES.create}
170
- createPath="/instruments/new"
171
- description="统一管理仪器档案、归属、管理员与开放状态"
172
- importDisabledReason="批量事务合同落地后启用,不用自定义接口绕过 Data API"
173
- onExport={() => message.info('导出将使用当前 Data API 查询条件')}
174
- readCapability={INSTRUMENT_CAPABILITIES.read}
175
- resource="instruments"
176
- surface={instrumentSurface}
177
- title="仪器资源"
178
- >
179
- <div className="oxa-filter-grid">
180
- <Select
181
- allowClear
182
- options={searchableFields}
183
- placeholder="搜索字段"
184
- value={draft.searchField}
185
- onChange={value =>
186
- setDraft(item => ({ ...item, searchField: value }))
187
- }
188
- />
189
- <Input
190
- allowClear
191
- prefix={<SearchOutlined />}
192
- placeholder="仪器中文名关键词"
193
- value={draft.keyword}
194
- onChange={event =>
195
- setDraft(value => ({ ...value, keyword: event.target.value }))
196
- }
197
- />
198
- <SurfaceFilterControl
199
- field={surfaceField('collegeId')}
200
- renderers={{
201
- renderFilter: context => (
202
- <AuthoritativeSelector
203
- operation="update"
204
- placeholder="所属学院"
205
- source="college"
206
- value={context.value as string | undefined}
207
- onChange={value => context.onChange(typeof value === 'string' ? value : undefined)}
208
- />
209
- ),
210
- }}
211
- onChange={value =>
212
- setDraft(item => ({
213
- ...item,
214
- collegeId: typeof value === 'string' ? value : undefined,
215
- }))
216
- }
217
- value={draft.collegeId}
218
- />
219
- <SurfaceFilterControl
220
- field={surfaceField('instrumentAdminIds')}
221
- onChange={value =>
222
- setDraft(item => ({
223
- ...item,
224
- instrumentAdminId: typeof value === 'string' ? value : undefined,
225
- }))
226
- }
227
- value={draft.instrumentAdminId}
228
- />
229
- <SurfaceFilterControl
230
- field={surfaceField('instrumentStatus')}
231
- onChange={value =>
232
- setDraft(item => ({ ...item, instrumentStatus: value as string | undefined }))
233
- }
234
- value={draft.instrumentStatus}
235
- />
236
- <SurfaceFilterControl
237
- field={surfaceField('openToOutside')}
238
- onChange={value =>
239
- setDraft(item => ({ ...item, openToOutside: value as boolean | undefined }))
240
- }
241
- value={draft.openToOutside}
242
- />
243
- <SurfaceFilterControl
244
- field={surfaceField('enabledDate')}
245
- onChange={value =>
246
- setDraft(item => ({ ...item, enabledDate: value as [string, string] | undefined }))
247
- }
248
- range
249
- value={draft.enabledDate}
250
- />
251
- <SurfaceFilterControl
252
- field={surfaceField('assetValue')}
253
- onChange={value =>
254
- setDraft(item => ({ ...item, assetValue: value as [number | undefined, number | undefined] | undefined }))
255
- }
256
- range
257
- value={draft.assetValue}
258
- />
259
- <Space>
260
- <Button
261
- type="primary"
262
- icon={<SearchOutlined />}
263
- onClick={() => {
264
- setPage(1);
265
- setQuery(draft);
266
- }}
267
- >
268
- 查询
269
- </Button>
270
- <Button
271
- onClick={() => {
272
- setDraft({});
273
- setQuery({});
274
- setPage(1);
275
- }}
276
- >
277
- 重置
278
- </Button>
279
- </Space>
280
- </div>
281
- <Table<InstrumentRecord>
282
- columns={columns}
283
- dataSource={data}
284
- loading={list.query.isLoading}
285
- rowKey="id"
286
- scroll={{ x: 1450 }}
287
- onChange={(_, __, sorter) => {
288
- const item = Array.isArray(sorter) ? sorter[0] : sorter;
289
- if (item?.field && item.order) {
290
- setSort({
291
- field: String(item.field),
292
- order: item.order === 'ascend' ? 'asc' : 'desc',
293
- });
294
- }
295
- }}
296
- pagination={{
297
- current: page,
298
- pageSize,
299
- total,
300
- showSizeChanger: true,
301
- onChange: (next, size) => {
302
- setPage(next);
303
- setPageSize(size);
304
- },
305
- }}
306
- />
307
- </ResourceListPage>
308
- );
309
- }
310
-
311
- function MobileInstrumentListPage() {
312
- const navigate = useNavigate();
313
- const { hasCapability } = useRuntime();
314
- const { message } = App.useApp();
315
- const [page, setPage] = useState(1);
316
- const [keyword, setKeyword] = useState('');
317
- const [query, setQuery] = useState<Partial<InstrumentListQuery>>({});
318
- const list = useList<InstrumentRecord>({
319
- resource: 'instruments',
320
- pagination: { currentPage: page, pageSize: instrumentSurface.list?.defaultPageSize || 20 },
321
- sorters: [{ field: defaultSort.field, order: defaultSort.order || 'asc' }],
322
- meta: { query },
323
- });
324
- const records = (list.result.data || []) as unknown as InstrumentRecord[];
325
- const canCreate = hasCapability(INSTRUMENT_CAPABILITIES.create);
326
- const canUpdate = hasCapability(INSTRUMENT_CAPABILITIES.update);
327
- const listFieldsForCard = listFields.filter(field =>
328
- ['instrumentCode', 'specModel', 'instrumentStatus', 'collegeId'].includes(field.key)
329
- );
330
- return (
331
- <MobileResourceListPage
332
- createCapability={INSTRUMENT_CAPABILITIES.create}
333
- createPath="/m/instruments/new"
334
- description="仪器档案、归属与开放状态"
335
- onExport={() => message.info('导出将使用当前 Data API 查询条件')}
336
- readCapability={INSTRUMENT_CAPABILITIES.read}
337
- resource="instruments"
338
- surface={instrumentSurface}
339
- title="仪器资源"
340
- >
341
- <div className="oxa-mobile-filter">
342
- <Select
343
- options={searchableFields}
344
- value={query.searchField || searchableFields[0]?.value}
345
- onChange={value => setQuery(item => ({ ...item, searchField: value }))}
346
- />
347
- <Input
348
- allowClear
349
- placeholder="搜索仪器名称、编号或资产编号"
350
- value={keyword}
351
- onChange={event => setKeyword(event.target.value)}
352
- onPressEnter={() => {
353
- setPage(1);
354
- setQuery(item => ({ ...item, keyword: keyword || undefined }));
355
- }}
356
- />
357
- <Button
358
- icon={<SearchOutlined />}
359
- onClick={() => {
360
- setPage(1);
361
- setQuery(item => ({ ...item, keyword: keyword || undefined }));
362
- }}
363
- type="primary"
364
- >
365
- 查询
366
- </Button>
367
- </div>
368
- {list.query.isLoading ? (
369
- <div className="oxa-page-loading"><Spin /></div>
370
- ) : list.query.isError ? (
371
- <Card><Typography.Text type="danger">{list.query.error?.message || '列表读取失败'}</Typography.Text></Card>
372
- ) : records.length ? (
373
- <div className="oxa-mobile-record-list">
374
- {records.map(record => (
375
- <Card className="oxa-mobile-record-card" key={record.id} size="small">
376
- <div className="oxa-mobile-record-heading">
377
- <a onClick={() => navigate(`/m/instruments/${record.id}`)}>{record.chineseName || '-'}</a>
378
- <SurfaceFieldValue
379
- field={surfaceField('instrumentStatus')}
380
- value={record.instrumentStatus}
381
- renderers={{
382
- renderValue: ({ field, value }) => <Tag color={value === 'normal' ? 'green' : 'default'}>{field.options?.find(option => option.value === value)?.label || String(value)}</Tag>,
383
- }}
384
- />
385
- </div>
386
- <div className="oxa-mobile-record-fields">
387
- {listFieldsForCard.map(field => (
388
- <div key={field.key}>
389
- <Typography.Text type="secondary">{field.label}</Typography.Text>
390
- <div><SurfaceFieldValue field={field} value={record[field.key]} /></div>
391
- </div>
392
- ))}
393
- </div>
394
- <Space>
395
- <Button onClick={() => navigate(`/m/instruments/${record.id}`)} size="small">查看</Button>
396
- {canUpdate && <Button onClick={() => navigate(`/m/instruments/${record.id}/edit`)} size="small">编辑</Button>}
397
- </Space>
398
- </Card>
399
- ))}
400
- </div>
401
- ) : (
402
- <Card><Empty description="暂无仪器资源" /></Card>
403
- )}
404
- <div className="oxa-mobile-pagination">
405
- <Button disabled={page <= 1} onClick={() => setPage(current => current - 1)}>上一页</Button>
406
- <Typography.Text>第 {page} 页</Typography.Text>
407
- <Button disabled={records.length < (instrumentSurface.list?.defaultPageSize || 20)} onClick={() => setPage(current => current + 1)}>下一页</Button>
408
- </div>
409
- {!canCreate && <Typography.Text type="secondary">当前角色无新增权限</Typography.Text>}
410
- </MobileResourceListPage>
411
- );
412
- }
413
-
414
- function renderListValue(
415
- field: SurfaceField,
416
- value: unknown,
417
- record: InstrumentRecord,
418
- navigate: ReturnType<typeof useNavigate>
419
- ) {
420
- if (field.key === 'image') {
421
- return value && typeof value === 'object' && 'name' in value ? (
422
- <Tag>{String((value as { name?: unknown }).name)}</Tag>
423
- ) : (
424
- <span className="oxa-muted">无图</span>
425
- );
426
- }
427
- if (field.key === 'chineseName') {
428
- return <a onClick={() => navigate(`/instruments/${record.id}`)}>{String(value || '-')}</a>;
429
- }
430
- return (
431
- <SurfaceFieldValue
432
- field={field}
433
- renderers={{
434
- renderValue: ({ field: current, value: currentValue }) => {
435
- if (current.key === 'collegeId') {
436
- return <ResolvedValueText source="college" value={String(currentValue)} />;
437
- }
438
- if (current.key === 'instrumentStatus') {
439
- const label = current.options?.find(option => option.value === currentValue)?.label || String(currentValue);
440
- return (
441
- <Tag color={currentValue === 'normal' ? 'green' : currentValue === 'maintenance' ? 'orange' : 'default'}>
442
- {label}
443
- </Tag>
444
- );
445
- }
446
- return undefined;
447
- },
448
- }}
449
- value={value}
450
- />
451
- );
452
- }
453
-
454
- function surfaceField(key: string): SurfaceField {
455
- const field = instrumentSurface.fields[key];
456
- return field ? { key, ...field } : { key, label: key, widget: 'text' };
457
- }
@@ -1,18 +0,0 @@
1
- import {
2
- instrumentFields,
3
- instrumentSurface,
4
- sectionTitles,
5
- type FieldKind,
6
- type InstrumentField,
7
- } from '../../../platform/data/instrument-surface.js';
8
-
9
- export { instrumentFields, instrumentSurface, sectionTitles };
10
- export type { FieldKind, InstrumentField };
11
-
12
- const instrumentResourceFieldCodes = new Set(
13
- instrumentFields.filter(field => !field.system).map(field => field.key)
14
- );
15
-
16
- export function isInstrumentResourceField(field: string) {
17
- return instrumentResourceFieldCodes.has(field);
18
- }
@@ -1,56 +0,0 @@
1
- import type { DataFileRef } from 'openxiangda-contracts/browser';
2
-
3
- export interface InstrumentRecord extends Record<string, unknown> {
4
- id: string;
5
- revision: number;
6
- instrumentCode: string;
7
- chineseName: string;
8
- englishName?: string;
9
- aliasName?: string;
10
- image?: DataFileRef | null;
11
- specModel: string;
12
- categoryId: string;
13
- instrumentSource?: string;
14
- manufacturer?: string;
15
- assetCode: string;
16
- assetValue?: number;
17
- enabledDate: string;
18
- usageStatus: string;
19
- instrumentStatus: string;
20
- openToOutside: boolean;
21
- subjectAreas?: string[];
22
- platformProperty?: string;
23
- collegeId: string;
24
- manageDepartmentId: string;
25
- useDepartmentId: string;
26
- instrumentAdminIds: string[];
27
- contactUserIds: string[];
28
- contactPhone: string;
29
- contactEmail?: string;
30
- locationId?: string;
31
- address?: string;
32
- serviceContent?: string;
33
- attachments?: DataFileRef[];
34
- updated_at?: string;
35
- }
36
-
37
- export interface InstrumentListQuery {
38
- page: number;
39
- pageSize: number;
40
- keyword?: string;
41
- searchField?: string;
42
- collegeId?: string;
43
- instrumentAdminId?: string;
44
- instrumentStatus?: string;
45
- openToOutside?: boolean;
46
- enabledDate?: [string, string];
47
- assetValue?: [number | undefined, number | undefined];
48
- sort?: { field: string; order: 'asc' | 'desc' };
49
- }
50
-
51
- export const INSTRUMENT_CAPABILITIES = {
52
- read: 'app:instrument-center:data:instruments:read',
53
- create: 'app:instrument-center:data:instruments:create',
54
- update: 'app:instrument-center:data:instruments:update',
55
- delete: 'app:instrument-center:data:instruments:delete',
56
- } as const;
@@ -1,21 +0,0 @@
1
- import { SCHEMA_VERSIONS, type DataResource } from 'openxiangda-contracts';
2
-
3
- export const colleges: DataResource = {
4
- schemaVersion: SCHEMA_VERSIONS.dataResource,
5
- appCode: 'instrument-center',
6
- code: 'colleges',
7
- name: '学院',
8
- schema: {
9
- fields: [
10
- { code: 'name', type: 'string', nullable: false, indexed: true },
11
- { code: 'enabled', type: 'boolean', nullable: false, indexed: true },
12
- ],
13
- },
14
- capabilities: {
15
- read: 'app:instrument-center:data:colleges:read',
16
- create: 'app:instrument-center:data:colleges:create',
17
- update: 'app:instrument-center:data:colleges:update',
18
- delete: 'app:instrument-center:data:colleges:delete',
19
- },
20
- fieldPolicies: {},
21
- };
@@ -1,125 +0,0 @@
1
- import type { DataResourceSurface } from 'openxiangda-contracts/browser';
2
-
3
- export type FieldKind =
4
- | 'text'
5
- | 'textarea'
6
- | 'number'
7
- | 'date'
8
- | 'boolean'
9
- | 'select'
10
- | 'multi'
11
- | 'email'
12
- | 'phone'
13
- | 'file'
14
- | 'scope'
15
- | 'directory-user'
16
- | 'directory-department';
17
-
18
- export interface InstrumentField {
19
- key: string;
20
- label: string;
21
- kind: FieldKind;
22
- section: keyof typeof sectionTitles;
23
- required?: boolean;
24
- system?: boolean;
25
- createCapability?: string;
26
- updateCapability?: string;
27
- multiple?: boolean;
28
- maxCount?: number;
29
- accept?: string;
30
- options?: Array<{ label: string; value: string }>;
31
- }
32
-
33
- const options = (pairs: Array<[string, string]>) =>
34
- pairs.map(([label, value]) => ({ label, value }));
35
-
36
- const listFieldCodes = new Set([
37
- 'image',
38
- 'chineseName',
39
- 'specModel',
40
- 'instrumentCode',
41
- 'assetCode',
42
- 'collegeId',
43
- 'instrumentAdminIds',
44
- 'instrumentStatus',
45
- 'openToOutside',
46
- ]);
47
- const searchableFieldCodes = new Set(['chineseName', 'instrumentCode', 'assetCode']);
48
- const sortableFieldCodes = new Set(['chineseName', 'instrumentCode', 'assetCode']);
49
-
50
- export const sectionTitles = {
51
- basic: '基本信息',
52
- asset: '资产信息',
53
- usage: '使用与归属',
54
- management: '管理与联系',
55
- materials: '地址与资料',
56
- } as const;
57
-
58
- // The declaration is shared by the platform resource and browser renderers.
59
- export const instrumentFields: InstrumentField[] = [
60
- { key: 'id', label: '记录 ID', kind: 'text', section: 'basic', system: true },
61
- { key: 'revision', label: '修订版本', kind: 'number', section: 'basic', system: true },
62
- { key: 'instrumentCode', label: '仪器编码', kind: 'text', section: 'basic', required: true, createCapability: 'app:instrument-center:instrument:field:instrument-code:create', updateCapability: 'app:instrument-center:instrument:field:instrument-code:update' },
63
- { key: 'chineseName', label: '仪器中文名', kind: 'text', section: 'basic', required: true },
64
- { key: 'englishName', label: '仪器英文名', kind: 'text', section: 'basic' },
65
- { key: 'aliasName', label: '别名', kind: 'text', section: 'basic' },
66
- { key: 'image', label: '仪器图片', kind: 'file', section: 'basic', maxCount: 1, accept: 'image/*' },
67
- { key: 'specModel', label: '规格型号', kind: 'text', section: 'basic', required: true },
68
- { key: 'categoryId', label: '仪器分类', kind: 'select', section: 'basic', required: true, options: options([['显微成像', 'analysis_microscope'], ['光谱分析', 'analysis_spectrum'], ['色谱质谱', 'analysis_ms']]) },
69
- { key: 'instrumentSource', label: '仪器来源', kind: 'select', section: 'basic', options: options([['购置', 'purchase'], ['捐赠', 'donation'], ['调拨', 'transfer']]) },
70
- { key: 'manufacturer', label: '制造商', kind: 'text', section: 'basic' },
71
- { key: 'assetCode', label: '资产编码', kind: 'text', section: 'asset', required: true, createCapability: 'app:instrument-center:instrument:field:asset-code:create', updateCapability: 'app:instrument-center:instrument:field:asset-code:update' },
72
- { key: 'assetValue', label: '资产价值(元)', kind: 'number', section: 'asset', createCapability: 'app:instrument-center:instrument:field:asset-value:create', updateCapability: 'app:instrument-center:instrument:field:asset-value:update' },
73
- { key: 'enabledDate', label: '启用日期', kind: 'date', section: 'asset', required: true },
74
- { key: 'usageStatus', label: '使用状况', kind: 'select', section: 'usage', required: true, options: options([['在用', 'active'], ['闲置', 'idle'], ['借出', 'lent']]) },
75
- { key: 'instrumentStatus', label: '仪器状态', kind: 'select', section: 'usage', required: true, options: options([['正常', 'normal'], ['维修', 'maintenance'], ['停用', 'disabled'], ['报废', 'scrapped']]) },
76
- { key: 'openToOutside', label: '是否对外开放', kind: 'boolean', section: 'usage', required: true },
77
- { key: 'subjectAreas', label: '学科领域', kind: 'multi', section: 'usage', options: options([['材料', 'material'], ['生命科学', 'life'], ['化学', 'chemistry'], ['物理', 'physics']]) },
78
- { key: 'platformProperty', label: '平台属性', kind: 'select', section: 'usage', options: options([['共享平台设备', 'shared'], ['课题组设备', 'research']]) },
79
- { key: 'collegeId', label: '所属学院', kind: 'scope', section: 'usage', required: true, createCapability: 'app:instrument-center:instrument:field:college-id:create', updateCapability: 'app:instrument-center:instrument:field:college-id:update' },
80
- { key: 'manageDepartmentId', label: '管理部门', kind: 'directory-department', section: 'management', required: true },
81
- { key: 'useDepartmentId', label: '使用部门', kind: 'directory-department', section: 'management', required: true },
82
- { key: 'instrumentAdminIds', label: '仪器管理员', kind: 'directory-user', section: 'management', required: true, multiple: true, createCapability: 'app:instrument-center:instrument:field:instrument-admin-ids:create', updateCapability: 'app:instrument-center:instrument:field:instrument-admin-ids:update' },
83
- { key: 'contactUserIds', label: '联系人', kind: 'directory-user', section: 'management', required: true, multiple: true },
84
- { key: 'contactPhone', label: '联系电话', kind: 'phone', section: 'management', required: true },
85
- { key: 'contactEmail', label: '联系邮箱', kind: 'email', section: 'management' },
86
- { key: 'locationId', label: '存放点', kind: 'select', section: 'materials', options: options([['A 区 101', 'A-101'], ['B 区 203', 'B-203']]) },
87
- { key: 'address', label: '详细地址', kind: 'textarea', section: 'materials' },
88
- { key: 'serviceContent', label: '服务内容与主要功能', kind: 'textarea', section: 'materials' },
89
- { key: 'attachments', label: '仪器资料', kind: 'file', section: 'materials', multiple: true, maxCount: 8 },
90
- ];
91
-
92
- export const instrumentSurface: DataResourceSurface = {
93
- // This resource keeps custom scope, file and audit UX; simple resources use the generated CRUD pages.
94
- generated: false,
95
- fields: Object.fromEntries(
96
- instrumentFields.map(field => [
97
- field.key,
98
- {
99
- label: field.label,
100
- widget: field.kind,
101
- section: field.section,
102
- ...(field.required !== undefined ? { requiredHint: field.required } : {}),
103
- ...(field.system !== undefined ? { system: field.system } : {}),
104
- ...(field.createCapability ? { createCapability: field.createCapability } : {}),
105
- ...(field.updateCapability ? { updateCapability: field.updateCapability } : {}),
106
- ...(field.multiple !== undefined ? { multiple: field.multiple } : {}),
107
- ...(field.maxCount !== undefined ? { maxCount: field.maxCount } : {}),
108
- ...(field.accept ? { accept: field.accept } : {}),
109
- ...(field.options ? { options: field.options } : {}),
110
- ...(listFieldCodes.has(field.key) ? { list: true } : {}),
111
- ...(searchableFieldCodes.has(field.key) ? { searchable: true } : {}),
112
- ...(sortableFieldCodes.has(field.key) ? { sortable: true } : {}),
113
- },
114
- ])
115
- ),
116
- list: {
117
- defaultPageSize: 20,
118
- searchableFields: ['chineseName', 'instrumentCode', 'assetCode'],
119
- filterFields: ['collegeId', 'instrumentAdminIds', 'instrumentStatus', 'openToOutside', 'enabledDate', 'assetValue'],
120
- defaultSort: { field: 'instrumentCode', order: 'asc' },
121
- },
122
- form: { layout: 'sections' },
123
- detail: { layout: 'sections' },
124
- mobile: { enabled: true },
125
- };