openxiangda 1.0.21 → 1.0.24

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 (100) hide show
  1. package/README.md +28 -10
  2. package/lib/cli.js +723 -11
  3. package/lib/workspace-init.js +13 -0
  4. package/openxiangda-skills/SKILL.md +26 -10
  5. package/openxiangda-skills/references/architecture-patterns.md +44 -22
  6. package/openxiangda-skills/references/automation-v3.md +2 -0
  7. package/openxiangda-skills/references/best-practices.md +163 -0
  8. package/openxiangda-skills/references/connector-resources.md +3 -0
  9. package/openxiangda-skills/references/notifications.md +80 -0
  10. package/openxiangda-skills/references/openxiangda-api.md +45 -0
  11. package/openxiangda-skills/references/pages/page-sdk.md +1 -0
  12. package/openxiangda-skills/references/pages/workspace-structure.md +5 -3
  13. package/openxiangda-skills/references/workspace-state.md +6 -0
  14. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +11 -7
  15. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +22 -4
  16. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +6 -1
  17. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +9 -1
  18. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +3 -0
  19. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +9 -0
  20. package/package.json +1 -1
  21. package/packages/sdk/dist/runtime/index.cjs +34 -2
  22. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  23. package/packages/sdk/dist/runtime/index.d.mts +66 -1
  24. package/packages/sdk/dist/runtime/index.d.ts +66 -1
  25. package/packages/sdk/dist/runtime/index.mjs +34 -2
  26. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  27. package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +32 -0
  28. package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +61 -0
  29. package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +44 -0
  30. package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +30 -0
  31. package/templates/sy-lowcode-app-workspace/examples/best-practices/module-structure.md +48 -0
  32. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/index.ts +2 -0
  33. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +35 -0
  34. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +24 -0
  35. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +17 -0
  36. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/index.ts +4 -0
  37. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +42 -0
  38. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +23 -0
  39. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +63 -0
  40. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +73 -0
  41. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +34 -0
  42. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +73 -0
  43. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +64 -0
  44. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/page.tsx +1 -0
  45. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +57 -0
  46. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/page.tsx +1 -0
  47. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +83 -0
  48. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/page.tsx +1 -0
  49. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +97 -0
  50. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/page.tsx +1 -0
  51. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/schema.ts +65 -0
  52. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/daily_ticket_digest/index.ts +44 -0
  53. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/sync_roles_to_platform/index.ts +33 -0
  54. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/App.tsx +7 -0
  55. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/WorkbenchPage.tsx +36 -0
  56. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/ConfigPanel.tsx +34 -0
  57. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/PreviewPanel.tsx +17 -0
  58. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/index.tsx +10 -0
  59. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/page.config.ts +9 -0
  60. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/reducer.ts +29 -0
  61. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +24 -0
  62. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/App.tsx +7 -0
  63. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/MobilePortalShell.tsx +31 -0
  64. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/index.tsx +10 -0
  65. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/modules/MobileHome.tsx +13 -0
  66. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/page.config.ts +14 -0
  67. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/routes.ts +13 -0
  68. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +11 -0
  69. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/App.tsx +7 -0
  70. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/PcPortalShell.tsx +35 -0
  71. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/components/PortalMetric.tsx +11 -0
  72. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/index.tsx +10 -0
  73. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/HomeModule.tsx +25 -0
  74. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/TicketsModule.tsx +14 -0
  75. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/page.config.ts +14 -0
  76. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/routes.ts +19 -0
  77. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +35 -0
  78. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/App.tsx +7 -0
  79. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/TicketOpsPage.tsx +105 -0
  80. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketActionTimeline.tsx +22 -0
  81. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +41 -0
  82. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +55 -0
  83. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/index.tsx +10 -0
  84. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/page.config.ts +9 -0
  85. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +35 -0
  86. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/automation.json +25 -0
  87. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/trigger.json +9 -0
  88. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/notifications/daily-ticket-digest.json +24 -0
  89. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +21 -0
  90. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +17 -0
  91. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/workflows/expense-approval-workflow.json +48 -0
  92. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/ConfirmAction.tsx +22 -0
  93. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/QueryState.tsx +37 -0
  94. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/StatusTag.tsx +20 -0
  95. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts +96 -0
  96. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/role-governance.ts +48 -0
  97. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +113 -0
  98. package/templates/sy-lowcode-app-workspace/package.json +1 -0
  99. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +11 -1
  100. package/templates/sy-lowcode-app-workspace/tsconfig.examples.json +24 -0
