openxiangda-cli 2.0.0-alpha.16 → 2.0.0-alpha.163

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/{auth/login.js → login.js} +27 -9
  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 +47 -0
  83. package/template/README.md +65 -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 +446 -0
  98. package/template/apps/web/e2e/resources.spec.ts +638 -0
  99. package/template/apps/web/e2e/theme-contract-fixture.css +15 -0
  100. package/template/apps/web/e2e/workflow-experience-fixture.tsx +247 -0
  101. package/template/apps/web/e2e/workflow.spec.ts +1066 -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 +34 -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 +65 -0
  110. package/template/apps/web/src/user-end/tailwind.css +17 -0
  111. package/template/apps/web/test/contracts.test.ts +161 -0
  112. package/template/apps/web/test/loopback.test.ts +68 -0
  113. package/template/apps/web/tsconfig.json +12 -0
  114. package/template/apps/web/vite.config.ts +41 -0
  115. package/template/apps/web/workflow-experience.e2e.html +15 -0
  116. package/template/appspec/app.md +36 -0
  117. package/template/openxiangda.config.ts +77 -0
  118. package/template/package.json +29 -0
  119. package/template/packages/contracts/package.json +23 -0
  120. package/template/packages/contracts/src/generated.ts +265 -0
  121. package/template/packages/contracts/src/index.ts +1 -0
  122. package/template/packages/contracts/tsconfig.json +9 -0
  123. package/template/pnpm-workspace.yaml +7 -0
  124. package/template/scripts/verify-template-budget.mjs +49 -0
  125. package/template/tsconfig.base.json +13 -0
  126. package/dist/commands/app/create.d.ts +0 -15
  127. package/dist/commands/app/create.d.ts.map +0 -1
  128. package/dist/commands/app/create.js +0 -42
  129. package/dist/commands/app/create.js.map +0 -1
  130. package/dist/commands/app/info.d.ts +0 -9
  131. package/dist/commands/app/info.d.ts.map +0 -1
  132. package/dist/commands/app/info.js +0 -10
  133. package/dist/commands/app/info.js.map +0 -1
  134. package/dist/commands/app/link.d.ts +0 -10
  135. package/dist/commands/app/link.d.ts.map +0 -1
  136. package/dist/commands/app/link.js +0 -16
  137. package/dist/commands/app/link.js.map +0 -1
  138. package/dist/commands/app/provision.d.ts +0 -9
  139. package/dist/commands/app/provision.d.ts.map +0 -1
  140. package/dist/commands/app/provision.js +0 -10
  141. package/dist/commands/app/provision.js.map +0 -1
  142. package/dist/commands/auth/login.d.ts +0 -11
  143. package/dist/commands/auth/login.d.ts.map +0 -1
  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 -30
  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 -60
  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,446 @@
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
+ } from 'openxiangda/react';
9
+ import type { DataFieldSurface, DataResourceSurface } from 'openxiangda/core';
10
+ import 'openxiangda/react/styles.css';
11
+ import './theme-contract-fixture.css';
12
+
13
+ const field = (
14
+ label: string,
15
+ type: DataFieldSurface['type'] = 'text.short',
16
+ widget: DataFieldSurface['widget'] = 'text',
17
+ extra: Partial<DataFieldSurface> = {}
18
+ ): DataFieldSurface => ({
19
+ label,
20
+ type,
21
+ widget,
22
+ readCapabilities: [],
23
+ createCapabilities: [],
24
+ updateCapabilities: [],
25
+ ...extra,
26
+ });
27
+
28
+ const fields: DataResourceSurface['fields'] = {
29
+ departmentMultiple: field('构成部门', 'department.multiple', 'directory-department', {
30
+ list: true,
31
+ section: '组织信息',
32
+ }),
33
+ departmentSingle: field('牵头部门', 'department.single', 'directory-department', {
34
+ list: true,
35
+ section: '组织信息',
36
+ }),
37
+ userMultiple: field('小组成员', 'user.multiple', 'directory-user', {
38
+ list: true,
39
+ section: '组织信息',
40
+ }),
41
+ resourceMultiple: field('关联资源', 'resource-ref.multiple', 'resource', {
42
+ list: true,
43
+ section: '组织信息',
44
+ source: {
45
+ kind: 'resource',
46
+ resourceCode: 'resource-01',
47
+ labelField: 'name',
48
+ searchFields: ['name'],
49
+ },
50
+ }),
51
+ enabled: field('启用状态', 'boolean', 'switch', {
52
+ list: true,
53
+ section: '组织信息',
54
+ }),
55
+ displayOrder: field('显示顺序', 'number.integer', 'number', {
56
+ list: true,
57
+ section: '组织信息',
58
+ }),
59
+ description: field('栏目说明', 'text.long', 'textarea', {
60
+ section: '栏目设置',
61
+ }),
62
+ status: field('状态', 'option.single', 'select', {
63
+ options: [
64
+ { label: '启用', value: 'enabled' },
65
+ { label: '停用', value: 'disabled' },
66
+ ],
67
+ section: '栏目设置',
68
+ }),
69
+ name: field('栏目名称', 'text.short', 'text', { section: '栏目设置' }),
70
+ code: field('栏目编码', 'text.short', 'text', { section: '栏目设置' }),
71
+ contentType: field('内容类型', 'option.single', 'select', {
72
+ options: [
73
+ { label: '文章', value: 'article' },
74
+ { label: '图集', value: 'gallery' },
75
+ ],
76
+ section: '栏目设置',
77
+ }),
78
+ manager: field('栏目管理员', 'user.single', 'directory-user', {
79
+ section: '管理信息',
80
+ }),
81
+ facility: field('配套设施', 'option.multiple', 'multi-select', {
82
+ options: [
83
+ { label: '投影仪', value: 'projector' },
84
+ { label: '白板', value: 'whiteboard' },
85
+ ],
86
+ section: '管理信息',
87
+ }),
88
+ openingDay: field('开放星期', 'option.single', 'select', {
89
+ options: [
90
+ { label: '工作日', value: 'weekday' },
91
+ { label: '周末', value: 'weekend' },
92
+ ],
93
+ section: '管理信息',
94
+ }),
95
+ };
96
+
97
+ const routeManifest = {
98
+ schemaVersion: 'openxiangda.application-route-manifest/v3',
99
+ appCode: 'openxiangda-application',
100
+ devicePolicy: {
101
+ kind: 'viewport-family',
102
+ mobileMaxWidthPx: 900,
103
+ desktopMinWidthPx: 901,
104
+ },
105
+ rootEntry: { code: 'application-root', desktop: '/', mobile: '/m/' },
106
+ authentication: {
107
+ desktop: { routeCode: 'application-login', path: '/login' },
108
+ mobile: { routeCode: 'application-login-mobile', path: '/m/login' },
109
+ },
110
+ routes: [],
111
+ digest:
112
+ '88831760f6e672a77babbf039b35a6febca7852092f1579c2721c99596a265c4',
113
+ } as const;
114
+
115
+ const surface: DataResourceSurface = {
116
+ mutationOwner: 'native',
117
+ generated: {
118
+ list: true,
119
+ detail: true,
120
+ create: true,
121
+ update: true,
122
+ delete: true,
123
+ },
124
+ fields,
125
+ list: {
126
+ defaultPageSize: 20,
127
+ searchableFields: ['name', 'code'],
128
+ filterFields: ['contentType', 'status', 'manager', 'facility', 'openingDay'],
129
+ defaultSort: { field: 'code', order: 'asc' },
130
+ },
131
+ form: {
132
+ layout: 'sections',
133
+ fieldOrder: [
134
+ 'code',
135
+ 'name',
136
+ 'contentType',
137
+ 'description',
138
+ 'status',
139
+ 'manager',
140
+ 'facility',
141
+ 'openingDay',
142
+ ],
143
+ },
144
+ detail: {
145
+ layout: 'sections',
146
+ fieldOrder: [
147
+ 'code',
148
+ 'name',
149
+ 'contentType',
150
+ 'description',
151
+ 'status',
152
+ 'manager',
153
+ 'facility',
154
+ 'openingDay',
155
+ ],
156
+ },
157
+ mobile: { enabled: true },
158
+ };
159
+
160
+ const { enabled, displayOrder, ...remainingFields } = fields;
161
+ const acceptanceSurface = new URLSearchParams(location.search).get('primary') === 'falsy'
162
+ ? { ...surface, fields: { enabled, displayOrder, ...remainingFields } }
163
+ : surface;
164
+
165
+ const names = [
166
+ '内容栏目',
167
+ '内容文章',
168
+ '内容专题',
169
+ '会员档案',
170
+ '门户轮播',
171
+ '工会小组',
172
+ '场地预约',
173
+ '场地不可用时段',
174
+ '场地档案',
175
+ '通知公告',
176
+ '活动报名',
177
+ '资料下载',
178
+ ];
179
+ const resourceDefinitions = Object.fromEntries(
180
+ names.map((name, index) => {
181
+ const code = `resource-${String(index + 1).padStart(2, '0')}`;
182
+ return [
183
+ code,
184
+ {
185
+ code,
186
+ name,
187
+ capabilities: {
188
+ read: `app:openxiangda-application:data:${code}:read`,
189
+ create: `app:openxiangda-application:data:${code}:create`,
190
+ update: `app:openxiangda-application:data:${code}:update`,
191
+ delete: `app:openxiangda-application:data:${code}:delete`,
192
+ },
193
+ surface: acceptanceSurface,
194
+ },
195
+ ];
196
+ })
197
+ );
198
+
199
+ function ContentOperationsPage() {
200
+ return (
201
+ <OpenXiangdaAdminPage
202
+ className="theme-contract-page"
203
+ data-testid="custom-admin-page"
204
+ >
205
+ <Typography.Title level={2}>内容业务操作</Typography.Title>
206
+ <div className="theme-contract-panel">
207
+ 自定义后台页语义主题面板
208
+ </div>
209
+ </OpenXiangdaAdminPage>
210
+ );
211
+ }
212
+
213
+ function ResourcePortalPage() {
214
+ return <Typography.Title level={2}>用户端内容门户</Typography.Title>;
215
+ }
216
+
217
+ const applicationContributions = defineApplicationContributions(
218
+ {
219
+ contentOperations: {
220
+ code: 'content-operations',
221
+ path: '/admin/operations/content',
222
+ label: '内容业务操作',
223
+ surface: 'admin',
224
+ capability: 'app:openxiangda-application:operations:read',
225
+ tabPersistence: 'session',
226
+ keepAlive: 'none',
227
+ },
228
+ resourcePortal: {
229
+ code: 'resource-portal',
230
+ path: '/resource-01',
231
+ label: '用户端内容门户',
232
+ surface: 'user',
233
+ tabPersistence: 'session',
234
+ keepAlive: 'none',
235
+ },
236
+ } as const,
237
+ {
238
+ pages: {
239
+ contentOperations: ContentOperationsPage,
240
+ resourcePortal: ResourcePortalPage,
241
+ },
242
+ resources: {
243
+ 'resource-01': {
244
+ toolbar: [
245
+ {
246
+ code: 'publish-selected',
247
+ label: '发布选中栏目',
248
+ capability: 'app:openxiangda-application:operations:read',
249
+ render: ({ selectedRecords }) => (
250
+ <Button>发布选中栏目({selectedRecords.length})</Button>
251
+ ),
252
+ },
253
+ ],
254
+ },
255
+ },
256
+ },
257
+ );
258
+
259
+ if (location.pathname.endsWith('/resource-experience.e2e.html')) {
260
+ history.replaceState({}, '', '/admin/resources/resource-01');
261
+ }
262
+
263
+ ReactDOM.createRoot(document.getElementById('root')!).render(
264
+ <React.StrictMode>
265
+ <OpenXiangdaApplication
266
+ appCode="openxiangda-application"
267
+ appName="资源体验验收"
268
+ adminNavigation={[
269
+ {
270
+ code: 'resources',
271
+ label: '数据管理',
272
+ icon: 'database',
273
+ order: 0,
274
+ items: [
275
+ { pageCode: 'resource:resource-01:list', label: '内容栏目', order: 0 },
276
+ { pageCode: 'resource:resource-02:list', label: '内容文章', order: 1 },
277
+ { pageCode: 'resource:resource-03:list', label: '内容专题', order: 2 },
278
+ { pageCode: 'resource:resource-04:list', label: '会员档案', order: 3 },
279
+ { pageCode: 'resource:resource-05:list', label: '门户轮播', order: 4 },
280
+ { pageCode: 'resource:resource-06:list', label: '工会小组', order: 5 },
281
+ { pageCode: 'resource:resource-07:list', label: '场地预约', order: 6 },
282
+ {
283
+ pageCode: 'resource:resource-08:list',
284
+ label: '场地不可用时段',
285
+ order: 7,
286
+ },
287
+ { pageCode: 'resource:resource-09:list', label: '场地档案', order: 8 },
288
+ { pageCode: 'resource:resource-10:list', label: '通知公告', order: 9 },
289
+ { pageCode: 'resource:resource-11:list', label: '活动报名', order: 10 },
290
+ { pageCode: 'resource:resource-12:list', label: '资料下载', order: 11 },
291
+ { pageCode: 'operation:content-operations', order: 12 },
292
+ ],
293
+ },
294
+ ]}
295
+ adminPages={[
296
+ {
297
+ code: 'resource:resource-01:list',
298
+ kind: 'resource-list',
299
+ path: '/admin/resources/resource-01',
300
+ label: '资源 01',
301
+ navigationEligible: true,
302
+ capability: resourceDefinitions['resource-01'].capabilities.read,
303
+ resourceCode: 'resource-01',
304
+ },
305
+ {
306
+ code: 'resource:resource-01:detail',
307
+ kind: 'resource-detail',
308
+ path: '/admin/resources/resource-01/:id',
309
+ label: '资源 01 详情',
310
+ navigationEligible: false,
311
+ capability: resourceDefinitions['resource-01'].capabilities.read,
312
+ resourceCode: 'resource-01',
313
+ },
314
+ {
315
+ code: 'resource:resource-01:create',
316
+ kind: 'resource-create',
317
+ path: '/admin/resources/resource-01/new',
318
+ label: '新增资源 01',
319
+ navigationEligible: false,
320
+ capability: resourceDefinitions['resource-01'].capabilities.create,
321
+ resourceCode: 'resource-01',
322
+ },
323
+ {
324
+ code: 'resource:resource-01:update',
325
+ kind: 'resource-update',
326
+ path: '/admin/resources/resource-01/:id/edit',
327
+ label: '编辑资源 01',
328
+ navigationEligible: false,
329
+ capability: resourceDefinitions['resource-01'].capabilities.update,
330
+ resourceCode: 'resource-01',
331
+ },
332
+ {
333
+ code: 'resource:resource-02:list',
334
+ kind: 'resource-list',
335
+ path: '/admin/resources/resource-02',
336
+ label: '内容文章',
337
+ navigationEligible: true,
338
+ capability: resourceDefinitions['resource-02'].capabilities.read,
339
+ resourceCode: 'resource-02',
340
+ },
341
+ {
342
+ code: 'resource:resource-03:list',
343
+ kind: 'resource-list',
344
+ path: '/admin/resources/resource-03',
345
+ label: '内容专题',
346
+ navigationEligible: true,
347
+ capability: resourceDefinitions['resource-03'].capabilities.read,
348
+ resourceCode: 'resource-03',
349
+ },
350
+ {
351
+ code: 'resource:resource-04:list',
352
+ kind: 'resource-list',
353
+ path: '/admin/resources/resource-04',
354
+ label: '会员档案',
355
+ navigationEligible: true,
356
+ capability: resourceDefinitions['resource-04'].capabilities.read,
357
+ resourceCode: 'resource-04',
358
+ },
359
+ {
360
+ code: 'resource:resource-05:list',
361
+ kind: 'resource-list',
362
+ path: '/admin/resources/resource-05',
363
+ label: '门户轮播',
364
+ navigationEligible: true,
365
+ capability: resourceDefinitions['resource-05'].capabilities.read,
366
+ resourceCode: 'resource-05',
367
+ },
368
+ {
369
+ code: 'resource:resource-06:list',
370
+ kind: 'resource-list',
371
+ path: '/admin/resources/resource-06',
372
+ label: '工会小组',
373
+ navigationEligible: true,
374
+ capability: resourceDefinitions['resource-06'].capabilities.read,
375
+ resourceCode: 'resource-06',
376
+ },
377
+ {
378
+ code: 'resource:resource-07:list',
379
+ kind: 'resource-list',
380
+ path: '/admin/resources/resource-07',
381
+ label: '场地预约',
382
+ navigationEligible: true,
383
+ capability: resourceDefinitions['resource-07'].capabilities.read,
384
+ resourceCode: 'resource-07',
385
+ },
386
+ {
387
+ code: 'resource:resource-08:list',
388
+ kind: 'resource-list',
389
+ path: '/admin/resources/resource-08',
390
+ label: '场地不可用时段',
391
+ navigationEligible: true,
392
+ capability: resourceDefinitions['resource-08'].capabilities.read,
393
+ resourceCode: 'resource-08',
394
+ },
395
+ {
396
+ code: 'resource:resource-09:list',
397
+ kind: 'resource-list',
398
+ path: '/admin/resources/resource-09',
399
+ label: '场地档案',
400
+ navigationEligible: true,
401
+ capability: resourceDefinitions['resource-09'].capabilities.read,
402
+ resourceCode: 'resource-09',
403
+ },
404
+ {
405
+ code: 'resource:resource-10:list',
406
+ kind: 'resource-list',
407
+ path: '/admin/resources/resource-10',
408
+ label: '通知公告',
409
+ navigationEligible: true,
410
+ capability: resourceDefinitions['resource-10'].capabilities.read,
411
+ resourceCode: 'resource-10',
412
+ },
413
+ {
414
+ code: 'resource:resource-11:list',
415
+ kind: 'resource-list',
416
+ path: '/admin/resources/resource-11',
417
+ label: '活动报名',
418
+ navigationEligible: true,
419
+ capability: resourceDefinitions['resource-11'].capabilities.read,
420
+ resourceCode: 'resource-11',
421
+ },
422
+ {
423
+ code: 'resource:resource-12:list',
424
+ kind: 'resource-list',
425
+ path: '/admin/resources/resource-12',
426
+ label: '资料下载',
427
+ navigationEligible: true,
428
+ capability: resourceDefinitions['resource-12'].capabilities.read,
429
+ resourceCode: 'resource-12',
430
+ },
431
+ {
432
+ code: 'operation:content-operations',
433
+ kind: 'operation',
434
+ path: '/admin/operations/content',
435
+ label: '内容业务操作',
436
+ navigationEligible: true,
437
+ capability: 'app:openxiangda-application:operations:read',
438
+ routeCode: 'content-operations',
439
+ },
440
+ ]}
441
+ contributions={applicationContributions}
442
+ resourceDefinitions={resourceDefinitions}
443
+ routeManifest={routeManifest}
444
+ />
445
+ </React.StrictMode>
446
+ );