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.
- package/README.md +28 -10
- package/lib/cli.js +271 -11
- package/lib/workspace-init.js +13 -0
- package/openxiangda-skills/SKILL.md +25 -10
- package/openxiangda-skills/references/architecture-patterns.md +44 -22
- package/openxiangda-skills/references/best-practices.md +163 -0
- package/openxiangda-skills/references/pages/workspace-structure.md +5 -3
- package/openxiangda-skills/references/workspace-state.md +6 -0
- package/openxiangda-skills/skills/openxiangda-app/SKILL.md +11 -7
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +22 -4
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +6 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +7 -1
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +3 -0
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +7 -0
- package/package.json +1 -1
- package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +32 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +61 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +44 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +30 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/module-structure.md +48 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/index.ts +2 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +35 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +24 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +17 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/index.ts +4 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +42 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +23 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +63 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +73 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +34 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +73 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +64 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +57 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +83 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +97 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/page.tsx +1 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/schema.ts +65 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/daily_ticket_digest/index.ts +44 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/sync_roles_to_platform/index.ts +33 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/App.tsx +7 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/WorkbenchPage.tsx +36 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/ConfigPanel.tsx +34 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/PreviewPanel.tsx +17 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/index.tsx +10 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/page.config.ts +9 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/reducer.ts +29 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +24 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/App.tsx +7 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/MobilePortalShell.tsx +31 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/index.tsx +10 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/modules/MobileHome.tsx +13 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/page.config.ts +14 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/routes.ts +13 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +11 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/App.tsx +7 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/PcPortalShell.tsx +35 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/components/PortalMetric.tsx +11 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/index.tsx +10 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/HomeModule.tsx +25 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/TicketsModule.tsx +14 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/page.config.ts +14 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/routes.ts +19 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +35 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/App.tsx +7 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/TicketOpsPage.tsx +105 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketActionTimeline.tsx +22 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +41 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +55 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/index.tsx +10 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/page.config.ts +9 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +35 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/automation.json +25 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/trigger.json +9 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/notifications/daily-ticket-digest.json +24 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +21 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +17 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/workflows/expense-approval-workflow.json +48 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/ConfirmAction.tsx +22 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/QueryState.tsx +37 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/StatusTag.tsx +20 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts +96 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/role-governance.ts +48 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +113 -0
- package/templates/sy-lowcode-app-workspace/package.json +1 -0
- package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +11 -1
- 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 @@
|
|
|
1
|
+
export { default } from "./schema";
|
|
@@ -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,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,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,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,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,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,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
|
+
});
|