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

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 (39) 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/Dockerfile +2 -1
  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/src/AuthoritativeSelector.tsx +298 -41
  15. package/template/apps/web/src/Shell.tsx +39 -211
  16. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
  17. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +229 -16
  18. package/template/apps/web/src/components/resource/SurfaceFields.tsx +142 -2
  19. package/template/apps/web/src/data-provider.ts +23 -102
  20. package/template/apps/web/src/main.tsx +35 -83
  21. package/template/apps/web/src/platform-client.ts +112 -424
  22. package/template/apps/web/src/runtime-meta.ts +1 -1
  23. package/template/apps/web/src/styles.css +285 -3
  24. package/template/apps/web/test/contracts.test.ts +43 -769
  25. package/template/openxiangda.config.ts +14 -81
  26. package/template/package.json +3 -3
  27. package/template/packages/contracts/src/generated.ts +7 -804
  28. package/template/apps/server/src/instrument-context.controller.ts +0 -25
  29. package/template/apps/web/e2e/instruments.spec.ts +0 -1338
  30. package/template/apps/web/src/CollegePage.tsx +0 -181
  31. package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
  32. package/template/apps/web/src/InstrumentForm.tsx +0 -380
  33. package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
  34. package/template/apps/web/src/InstrumentListPage.tsx +0 -457
  35. package/template/apps/web/src/fields.ts +0 -18
  36. package/template/apps/web/src/instrument.ts +0 -56
  37. package/template/platform/data/colleges.ts +0 -21
  38. package/template/platform/data/instrument-surface.ts +0 -125
  39. package/template/platform/data/instruments.ts +0 -83
