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