openxiangda-cli 2.0.0-alpha.182 → 2.0.0-alpha.185

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 (50) hide show
  1. package/dist/base.js +2 -2
  2. package/dist/base.js.map +1 -1
  3. package/dist/commands/admin.d.ts.map +1 -1
  4. package/dist/commands/admin.js +5 -3
  5. package/dist/commands/admin.js.map +1 -1
  6. package/dist/commands/create.d.ts +1 -0
  7. package/dist/commands/create.d.ts.map +1 -1
  8. package/dist/commands/create.js +34 -16
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/dev.d.ts.map +1 -1
  11. package/dist/commands/dev.js +1 -1
  12. package/dist/commands/dev.js.map +1 -1
  13. package/dist/commands/login.js +1 -1
  14. package/dist/commands/login.js.map +1 -1
  15. package/dist/commands/spec.js +1 -1
  16. package/dist/commands/spec.js.map +1 -1
  17. package/package.json +5 -5
  18. package/template/AGENTS.md +2 -2
  19. package/template/apps/web/e2e/README.md +7 -0
  20. package/template/apps/web/package.json +2 -2
  21. package/template/apps/web/playwright.config.ts +3 -5
  22. package/template/apps/web/vite.config.ts +1 -9
  23. package/template/appspec/app.md +5 -1
  24. package/template/package.json +1 -1
  25. package/template/apps/web/data-api-live.e2e.html +0 -15
  26. package/template/apps/web/e2e/client-fixture.ts +0 -5
  27. package/template/apps/web/e2e/component-defaults-fixture.css +0 -15
  28. package/template/apps/web/e2e/data-api-live-fixture.tsx +0 -162
  29. package/template/apps/web/e2e/data-api-live.spec.ts +0 -144
  30. package/template/apps/web/e2e/field-protocol-fixture.tsx +0 -387
  31. package/template/apps/web/e2e/field-protocol.spec.ts +0 -285
  32. package/template/apps/web/e2e/mobile-files.spec.ts +0 -96
  33. package/template/apps/web/e2e/mobile-reference-fixture.tsx +0 -314
  34. package/template/apps/web/e2e/mobile-reference.spec.ts +0 -360
  35. package/template/apps/web/e2e/mobile-selection.spec.ts +0 -199
  36. package/template/apps/web/e2e/resource-experience-fixture.tsx +0 -491
  37. package/template/apps/web/e2e/resource-platform-mock.ts +0 -202
  38. package/template/apps/web/e2e/resources.spec.ts +0 -488
  39. package/template/apps/web/e2e/standard-admin.spec.ts +0 -511
  40. package/template/apps/web/e2e/workflow-entry-fixture.tsx +0 -107
  41. package/template/apps/web/e2e/workflow-entry.spec.ts +0 -556
  42. package/template/apps/web/e2e/workflow-experience-fixture.tsx +0 -338
  43. package/template/apps/web/e2e/workflow.spec.ts +0 -1265
  44. package/template/apps/web/field-protocol.e2e.html +0 -22
  45. package/template/apps/web/mobile-reference.e2e.html +0 -27
  46. package/template/apps/web/resource-experience.e2e.html +0 -16
  47. package/template/apps/web/scripts/check.mjs +0 -43
  48. package/template/apps/web/test/contracts.test.ts +0 -163
  49. package/template/apps/web/workflow-entry.e2e.html +0 -15
  50. package/template/apps/web/workflow-experience.e2e.html +0 -15
