openxiangda 1.0.22 → 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 (89) hide show
  1. package/README.md +28 -10
  2. package/lib/cli.js +271 -11
  3. package/lib/workspace-init.js +13 -0
  4. package/openxiangda-skills/SKILL.md +25 -10
  5. package/openxiangda-skills/references/architecture-patterns.md +44 -22
  6. package/openxiangda-skills/references/best-practices.md +163 -0
  7. package/openxiangda-skills/references/pages/workspace-structure.md +5 -3
  8. package/openxiangda-skills/references/workspace-state.md +6 -0
  9. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +11 -7
  10. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +22 -4
  11. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +6 -1
  12. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +7 -1
  13. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +3 -0
  14. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +7 -0
  15. package/package.json +1 -1
  16. package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +32 -0
  17. package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +61 -0
  18. package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +44 -0
  19. package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +30 -0
  20. package/templates/sy-lowcode-app-workspace/examples/best-practices/module-structure.md +48 -0
  21. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/index.ts +2 -0
  22. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +35 -0
  23. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +24 -0
  24. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +17 -0
  25. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/index.ts +4 -0
  26. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +42 -0
  27. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +23 -0
  28. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +63 -0
  29. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +73 -0
  30. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +34 -0
  31. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +73 -0
  32. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +64 -0
  33. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/page.tsx +1 -0
  34. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +57 -0
  35. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/page.tsx +1 -0
  36. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +83 -0
  37. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/page.tsx +1 -0
  38. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +97 -0
  39. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/page.tsx +1 -0
  40. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/schema.ts +65 -0
  41. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/daily_ticket_digest/index.ts +44 -0
  42. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/sync_roles_to_platform/index.ts +33 -0
  43. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/App.tsx +7 -0
  44. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/WorkbenchPage.tsx +36 -0
  45. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/ConfigPanel.tsx +34 -0
  46. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/PreviewPanel.tsx +17 -0
  47. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/index.tsx +10 -0
  48. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/page.config.ts +9 -0
  49. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/reducer.ts +29 -0
  50. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +24 -0
  51. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/App.tsx +7 -0
  52. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/MobilePortalShell.tsx +31 -0
  53. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/index.tsx +10 -0
  54. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/modules/MobileHome.tsx +13 -0
  55. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/page.config.ts +14 -0
  56. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/routes.ts +13 -0
  57. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +11 -0
  58. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/App.tsx +7 -0
  59. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/PcPortalShell.tsx +35 -0
  60. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/components/PortalMetric.tsx +11 -0
  61. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/index.tsx +10 -0
  62. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/HomeModule.tsx +25 -0
  63. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/TicketsModule.tsx +14 -0
  64. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/page.config.ts +14 -0
  65. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/routes.ts +19 -0
  66. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +35 -0
  67. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/App.tsx +7 -0
  68. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/TicketOpsPage.tsx +105 -0
  69. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketActionTimeline.tsx +22 -0
  70. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +41 -0
  71. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +55 -0
  72. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/index.tsx +10 -0
  73. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/page.config.ts +9 -0
  74. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +35 -0
  75. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/automation.json +25 -0
  76. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/trigger.json +9 -0
  77. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/notifications/daily-ticket-digest.json +24 -0
  78. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +21 -0
  79. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +17 -0
  80. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/workflows/expense-approval-workflow.json +48 -0
  81. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/ConfirmAction.tsx +22 -0
  82. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/QueryState.tsx +37 -0
  83. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/StatusTag.tsx +20 -0
  84. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts +96 -0
  85. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/role-governance.ts +48 -0
  86. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +113 -0
  87. package/templates/sy-lowcode-app-workspace/package.json +1 -0
  88. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +11 -1
  89. package/templates/sy-lowcode-app-workspace/tsconfig.examples.json +24 -0
@@ -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
+ }
@@ -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,25 @@
1
+ import { Card, Col, Row, Typography } from "antd";
2
+
3
+ import { PortalMetric } from "../components/PortalMetric";
4
+
5
+ export function HomeModule() {
6
+ return (
7
+ <section className="bp-pc-shell__module">
8
+ <Typography.Title level={3}>工作台</Typography.Title>
9
+ <Row gutter={[16, 16]}>
10
+ <Col span={8}>
11
+ <PortalMetric label="待处理工单" value="18" trend="较昨日 +3" />
12
+ </Col>
13
+ <Col span={8}>
14
+ <PortalMetric label="本周完成" value="64" trend="完成率 92%" />
15
+ </Col>
16
+ <Col span={8}>
17
+ <PortalMetric label="超时风险" value="5" trend="需要关注" />
18
+ </Col>
19
+ </Row>
20
+ <Card className="bp-pc-shell__card" title="设计说明">
21
+ PC 门户只做路由和模块组合,业务查询、权限判断、状态流转复用 domain 和 service。
22
+ </Card>
23
+ </section>
24
+ );
25
+ }
@@ -0,0 +1,14 @@
1
+ import { Card, Typography } from "antd";
2
+
3
+ import type { PcPortalRoute } from "../routes";
4
+
5
+ export function TicketsModule({ route }: { route: PcPortalRoute }) {
6
+ return (
7
+ <section className="bp-pc-shell__module">
8
+ <Typography.Title level={3}>{route === "tickets" ? "工单" : "模块"}</Typography.Title>
9
+ <Card>
10
+ 复制模板到真实项目后,在这里组合 `service-ticket-ops` 或角色治理模块。
11
+ </Card>
12
+ </section>
13
+ );
14
+ }
@@ -0,0 +1,14 @@
1
+ import { definePageConfig } from "@/types/app-workspace.types";
2
+
3
+ export default definePageConfig({
4
+ code: "pc_portal_shell",
5
+ name: "PC 工作门户",
6
+ description: "PC app-shell 入口示例",
7
+ route: { pathKey: "pc_portal_shell" },
8
+ entry: {
9
+ mode: "app-shell",
10
+ hidePlatformNav: true,
11
+ defaultRoute: "home",
12
+ },
13
+ menu: { name: "PC 工作门户" },
14
+ });