@@ -1,380 +0,0 @@
1
- import {
2
- App,
3
- Button,
4
- Card,
5
- Descriptions,
6
- Form,
7
- Tag,
8
- Timeline,
9
- Typography,
10
- } from 'antd';
11
- import dayjs from 'dayjs';
12
- import { useEffect, useState } from 'react';
13
- import type { DataAuditEntry } from 'openxiangda-contracts/browser';
14
- import {
15
- AuthoritativeSelector,
16
- ResolvedValueText,
17
- } from './AuthoritativeSelector';
18
- import {
19
- MobileSurfaceFieldControl,
20
- SurfaceFieldControl,
21
- SurfaceFieldValue,
22
- type SurfaceField,
23
- } from './components/resource/SurfaceFields';
24
- import { instrumentFields, sectionTitles, type InstrumentField } from './fields';
25
- import type { InstrumentRecord } from './instrument';
26
- import { instrumentDataApi } from './platform-client';
27
- import { useRuntime } from './runtime';
28
- import { instrumentSurface } from '../../../platform/data/instrument-surface.js';
29
-
30
- type Values = Record<string, unknown>;
31
- const createDefaults = {
32
- openToOutside: false,
33
- instrumentStatus: 'normal',
34
- usageStatus: 'active',
35
- } as const;
36
-
37
- export function instrumentFieldAllowed(
38
- field: InstrumentField,
39
- mode: 'create' | 'edit',
40
- hasCapability: (capability: string) => boolean
41
- ) {
42
- const capability =
43
- mode === 'create' ? field.createCapability : field.updateCapability;
44
- return !capability || hasCapability(capability);
45
- }
46
-
47
- export function sanitizeInstrumentValues(
48
- values: Values,
49
- mode: 'create' | 'edit',
50
- hasCapability: (capability: string) => boolean
51
- ): Values {
52
- return Object.fromEntries(
53
- instrumentFields
54
- .filter(
55
- field =>
56
- !field.system && instrumentFieldAllowed(field, mode, hasCapability)
57
- )
58
- .filter(field => Object.prototype.hasOwnProperty.call(values, field.key))
59
- .map(field => [field.key, values[field.key]])
60
- );
61
- }
62
-
63
- export function InstrumentForm({
64
- mode,
65
- record,
66
- variant = 'desktop',
67
- onCancel,
68
- onSubmit,
69
- auditEntries = [],
70
- auditError,
71
- }: {
72
- mode: 'create' | 'edit' | 'detail';
73
- record?: InstrumentRecord;
74
- variant?: 'desktop' | 'mobile';
75
- onCancel: () => void;
76
- onSubmit?: (values: Values) => Promise<void>;
77
- auditEntries?: DataAuditEntry[];
78
- auditError?: string;
79
- }) {
80
- const [form] = Form.useForm();
81
- const { message } = App.useApp();
82
- const { hasCapability } = useRuntime();
83
- const [submitting, setSubmitting] = useState(false);
84
- useEffect(() => {
85
- if (record) {
86
- form.setFieldsValue({
87
- ...record,
88
- enabledDate: record.enabledDate
89
- ? variant === 'mobile'
90
- ? record.enabledDate
91
- : dayjs(record.enabledDate)
92
- : undefined,
93
- });
94
- }
95
- }, [form, record]);
96
- if (mode === 'detail' && record) {
97
- return (
98
- <Detail
99
- auditEntries={auditEntries}
100
- auditError={auditError}
101
- record={record}
102
- variant={variant}
103
- />
104
- );
105
- }
106
- const isCreate = mode === 'create';
107
- const writeMode = isCreate ? 'create' : 'edit';
108
- const submit = async (values: Values) => {
109
- if (submitting) return;
110
- const normalized = {
111
- ...(isCreate ? createDefaults : {}),
112
- ...values,
113
- enabledDate:
114
- values.enabledDate &&
115
- typeof values.enabledDate === 'object' &&
116
- 'format' in values.enabledDate
117
- ? (values.enabledDate as { format(pattern: string): string }).format(
118
- 'YYYY-MM-DD'
119
- )
120
- : values.enabledDate,
121
- };
122
- setSubmitting(true);
123
- try {
124
- await onSubmit?.(
125
- sanitizeInstrumentValues(normalized, writeMode, hasCapability)
126
- );
127
- } catch (error) {
128
- message.error(error instanceof Error ? error.message : String(error));
129
- } finally {
130
- setSubmitting(false);
131
- }
132
- };
133
- const groups = Object.entries(sectionTitles).map(([key, title]) => ({
134
- key,
135
- title,
136
- fields: instrumentFields
137
- .filter(field => field.section === key && !field.system)
138
- .map(field => field.key),
139
- }));
140
- const FieldControl =
141
- variant === 'mobile' ? MobileSurfaceFieldControl : SurfaceFieldControl;
142
- return (
143
- <Form
144
- className={`oxa-instrument-form oxa-form-full${variant === 'mobile' ? ' oxa-mobile-form' : ''}`}
145
- form={form}
146
- initialValues={isCreate ? createDefaults : undefined}
147
- layout="vertical"
148
- onFinish={values => void submit(values)}
149
- scrollToFirstError={{ block: 'center' }}
150
- >
151
- <div className="oxa-sections">
152
- {groups.map(group => (
153
- <Card className="oxa-section-card" key={group.key} title={group.title}>
154
- <div className="oxa-grid">
155
- {group.fields
156
- .map(key => instrumentFields.find(field => field.key === key))
157
- .filter((field): field is InstrumentField => Boolean(field))
158
- .map(field => (
159
- <SurfaceFieldControl
160
- disabled={
161
- !instrumentFieldAllowed(field, writeMode, hasCapability)
162
- }
163
- field={surfaceField(field.key)}
164
- key={field.key}
165
- operation={isCreate ? 'create' : 'update'}
166
- recordId={record?.id}
167
- renderers={{
168
- renderExtra: context =>
169
- context.disabled
170
- ? '当前角色不可修改此字段'
171
- : context.field.widget === 'scope'
172
- ? '仅显示当前角色可管理的学院'
173
- : context.field.key === 'instrumentAdminIds'
174
- ? '管理员选择结果将参与仪器数据权限判断'
175
- : undefined,
176
- renderScope: context => (
177
- <AuthoritativeSelector
178
- disabled={context.disabled}
179
- operation={context.operation}
180
- placeholder={`请选择${context.field.label}`}
181
- source="college"
182
- />
183
- ),
184
- upload: (field, file, recordId) =>
185
- instrumentDataApi.upload(
186
- field.key as 'image' | 'attachments',
187
- file,
188
- recordId
189
- ),
190
- }}
191
- />
192
- ))}
193
- </div>
194
- </Card>
195
- ))}
196
- </div>
197
- <div className="oxa-actions">
198
- <Button disabled={submitting} onClick={onCancel}>
199
- 取消
200
- </Button>
201
- <Button
202
- data-testid="save-instrument"
203
- htmlType="submit"
204
- loading={submitting}
205
- type="primary"
206
- >
207
- {isCreate ? '保存' : '保存修改'}
208
- </Button>
209
- </div>
210
- </Form>
211
- );
212
- }
213
-
214
- function Detail({
215
- record,
216
- auditEntries,
217
- auditError,
218
- variant = 'desktop',
219
- }: {
220
- record: InstrumentRecord;
221
- auditEntries: DataAuditEntry[];
222
- auditError?: string;
223
- variant?: 'desktop' | 'mobile';
224
- }) {
225
- const groups = Object.entries(sectionTitles)
226
- .map(([section, title]) => ({
227
- section,
228
- title,
229
- fields: instrumentFields.filter(
230
- field =>
231
- field.section === section &&
232
- !field.system &&
233
- Object.prototype.hasOwnProperty.call(record, field.key)
234
- ),
235
- }))
236
- .filter(group => group.fields.length > 0);
237
- return (
238
- <div className={`oxa-detail-layout${variant === 'mobile' ? ' oxa-mobile-detail' : ''}`}>
239
- <div className="oxa-detail-main">
240
- {groups.map(group => (
241
- <Card className="oxa-section-card" key={group.section} title={group.title}>
242
- <Descriptions
243
- column={{ xs: 1, sm: 2, lg: 3 }}
244
- items={group.fields.map(field => ({
245
- key: field.key,
246
- label: field.label,
247
- span:
248
- surfaceField(field.key).widget === 'textarea' ||
249
- surfaceField(field.key).widget === 'file'
250
- ? 3
251
- : 1,
252
- children: (
253
- <SurfaceFieldValue
254
- field={surfaceField(field.key)}
255
- renderers={{
256
- renderValue: ({ field: surface, value }) => {
257
- if (surface.widget === 'scope') {
258
- return (
259
- <ResolvedValueText
260
- source="college"
261
- value={String(value)}
262
- />
263
- );
264
- }
265
- if (
266
- surface.key === 'instrumentStatus' ||
267
- surface.key === 'usageStatus'
268
- ) {
269
- const label =
270
- surface.options?.find(
271
- option => option.value === value
272
- )?.label || String(value);
273
- return (
274
- <Tag
275
- color={
276
- value === 'normal' || value === 'active'
277
- ? 'green'
278
- : 'default'
279
- }
280
- >
281
- {label}
282
- </Tag>
283
- );
284
- }
285
- return undefined;
286
- },
287
- }}
288
- value={record[field.key]}
289
- />
290
- ),
291
- }))}
292
- />
293
- </Card>
294
- ))}
295
- </div>
296
- <aside className="oxa-detail-aside">
297
- <Card className="oxa-section-card" title="记录信息">
298
- <Descriptions
299
- column={1}
300
- items={[
301
- { key: 'revision', label: '数据版本', children: record.revision },
302
- {
303
- key: 'updatedAt',
304
- label: '更新时间',
305
- children: formatDateTime(record.updated_at),
306
- },
307
- {
308
- key: 'status',
309
- label: '仪器状态',
310
- children: (
311
- <SurfaceFieldValue
312
- field={surfaceField('instrumentStatus')}
313
- renderers={{
314
- renderValue: ({ field, value }) => {
315
- const label =
316
- field.options?.find(
317
- option => option.value === value
318
- )?.label || String(value);
319
- return (
320
- <Tag
321
- color={
322
- value === 'normal' || value === 'active'
323
- ? 'green'
324
- : 'default'
325
- }
326
- >
327
- {label}
328
- </Tag>
329
- );
330
- },
331
- }}
332
- value={record.instrumentStatus}
333
- />
334
- ),
335
- },
336
- ]}
337
- />
338
- </Card>
339
- <Card className="oxa-section-card" id="instrument-audit" title="最近变更">
340
- {auditError ? (
341
- <Typography.Text type="secondary">审计记录暂不可用</Typography.Text>
342
- ) : auditEntries.length ? (
343
- <Timeline
344
- items={auditEntries.slice(0, 5).map(entry => ({
345
- children: (
346
- <div className="oxa-audit-entry">
347
- <strong>{auditLabel(entry.operation)}</strong>
348
- <span>{formatDateTime(entry.occurredAt)}</span>
349
- {entry.actor.userId && (
350
- <ResolvedValueText source="user" value={entry.actor.userId} />
351
- )}
352
- </div>
353
- ),
354
- }))}
355
- />
356
- ) : (
357
- <Typography.Text type="secondary">暂无变更记录</Typography.Text>
358
- )}
359
- </Card>
360
- </aside>
361
- </div>
362
- );
363
- }
364
-
365
- function surfaceField(key: string): SurfaceField {
366
- const field = instrumentSurface.fields[key];
367
- return field ? { key, ...field } : { key, label: key, widget: 'text' };
368
- }
369
-
370
- function auditLabel(operation: DataAuditEntry['operation']) {
371
- return {
372
- created: '创建仪器档案',
373
- updated: '更新仪器信息',
374
- deleted: '删除仪器档案',
375
- }[operation];
376
- }
377
-
378
- function formatDateTime(value?: string) {
379
- return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-';
380
- }
@@ -1,82 +0,0 @@
1
- import { useCreate, useOne, useUpdate } from '@refinedev/core';
2
- import { App } from 'antd';
3
- import { useNavigate, useParams } from 'react-router-dom';
4
- import {
5
- MobileResourceFormPage,
6
- ResourceFormPage,
7
- } from './components/resource/StandardResourcePages';
8
- import { InstrumentForm } from './InstrumentForm';
9
- import { INSTRUMENT_CAPABILITIES, type InstrumentRecord } from './instrument';
10
- import { instrumentSurface } from '../../../platform/data/instrument-surface.js';
11
-
12
- export function InstrumentFormPage({
13
- mode,
14
- variant = 'desktop',
15
- }: {
16
- mode: 'create' | 'edit';
17
- variant?: 'desktop' | 'mobile';
18
- }) {
19
- const { id = '' } = useParams();
20
- const navigate = useNavigate();
21
- const { message } = App.useApp();
22
- const create = useCreate();
23
- const update = useUpdate();
24
- const query = useOne<InstrumentRecord>({
25
- resource: 'instruments',
26
- id,
27
- queryOptions: { enabled: mode === 'edit' },
28
- });
29
- const record = query.result;
30
- const Page = variant === 'mobile' ? MobileResourceFormPage : ResourceFormPage;
31
- const listPath = variant === 'mobile' ? '/m/instruments' : '/instruments';
32
- const detailPath = variant === 'mobile' ? `/m/instruments/${id}` : `/instruments/${id}`;
33
- return (
34
- <Page
35
- backLabel={mode === 'edit' ? '返回仪器详情' : '返回仪器资源'}
36
- backPath={mode === 'edit' ? detailPath : listPath}
37
- capability={
38
- mode === 'create'
39
- ? INSTRUMENT_CAPABILITIES.create
40
- : INSTRUMENT_CAPABILITIES.update
41
- }
42
- detailPath={record ? detailPath : undefined}
43
- loading={mode === 'edit' && query.query.isLoading}
44
- mode={mode}
45
- notFound={mode === 'edit' && !record}
46
- recordSummary={
47
- record && `${record.chineseName} · ${record.instrumentCode} · 当前版本 ${record.revision}`
48
- }
49
- resource="instruments"
50
- surface={instrumentSurface}
51
- title="仪器"
52
- >
53
- <InstrumentForm
54
- mode={mode}
55
- variant={variant}
56
- onCancel={() =>
57
- navigate(mode === 'edit' ? detailPath : listPath)
58
- }
59
- onSubmit={async values => {
60
- if (mode === 'create') {
61
- await create.mutateAsync({
62
- resource: 'instruments',
63
- values,
64
- });
65
- message.success('仪器已创建');
66
- navigate(listPath);
67
- } else {
68
- const result = await update.mutateAsync({
69
- resource: 'instruments',
70
- id,
71
- values,
72
- meta: { expectedRevision: record!.revision },
73
- });
74
- message.success('修改已保存');
75
- navigate(variant === 'mobile' ? `/m/instruments/${result.data.id}` : `/instruments/${result.data.id}`);
76
- }
77
- }}
78
- record={record}
79
- />
80
- </Page>
81
- );
82
- }