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.
Files changed (94) hide show
  1. package/README.md +28 -10
  2. package/lib/cli.js +351 -20
  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/best-practices.md +180 -0
  7. package/openxiangda-skills/references/component-guide.md +34 -8
  8. package/openxiangda-skills/references/pages/publish-flow.md +26 -0
  9. package/openxiangda-skills/references/pages/workspace-structure.md +5 -3
  10. package/openxiangda-skills/references/workspace-state.md +6 -0
  11. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +12 -7
  12. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +34 -4
  13. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +13 -1
  14. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +22 -1
  15. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +3 -0
  16. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +7 -0
  17. package/package.json +1 -1
  18. package/packages/sdk/src/build-source/scripts/publish-all.mjs +44 -5
  19. package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +95 -0
  20. package/packages/sdk/src/build-source/scripts/utils/incremental.test.ts +62 -0
  21. package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +32 -0
  22. package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +61 -0
  23. package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +44 -0
  24. package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +36 -0
  25. package/templates/sy-lowcode-app-workspace/examples/best-practices/module-structure.md +48 -0
  26. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/index.ts +2 -0
  27. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +35 -0
  28. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.ts +24 -0
  29. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/types.ts +17 -0
  30. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/index.ts +4 -0
  31. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.test.ts +42 -0
  32. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/permissions.ts +23 -0
  33. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.test.ts +63 -0
  34. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/state-machine.ts +73 -0
  35. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.test.ts +34 -0
  36. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/ticket-query.ts +73 -0
  37. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/service-ticket/types.ts +64 -0
  38. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/page.tsx +1 -0
  39. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +57 -0
  40. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/page.tsx +1 -0
  41. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/customer-profile/schema.ts +83 -0
  42. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/page.tsx +1 -0
  43. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +97 -0
  44. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/page.tsx +1 -0
  45. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/ticket-action-log/schema.ts +65 -0
  46. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/daily_ticket_digest/index.ts +44 -0
  47. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/js-code-nodes/sync_roles_to_platform/index.ts +33 -0
  48. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/App.tsx +7 -0
  49. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/WorkbenchPage.tsx +36 -0
  50. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/ConfigPanel.tsx +34 -0
  51. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/components/PreviewPanel.tsx +17 -0
  52. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/index.tsx +10 -0
  53. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/page.config.ts +9 -0
  54. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/reducer.ts +29 -0
  55. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +24 -0
  56. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/App.tsx +7 -0
  57. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/MobilePortalShell.tsx +31 -0
  58. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/index.tsx +10 -0
  59. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/modules/MobileHome.tsx +13 -0
  60. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/page.config.ts +14 -0
  61. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/routes.ts +13 -0
  62. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +11 -0
  63. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/App.tsx +7 -0
  64. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/PcPortalShell.tsx +35 -0
  65. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/components/PortalMetric.tsx +11 -0
  66. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/index.tsx +10 -0
  67. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/HomeModule.tsx +25 -0
  68. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/modules/TicketsModule.tsx +14 -0
  69. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/page.config.ts +14 -0
  70. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/routes.ts +19 -0
  71. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +35 -0
  72. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/App.tsx +7 -0
  73. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/TicketOpsPage.tsx +105 -0
  74. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketActionTimeline.tsx +22 -0
  75. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketDetailDrawer.tsx +41 -0
  76. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/components/TicketTableActions.tsx +55 -0
  77. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/index.tsx +10 -0
  78. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/page.config.ts +9 -0
  79. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +35 -0
  80. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/automation.json +25 -0
  81. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/automations/daily-ticket-digest/trigger.json +9 -0
  82. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/notifications/daily-ticket-digest.json +24 -0
  83. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/form-groups/service-ticket-college.json +21 -0
  84. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/permissions/roles.json +17 -0
  85. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/resources/workflows/expense-approval-workflow.json +48 -0
  86. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/ConfirmAction.tsx +22 -0
  87. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/QueryState.tsx +37 -0
  88. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/StatusTag.tsx +20 -0
  89. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts +96 -0
  90. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/role-governance.ts +48 -0
  91. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/services/service-ticket.ts +113 -0
  92. package/templates/sy-lowcode-app-workspace/package.json +2 -0
  93. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +11 -1
  94. 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,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
+ });
@@ -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,7 @@
1
+ import "./styles.css";
2
+
3
+ import { TicketOpsPage } from "./TicketOpsPage";
4
+
5
+ export default function App() {
6
+ return <TicketOpsPage />;
7
+ }
@@ -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
+ }