openxiangda 1.0.22 → 1.0.25
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 +351 -20
- package/lib/workspace-init.js +13 -0
- package/openxiangda-skills/SKILL.md +26 -10
- package/openxiangda-skills/references/architecture-patterns.md +44 -22
- package/openxiangda-skills/references/best-practices.md +180 -0
- package/openxiangda-skills/references/component-guide.md +34 -8
- package/openxiangda-skills/references/pages/publish-flow.md +26 -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 +12 -7
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +34 -4
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +13 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +22 -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/packages/sdk/src/build-source/scripts/publish-all.mjs +44 -5
- package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +95 -0
- package/packages/sdk/src/build-source/scripts/utils/incremental.test.ts +62 -0
- 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 +36 -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 +2 -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,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
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type PcPortalRoute = "home" | "tickets" | "roles" | "settings";
|
|
2
|
+
|
|
3
|
+
export const defaultPcPortalRoute: PcPortalRoute = "home";
|
|
4
|
+
|
|
5
|
+
export const pcPortalRoutes: Array<{
|
|
6
|
+
key: PcPortalRoute;
|
|
7
|
+
label: string;
|
|
8
|
+
}> = [
|
|
9
|
+
{ key: "home", label: "工作台" },
|
|
10
|
+
{ key: "tickets", label: "工单" },
|
|
11
|
+
{ key: "roles", label: "角色治理" },
|
|
12
|
+
{ key: "settings", label: "设置" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function parsePcPortalRoute(value?: string): PcPortalRoute {
|
|
16
|
+
return pcPortalRoutes.some((item) => item.key === value)
|
|
17
|
+
? (value as PcPortalRoute)
|
|
18
|
+
: defaultPcPortalRoute;
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.bp-pc-shell {
|
|
2
|
+
min-height: 100%;
|
|
3
|
+
background: var(--sy-color-bg-layout);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.bp-pc-shell__sider {
|
|
7
|
+
border-right: 1px solid var(--sy-color-border-secondary);
|
|
8
|
+
background: var(--sy-color-bg-container);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.bp-pc-shell__brand {
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: var(--sy-spacing-lg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.bp-pc-shell__content {
|
|
17
|
+
padding: var(--sy-spacing-lg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.bp-pc-shell__module {
|
|
21
|
+
display: grid;
|
|
22
|
+
gap: var(--sy-spacing-md);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.bp-pc-shell__card,
|
|
26
|
+
.bp-portal-metric {
|
|
27
|
+
border-radius: var(--sy-radius-lg);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bp-portal-metric__value {
|
|
31
|
+
margin: var(--sy-spacing-sm) 0;
|
|
32
|
+
color: var(--sy-color-text);
|
|
33
|
+
font-size: var(--sy-font-size-2xl);
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { App as AntdApp, Button, Input, Space, Typography } from "antd";
|
|
2
|
+
import { DataManagementList } from "openxiangda";
|
|
3
|
+
import { usePageContext, usePageSdk } from "openxiangda/runtime";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
|
|
6
|
+
import type { TicketOperator, TicketRecord } from "@/domain/service-ticket";
|
|
7
|
+
import { QueryState } from "@/shared/components/QueryState";
|
|
8
|
+
import { SERVICE_TICKET_FORM_UUID } from "@/shared/services/service-ticket";
|
|
9
|
+
import { useTicketOps } from "@/shared/hooks/useTicketOps";
|
|
10
|
+
|
|
11
|
+
import { TicketActionTimeline } from "./components/TicketActionTimeline";
|
|
12
|
+
import { TicketDetailDrawer } from "./components/TicketDetailDrawer";
|
|
13
|
+
import { buildTicketRowActions } from "./components/TicketTableActions";
|
|
14
|
+
|
|
15
|
+
const { Title, Text } = Typography;
|
|
16
|
+
|
|
17
|
+
function useOperator(): TicketOperator {
|
|
18
|
+
const context = usePageContext();
|
|
19
|
+
const roleCodes = Array.isArray(context.permissions.roleCodes)
|
|
20
|
+
? (context.permissions.roleCodes as string[])
|
|
21
|
+
: [];
|
|
22
|
+
return {
|
|
23
|
+
userId: context.user.id,
|
|
24
|
+
userName: context.user.name || context.user.username,
|
|
25
|
+
roleCodes,
|
|
26
|
+
departmentIds:
|
|
27
|
+
context.user.departments
|
|
28
|
+
?.map((item) => item.id)
|
|
29
|
+
.filter((id): id is string => Boolean(id)) || [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function TicketOpsPage() {
|
|
34
|
+
const sdk = usePageSdk();
|
|
35
|
+
const context = usePageContext();
|
|
36
|
+
const operator = useOperator();
|
|
37
|
+
const ticketOps = useTicketOps(sdk, operator);
|
|
38
|
+
const [selectedTicket, setSelectedTicket] = useState<TicketRecord | null>(null);
|
|
39
|
+
const [keyword, setKeyword] = useState("");
|
|
40
|
+
const { message } = AntdApp.useApp();
|
|
41
|
+
|
|
42
|
+
const rowActions = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
buildTicketRowActions({
|
|
45
|
+
operator,
|
|
46
|
+
submittingAction: ticketOps.submittingAction,
|
|
47
|
+
onAction: async (ticket, action) => {
|
|
48
|
+
await ticketOps.runAction(ticket, action);
|
|
49
|
+
message.success("工单状态已更新");
|
|
50
|
+
},
|
|
51
|
+
onDetail: setSelectedTicket,
|
|
52
|
+
}),
|
|
53
|
+
[message, operator, ticketOps],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<main className="bp-ticket-ops">
|
|
58
|
+
<section className="bp-ticket-ops__header">
|
|
59
|
+
<div>
|
|
60
|
+
<Title level={3} className="bp-ticket-ops__title">
|
|
61
|
+
工单运营台
|
|
62
|
+
</Title>
|
|
63
|
+
<Text type="secondary">
|
|
64
|
+
状态流转由 domain/state-machine 控制,页面只组合筛选、列表和操作。
|
|
65
|
+
</Text>
|
|
66
|
+
</div>
|
|
67
|
+
<Space>
|
|
68
|
+
<Input.Search
|
|
69
|
+
allowClear
|
|
70
|
+
value={keyword}
|
|
71
|
+
placeholder="按标题或描述查询"
|
|
72
|
+
onChange={(event) => setKeyword(event.target.value)}
|
|
73
|
+
onSearch={(nextKeyword) =>
|
|
74
|
+
ticketOps.setSearch((current) => ({
|
|
75
|
+
...current,
|
|
76
|
+
keyword: nextKeyword,
|
|
77
|
+
}))
|
|
78
|
+
}
|
|
79
|
+
/>
|
|
80
|
+
<Button loading={ticketOps.refreshing} onClick={ticketOps.reload}>
|
|
81
|
+
刷新
|
|
82
|
+
</Button>
|
|
83
|
+
</Space>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<QueryState error={ticketOps.error} onRetry={ticketOps.reload} />
|
|
87
|
+
|
|
88
|
+
<DataManagementList
|
|
89
|
+
appType={context.app.appType}
|
|
90
|
+
formUuid={SERVICE_TICKET_FORM_UUID}
|
|
91
|
+
title="工单列表"
|
|
92
|
+
formTitle="服务工单"
|
|
93
|
+
fullHeight={false}
|
|
94
|
+
rowActions={rowActions}
|
|
95
|
+
maxVisibleRowActions={3}
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<TicketDetailDrawer
|
|
99
|
+
ticket={selectedTicket}
|
|
100
|
+
onClose={() => setSelectedTicket(null)}
|
|
101
|
+
extra={<TicketActionTimeline ticketId={selectedTicket?.formInstanceId || ""} />}
|
|
102
|
+
/>
|
|
103
|
+
</main>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Timeline, Typography } from "antd";
|
|
2
|
+
|
|
3
|
+
export function TicketActionTimeline({ ticketId }: { ticketId: string }) {
|
|
4
|
+
if (!ticketId) return null;
|
|
5
|
+
return (
|
|
6
|
+
<section className="bp-ticket-timeline">
|
|
7
|
+
<Typography.Title level={5}>操作日志</Typography.Title>
|
|
8
|
+
<Timeline
|
|
9
|
+
items={[
|
|
10
|
+
{
|
|
11
|
+
color: "blue",
|
|
12
|
+
children: "复制本模板后,在 service 层调用 queryTicketActionLogs 加载真实日志。",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
color: "gray",
|
|
16
|
+
children: `当前工单实例: ${ticketId}`,
|
|
17
|
+
},
|
|
18
|
+
]}
|
|
19
|
+
/>
|
|
20
|
+
</section>
|
|
21
|
+
);
|
|
22
|
+
}
|