@@ -1,162 +0,0 @@
1
- import { Refine } from '@refinedev/core';
2
- import { App as AntdApp } from 'antd';
3
- import React from 'react';
4
- import ReactDOM from 'react-dom/client';
5
- import { MemoryRouter, Route, Routes } from 'react-router-dom';
6
- import {
7
- OpenXiangdaUiProvider,
8
- createApplicationProvider,
9
- GeneratedResourcePage,
10
- OpenXiangdaResourceDefinitionsProvider,
11
- RuntimeBoundary,
12
- type GeneratedResourceDefinition,
13
- } from 'openxiangda/react';
14
- import 'openxiangda/react/styles.css';
15
-
16
- const code = 'field-records';
17
- const capabilityBase = `app:native-projection-app:data:${code}`;
18
- const capabilities = {
19
- read: `${capabilityBase}:read`,
20
- create: `${capabilityBase}:create`,
21
- update: `${capabilityBase}:update`,
22
- delete: `${capabilityBase}:delete`,
23
- };
24
-
25
- function fieldCapabilities() {
26
- return {
27
- readCapabilities: [capabilities.read],
28
- createCapabilities: [capabilities.create],
29
- updateCapabilities: [capabilities.update],
30
- };
31
- }
32
-
33
- export const liveDefinition: GeneratedResourceDefinition = {
34
- code,
35
- name: '字段协议记录',
36
- capabilities,
37
- surface: {
38
- mutationOwner: 'native',
39
- generated: {
40
- list: true,
41
- detail: true,
42
- create: true,
43
- update: true,
44
- delete: true,
45
- },
46
- fields: {
47
- text_short: {
48
- label: '标题',
49
- type: 'text.short',
50
- widget: 'text',
51
- requiredHint: true,
52
- maxLength: 120,
53
- list: true,
54
- searchable: true,
55
- sortable: true,
56
- ...fieldCapabilities(),
57
- },
58
- option_single: {
59
- label: '单选快照',
60
- type: 'option.single',
61
- widget: 'radio',
62
- options: [
63
- { label: 'Open', value: 'open' },
64
- { label: 'Closed', value: 'closed' },
65
- ],
66
- list: true,
67
- ...fieldCapabilities(),
68
- },
69
- option_multiple: {
70
- label: '多选快照',
71
- type: 'option.multiple',
72
- widget: 'checkbox',
73
- options: [
74
- { label: 'Open', value: 'open' },
75
- { label: 'Urgent', value: 'urgent' },
76
- ],
77
- list: true,
78
- ...fieldCapabilities(),
79
- },
80
- location_value: {
81
- label: '精确定位',
82
- type: 'location',
83
- widget: 'location',
84
- list: true,
85
- ...fieldCapabilities(),
86
- },
87
- },
88
- list: {
89
- defaultPageSize: 20,
90
- searchableFields: ['text_short'],
91
- filterFields: ['option_single'],
92
- defaultSort: { field: 'text_short', order: 'asc' },
93
- },
94
- form: { layout: 'sections' },
95
- detail: { layout: 'sections' },
96
- mobile: { enabled: true },
97
- },
98
- };
99
-
100
- const definitions = { [code]: liveDefinition };
101
- const provider = createApplicationProvider(definitions);
102
- const routeBase = `/admin/resources/${code}`;
103
- const routePaths = {
104
- fallback: '/admin',
105
- list: routeBase,
106
- create: `${routeBase}/new`,
107
- detail: `${routeBase}/:id`,
108
- edit: `${routeBase}/:id/edit`,
109
- };
110
- const initialPath =
111
- new URLSearchParams(window.location.search).get('initial') || routeBase;
112
-
113
- function resourcePage(mode: 'list' | 'create' | 'edit' | 'detail') {
114
- return (
115
- <GeneratedResourcePage
116
- definition={liveDefinition}
117
- mode={mode}
118
- paths={routePaths}
119
- resourceCode={code}
120
- />
121
- );
122
- }
123
-
124
- ReactDOM.createRoot(document.getElementById('root')!).render(
125
- <React.StrictMode>
126
- <OpenXiangdaResourceDefinitionsProvider definitions={definitions}>
127
- <OpenXiangdaUiProvider>
128
- <AntdApp>
129
- <RuntimeBoundary>
130
- <MemoryRouter initialEntries={[initialPath]}>
131
- <Refine
132
- dataProvider={provider}
133
- resources={[
134
- {
135
- name: code,
136
- list: routePaths.list,
137
- create: routePaths.create,
138
- edit: routePaths.edit,
139
- show: routePaths.detail,
140
- },
141
- ]}
142
- >
143
- <Routes>
144
- <Route path={routePaths.list} element={resourcePage('list')} />
145
- <Route path={routePaths.create} element={resourcePage('create')} />
146
- <Route
147
- path={routePaths.detail}
148
- element={resourcePage('detail')}
149
- />
150
- <Route
151
- path={routePaths.edit}
152
- element={resourcePage('edit')}
153
- />
154
- </Routes>
155
- </Refine>
156
- </MemoryRouter>
157
- </RuntimeBoundary>
158
- </AntdApp>
159
- </OpenXiangdaUiProvider>
160
- </OpenXiangdaResourceDefinitionsProvider>
161
- </React.StrictMode>
162
- );
@@ -1,144 +0,0 @@
1
- import { expect, test } from '@playwright/test';
2
-
3
- const liveEnabled = process.env.OPENXIANGDA_LIVE_DATA_API === '1';
4
- const title = 'UI live field record';
5
-
6
- test.describe('@live-data-api standard resource UI', () => {
7
- test.setTimeout(90_000);
8
- test.skip(
9
- !liveEnabled,
10
- 'Requires the disposable PostgreSQL Native Data API acceptance bridge.'
11
- );
12
-
13
- test.beforeEach(async ({ context }) => {
14
- await context.addCookies([
15
- {
16
- name: 'openxiangda-field-ui-role',
17
- value: 'super',
18
- domain: '127.0.0.1',
19
- path: '/',
20
- },
21
- ]);
22
- });
23
-
24
- test('runs create, list, query, get, update, audit and delete through the standard page', async ({
25
- context,
26
- page,
27
- }) => {
28
- await context.grantPermissions(['geolocation']);
29
- await context.setGeolocation({
30
- longitude: 120.123456,
31
- latitude: 30.234567,
32
- accuracy: 4,
33
- });
34
- await page.goto(
35
- '/data-api-live.e2e.html?initial=/admin/resources/field-records',
36
- );
37
- await expect(page.locator('.oxa-list-surface')).toBeVisible();
38
-
39
- await page.getByRole('button', { name: '新增字段协议记录' }).click();
40
- await expect(page.getByRole('heading', { name: '新增字段协议记录' })).toBeVisible();
41
- await page.getByLabel('标题').fill(title);
42
- await page.getByRole('radio', { name: 'Open' }).check();
43
- const openCheckbox = page.getByRole('checkbox', { name: 'Open' });
44
- const urgentCheckbox = page.getByRole('checkbox', { name: 'Urgent' });
45
- await openCheckbox.click();
46
- await urgentCheckbox.click();
47
- await expect(openCheckbox).toBeChecked();
48
- await expect(urgentCheckbox).toBeChecked();
49
- await page.getByRole('button', { name: '浏览器定位' }).click();
50
- await expect(page.getByText('120.123456, 30.234567')).toBeVisible();
51
- await page.getByRole('button', { name: /^保\s*存$/ }).click();
52
-
53
- await expect(page.locator('.oxa-list-surface')).toBeVisible();
54
- await page.locator('.oxa-search-panel .ant-select').first().click();
55
- await page.getByText('标题', { exact: true }).last().click();
56
- await page.getByPlaceholder('关键词').fill(title);
57
- await page.getByRole('button', { name: /查询/ }).click();
58
- const createdRow = page.getByRole('row').filter({ hasText: title });
59
- await expect(createdRow).toHaveCount(1);
60
- await expect(createdRow).toContainText('Open');
61
- await expect(createdRow).toContainText('Urgent');
62
- await expect(createdRow).toContainText('120.123456, 30.234567');
63
-
64
- await createdRow.getByRole('button', { name: '查看' }).click();
65
- await expect(page.getByRole('heading', { name: '字段协议记录详情' })).toBeVisible();
66
- await expect(page.getByText(title, { exact: true })).toBeVisible();
67
- await expect(page.getByText(/浏览器定位/)).toBeVisible();
68
- await page.locator('.oxa-audit-collapse .ant-collapse-header').click();
69
- await expect(page.getByText('创建', { exact: true })).toBeVisible();
70
- await page.screenshot({
71
- fullPage: true,
72
- path: 'test-results/data-api-live-detail.png',
73
- });
74
-
75
- await page.getByRole('button', { name: /编辑/ }).click();
76
- await page.getByLabel('标题').fill(`${title} updated`);
77
- await page.getByRole('radio', { name: 'Closed' }).check();
78
- await page.getByRole('checkbox', { name: 'Open' }).click();
79
- await expect(page.getByRole('checkbox', { name: 'Open' })).not.toBeChecked();
80
- await page.getByRole('button', { name: /^保存修改$/ }).click();
81
- await expect(page.getByText(`${title} updated`, { exact: true })).toBeVisible();
82
- await expect(page.getByText('Closed', { exact: true })).toBeVisible();
83
-
84
- await page.getByRole('button', { name: '返回列表' }).click();
85
- const updatedRow = page
86
- .getByRole('row')
87
- .filter({ hasText: `${title} updated` });
88
- await expect(updatedRow).toHaveCount(1);
89
- await updatedRow.getByRole('button', { name: '删除' }).click();
90
- await page.getByRole('button', { name: /^确\s*定$/ }).click();
91
- await expect(updatedRow).toHaveCount(0);
92
- });
93
-
94
- test('applies PostgreSQL row scope to the standard list and query UI', async ({
95
- context,
96
- page,
97
- }) => {
98
- await context.clearCookies();
99
- await context.addCookies([
100
- {
101
- name: 'openxiangda-field-ui-role',
102
- value: 'restricted',
103
- domain: '127.0.0.1',
104
- path: '/',
105
- },
106
- ]);
107
- await page.goto(
108
- '/data-api-live.e2e.html?initial=/admin/resources/field-records',
109
- );
110
- await expect(page.locator('.oxa-list-surface')).toBeVisible();
111
- await page.locator('.oxa-search-panel .ant-select').first().click();
112
- await page.getByText('标题', { exact: true }).last().click();
113
- await page.getByPlaceholder('关键词').fill('UI RLS');
114
- await page.getByRole('button', { name: /查询/ }).click();
115
- await expect(page.getByText('UI RLS visible', { exact: true })).toBeVisible();
116
- await expect(page.getByText('UI RLS hidden', { exact: true })).toHaveCount(0);
117
- await page.screenshot({
118
- fullPage: true,
119
- path: 'test-results/data-api-live-rls.png',
120
- });
121
- });
122
-
123
- test('fails closed in the standard page when the role has no read capability', async ({
124
- context,
125
- page,
126
- }) => {
127
- await context.clearCookies();
128
- await context.addCookies([
129
- {
130
- name: 'openxiangda-field-ui-role',
131
- value: 'denied',
132
- domain: '127.0.0.1',
133
- path: '/',
134
- },
135
- ]);
136
- await page.goto(
137
- '/data-api-live.e2e.html?initial=/admin/resources/field-records',
138
- );
139
- await expect(
140
- page.getByText('当前平台用户无字段协议记录页面权限', { exact: true })
141
- ).toBeVisible();
142
- await expect(page.locator('.oxa-list-surface')).toHaveCount(0);
143
- });
144
- });
@@ -1,387 +0,0 @@
1
- import { App, Button, ConfigProvider, Form, Typography } from 'antd';
2
- import type {
3
- DataAuditEntry,
4
- DataFieldSurface,
5
- DataFileRef,
6
- } from 'openxiangda/core';
7
- import React, { useState } from 'react';
8
- import ReactDOM from 'react-dom/client';
9
- import {
10
- auditFieldChange,
11
- fieldValueForData,
12
- fieldValueForForm,
13
- MobileSurfaceFieldControl,
14
- SurfaceAuditFieldValue,
15
- SurfaceFieldControl,
16
- SurfaceFieldValue,
17
- type SurfaceField,
18
- type SurfaceFieldRenderers,
19
- } from 'openxiangda/field-kit';
20
- import { Button as MobileButton, Input as MobileInput, MobileSurface, Popup } from 'openxiangda/mobile';
21
-
22
- if (new URLSearchParams(location.search).get('mode') === 'mobile-surface') {
23
- await import('openxiangda/mobile/styles.css');
24
- } else {
25
- await import('openxiangda/react/styles.css');
26
- }
27
-
28
- const options = [
29
- { label: '启用', value: 'enabled' },
30
- { label: '停用', value: 'disabled' },
31
- ];
32
- const source = {
33
- kind: 'resource' as const,
34
- resourceCode: 'field-lookups',
35
- labelField: 'name',
36
- searchFields: ['name'],
37
- snapshotFields: ['code'],
38
- };
39
- const field = (
40
- key: string,
41
- type: string,
42
- widget: string,
43
- extra: Record<string, unknown> = {}
44
- ) =>
45
- ({
46
- key,
47
- label: key,
48
- type,
49
- widget,
50
- readCapabilities: [],
51
- createCapabilities: [],
52
- updateCapabilities: [],
53
- ...(type === 'date-range' || type === 'datetime-range'
54
- ? { rangeBoundary: 'closed' as const }
55
- : {}),
56
- ...extra,
57
- } as unknown as SurfaceField);
58
-
59
- const fields = [
60
- field('单行文本', 'text.short', 'text', { maxLength: 120 }),
61
- field('邮箱', 'text.short', 'email'),
62
- field('电话', 'text.short', 'phone'),
63
- field('多行文本', 'text.long', 'textarea'),
64
- field('富文本', 'text.rich', 'rich-text'),
65
- field('整数', 'number.integer', 'number', { scale: 0 }),
66
- field('小数', 'number.decimal', 'number', { scale: 2 }),
67
- field('金额', 'number.decimal', 'money', { scale: 2 }),
68
- field('百分比', 'number.decimal', 'percent', { scale: 2 }),
69
- field('布尔', 'boolean', 'switch'),
70
- field('日期', 'date', 'date'),
71
- field('时间', 'time', 'time', { timePrecision: 'second' }),
72
- field('日期时间', 'datetime', 'datetime'),
73
- field('日期范围', 'date-range', 'date-range'),
74
- field('日期时间范围', 'datetime-range', 'datetime-range'),
75
- field('静态单选下拉', 'option.single', 'select', { options }),
76
- field('静态多选下拉', 'option.multiple', 'multi-select', { options }),
77
- field('单选按钮', 'option.single', 'radio', { options }),
78
- field('复选框', 'option.multiple', 'checkbox', { options }),
79
- field('动态单选下拉', 'resource-ref.single', 'select', { source }),
80
- field('动态多选下拉', 'resource-ref.multiple', 'multi-select', { source }),
81
- field('级联单选', 'cascade.single', 'cascade', {
82
- options: [
83
- {
84
- label: '设备',
85
- value: 'equipment',
86
- children: [{ label: '显微镜', value: 'microscope' }, { label: '光谱仪', value: 'spectrometer' }],
87
- },
88
- { label: '服务', value: 'service', children: [{ label: '培训', value: 'training' }] },
89
- ],
90
- }),
91
- field('级联多选', 'cascade.multiple', 'cascade', {
92
- options: [
93
- {
94
- label: '设备',
95
- value: 'equipment',
96
- children: [{ label: '显微镜', value: 'microscope' }, { label: '光谱仪', value: 'spectrometer' }],
97
- },
98
- { label: '服务', value: 'service', children: [{ label: '培训', value: 'training' }] },
99
- ],
100
- }),
101
- field('成员单选', 'user.single', 'directory-user'),
102
- field('成员多选', 'user.multiple', 'directory-user'),
103
- field('部门单选', 'department.single', 'directory-department'),
104
- field('部门多选', 'department.multiple', 'directory-department'),
105
- field('资源单选', 'resource-ref.single', 'resource', { source }),
106
- field('资源多选', 'resource-ref.multiple', 'resource', { source }),
107
- field('附件', 'file', 'attachment', {
108
- accept: ['.pdf', 'text/plain'],
109
- maxCount: 3,
110
- maxSizeMb: 20,
111
- }),
112
- field('图片', 'image', 'image', {
113
- accept: ['image/*'],
114
- maxCount: 3,
115
- maxSizeMb: 20,
116
- }),
117
- field('业务签名', 'signature', 'signature'),
118
- field('地址', 'address', 'address'),
119
- field('精确定位', 'location', 'location'),
120
- field('UUID', 'uuid', 'readonly', { requiredHint: true }),
121
- field('JSON', 'json', 'json'),
122
- field('流水号', 'serial-number', 'readonly'),
123
- field('子表', 'subtable', 'subtable'),
124
- ] satisfies Array<DataFieldSurface & { key: string }>;
125
-
126
- const user = { label: '张三', value: 'user-1', employeeNo: 'E1001' };
127
- const department = {
128
- label: '理学院',
129
- value: 'college-a',
130
- fullPath: '学校 / 理学院',
131
- };
132
- const reference = {
133
- label: '显微镜',
134
- value: 'resource-1',
135
- resourceCode: 'field-lookups',
136
- snapshot: { code: 'M-001' },
137
- };
138
- const fileRef: DataFileRef = {
139
- schemaVersion: 'openxiangda.data-file-ref/v2',
140
- id: '11111111-1111-4111-8111-111111111111',
141
- name: '验收附件.pdf',
142
- size: 1280,
143
- contentType: 'application/pdf',
144
- };
145
- const imageRef: DataFileRef = {
146
- ...fileRef,
147
- id: '22222222-2222-4222-8222-222222222222',
148
- name: '验收图片.png',
149
- contentType: 'image/png',
150
- width: 640,
151
- height: 480,
152
- previewUrl: '/service/openxiangda-api/v2/applications/field-protocol-fixture/native/data/field-records/files/22222222-2222-4222-8222-222222222222/content?disposition=inline',
153
- thumbnailUrl: '/service/openxiangda-api/v2/applications/field-protocol-fixture/native/data/field-records/files/22222222-2222-4222-8222-222222222222/content?variant=thumbnail&disposition=inline',
154
- };
155
- const values: Record<string, unknown> = {
156
- 单行文本: '仪器验收记录',
157
- 邮箱: 'user@example.com',
158
- 电话: '13800000000',
159
- 多行文本: '第一行\n第二行',
160
- 富文本: `<p>已保存的<strong>富文本</strong></p><img src="${imageRef.previewUrl}" alt="富文本图片">`,
161
- 整数: -7,
162
- 小数: 12.34,
163
- 金额: 5600.5,
164
- 百分比: 98.5,
165
- 布尔: true,
166
- 日期: '2026-08-24',
167
- 时间: '10:20:30',
168
- 日期时间: '2026-08-24T02:20:30.000Z',
169
- 日期范围: { start: '2026-08-01', end: '2026-08-31' },
170
- 日期时间范围: {
171
- start: '2026-08-01T00:00:00.000Z',
172
- end: '2026-08-31T23:59:59.000Z',
173
- },
174
- 静态单选下拉: options[0],
175
- 静态多选下拉: options,
176
- 单选按钮: options[0],
177
- 复选框: options,
178
- 动态单选下拉: reference,
179
- 动态多选下拉: [reference],
180
- 级联单选: [
181
- { label: '设备', value: 'equipment' },
182
- { label: '显微镜', value: 'microscope' },
183
- ],
184
- 级联多选: [
185
- [
186
- { label: '设备', value: 'equipment' },
187
- { label: '显微镜', value: 'microscope' },
188
- ],
189
- ],
190
- 成员单选: user,
191
- 成员多选: [user],
192
- 部门单选: department,
193
- 部门多选: [department],
194
- 资源单选: reference,
195
- 资源多选: [reference],
196
- 附件: [fileRef],
197
- 图片: [imageRef],
198
- 业务签名: {
199
- file: {
200
- ...fileRef,
201
- id: '33333333-3333-4333-8333-333333333333',
202
- name: 'signature.png',
203
- contentType: 'image/png',
204
- },
205
- signer: user,
206
- signedAt: '2026-08-24T02:20:30.000Z',
207
- hash: 'a'.repeat(64),
208
- points: [{ x: 1, y: 1, t: 1 }],
209
- },
210
- 地址: {
211
- country: { label: '中国', value: 'CN' },
212
- province: { label: '浙江省', value: '330000' },
213
- city: { label: '杭州市', value: '330100' },
214
- district: { label: '西湖区', value: '330106' },
215
- detail: '文一路 1 号',
216
- fullAddress: '浙江省杭州市西湖区文一路 1 号',
217
- },
218
- 精确定位: {
219
- source: 'browser',
220
- longitude: 120.123456,
221
- latitude: 30.234567,
222
- accuracy: 5,
223
- capturedAt: '2026-08-24T02:20:30.000Z',
224
- },
225
- JSON: { status: 'ok', nested: { count: 1 } },
226
- 流水号: 'FP-001000',
227
- 子表: [{ item_name: '第一行' }, { item_name: '第二行' }],
228
- };
229
-
230
- const auditEntry: DataAuditEntry = {
231
- id: 'field-protocol-event',
232
- operation: 'created',
233
- recordId: 'field-protocol-record',
234
- revision: 1,
235
- actor: { principalType: 'user', subjectId: 'user-1' },
236
- correlation: {
237
- eventId: 'field-protocol-event',
238
- requestId: null,
239
- traceId: null,
240
- environmentKey: 'preproduction',
241
- appVersionId: 'field-protocol-version',
242
- environmentHeadRevision: 1,
243
- capturePlanRevision: 1,
244
- cause: { eventId: null, subscriptionCode: null, depth: 0 },
245
- },
246
- changes: Object.fromEntries(
247
- fields.map(item => [item.key, { after: values[item.key] ?? null }])
248
- ),
249
- projection: {},
250
- occurredAt: '2026-08-25T00:00:00.000Z',
251
- };
252
-
253
- const renderers: SurfaceFieldRenderers = {
254
- signer: user,
255
- upload: async (_field, file) => {
256
- if (new URLSearchParams(location.search).has('controlledUploads')) {
257
- const response = await fetch('/__field-upload-fixture', { method: 'POST', body: file, headers: { 'x-file-name': encodeURIComponent(file.name) } });
258
- if (!response.ok) throw new Error(await response.text());
259
- }
260
- return ({
261
- schemaVersion: 'openxiangda.data-file-ref/v2',
262
- id: crypto.randomUUID(),
263
- name: file.name,
264
- size: file.size,
265
- contentType: file.type,
266
- });
267
- },
268
- renderSubtable: () => (
269
- <div className="field-protocol-subtable">2 行子表 · 新增 · 删除 · 排序</div>
270
- ),
271
- renderValue: ({ field: item, value }) =>
272
- item.type === 'subtable'
273
- ? `${Array.isArray(value) ? value.length : 0} 行子表`
274
- : undefined,
275
- };
276
-
277
- function MobileFacadeAcceptance() {
278
- const [open, setOpen] = useState(false);
279
- const [value, setValue] = useState('');
280
- return <MobileSurface>
281
- <MobileInput aria-label="平台移动输入" value={value} onChange={setValue} />
282
- <MobileButton onClick={() => setOpen(true)}>打开移动弹层</MobileButton>
283
- <Popup visible={open} onMaskClick={() => setOpen(false)}>
284
- <div data-mobile-popup>
285
- <p>已输入:{value}</p>
286
- <MobileButton onClick={() => setOpen(false)}>关闭弹层</MobileButton>
287
- </div>
288
- </Popup>
289
- </MobileSurface>;
290
- }
291
-
292
- function AcceptancePage() {
293
- const mobile = new URLSearchParams(location.search).get('mode') === 'mobile';
294
- const Control = mobile ? MobileSurfaceFieldControl : SurfaceFieldControl;
295
- const [form] = Form.useForm();
296
- const [saved, setSaved] = useState<Record<string, unknown>>();
297
- return (
298
- <ConfigProvider><App>
299
- <main className={`field-protocol-page ${mobile ? 'is-mobile' : 'is-desktop'}`}>
300
- <header>
301
- <Typography.Title level={mobile ? 4 : 2}>
302
- Field Kit 协议验收
303
- </Typography.Title>
304
- <Typography.Text type="secondary">
305
- {mobile ? '移动编辑态' : '桌面编辑态与已存快照只读态'}
306
- </Typography.Text>
307
- </header>
308
- <section aria-label={mobile ? '移动编辑字段' : '桌面编辑字段'} className="field-protocol-edit">
309
- <Form form={form} initialValues={values} layout="vertical" onFinish={current => {
310
- setSaved(Object.fromEntries(fields.map(item => [item.key, fieldValueForData(item, current[item.key])])));
311
- }}>
312
- <div className="field-protocol-grid">
313
- {fields.map(item => (
314
- <div data-field-code={item.key} key={item.key}>
315
- <Control
316
- disabled={item.widget === 'readonly'}
317
- field={item}
318
- operation="create"
319
- renderers={renderers}
320
- resourceCode="field-records"
321
- />
322
- </div>
323
- ))}
324
- </div>
325
- <Button htmlType="submit">验证表单</Button>
326
- {saved && <Button onClick={() => form.setFieldsValue(Object.fromEntries(fields.map(item => [item.key, fieldValueForForm(item, saved[item.key])])))}>重新载入已保存值</Button>}
327
- <output data-saved-values>{saved ? JSON.stringify(saved) : ''}</output>
328
- </Form>
329
- </section>
330
- {!mobile && (
331
- <section aria-label="已存快照只读字段" className="field-protocol-readonly">
332
- <Typography.Title level={3}>已存快照</Typography.Title>
333
- <div className="field-protocol-read-grid">
334
- {fields.map(item => (
335
- <div data-read-field={item.key} key={item.key}>
336
- <strong>{item.label}</strong>
337
- <SurfaceFieldValue
338
- field={item}
339
- renderers={renderers}
340
- resourceCode="field-records"
341
- value={values[item.key]}
342
- />
343
- </div>
344
- ))}
345
- </div>
346
- </section>
347
- )}
348
- {!mobile && (
349
- <section aria-label="当前协议字段变更记录" className="field-protocol-readonly">
350
- <Typography.Title level={3}>当前协议字段变更记录</Typography.Title>
351
- <div className="field-protocol-read-grid">
352
- {fields.map(item => (
353
- <div data-audit-field={item.key} key={item.key}>
354
- <strong>{item.label}</strong>
355
- <SurfaceAuditFieldValue
356
- field={item}
357
- resourceCode="field-records"
358
- value={auditFieldChange(auditEntry, item.key)?.after}
359
- />
360
- </div>
361
- ))}
362
- <div data-audit-digest="rich-text">
363
- <strong>富文本摘要</strong>
364
- <SurfaceAuditFieldValue
365
- field={fields[4]}
366
- value={{
367
- kind: 'digest',
368
- truncated: true,
369
- valueType: 'text.rich',
370
- bytes: 65_537,
371
- sha256: 'a'.repeat(64),
372
- }}
373
- />
374
- </div>
375
- </div>
376
- </section>
377
- )}
378
- </main>
379
- </App></ConfigProvider>
380
- );
381
- }
382
-
383
- ReactDOM.createRoot(document.getElementById('root')!).render(
384
- <React.StrictMode>
385
- {new URLSearchParams(location.search).get('mode') === 'mobile-surface' ? <MobileFacadeAcceptance /> : <AcceptancePage />}
386
- </React.StrictMode>
387
- );