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,181 +0,0 @@
1
- import { EditOutlined, PlusOutlined } from '@ant-design/icons';
2
- import {
3
- App,
4
- Button,
5
- Card,
6
- Drawer,
7
- Form,
8
- Input,
9
- Result,
10
- Space,
11
- Switch,
12
- Table,
13
- Tag,
14
- } from 'antd';
15
- import { useEffect, useState } from 'react';
16
- import { Shell } from './Shell';
17
- import {
18
- collegeDataApi,
19
- type CollegeRecord,
20
- } from './platform-client';
21
- import { useRuntime } from './runtime';
22
-
23
- const capabilities = {
24
- read: 'app:instrument-center:data:colleges:read',
25
- create: 'app:instrument-center:data:colleges:create',
26
- update: 'app:instrument-center:data:colleges:update',
27
- } as const;
28
-
29
- export function CollegePage() {
30
- const { hasCapability } = useRuntime();
31
- const { message } = App.useApp();
32
- const [form] = Form.useForm<{ name: string; enabled: boolean }>();
33
- const [rows, setRows] = useState<CollegeRecord[]>([]);
34
- const [loading, setLoading] = useState(false);
35
- const [editing, setEditing] = useState<CollegeRecord>();
36
- const [open, setOpen] = useState(false);
37
- const [loadError, setLoadError] = useState('');
38
- const refresh = async () => {
39
- setLoading(true);
40
- setLoadError('');
41
- try {
42
- setRows((await collegeDataApi.list()).rows);
43
- } catch (error) {
44
- const reason = error instanceof Error ? error.message : String(error);
45
- setLoadError(reason);
46
- message.error(reason);
47
- } finally {
48
- setLoading(false);
49
- }
50
- };
51
- useEffect(() => {
52
- if (hasCapability(capabilities.read)) void refresh();
53
- }, []);
54
- if (!hasCapability(capabilities.read)) {
55
- return (
56
- <Shell>
57
- <Result status="403" title="当前平台用户无学院字典权限" />
58
- </Shell>
59
- );
60
- }
61
- const submit = async () => {
62
- const values = await form.validateFields();
63
- if (editing) {
64
- await collegeDataApi.update(editing.id, editing.revision, values);
65
- } else {
66
- await collegeDataApi.create(values);
67
- }
68
- setOpen(false);
69
- setEditing(undefined);
70
- form.resetFields();
71
- message.success(editing ? '学院已更新' : '学院已创建');
72
- await refresh();
73
- };
74
- return (
75
- <Shell>
76
- <Card>
77
- <div className="oxa-page-heading">
78
- <div>
79
- <h1 className="oxa-title">学院字典</h1>
80
- <div className="oxa-muted">
81
- 仪器归属保存学院记录 UUID;停用不会改写已有仪器
82
- </div>
83
- </div>
84
- {hasCapability(capabilities.create) && (
85
- <Button
86
- icon={<PlusOutlined />}
87
- onClick={() => {
88
- setEditing(undefined);
89
- form.setFieldsValue({ name: '', enabled: true });
90
- setOpen(true);
91
- }}
92
- type="primary"
93
- >
94
- 新增学院
95
- </Button>
96
- )}
97
- </div>
98
- {loadError ? (
99
- <Result
100
- extra={<Button onClick={() => void refresh()}>重新加载</Button>}
101
- status="error"
102
- subTitle={loadError}
103
- title="学院字典暂不可用"
104
- />
105
- ) : (
106
- <Table<CollegeRecord>
107
- dataSource={rows}
108
- loading={loading}
109
- pagination={false}
110
- rowKey="id"
111
- columns={[
112
- { title: '学院名称', dataIndex: 'name' },
113
- {
114
- title: '状态',
115
- dataIndex: 'enabled',
116
- render: value => (
117
- <Tag color={value ? 'green' : 'default'}>
118
- {value ? '启用' : '停用'}
119
- </Tag>
120
- ),
121
- },
122
- {
123
- title: '操作',
124
- render: (_, record) =>
125
- hasCapability(capabilities.update) ? (
126
- <Button
127
- icon={<EditOutlined />}
128
- onClick={() => {
129
- setEditing(record);
130
- form.setFieldsValue({
131
- name: record.name,
132
- enabled: record.enabled,
133
- });
134
- setOpen(true);
135
- }}
136
- >
137
- 编辑
138
- </Button>
139
- ) : null,
140
- },
141
- ]}
142
- />
143
- )}
144
- </Card>
145
- <Drawer
146
- className="oxa-dictionary-drawer"
147
- destroyOnHidden
148
- extra={
149
- <Space>
150
- <Button onClick={() => setOpen(false)}>取消</Button>
151
- <Button
152
- onClick={() =>
153
- void submit().catch(error => message.error(error.message))
154
- }
155
- type="primary"
156
- >
157
- 保存
158
- </Button>
159
- </Space>
160
- }
161
- onClose={() => setOpen(false)}
162
- open={open}
163
- size={520}
164
- title={editing ? '编辑学院' : '新增学院'}
165
- >
166
- <Form form={form} layout="vertical">
167
- <Form.Item
168
- label="学院名称"
169
- name="name"
170
- rules={[{ required: true, message: '请输入学院名称' }]}
171
- >
172
- <Input />
173
- </Form.Item>
174
- <Form.Item label="启用" name="enabled" valuePropName="checked">
175
- <Switch />
176
- </Form.Item>
177
- </Form>
178
- </Drawer>
179
- </Shell>
180
- );
181
- }
@@ -1,176 +0,0 @@
1
- import { Space, Tag, Typography } from 'antd';
2
- import { useEffect, useState } from 'react';
3
- import type { DataAuditEntry } from 'openxiangda-contracts/browser';
4
- import { useNavigate, useParams } from 'react-router-dom';
5
- import { useOne } from '@refinedev/core';
6
- import {
7
- MobileResourceDetailPage,
8
- ResourceDetailPage,
9
- } from './components/resource/StandardResourcePages';
10
- import { InstrumentForm } from './InstrumentForm';
11
- import { INSTRUMENT_CAPABILITIES, type InstrumentRecord } from './instrument';
12
- import { instrumentSurface } from '../../../platform/data/instrument-surface.js';
13
- import { instrumentDataApi } from './platform-client';
14
-
15
- export function InstrumentDetailPage({ variant = 'desktop' }: { variant?: 'desktop' | 'mobile' }) {
16
- return variant === 'mobile' ? <MobileInstrumentDetailPage /> : <DesktopInstrumentDetailPage />;
17
- }
18
-
19
- function DesktopInstrumentDetailPage() {
20
- const { id = '' } = useParams();
21
- const navigate = useNavigate();
22
- const [auditEntries, setAuditEntries] = useState<DataAuditEntry[]>([]);
23
- const [auditError, setAuditError] = useState('');
24
- const query = useOne<InstrumentRecord>({
25
- resource: 'instruments',
26
- id,
27
- queryOptions: { retry: false },
28
- });
29
- const record = query.result;
30
- useEffect(() => {
31
- if (!record?.id) return;
32
- let active = true;
33
- setAuditError('');
34
- void instrumentDataApi.audit(record.id).then(
35
- page => {
36
- if (active) setAuditEntries(page.items || []);
37
- },
38
- error => {
39
- if (active)
40
- setAuditError(error instanceof Error ? error.message : String(error));
41
- }
42
- );
43
- return () => {
44
- active = false;
45
- };
46
- }, [record?.id, record?.revision]);
47
- return (
48
- <ResourceDetailPage
49
- backLabel="返回仪器资源"
50
- backPath="/instruments"
51
- editCapability={INSTRUMENT_CAPABILITIES.update}
52
- editPath={`/instruments/${id}/edit`}
53
- error={
54
- query.query.isError || (!query.query.isLoading && !record)
55
- ? query.query.error?.message || '记录不存在'
56
- : undefined
57
- }
58
- hero={
59
- record ? (
60
- <div>
61
- <Space align="center" size={10}>
62
- <Typography.Title level={2}>仪器详情</Typography.Title>
63
- <Tag color={record.instrumentStatus === 'normal' ? 'green' : 'default'}>
64
- {record.instrumentStatus === 'normal' ? '启用' : '非正常'}
65
- </Tag>
66
- </Space>
67
- <Typography.Title className="oxa-instrument-name" level={3}>
68
- {record.chineseName}
69
- </Typography.Title>
70
- <Typography.Text type="secondary">
71
- {record.instrumentCode} · {record.specModel}
72
- </Typography.Text>
73
- </div>
74
- ) : null
75
- }
76
- loading={query.query.isLoading}
77
- meta={
78
- record ? (
79
- <>
80
- <span>最近更新 {formatDateTime(record.updated_at)}</span>
81
- <span>版本 {record.revision}</span>
82
- </>
83
- ) : null
84
- }
85
- readCapability={INSTRUMENT_CAPABILITIES.read}
86
- resource="instruments"
87
- surface={instrumentSurface}
88
- title="仪器"
89
- auditTargetId="instrument-audit"
90
- >
91
- {record && (
92
- <InstrumentForm
93
- auditEntries={auditEntries}
94
- auditError={auditError}
95
- mode="detail"
96
- onCancel={() => navigate('/instruments')}
97
- record={record}
98
- />
99
- )}
100
- </ResourceDetailPage>
101
- );
102
- }
103
-
104
- function MobileInstrumentDetailPage() {
105
- const { id = '' } = useParams();
106
- const navigate = useNavigate();
107
- const [auditEntries, setAuditEntries] = useState<DataAuditEntry[]>([]);
108
- const [auditError, setAuditError] = useState('');
109
- const query = useOne<InstrumentRecord>({
110
- resource: 'instruments',
111
- id,
112
- queryOptions: { retry: false },
113
- });
114
- const record = query.result;
115
- useEffect(() => {
116
- if (!record?.id) return;
117
- let active = true;
118
- setAuditError('');
119
- void instrumentDataApi.audit(record.id).then(
120
- page => {
121
- if (active) setAuditEntries(page.items || []);
122
- },
123
- error => {
124
- if (active) setAuditError(error instanceof Error ? error.message : String(error));
125
- }
126
- );
127
- return () => {
128
- active = false;
129
- };
130
- }, [record?.id, record?.revision]);
131
- return (
132
- <MobileResourceDetailPage
133
- backLabel="返回仪器资源"
134
- backPath="/m/instruments"
135
- editCapability={INSTRUMENT_CAPABILITIES.update}
136
- editPath={`/m/instruments/${id}/edit`}
137
- error={query.query.isError || (!query.query.isLoading && !record) ? query.query.error?.message || '记录不存在' : undefined}
138
- hero={
139
- record ? (
140
- <div>
141
- <Space align="center" size={8}>
142
- <Typography.Title level={3}>仪器详情</Typography.Title>
143
- <Tag color={record.instrumentStatus === 'normal' ? 'green' : 'default'}>
144
- {record.instrumentStatus === 'normal' ? '启用' : '非正常'}
145
- </Tag>
146
- </Space>
147
- <Typography.Title className="oxa-instrument-name" level={4}>{record.chineseName}</Typography.Title>
148
- <Typography.Text type="secondary">{record.instrumentCode} · {record.specModel}</Typography.Text>
149
- </div>
150
- ) : null
151
- }
152
- loading={query.query.isLoading}
153
- meta={record ? <><span>最近更新 {formatDateTime(record.updated_at)}</span><span>版本 {record.revision}</span></> : null}
154
- readCapability={INSTRUMENT_CAPABILITIES.read}
155
- resource="instruments"
156
- surface={instrumentSurface}
157
- title="仪器"
158
- auditTargetId="instrument-audit"
159
- >
160
- {record && (
161
- <InstrumentForm
162
- auditEntries={auditEntries}
163
- auditError={auditError}
164
- mode="detail"
165
- onCancel={() => navigate('/m/instruments')}
166
- record={record}
167
- variant="mobile"
168
- />
169
- )}
170
- </MobileResourceDetailPage>
171
- );
172
- }
173
-
174
- function formatDateTime(value?: string) {
175
- return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-';
176
- }