openxiangda-cli 2.0.0-alpha.14 → 2.0.0-alpha.140

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 (276) hide show
  1. package/README.md +51 -15
  2. package/bin/run.js +79 -1
  3. package/dist/base.d.ts +59 -5
  4. package/dist/base.d.ts.map +1 -1
  5. package/dist/base.js +213 -10
  6. package/dist/base.js.map +1 -1
  7. package/dist/commands/accept.d.ts +28 -0
  8. package/dist/commands/accept.d.ts.map +1 -0
  9. package/dist/commands/accept.js +21 -0
  10. package/dist/commands/accept.js.map +1 -0
  11. package/dist/commands/cancel.d.ts +19 -1
  12. package/dist/commands/cancel.d.ts.map +1 -1
  13. package/dist/commands/cancel.js +1 -1
  14. package/dist/commands/cancel.js.map +1 -1
  15. package/dist/commands/check.d.ts +22 -1
  16. package/dist/commands/check.d.ts.map +1 -1
  17. package/dist/commands/check.js +17 -4
  18. package/dist/commands/check.js.map +1 -1
  19. package/dist/commands/create.d.ts +39 -0
  20. package/dist/commands/create.d.ts.map +1 -0
  21. package/dist/commands/create.js +457 -0
  22. package/dist/commands/create.js.map +1 -0
  23. package/dist/commands/deploy.d.ts +26 -8
  24. package/dist/commands/deploy.d.ts.map +1 -1
  25. package/dist/commands/deploy.js +31 -17
  26. package/dist/commands/deploy.js.map +1 -1
  27. package/dist/commands/dev.d.ts +23 -1
  28. package/dist/commands/dev.d.ts.map +1 -1
  29. package/dist/commands/dev.js +28 -4
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/commands/login.d.ts +29 -0
  32. package/dist/commands/login.d.ts.map +1 -0
  33. package/dist/commands/login.js +73 -0
  34. package/dist/commands/login.js.map +1 -0
  35. package/dist/commands/logs.d.ts +22 -2
  36. package/dist/commands/logs.d.ts.map +1 -1
  37. package/dist/commands/logs.js +9 -5
  38. package/dist/commands/logs.js.map +1 -1
  39. package/dist/commands/retry.d.ts +19 -1
  40. package/dist/commands/retry.d.ts.map +1 -1
  41. package/dist/commands/retry.js +5 -2
  42. package/dist/commands/retry.js.map +1 -1
  43. package/dist/commands/rollback.d.ts +23 -4
  44. package/dist/commands/rollback.d.ts.map +1 -1
  45. package/dist/commands/rollback.js +18 -5
  46. package/dist/commands/rollback.js.map +1 -1
  47. package/dist/commands/skill.d.ts +31 -0
  48. package/dist/commands/skill.d.ts.map +1 -0
  49. package/dist/commands/skill.js +43 -0
  50. package/dist/commands/skill.js.map +1 -0
  51. package/dist/commands/spec.d.ts +39 -0
  52. package/dist/commands/spec.d.ts.map +1 -0
  53. package/dist/commands/spec.js +117 -0
  54. package/dist/commands/spec.js.map +1 -0
  55. package/dist/commands/start.d.ts +28 -0
  56. package/dist/commands/start.d.ts.map +1 -0
  57. package/dist/commands/start.js +18 -0
  58. package/dist/commands/start.js.map +1 -0
  59. package/dist/commands/status.d.ts +19 -1
  60. package/dist/commands/status.d.ts.map +1 -1
  61. package/dist/commands/status.js +1 -1
  62. package/dist/commands/status.js.map +1 -1
  63. package/dist/commands/stop.d.ts +28 -0
  64. package/dist/commands/stop.d.ts.map +1 -0
  65. package/dist/commands/stop.js +18 -0
  66. package/dist/commands/stop.js.map +1 -0
  67. package/dist/create-workspace.d.ts +67 -0
  68. package/dist/create-workspace.d.ts.map +1 -0
  69. package/dist/create-workspace.js +631 -0
  70. package/dist/create-workspace.js.map +1 -0
  71. package/dist/studio-events.d.ts +9 -0
  72. package/dist/studio-events.d.ts.map +1 -0
  73. package/dist/studio-events.js +30 -0
  74. package/dist/studio-events.js.map +1 -0
  75. package/dist/toolchain-capsule.d.ts +4 -0
  76. package/dist/toolchain-capsule.d.ts.map +1 -0
  77. package/dist/toolchain-capsule.js +116 -0
  78. package/dist/toolchain-capsule.js.map +1 -0
  79. package/package.json +16 -9
  80. package/template/.dockerignore +23 -0
  81. package/template/.env.example +3 -0
  82. package/template/AGENTS.md +28 -0
  83. package/template/README.md +58 -0
  84. package/template/_gitignore +14 -0
  85. package/template/apps/server/Dockerfile +20 -0
  86. package/template/apps/server/package.json +27 -0
  87. package/template/apps/server/src/app.module.ts +23 -0
  88. package/template/apps/server/src/main.ts +5 -0
  89. package/template/apps/server/test/smoke.test.ts +17 -0
  90. package/template/apps/server/tsconfig.json +9 -0
  91. package/template/apps/web/data-api-live.e2e.html +15 -0
  92. package/template/apps/web/e2e/client-fixture.ts +5 -0
  93. package/template/apps/web/e2e/data-api-live-fixture.tsx +162 -0
  94. package/template/apps/web/e2e/data-api-live.spec.ts +144 -0
  95. package/template/apps/web/e2e/field-protocol-fixture.tsx +350 -0
  96. package/template/apps/web/e2e/field-protocol.spec.ts +195 -0
  97. package/template/apps/web/e2e/resource-experience-fixture.tsx +450 -0
  98. package/template/apps/web/e2e/resources.spec.ts +612 -0
  99. package/template/apps/web/e2e/theme-contract-fixture.css +15 -0
  100. package/template/apps/web/e2e/workflow-experience-fixture.tsx +300 -0
  101. package/template/apps/web/e2e/workflow.spec.ts +809 -0
  102. package/template/apps/web/field-protocol.e2e.html +15 -0
  103. package/template/apps/web/index.html +12 -0
  104. package/template/apps/web/package.json +32 -0
  105. package/template/apps/web/playwright.config.ts +47 -0
  106. package/template/apps/web/resource-experience.e2e.html +15 -0
  107. package/template/apps/web/scripts/check.mjs +43 -0
  108. package/template/apps/web/scripts/verify-build.mjs +6 -0
  109. package/template/apps/web/src/main.tsx +62 -0
  110. package/template/apps/web/test/contracts.test.ts +155 -0
  111. package/template/apps/web/test/loopback.test.ts +62 -0
  112. package/template/apps/web/tsconfig.json +12 -0
  113. package/template/apps/web/vite.config.ts +40 -0
  114. package/template/apps/web/workflow-experience.e2e.html +15 -0
  115. package/template/appspec/app.md +36 -0
  116. package/template/openxiangda.config.ts +77 -0
  117. package/template/package.json +29 -0
  118. package/template/packages/contracts/package.json +23 -0
  119. package/template/packages/contracts/src/generated.ts +263 -0
  120. package/template/packages/contracts/src/index.ts +1 -0
  121. package/template/packages/contracts/tsconfig.json +9 -0
  122. package/template/pnpm-workspace.yaml +7 -0
  123. package/template/scripts/verify-template-budget.mjs +49 -0
  124. package/template/tsconfig.base.json +13 -0
  125. package/dist/commands/app/create.d.ts +0 -15
  126. package/dist/commands/app/create.d.ts.map +0 -1
  127. package/dist/commands/app/create.js +0 -42
  128. package/dist/commands/app/create.js.map +0 -1
  129. package/dist/commands/app/info.d.ts +0 -9
  130. package/dist/commands/app/info.d.ts.map +0 -1
  131. package/dist/commands/app/info.js +0 -10
  132. package/dist/commands/app/info.js.map +0 -1
  133. package/dist/commands/app/link.d.ts +0 -10
  134. package/dist/commands/app/link.d.ts.map +0 -1
  135. package/dist/commands/app/link.js +0 -16
  136. package/dist/commands/app/link.js.map +0 -1
  137. package/dist/commands/app/provision.d.ts +0 -9
  138. package/dist/commands/app/provision.d.ts.map +0 -1
  139. package/dist/commands/app/provision.js +0 -10
  140. package/dist/commands/app/provision.js.map +0 -1
  141. package/dist/commands/auth/login.d.ts +0 -10
  142. package/dist/commands/auth/login.d.ts.map +0 -1
  143. package/dist/commands/auth/login.js +0 -34
  144. package/dist/commands/auth/login.js.map +0 -1
  145. package/dist/commands/auth/logout.d.ts +0 -6
  146. package/dist/commands/auth/logout.d.ts.map +0 -1
  147. package/dist/commands/auth/logout.js +0 -10
  148. package/dist/commands/auth/logout.js.map +0 -1
  149. package/dist/commands/auth/status.d.ts +0 -6
  150. package/dist/commands/auth/status.d.ts.map +0 -1
  151. package/dist/commands/auth/status.js +0 -19
  152. package/dist/commands/auth/status.js.map +0 -1
  153. package/dist/commands/build.d.ts +0 -11
  154. package/dist/commands/build.d.ts.map +0 -1
  155. package/dist/commands/build.js +0 -23
  156. package/dist/commands/build.js.map +0 -1
  157. package/dist/commands/doctor.d.ts +0 -9
  158. package/dist/commands/doctor.d.ts.map +0 -1
  159. package/dist/commands/doctor.js +0 -7
  160. package/dist/commands/doctor.js.map +0 -1
  161. package/dist/commands/event/delivery/list.d.ts +0 -10
  162. package/dist/commands/event/delivery/list.d.ts.map +0 -1
  163. package/dist/commands/event/delivery/list.js +0 -14
  164. package/dist/commands/event/delivery/list.js.map +0 -1
  165. package/dist/commands/event/delivery/replay.d.ts +0 -13
  166. package/dist/commands/event/delivery/replay.d.ts.map +0 -1
  167. package/dist/commands/event/delivery/replay.js +0 -16
  168. package/dist/commands/event/delivery/replay.js.map +0 -1
  169. package/dist/commands/event/subscription/list.d.ts +0 -9
  170. package/dist/commands/event/subscription/list.d.ts.map +0 -1
  171. package/dist/commands/event/subscription/list.js +0 -10
  172. package/dist/commands/event/subscription/list.js.map +0 -1
  173. package/dist/commands/event/subscription/rotate-secret.d.ts +0 -15
  174. package/dist/commands/event/subscription/rotate-secret.d.ts.map +0 -1
  175. package/dist/commands/event/subscription/rotate-secret.js +0 -33
  176. package/dist/commands/event/subscription/rotate-secret.js.map +0 -1
  177. package/dist/commands/event/subscription/status.d.ts +0 -14
  178. package/dist/commands/event/subscription/status.d.ts.map +0 -1
  179. package/dist/commands/event/subscription/status.js +0 -19
  180. package/dist/commands/event/subscription/status.js.map +0 -1
  181. package/dist/commands/event/timer/list.d.ts +0 -9
  182. package/dist/commands/event/timer/list.d.ts.map +0 -1
  183. package/dist/commands/event/timer/list.js +0 -10
  184. package/dist/commands/event/timer/list.js.map +0 -1
  185. package/dist/commands/event/timer/status.d.ts +0 -14
  186. package/dist/commands/event/timer/status.d.ts.map +0 -1
  187. package/dist/commands/event/timer/status.js +0 -19
  188. package/dist/commands/event/timer/status.js.map +0 -1
  189. package/dist/commands/generate.d.ts +0 -10
  190. package/dist/commands/generate.d.ts.map +0 -1
  191. package/dist/commands/generate.js +0 -8
  192. package/dist/commands/generate.js.map +0 -1
  193. package/dist/commands/oauth/audit/list.d.ts +0 -12
  194. package/dist/commands/oauth/audit/list.d.ts.map +0 -1
  195. package/dist/commands/oauth/audit/list.js +0 -20
  196. package/dist/commands/oauth/audit/list.js.map +0 -1
  197. package/dist/commands/oauth/client/create.d.ts +0 -13
  198. package/dist/commands/oauth/client/create.d.ts.map +0 -1
  199. package/dist/commands/oauth/client/create.js +0 -34
  200. package/dist/commands/oauth/client/create.js.map +0 -1
  201. package/dist/commands/oauth/client/list.d.ts +0 -9
  202. package/dist/commands/oauth/client/list.d.ts.map +0 -1
  203. package/dist/commands/oauth/client/list.js +0 -10
  204. package/dist/commands/oauth/client/list.js.map +0 -1
  205. package/dist/commands/oauth/client/revoke.d.ts +0 -12
  206. package/dist/commands/oauth/client/revoke.d.ts.map +0 -1
  207. package/dist/commands/oauth/client/revoke.js +0 -12
  208. package/dist/commands/oauth/client/revoke.js.map +0 -1
  209. package/dist/commands/oauth/client/rotate.d.ts +0 -13
  210. package/dist/commands/oauth/client/rotate.d.ts.map +0 -1
  211. package/dist/commands/oauth/client/rotate.js +0 -20
  212. package/dist/commands/oauth/client/rotate.js.map +0 -1
  213. package/dist/commands/oauth/client/update.d.ts +0 -15
  214. package/dist/commands/oauth/client/update.d.ts.map +0 -1
  215. package/dist/commands/oauth/client/update.js +0 -23
  216. package/dist/commands/oauth/client/update.js.map +0 -1
  217. package/dist/commands/oauth/runtime/rotate.d.ts +0 -12
  218. package/dist/commands/oauth/runtime/rotate.d.ts.map +0 -1
  219. package/dist/commands/oauth/runtime/rotate.js +0 -31
  220. package/dist/commands/oauth/runtime/rotate.js.map +0 -1
  221. package/dist/commands/oauth/runtime/status.d.ts +0 -10
  222. package/dist/commands/oauth/runtime/status.d.ts.map +0 -1
  223. package/dist/commands/oauth/runtime/status.js +0 -17
  224. package/dist/commands/oauth/runtime/status.js.map +0 -1
  225. package/dist/commands/promote.d.ts +0 -13
  226. package/dist/commands/promote.d.ts.map +0 -1
  227. package/dist/commands/promote.js +0 -9
  228. package/dist/commands/promote.js.map +0 -1
  229. package/dist/commands/secret/audit/list.d.ts +0 -14
  230. package/dist/commands/secret/audit/list.d.ts.map +0 -1
  231. package/dist/commands/secret/audit/list.js +0 -16
  232. package/dist/commands/secret/audit/list.js.map +0 -1
  233. package/dist/commands/secret/create.d.ts +0 -18
  234. package/dist/commands/secret/create.d.ts.map +0 -1
  235. package/dist/commands/secret/create.js +0 -33
  236. package/dist/commands/secret/create.js.map +0 -1
  237. package/dist/commands/secret/delete.d.ts +0 -15
  238. package/dist/commands/secret/delete.d.ts.map +0 -1
  239. package/dist/commands/secret/delete.js +0 -20
  240. package/dist/commands/secret/delete.js.map +0 -1
  241. package/dist/commands/secret/list.d.ts +0 -10
  242. package/dist/commands/secret/list.d.ts.map +0 -1
  243. package/dist/commands/secret/list.js +0 -14
  244. package/dist/commands/secret/list.js.map +0 -1
  245. package/dist/commands/secret/rotate.d.ts +0 -18
  246. package/dist/commands/secret/rotate.d.ts.map +0 -1
  247. package/dist/commands/secret/rotate.js +0 -30
  248. package/dist/commands/secret/rotate.js.map +0 -1
  249. package/dist/commands/secret/update.d.ts +0 -17
  250. package/dist/commands/secret/update.d.ts.map +0 -1
  251. package/dist/commands/secret/update.js +0 -37
  252. package/dist/commands/secret/update.js.map +0 -1
  253. package/dist/commands/skill/install.d.ts +0 -11
  254. package/dist/commands/skill/install.d.ts.map +0 -1
  255. package/dist/commands/skill/install.js +0 -52
  256. package/dist/commands/skill/install.js.map +0 -1
  257. package/dist/commands/skill/validate.d.ts +0 -9
  258. package/dist/commands/skill/validate.d.ts.map +0 -1
  259. package/dist/commands/skill/validate.js +0 -32
  260. package/dist/commands/skill/validate.js.map +0 -1
  261. package/dist/commands/test.d.ts +0 -9
  262. package/dist/commands/test.d.ts.map +0 -1
  263. package/dist/commands/test.js +0 -7
  264. package/dist/commands/test.js.map +0 -1
  265. package/dist/commands/workflow/provider/list.d.ts +0 -10
  266. package/dist/commands/workflow/provider/list.d.ts.map +0 -1
  267. package/dist/commands/workflow/provider/list.js +0 -14
  268. package/dist/commands/workflow/provider/list.js.map +0 -1
  269. package/dist/commands/workflow/provider/rotate.d.ts +0 -15
  270. package/dist/commands/workflow/provider/rotate.d.ts.map +0 -1
  271. package/dist/commands/workflow/provider/rotate.js +0 -24
  272. package/dist/commands/workflow/provider/rotate.js.map +0 -1
  273. package/dist/secret-value.d.ts +0 -5
  274. package/dist/secret-value.d.ts.map +0 -1
  275. package/dist/secret-value.js +0 -15
  276. package/dist/secret-value.js.map +0 -1