@@ -0,0 +1,83 @@
1
+ import { createFormSchema } from "@/shared/form-schema";
2
+
3
+ export default createFormSchema({
4
+ formMeta: {
5
+ formUuid: "",
6
+ appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
7
+ title: "客户档案",
8
+ },
9
+ fields: [
10
+ {
11
+ fieldId: "customerName",
12
+ componentName: "TextField",
13
+ label: "客户名称",
14
+ required: true,
15
+ placeholder: "请输入客户或组织名称",
16
+ },
17
+ {
18
+ fieldId: "customerLevel",
19
+ componentName: "SelectField",
20
+ label: "客户等级",
21
+ required: true,
22
+ options: [
23
+ { label: "重点客户", value: "key" },
24
+ { label: "普通客户", value: "normal" },
25
+ { label: "潜在客户", value: "prospect" },
26
+ ],
27
+ },
28
+ {
29
+ fieldId: "ownerUser",
30
+ componentName: "UserSelectField",
31
+ label: "负责人",
32
+ required: true,
33
+ },
34
+ {
35
+ fieldId: "ownerDept",
36
+ componentName: "DepartmentSelectField",
37
+ label: "负责部门",
38
+ required: true,
39
+ },
40
+ {
41
+ fieldId: "contactPhone",
42
+ componentName: "TextField",
43
+ label: "联系电话",
44
+ rules: [{ preset: "phone", message: "请输入有效手机号" }],
45
+ },
46
+ {
47
+ fieldId: "attachments",
48
+ componentName: "AttachmentField",
49
+ label: "附件",
50
+ maxCount: 5,
51
+ },
52
+ {
53
+ fieldId: "contacts",
54
+ componentName: "SubFormField",
55
+ label: "联系人",
56
+ columns: [
57
+ {
58
+ fieldId: "contactName",
59
+ componentName: "TextField",
60
+ label: "姓名",
61
+ required: true,
62
+ },
63
+ {
64
+ fieldId: "contactRole",
65
+ componentName: "TextField",
66
+ label: "角色",
67
+ },
68
+ {
69
+ fieldId: "contactMobile",
70
+ componentName: "TextField",
71
+ label: "手机",
72
+ rules: [{ preset: "phone", message: "请输入有效手机号" }],
73
+ },
74
+ ],
75
+ },
76
+ {
77
+ fieldId: "remark",
78
+ componentName: "TextAreaField",
79
+ label: "备注",
80
+ placeholder: "记录客户背景、跟进偏好或风险点",
81
+ },
82
+ ],
83
+ });
@@ -0,0 +1,97 @@
1
+ import { createFormSchema } from "@/shared/form-schema";
2
+
3
+ export default createFormSchema({
4
+ formMeta: {
5
+ formUuid: "",
6
+ appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
7
+ title: "服务工单",
8
+ },
9
+ fields: [
10
+ {
11
+ fieldId: "title",
12
+ componentName: "TextField",
13
+ label: "标题",
14
+ required: true,
15
+ },
16
+ {
17
+ fieldId: "status",
18
+ componentName: "SelectField",
19
+ label: "状态",
20
+ required: true,
21
+ defaultValue: { label: "新建", value: "new" },
22
+ options: [
23
+ { label: "新建", value: "new" },
24
+ { label: "已受理", value: "accepted" },
25
+ { label: "处理中", value: "processing" },
26
+ { label: "挂起", value: "paused" },
27
+ { label: "已解决", value: "resolved" },
28
+ { label: "已关闭", value: "closed" },
29
+ { label: "已取消", value: "cancelled" },
30
+ ],
31
+ },
32
+ {
33
+ fieldId: "priority",
34
+ componentName: "SelectField",
35
+ label: "优先级",
36
+ defaultValue: { label: "普通", value: "normal" },
37
+ options: [
38
+ { label: "紧急", value: "urgent" },
39
+ { label: "高", value: "high" },
40
+ { label: "普通", value: "normal" },
41
+ { label: "低", value: "low" },
42
+ ],
43
+ },
44
+ {
45
+ fieldId: "ownerUserId",
46
+ componentName: "TextField",
47
+ label: "负责人 ID",
48
+ tips: "权限隔离冗余字段,服务层从人员字段同步写入。",
49
+ },
50
+ {
51
+ fieldId: "ownerDeptId",
52
+ componentName: "TextField",
53
+ label: "负责部门 ID",
54
+ tips: "权限隔离冗余字段,权限组按此字段配置条件。",
55
+ },
56
+ {
57
+ fieldId: "collegeId",
58
+ componentName: "TextField",
59
+ label: "学院 ID",
60
+ tips: "示例冗余字段,可换成业务组织维度。",
61
+ },
62
+ {
63
+ fieldId: "classId",
64
+ componentName: "TextField",
65
+ label: "班级 ID",
66
+ tips: "示例冗余字段,可换成业务组织维度。",
67
+ },
68
+ {
69
+ fieldId: "currentOwner",
70
+ componentName: "UserSelectField",
71
+ label: "当前处理人",
72
+ },
73
+ {
74
+ fieldId: "requester",
75
+ componentName: "UserSelectField",
76
+ label: "提交人",
77
+ required: true,
78
+ },
79
+ {
80
+ fieldId: "description",
81
+ componentName: "TextAreaField",
82
+ label: "问题描述",
83
+ required: true,
84
+ },
85
+ {
86
+ fieldId: "lastActionAt",
87
+ componentName: "DateField",
88
+ label: "最近操作时间",
89
+ },
90
+ {
91
+ fieldId: "attachments",
92
+ componentName: "AttachmentField",
93
+ label: "附件",
94
+ maxCount: 8,
95
+ },
96
+ ],
97
+ });
@@ -0,0 +1,65 @@
1
+ import { createFormSchema } from "@/shared/form-schema";
2
+
3
+ export default createFormSchema({
4
+ formMeta: {
5
+ formUuid: "",
6
+ appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
7
+ title: "工单操作日志",
8
+ },
9
+ fields: [
10
+ {
11
+ fieldId: "ticketId",
12
+ componentName: "TextField",
13
+ label: "工单实例 ID",
14
+ required: true,
15
+ },
16
+ {
17
+ fieldId: "action",
18
+ componentName: "SelectField",
19
+ label: "动作",
20
+ required: true,
21
+ options: [
22
+ { label: "受理", value: "accept" },
23
+ { label: "开始处理", value: "start" },
24
+ { label: "挂起", value: "pause" },
25
+ { label: "恢复", value: "resume" },
26
+ { label: "解决", value: "resolve" },
27
+ { label: "关闭", value: "close" },
28
+ { label: "取消", value: "cancel" },
29
+ ],
30
+ },
31
+ {
32
+ fieldId: "fromStatus",
33
+ componentName: "TextField",
34
+ label: "原状态",
35
+ },
36
+ {
37
+ fieldId: "toStatus",
38
+ componentName: "TextField",
39
+ label: "新状态",
40
+ required: true,
41
+ },
42
+ {
43
+ fieldId: "operatorId",
44
+ componentName: "TextField",
45
+ label: "操作者 ID",
46
+ required: true,
47
+ },
48
+ {
49
+ fieldId: "operatorName",
50
+ componentName: "TextField",
51
+ label: "操作者",
52
+ },
53
+ {
54
+ fieldId: "comment",
55
+ componentName: "TextAreaField",
56
+ label: "说明",
57
+ },
58
+ {
59
+ fieldId: "operatedAt",
60
+ componentName: "DateField",
61
+ label: "操作时间",
62
+ required: true,
63
+ },
64
+ ],
65
+ });
@@ -0,0 +1,44 @@
1
+ export default async function dailyTicketDigest(ctx: any) {
2
+ const appType = ctx.app.appType;
3
+ const pageSize = 50;
4
+ let currentPage = 1;
5
+ let total = 0;
6
+ let riskCount = 0;
7
+
8
+ while (currentPage <= 20) {
9
+ const result = await ctx.methods.queryManyData(appType, "FORM_SERVICE_TICKET", {
10
+ currentPage,
11
+ pageSize,
12
+ filters: {
13
+ logic: "AND",
14
+ rules: [
15
+ {
16
+ key: "status",
17
+ componentName: "SelectField",
18
+ operator: "IN",
19
+ value: ["new", "accepted", "processing", "paused"],
20
+ },
21
+ ],
22
+ },
23
+ });
24
+ const rows = result?.data || result?.list || [];
25
+ total += rows.length;
26
+ riskCount += rows.filter((row: any) => row.priority?.value === "urgent").length;
27
+ if (rows.length < pageSize) break;
28
+ currentPage += 1;
29
+ }
30
+
31
+ if (total > 0 && ctx.operator?.userId) {
32
+ await ctx.notification.sendByType({
33
+ notificationType: "daily_ticket_digest",
34
+ recipientId: ctx.operator.userId,
35
+ payload: {
36
+ title: "每日工单摘要",
37
+ count: total,
38
+ riskCount,
39
+ },
40
+ });
41
+ }
42
+
43
+ return { total, riskCount };
44
+ }
@@ -0,0 +1,33 @@
1
+ export default async function syncRolesToPlatform(ctx: any) {
2
+ const appType = ctx.app.appType;
3
+ const roles = await ctx.methods.queryManyData(appType, "FORM_APP_ROLE", {
4
+ currentPage: 1,
5
+ pageSize: 100,
6
+ filters: {
7
+ logic: "AND",
8
+ rules: [
9
+ {
10
+ key: "enabled",
11
+ componentName: "RadioField",
12
+ operator: "EQ",
13
+ value: "enabled",
14
+ },
15
+ ],
16
+ },
17
+ });
18
+
19
+ const rows = roles?.data || roles?.list || [];
20
+ const synced: string[] = [];
21
+ for (const row of rows) {
22
+ const roleCode = String(row.roleCode || "").trim().toLowerCase();
23
+ if (!roleCode) continue;
24
+ await ctx.platform.api.post(`/openxiangda-api/v1/apps/${appType}/roles`, {
25
+ code: roleCode,
26
+ name: row.roleName || roleCode,
27
+ description: "由业务角色维护表同步",
28
+ });
29
+ synced.push(roleCode);
30
+ }
31
+
32
+ return { synced };
33
+ }
@@ -0,0 +1,7 @@
1
+ import "./styles.css";
2
+
3
+ import { WorkbenchPage } from "./WorkbenchPage";
4
+
5
+ export default function App() {
6
+ return <WorkbenchPage />;
7
+ }
@@ -0,0 +1,36 @@
1
+ import { Button, Card, Space, Typography } from "antd";
2
+ import { useReducer } from "react";
3
+
4
+ import { ConfigPanel } from "./components/ConfigPanel";
5
+ import { PreviewPanel } from "./components/PreviewPanel";
6
+ import { initialWorkbenchState, workbenchReducer } from "./reducer";
7
+
8
+ export function WorkbenchPage() {
9
+ const [state, dispatch] = useReducer(workbenchReducer, initialWorkbenchState);
10
+ return (
11
+ <main className="bp-workbench">
12
+ <section className="bp-workbench__header">
13
+ <div>
14
+ <Typography.Title level={3}>交互工作台</Typography.Title>
15
+ <Typography.Text type="secondary">
16
+ reducer 管理交互状态,面板拆分,页面只负责组合。
17
+ </Typography.Text>
18
+ </div>
19
+ <Space>
20
+ <Button disabled={!state.dirty} onClick={() => dispatch({ type: "saved" })}>
21
+ 保存
22
+ </Button>
23
+ <Button type="primary">发布</Button>
24
+ </Space>
25
+ </section>
26
+ <section className="bp-workbench__grid">
27
+ <Card title="配置">
28
+ <ConfigPanel state={state} dispatch={dispatch} />
29
+ </Card>
30
+ <Card title="预览">
31
+ <PreviewPanel state={state} />
32
+ </Card>
33
+ </section>
34
+ </main>
35
+ );
36
+ }
@@ -0,0 +1,34 @@
1
+ import { Input, Segmented, Space } from "antd";
2
+ import type { Dispatch } from "react";
3
+
4
+ import type { WorkbenchAction, WorkbenchState } from "../reducer";
5
+
6
+ export function ConfigPanel(props: {
7
+ state: WorkbenchState;
8
+ dispatch: Dispatch<WorkbenchAction>;
9
+ }) {
10
+ return (
11
+ <Space direction="vertical" className="bp-workbench__panel" size="middle">
12
+ <Input
13
+ value={props.state.draftName}
14
+ onChange={(event) =>
15
+ props.dispatch({ type: "rename", name: event.target.value })
16
+ }
17
+ />
18
+ <Segmented
19
+ value={props.state.selectedMode}
20
+ options={[
21
+ { label: "规划", value: "plan" },
22
+ { label: "预览", value: "preview" },
23
+ { label: "发布", value: "publish" },
24
+ ]}
25
+ onChange={(mode) =>
26
+ props.dispatch({
27
+ type: "selectMode",
28
+ mode: mode as WorkbenchState["selectedMode"],
29
+ })
30
+ }
31
+ />
32
+ </Space>
33
+ );
34
+ }
@@ -0,0 +1,17 @@
1
+ import { Descriptions, Tag } from "antd";
2
+
3
+ import type { WorkbenchState } from "../reducer";
4
+
5
+ export function PreviewPanel({ state }: { state: WorkbenchState }) {
6
+ return (
7
+ <Descriptions column={1} size="small">
8
+ <Descriptions.Item label="方案">{state.draftName}</Descriptions.Item>
9
+ <Descriptions.Item label="模式">{state.selectedMode}</Descriptions.Item>
10
+ <Descriptions.Item label="状态">
11
+ <Tag color={state.dirty ? "warning" : "success"}>
12
+ {state.dirty ? "有未保存修改" : "已保存"}
13
+ </Tag>
14
+ </Descriptions.Item>
15
+ </Descriptions>
16
+ );
17
+ }
@@ -0,0 +1,10 @@
1
+ import { createReactPage } from "openxiangda/runtime";
2
+
3
+ import App from "./App";
4
+
5
+ const page = createReactPage(App);
6
+
7
+ export const mount = page.mount;
8
+ export const update = page.update;
9
+ export const unmount = page.unmount;
10
+ export default page;
@@ -0,0 +1,9 @@
1
+ import { definePageConfig } from "@/types/app-workspace.types";
2
+
3
+ export default definePageConfig({
4
+ code: "interactive_workbench",
5
+ name: "交互工作台",
6
+ description: "纯复杂交互页面示例",
7
+ route: { pathKey: "interactive_workbench" },
8
+ menu: { name: "交互工作台" },
9
+ });
@@ -0,0 +1,29 @@
1
+ export interface WorkbenchState {
2
+ selectedMode: "plan" | "preview" | "publish";
3
+ draftName: string;
4
+ dirty: boolean;
5
+ }
6
+
7
+ export type WorkbenchAction =
8
+ | { type: "selectMode"; mode: WorkbenchState["selectedMode"] }
9
+ | { type: "rename"; name: string }
10
+ | { type: "saved" };
11
+
12
+ export const initialWorkbenchState: WorkbenchState = {
13
+ selectedMode: "plan",
14
+ draftName: "运营方案",
15
+ dirty: false,
16
+ };
17
+
18
+ export function workbenchReducer(
19
+ state: WorkbenchState,
20
+ action: WorkbenchAction,
21
+ ): WorkbenchState {
22
+ if (action.type === "selectMode") {
23
+ return { ...state, selectedMode: action.mode };
24
+ }
25
+ if (action.type === "rename") {
26
+ return { ...state, draftName: action.name, dirty: true };
27
+ }
28
+ return { ...state, dirty: false };
29
+ }
@@ -0,0 +1,24 @@
1
+ .bp-workbench {
2
+ display: grid;
3
+ gap: var(--sy-spacing-lg);
4
+ min-height: 100%;
5
+ padding: var(--sy-spacing-lg);
6
+ background: var(--sy-color-bg-layout);
7
+ }
8
+
9
+ .bp-workbench__header {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ gap: var(--sy-spacing-md);
14
+ }
15
+
16
+ .bp-workbench__grid {
17
+ display: grid;
18
+ grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
19
+ gap: var(--sy-spacing-md);
20
+ }
21
+
22
+ .bp-workbench__panel {
23
+ width: 100%;
24
+ }
@@ -0,0 +1,7 @@
1
+ import "./styles.css";
2
+
3
+ import { MobilePortalShell } from "./MobilePortalShell";
4
+
5
+ export default function App() {
6
+ return <MobilePortalShell />;
7
+ }
@@ -0,0 +1,31 @@
1
+ import { Card, NavBar, SafeArea, TabBar } from "antd-mobile";
2
+ import { useNavigation, usePageRoute } from "openxiangda/runtime";
3
+
4
+ import { mobilePortalRoutes, parseMobilePortalRoute } from "./routes";
5
+ import { MobileHome } from "./modules/MobileHome";
6
+
7
+ export function MobilePortalShell() {
8
+ const route = parseMobilePortalRoute(usePageRoute().query.route as string);
9
+ const navigation = useNavigation();
10
+
11
+ return (
12
+ <main className="bp-mobile-shell">
13
+ <NavBar back={null}>移动工作门户</NavBar>
14
+ <section className="bp-mobile-shell__content">
15
+ {route === "home" ? (
16
+ <MobileHome />
17
+ ) : (
18
+ <Card title={route === "tickets" ? "工单" : "我的"}>
19
+ 移动端仅保留高频任务,业务规则复用 PC 的 domain/service。
20
+ </Card>
21
+ )}
22
+ </section>
23
+ <TabBar activeKey={route} onChange={(key) => navigation.replaceRoute(key)}>
24
+ {mobilePortalRoutes.map((item) => (
25
+ <TabBar.Item key={item.key} title={item.label} />
26
+ ))}
27
+ </TabBar>
28
+ <SafeArea position="bottom" />
29
+ </main>
30
+ );
31
+ }
@@ -0,0 +1,10 @@
1
+ import { createReactPage } from "openxiangda/runtime";
2
+
3
+ import App from "./App";
4
+
5
+ const page = createReactPage(App);
6
+
7
+ export const mount = page.mount;
8
+ export const update = page.update;
9
+ export const unmount = page.unmount;
10
+ export default page;
@@ -0,0 +1,13 @@
1
+ import { Card, List } from "antd-mobile";
2
+
3
+ export function MobileHome() {
4
+ return (
5
+ <Card title="今日概览">
6
+ <List>
7
+ <List.Item extra="18">待处理</List.Item>
8
+ <List.Item extra="5">超时风险</List.Item>
9
+ <List.Item extra="64">本周完成</List.Item>
10
+ </List>
11
+ </Card>
12
+ );
13
+ }
@@ -0,0 +1,14 @@
1
+ import { definePageConfig } from "@/types/app-workspace.types";
2
+
3
+ export default definePageConfig({
4
+ code: "mobile_portal_shell",
5
+ name: "移动工作门户",
6
+ description: "Mobile app-shell 入口示例",
7
+ route: { pathKey: "mobile_portal_shell" },
8
+ entry: {
9
+ mode: "app-shell",
10
+ hidePlatformNav: true,
11
+ defaultRoute: "home",
12
+ },
13
+ menu: { name: "移动工作门户" },
14
+ });
@@ -0,0 +1,13 @@
1
+ export type MobilePortalRoute = "home" | "tickets" | "mine";
2
+
3
+ export const mobilePortalRoutes: Array<{ key: MobilePortalRoute; label: string }> = [
4
+ { key: "home", label: "首页" },
5
+ { key: "tickets", label: "工单" },
6
+ { key: "mine", label: "我的" },
7
+ ];
8
+
9
+ export function parseMobilePortalRoute(value?: string): MobilePortalRoute {
10
+ return mobilePortalRoutes.some((item) => item.key === value)
11
+ ? (value as MobilePortalRoute)
12
+ : "home";
13
+ }
@@ -0,0 +1,11 @@
1
+ .bp-mobile-shell {
2
+ display: flex;
3
+ min-height: 100%;
4
+ flex-direction: column;
5
+ background: var(--sy-color-bg-layout);
6
+ }
7
+
8
+ .bp-mobile-shell__content {
9
+ flex: 1;
10
+ padding: var(--sy-spacing-md);
11
+ }
@@ -0,0 +1,7 @@
1
+ import "./styles.css";
2
+
3
+ import { PcPortalShell } from "./PcPortalShell";
4
+
5
+ export default function App() {
6
+ return <PcPortalShell />;
7
+ }
@@ -0,0 +1,35 @@
1
+ import { Layout, Menu, Typography } from "antd";
2
+ import { useNavigation, usePageRoute } from "openxiangda/runtime";
3
+
4
+ import { pcPortalRoutes, parsePcPortalRoute } from "./routes";
5
+ import { HomeModule } from "./modules/HomeModule";
6
+ import { TicketsModule } from "./modules/TicketsModule";
7
+
8
+ const { Content, Sider } = Layout;
9
+
10
+ export function PcPortalShell() {
11
+ const route = parsePcPortalRoute(usePageRoute().query.route as string);
12
+ const navigation = useNavigation();
13
+
14
+ return (
15
+ <Layout className="bp-pc-shell">
16
+ <Sider width={220} className="bp-pc-shell__sider">
17
+ <Typography.Title level={4} className="bp-pc-shell__brand">
18
+ 运营门户
19
+ </Typography.Title>
20
+ <Menu
21
+ mode="inline"
22
+ selectedKeys={[route]}
23
+ items={pcPortalRoutes.map((item) => ({
24
+ key: item.key,
25
+ label: item.label,
26
+ }))}
27
+ onClick={(item) => navigation.replaceRoute(String(item.key))}
28
+ />
29
+ </Sider>
30
+ <Content className="bp-pc-shell__content">
31
+ {route === "home" ? <HomeModule /> : <TicketsModule route={route} />}
32
+ </Content>
33
+ </Layout>
34
+ );
35
+ }
@@ -0,0 +1,11 @@
1
+ import { Card, Typography } from "antd";
2
+
3
+ export function PortalMetric(props: { label: string; value: string; trend: string }) {
4
+ return (
5
+ <Card className="bp-portal-metric">
6
+ <Typography.Text type="secondary">{props.label}</Typography.Text>
7
+ <div className="bp-portal-metric__value">{props.value}</div>
8
+ <Typography.Text type="secondary">{props.trend}</Typography.Text>
9
+ </Card>
10
+ );
11
+ }