@@ -0,0 +1,195 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ const fieldCount = 38;
4
+
5
+ test.describe('Field Kit protocol acceptance', () => {
6
+ test.use({
7
+ geolocation: { longitude: 120.123456, latitude: 30.234567 },
8
+ permissions: ['geolocation'],
9
+ });
10
+
11
+ test.beforeEach(async ({ page }) => {
12
+ await page.route('**/china-divisions/**', async route => {
13
+ await route.fulfill({
14
+ contentType: 'application/json',
15
+ json: {
16
+ code: 200,
17
+ data: [{
18
+ adcode: '330000',
19
+ name: '浙江省',
20
+ level: 'province',
21
+ hasChildren: true,
22
+ }],
23
+ },
24
+ });
25
+ });
26
+ await page.route('**/files/*/content?*', async route => {
27
+ await route.fulfill({
28
+ body: Buffer.from(
29
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
30
+ 'base64'
31
+ ),
32
+ contentType: 'image/png',
33
+ status: 200,
34
+ });
35
+ });
36
+ });
37
+
38
+ test('renders and operates every desktop edit/read component', async ({ page }) => {
39
+ const pageErrors: string[] = [];
40
+ const addressRequests: string[] = [];
41
+ page.on('pageerror', error => pageErrors.push(error.message));
42
+ page.on('request', request => {
43
+ if (request.url().includes('/china-divisions/')) {
44
+ addressRequests.push(request.url());
45
+ }
46
+ });
47
+ await page.setViewportSize({ width: 1440, height: 1100 });
48
+ await page.goto('/field-protocol.e2e.html');
49
+ await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
50
+ await expect(page.locator('[data-read-field]')).toHaveCount(fieldCount);
51
+ await expect(page.locator('[data-audit-field]')).toHaveCount(fieldCount);
52
+ await expect(page.locator('[data-field-code="静态单选下拉"] .ant-select')).toBeVisible();
53
+ await expect(page.locator('[data-field-code="单选按钮"] .ant-radio-group')).toBeVisible();
54
+ await expect(page.locator('[data-field-code="复选框"] .ant-checkbox-group')).toBeVisible();
55
+ await expect(page.locator('[data-field-code="时间"] input')).toBeVisible();
56
+ await expect(page.locator('[data-field-code="精确定位"] input')).toHaveCount(0);
57
+ await expect(page.locator('[data-field-code="精确定位"]')).not.toContainText('手工');
58
+ await expect(page.locator('.ant-alert-error')).toHaveCount(0);
59
+ await expect.poll(() => addressRequests.length).toBeGreaterThan(0);
60
+ expect(addressRequests.every(url => new URL(url).pathname.startsWith('/service/china-divisions/'))).toBe(true);
61
+
62
+ await page.getByRole('button', { name: '验证表单' }).click();
63
+ await expect(page.locator('[data-field-code="UUID"] .ant-form-item-has-error')).toHaveCount(0);
64
+
65
+ await expect(page.locator('[data-read-field="图片"] img.oxa-file-thumbnail')).toHaveAttribute('src', /blob:/);
66
+ await expect(page.locator('[data-read-field="富文本"] img')).toHaveAttribute('src', /blob:/);
67
+ expect(await page.locator('[data-read-field="图片"] img.oxa-file-thumbnail').evaluate(
68
+ image => (image as HTMLImageElement).naturalWidth
69
+ )).toBeGreaterThan(0);
70
+ expect(await page.locator('[data-read-field="富文本"] img').evaluate(
71
+ image => (image as HTMLImageElement).naturalWidth
72
+ )).toBeGreaterThan(0);
73
+ await expect(page.locator('[aria-label="当前协议字段变更记录"]')).not.toContainText('[object Object]');
74
+ await expect(page.locator('[data-audit-digest="rich-text"]')).toContainText('大值摘要');
75
+
76
+ await page.locator('[data-field-code="精确定位"]').getByRole('button', { name: '浏览器定位' }).click();
77
+ await expect(page.locator('[data-field-code="精确定位"]')).toContainText('120.123456');
78
+ await expect(page.locator('[data-field-code="精确定位"]')).toContainText('30.234567');
79
+
80
+ await page
81
+ .locator('[data-field-code="业务签名"]')
82
+ .getByRole('button', { name: /(?:开始|重新)签名/ })
83
+ .click();
84
+ const canvas = page.locator('canvas[aria-label="手写签名画布"]');
85
+ await expect(canvas).toBeVisible();
86
+ const box = await canvas.boundingBox();
87
+ expect(box).not.toBeNull();
88
+ await canvas.dispatchEvent('pointerdown', {
89
+ pointerId: 1,
90
+ pointerType: 'pen',
91
+ buttons: 1,
92
+ clientX: box!.x + 30,
93
+ clientY: box!.y + 40,
94
+ });
95
+ for (let step = 1; step <= 8; step += 1) {
96
+ await canvas.dispatchEvent('pointermove', {
97
+ pointerId: 1,
98
+ pointerType: 'pen',
99
+ buttons: 1,
100
+ clientX: box!.x + 30 + (130 * step) / 8,
101
+ clientY: box!.y + 40 + (80 * step) / 8,
102
+ });
103
+ }
104
+ await canvas.dispatchEvent('pointerup', {
105
+ pointerId: 1,
106
+ pointerType: 'pen',
107
+ buttons: 0,
108
+ clientX: box!.x + 160,
109
+ clientY: box!.y + 120,
110
+ });
111
+ const saveSignature = page.getByRole('button', { name: '保存签名' });
112
+ await expect(saveSignature).toBeEnabled();
113
+ await saveSignature.click();
114
+ await expect(page.locator('[data-field-code="业务签名"]')).not.toContainText('尚未签名');
115
+
116
+ expect(pageErrors).toEqual([]);
117
+
118
+ expect(
119
+ await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
120
+ ).toBe(true);
121
+ await page.screenshot({ fullPage: true, path: 'test-results/field-protocol-desktop.png' });
122
+ });
123
+
124
+ test('resource selectors query once when selection changes', async ({ page }) => {
125
+ const requestCounts = new Map<string, number>();
126
+ await page.route('**/source/query', async route => {
127
+ const match = new URL(route.request().url()).pathname.match(/\/fields\/([^/]+)\/source\/query$/);
128
+ const fieldCode = decodeURIComponent(match?.[1] || 'unknown');
129
+ requestCounts.set(fieldCode, (requestCounts.get(fieldCode) || 0) + 1);
130
+ const body = route.request().postDataJSON() as { operation?: 'create' | 'update' };
131
+ await route.fulfill({
132
+ contentType: 'application/json',
133
+ json: {
134
+ schemaVersion: 'openxiangda.data-field-source-page/v2',
135
+ environment: {
136
+ id: 'field-protocol-environment',
137
+ key: 'preproduction',
138
+ activeAppVersionId: 'field-protocol-version',
139
+ headRevision: 1,
140
+ authzRevisionId: 'field-protocol-authz',
141
+ authzVersion: 1,
142
+ scopeDataVersion: 'field-protocol-scope',
143
+ },
144
+ resourceCode: 'field-records',
145
+ fieldCode,
146
+ sourceResourceCode: 'field-lookups',
147
+ operation: body.operation || 'create',
148
+ items: [{
149
+ label: `候选-${fieldCode}`,
150
+ value: `candidate-${fieldCode}`,
151
+ resourceCode: 'field-lookups',
152
+ snapshot: { code: `code-${fieldCode}` },
153
+ }],
154
+ nextCursor: null,
155
+ },
156
+ });
157
+ });
158
+
159
+ await page.goto('/field-protocol.e2e.html');
160
+ await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
161
+ await page.waitForLoadState('networkidle');
162
+ for (const fieldCode of ['资源单选', '资源多选']) {
163
+ const combobox = page
164
+ .locator(`[data-field-code="${fieldCode}"]`)
165
+ .getByRole('combobox');
166
+ await expect(combobox).toBeVisible();
167
+ await combobox.click();
168
+ const dropdown = page.locator('.ant-select-dropdown:visible');
169
+ await expect(dropdown).toBeVisible();
170
+ await expect.poll(() => requestCounts.get(fieldCode) || 0).toBe(1);
171
+ await dropdown
172
+ .locator('.ant-select-item-option')
173
+ .filter({ hasText: `候选-${fieldCode}` })
174
+ .click();
175
+ await page.waitForTimeout(500);
176
+ expect(requestCounts.get(fieldCode)).toBe(1);
177
+ await page.keyboard.press('Escape');
178
+ await expect(dropdown).toBeHidden();
179
+ }
180
+ });
181
+
182
+ test('renders every mobile control without horizontal overflow', async ({ page }) => {
183
+ await page.setViewportSize({ width: 390, height: 844 });
184
+ await page.goto('/field-protocol.e2e.html?mode=mobile');
185
+ await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
186
+ await expect(page.locator('.oxa-mobile-field')).toHaveCount(fieldCount);
187
+ await expect(page.locator('[data-field-code="精确定位"] input')).toHaveCount(0);
188
+ await expect(page.locator('[data-field-code="附件"] input[type="file"]')).toBeHidden();
189
+ await expect(page.locator('.ant-alert-error')).toHaveCount(0);
190
+ expect(
191
+ await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
192
+ ).toBe(true);
193
+ await page.screenshot({ fullPage: true, path: 'test-results/field-protocol-mobile.png' });
194
+ });
195
+ });
@@ -0,0 +1,450 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { Button, Typography } from 'antd';
4
+ import {
5
+ defineApplicationContributions,
6
+ OpenXiangdaAdminPage,
7
+ OpenXiangdaApplication,
8
+ useOpenXiangdaTheme,
9
+ } from 'openxiangda/react';
10
+ import type { DataFieldSurface, DataResourceSurface } from 'openxiangda/core';
11
+ import 'openxiangda/react/styles.css';
12
+ import './theme-contract-fixture.css';
13
+
14
+ const field = (
15
+ label: string,
16
+ type: DataFieldSurface['type'] = 'text.short',
17
+ widget: DataFieldSurface['widget'] = 'text',
18
+ extra: Partial<DataFieldSurface> = {}
19
+ ): DataFieldSurface => ({
20
+ label,
21
+ type,
22
+ widget,
23
+ readCapabilities: [],
24
+ createCapabilities: [],
25
+ updateCapabilities: [],
26
+ ...extra,
27
+ });
28
+
29
+ const fields: DataResourceSurface['fields'] = {
30
+ departmentMultiple: field('构成部门', 'department.multiple', 'directory-department', {
31
+ list: true,
32
+ section: '组织信息',
33
+ }),
34
+ departmentSingle: field('牵头部门', 'department.single', 'directory-department', {
35
+ list: true,
36
+ section: '组织信息',
37
+ }),
38
+ userMultiple: field('小组成员', 'user.multiple', 'directory-user', {
39
+ list: true,
40
+ section: '组织信息',
41
+ }),
42
+ resourceMultiple: field('关联资源', 'resource-ref.multiple', 'resource', {
43
+ list: true,
44
+ section: '组织信息',
45
+ source: {
46
+ kind: 'resource',
47
+ resourceCode: 'resource-01',
48
+ labelField: 'name',
49
+ searchFields: ['name'],
50
+ },
51
+ }),
52
+ enabled: field('启用状态', 'boolean', 'switch', {
53
+ list: true,
54
+ section: '组织信息',
55
+ }),
56
+ displayOrder: field('显示顺序', 'number.integer', 'number', {
57
+ list: true,
58
+ section: '组织信息',
59
+ }),
60
+ description: field('栏目说明', 'text.long', 'textarea', {
61
+ section: '栏目设置',
62
+ }),
63
+ status: field('状态', 'option.single', 'select', {
64
+ options: [
65
+ { label: '启用', value: 'enabled' },
66
+ { label: '停用', value: 'disabled' },
67
+ ],
68
+ section: '栏目设置',
69
+ }),
70
+ name: field('栏目名称', 'text.short', 'text', { section: '栏目设置' }),
71
+ code: field('栏目编码', 'text.short', 'text', { section: '栏目设置' }),
72
+ contentType: field('内容类型', 'option.single', 'select', {
73
+ options: [
74
+ { label: '文章', value: 'article' },
75
+ { label: '图集', value: 'gallery' },
76
+ ],
77
+ section: '栏目设置',
78
+ }),
79
+ manager: field('栏目管理员', 'user.single', 'directory-user', {
80
+ section: '管理信息',
81
+ }),
82
+ facility: field('配套设施', 'option.multiple', 'multi-select', {
83
+ options: [
84
+ { label: '投影仪', value: 'projector' },
85
+ { label: '白板', value: 'whiteboard' },
86
+ ],
87
+ section: '管理信息',
88
+ }),
89
+ openingDay: field('开放星期', 'option.single', 'select', {
90
+ options: [
91
+ { label: '工作日', value: 'weekday' },
92
+ { label: '周末', value: 'weekend' },
93
+ ],
94
+ section: '管理信息',
95
+ }),
96
+ };
97
+
98
+ const routeManifest = {
99
+ schemaVersion: 'openxiangda.application-route-manifest/v3',
100
+ appCode: 'openxiangda-application',
101
+ devicePolicy: {
102
+ kind: 'viewport-family',
103
+ mobileMaxWidthPx: 900,
104
+ desktopMinWidthPx: 901,
105
+ },
106
+ rootEntry: { code: 'application-root', desktop: '/', mobile: '/m/' },
107
+ authentication: {
108
+ desktop: { routeCode: 'application-login', path: '/login' },
109
+ mobile: { routeCode: 'application-login-mobile', path: '/m/login' },
110
+ },
111
+ routes: [],
112
+ digest:
113
+ '88831760f6e672a77babbf039b35a6febca7852092f1579c2721c99596a265c4',
114
+ } as const;
115
+
116
+ const surface: DataResourceSurface = {
117
+ mutationOwner: 'native',
118
+ generated: {
119
+ list: true,
120
+ detail: true,
121
+ create: true,
122
+ update: true,
123
+ delete: true,
124
+ },
125
+ fields,
126
+ list: {
127
+ defaultPageSize: 20,
128
+ searchableFields: ['name', 'code'],
129
+ filterFields: ['contentType', 'status', 'manager', 'facility', 'openingDay'],
130
+ defaultSort: { field: 'code', order: 'asc' },
131
+ },
132
+ form: {
133
+ layout: 'sections',
134
+ fieldOrder: [
135
+ 'code',
136
+ 'name',
137
+ 'contentType',
138
+ 'description',
139
+ 'status',
140
+ 'manager',
141
+ 'facility',
142
+ 'openingDay',
143
+ ],
144
+ },
145
+ detail: {
146
+ layout: 'sections',
147
+ fieldOrder: [
148
+ 'code',
149
+ 'name',
150
+ 'contentType',
151
+ 'description',
152
+ 'status',
153
+ 'manager',
154
+ 'facility',
155
+ 'openingDay',
156
+ ],
157
+ },
158
+ mobile: { enabled: true },
159
+ };
160
+
161
+ const { enabled, displayOrder, ...remainingFields } = fields;
162
+ const acceptanceSurface = new URLSearchParams(location.search).get('primary') === 'falsy'
163
+ ? { ...surface, fields: { enabled, displayOrder, ...remainingFields } }
164
+ : surface;
165
+
166
+ const names = [
167
+ '内容栏目',
168
+ '内容文章',
169
+ '内容专题',
170
+ '会员档案',
171
+ '门户轮播',
172
+ '工会小组',
173
+ '场地预约',
174
+ '场地不可用时段',
175
+ '场地档案',
176
+ '通知公告',
177
+ '活动报名',
178
+ '资料下载',
179
+ ];
180
+ const resourceDefinitions = Object.fromEntries(
181
+ names.map((name, index) => {
182
+ const code = `resource-${String(index + 1).padStart(2, '0')}`;
183
+ return [
184
+ code,
185
+ {
186
+ code,
187
+ name,
188
+ capabilities: {
189
+ read: `app:openxiangda-application:data:${code}:read`,
190
+ create: `app:openxiangda-application:data:${code}:create`,
191
+ update: `app:openxiangda-application:data:${code}:update`,
192
+ delete: `app:openxiangda-application:data:${code}:delete`,
193
+ },
194
+ surface: acceptanceSurface,
195
+ },
196
+ ];
197
+ })
198
+ );
199
+
200
+ function ContentOperationsPage() {
201
+ const { effectiveAppearance, tokens } = useOpenXiangdaTheme();
202
+ return (
203
+ <OpenXiangdaAdminPage
204
+ className="theme-contract-page"
205
+ data-primary={tokens.primary}
206
+ data-theme={effectiveAppearance}
207
+ data-testid="custom-admin-page"
208
+ >
209
+ <Typography.Title level={2}>内容业务操作</Typography.Title>
210
+ <div className="theme-contract-panel">
211
+ 自定义后台页语义主题面板
212
+ </div>
213
+ </OpenXiangdaAdminPage>
214
+ );
215
+ }
216
+
217
+ function ResourcePortalPage() {
218
+ return <Typography.Title level={2}>用户端内容门户</Typography.Title>;
219
+ }
220
+
221
+ const applicationContributions = defineApplicationContributions(
222
+ {
223
+ contentOperations: {
224
+ code: 'content-operations',
225
+ path: '/admin/operations/content',
226
+ label: '内容业务操作',
227
+ surface: 'admin',
228
+ capability: 'app:openxiangda-application:operations:read',
229
+ tabPersistence: 'session',
230
+ keepAlive: 'none',
231
+ },
232
+ resourcePortal: {
233
+ code: 'resource-portal',
234
+ path: '/resource-01',
235
+ label: '用户端内容门户',
236
+ surface: 'user',
237
+ tabPersistence: 'session',
238
+ keepAlive: 'none',
239
+ },
240
+ } as const,
241
+ {
242
+ pages: {
243
+ contentOperations: ContentOperationsPage,
244
+ resourcePortal: ResourcePortalPage,
245
+ },
246
+ resources: {
247
+ 'resource-01': {
248
+ toolbar: [
249
+ {
250
+ code: 'publish-selected',
251
+ label: '发布选中栏目',
252
+ capability: 'app:openxiangda-application:operations:read',
253
+ render: ({ selectedRecords }) => (
254
+ <Button>发布选中栏目({selectedRecords.length})</Button>
255
+ ),
256
+ },
257
+ ],
258
+ },
259
+ },
260
+ },
261
+ );
262
+
263
+ if (location.pathname.endsWith('/resource-experience.e2e.html')) {
264
+ history.replaceState({}, '', '/admin/resources/resource-01');
265
+ }
266
+
267
+ ReactDOM.createRoot(document.getElementById('root')!).render(
268
+ <React.StrictMode>
269
+ <OpenXiangdaApplication
270
+ appCode="openxiangda-application"
271
+ appName="资源体验验收"
272
+ adminNavigation={[
273
+ {
274
+ code: 'resources',
275
+ label: '数据管理',
276
+ icon: 'database',
277
+ order: 0,
278
+ items: [
279
+ { pageCode: 'resource:resource-01:list', label: '内容栏目', order: 0 },
280
+ { pageCode: 'resource:resource-02:list', label: '内容文章', order: 1 },
281
+ { pageCode: 'resource:resource-03:list', label: '内容专题', order: 2 },
282
+ { pageCode: 'resource:resource-04:list', label: '会员档案', order: 3 },
283
+ { pageCode: 'resource:resource-05:list', label: '门户轮播', order: 4 },
284
+ { pageCode: 'resource:resource-06:list', label: '工会小组', order: 5 },
285
+ { pageCode: 'resource:resource-07:list', label: '场地预约', order: 6 },
286
+ {
287
+ pageCode: 'resource:resource-08:list',
288
+ label: '场地不可用时段',
289
+ order: 7,
290
+ },
291
+ { pageCode: 'resource:resource-09:list', label: '场地档案', order: 8 },
292
+ { pageCode: 'resource:resource-10:list', label: '通知公告', order: 9 },
293
+ { pageCode: 'resource:resource-11:list', label: '活动报名', order: 10 },
294
+ { pageCode: 'resource:resource-12:list', label: '资料下载', order: 11 },
295
+ { pageCode: 'operation:content-operations', order: 12 },
296
+ ],
297
+ },
298
+ ]}
299
+ adminPages={[
300
+ {
301
+ code: 'resource:resource-01:list',
302
+ kind: 'resource-list',
303
+ path: '/admin/resources/resource-01',
304
+ label: '资源 01',
305
+ navigationEligible: true,
306
+ capability: resourceDefinitions['resource-01'].capabilities.read,
307
+ resourceCode: 'resource-01',
308
+ },
309
+ {
310
+ code: 'resource:resource-01:detail',
311
+ kind: 'resource-detail',
312
+ path: '/admin/resources/resource-01/:id',
313
+ label: '资源 01 详情',
314
+ navigationEligible: false,
315
+ capability: resourceDefinitions['resource-01'].capabilities.read,
316
+ resourceCode: 'resource-01',
317
+ },
318
+ {
319
+ code: 'resource:resource-01:create',
320
+ kind: 'resource-create',
321
+ path: '/admin/resources/resource-01/new',
322
+ label: '新增资源 01',
323
+ navigationEligible: false,
324
+ capability: resourceDefinitions['resource-01'].capabilities.create,
325
+ resourceCode: 'resource-01',
326
+ },
327
+ {
328
+ code: 'resource:resource-01:update',
329
+ kind: 'resource-update',
330
+ path: '/admin/resources/resource-01/:id/edit',
331
+ label: '编辑资源 01',
332
+ navigationEligible: false,
333
+ capability: resourceDefinitions['resource-01'].capabilities.update,
334
+ resourceCode: 'resource-01',
335
+ },
336
+ {
337
+ code: 'resource:resource-02:list',
338
+ kind: 'resource-list',
339
+ path: '/admin/resources/resource-02',
340
+ label: '内容文章',
341
+ navigationEligible: true,
342
+ capability: resourceDefinitions['resource-02'].capabilities.read,
343
+ resourceCode: 'resource-02',
344
+ },
345
+ {
346
+ code: 'resource:resource-03:list',
347
+ kind: 'resource-list',
348
+ path: '/admin/resources/resource-03',
349
+ label: '内容专题',
350
+ navigationEligible: true,
351
+ capability: resourceDefinitions['resource-03'].capabilities.read,
352
+ resourceCode: 'resource-03',
353
+ },
354
+ {
355
+ code: 'resource:resource-04:list',
356
+ kind: 'resource-list',
357
+ path: '/admin/resources/resource-04',
358
+ label: '会员档案',
359
+ navigationEligible: true,
360
+ capability: resourceDefinitions['resource-04'].capabilities.read,
361
+ resourceCode: 'resource-04',
362
+ },
363
+ {
364
+ code: 'resource:resource-05:list',
365
+ kind: 'resource-list',
366
+ path: '/admin/resources/resource-05',
367
+ label: '门户轮播',
368
+ navigationEligible: true,
369
+ capability: resourceDefinitions['resource-05'].capabilities.read,
370
+ resourceCode: 'resource-05',
371
+ },
372
+ {
373
+ code: 'resource:resource-06:list',
374
+ kind: 'resource-list',
375
+ path: '/admin/resources/resource-06',
376
+ label: '工会小组',
377
+ navigationEligible: true,
378
+ capability: resourceDefinitions['resource-06'].capabilities.read,
379
+ resourceCode: 'resource-06',
380
+ },
381
+ {
382
+ code: 'resource:resource-07:list',
383
+ kind: 'resource-list',
384
+ path: '/admin/resources/resource-07',
385
+ label: '场地预约',
386
+ navigationEligible: true,
387
+ capability: resourceDefinitions['resource-07'].capabilities.read,
388
+ resourceCode: 'resource-07',
389
+ },
390
+ {
391
+ code: 'resource:resource-08:list',
392
+ kind: 'resource-list',
393
+ path: '/admin/resources/resource-08',
394
+ label: '场地不可用时段',
395
+ navigationEligible: true,
396
+ capability: resourceDefinitions['resource-08'].capabilities.read,
397
+ resourceCode: 'resource-08',
398
+ },
399
+ {
400
+ code: 'resource:resource-09:list',
401
+ kind: 'resource-list',
402
+ path: '/admin/resources/resource-09',
403
+ label: '场地档案',
404
+ navigationEligible: true,
405
+ capability: resourceDefinitions['resource-09'].capabilities.read,
406
+ resourceCode: 'resource-09',
407
+ },
408
+ {
409
+ code: 'resource:resource-10:list',
410
+ kind: 'resource-list',
411
+ path: '/admin/resources/resource-10',
412
+ label: '通知公告',
413
+ navigationEligible: true,
414
+ capability: resourceDefinitions['resource-10'].capabilities.read,
415
+ resourceCode: 'resource-10',
416
+ },
417
+ {
418
+ code: 'resource:resource-11:list',
419
+ kind: 'resource-list',
420
+ path: '/admin/resources/resource-11',
421
+ label: '活动报名',
422
+ navigationEligible: true,
423
+ capability: resourceDefinitions['resource-11'].capabilities.read,
424
+ resourceCode: 'resource-11',
425
+ },
426
+ {
427
+ code: 'resource:resource-12:list',
428
+ kind: 'resource-list',
429
+ path: '/admin/resources/resource-12',
430
+ label: '资料下载',
431
+ navigationEligible: true,
432
+ capability: resourceDefinitions['resource-12'].capabilities.read,
433
+ resourceCode: 'resource-12',
434
+ },
435
+ {
436
+ code: 'operation:content-operations',
437
+ kind: 'operation',
438
+ path: '/admin/operations/content',
439
+ label: '内容业务操作',
440
+ navigationEligible: true,
441
+ capability: 'app:openxiangda-application:operations:read',
442
+ routeCode: 'content-operations',
443
+ },
444
+ ]}
445
+ contributions={applicationContributions}
446
+ resourceDefinitions={resourceDefinitions}
447
+ routeManifest={routeManifest}
448
+ />
449
+ </React.StrictMode>
450
+ );