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,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
+ }
@@ -0,0 +1,41 @@
1
+ import { Descriptions, Drawer, Typography } from "antd";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { StatusTag } from "@/shared/components/StatusTag";
5
+ import type { TicketRecord } from "@/domain/service-ticket";
6
+
7
+ export function TicketDetailDrawer(props: {
8
+ ticket: TicketRecord | null;
9
+ extra?: ReactNode;
10
+ onClose: () => void;
11
+ }) {
12
+ return (
13
+ <Drawer
14
+ title="工单详情"
15
+ open={Boolean(props.ticket)}
16
+ width={520}
17
+ onClose={props.onClose}
18
+ >
19
+ {props.ticket ? (
20
+ <div className="bp-ticket-detail">
21
+ <Descriptions column={1} size="small">
22
+ <Descriptions.Item label="标题">{props.ticket.title}</Descriptions.Item>
23
+ <Descriptions.Item label="状态">
24
+ <StatusTag status={props.ticket.status} />
25
+ </Descriptions.Item>
26
+ <Descriptions.Item label="负责人">
27
+ {props.ticket.currentOwner?.label || props.ticket.ownerUserId || "-"}
28
+ </Descriptions.Item>
29
+ <Descriptions.Item label="归属部门">
30
+ {props.ticket.ownerDeptId || "-"}
31
+ </Descriptions.Item>
32
+ </Descriptions>
33
+ <Typography.Paragraph className="bp-ticket-detail__description">
34
+ {props.ticket.description || "暂无描述"}
35
+ </Typography.Paragraph>
36
+ {props.extra}
37
+ </div>
38
+ ) : null}
39
+ </Drawer>
40
+ );
41
+ }
@@ -0,0 +1,55 @@
1
+ import type { DataManagementRowAction } from "openxiangda";
2
+
3
+ import {
4
+ getAvailableTicketActions,
5
+ ticketActionLabels,
6
+ type TicketAction,
7
+ type TicketOperator,
8
+ type TicketRecord,
9
+ } from "@/domain/service-ticket";
10
+
11
+ const normalizeTicket = (record: any): TicketRecord => ({
12
+ formInstanceId:
13
+ record.formInstanceId || record.formInstId || record.id || record.formData?.formInstanceId,
14
+ title: record.title || record.formData?.title || "未命名工单",
15
+ status: record.status || record.formData?.status?.value || record.formData?.status || "new",
16
+ priority: record.priority?.value || record.formData?.priority?.value,
17
+ ownerUserId: record.ownerUserId || record.formData?.ownerUserId,
18
+ ownerDeptId: record.ownerDeptId || record.formData?.ownerDeptId,
19
+ collegeId: record.collegeId || record.formData?.collegeId,
20
+ classId: record.classId || record.formData?.classId,
21
+ requester: record.requester || record.formData?.requester,
22
+ currentOwner: record.currentOwner || record.formData?.currentOwner,
23
+ description: record.description || record.formData?.description,
24
+ lastActionAt: record.lastActionAt || record.formData?.lastActionAt,
25
+ });
26
+
27
+ export function buildTicketRowActions(options: {
28
+ operator: TicketOperator;
29
+ submittingAction: TicketAction | null;
30
+ onAction: (ticket: TicketRecord, action: TicketAction) => Promise<void>;
31
+ onDetail: (ticket: TicketRecord) => void;
32
+ }): DataManagementRowAction[] {
33
+ return [
34
+ {
35
+ key: "detail",
36
+ label: "详情",
37
+ onClick: (record) => options.onDetail(normalizeTicket(record)),
38
+ },
39
+ ...(["accept", "start", "pause", "resume", "resolve", "close", "cancel"] as TicketAction[]).map(
40
+ (action) => ({
41
+ key: action,
42
+ label:
43
+ options.submittingAction === action
44
+ ? `${ticketActionLabels[action]}中`
45
+ : ticketActionLabels[action],
46
+ danger: action === "cancel",
47
+ onClick: (record: any) => {
48
+ const ticket = normalizeTicket(record);
49
+ if (!getAvailableTicketActions(ticket, options.operator).includes(action)) return;
50
+ void options.onAction(ticket, action);
51
+ },
52
+ }),
53
+ ),
54
+ ];
55
+ }
@@ -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: "service_ticket_ops",
5
+ name: "工单运营台",
6
+ description: "状态流转型业务的数据管理页示例",
7
+ route: { pathKey: "service_ticket_ops" },
8
+ menu: { name: "工单运营台" },
9
+ });
@@ -0,0 +1,35 @@
1
+ .bp-ticket-ops {
2
+ min-height: 100%;
3
+ padding: var(--sy-spacing-lg);
4
+ background: var(--sy-color-bg-layout);
5
+ }
6
+
7
+ .bp-ticket-ops__header {
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: space-between;
11
+ gap: var(--sy-spacing-md);
12
+ margin-bottom: var(--sy-spacing-md);
13
+ }
14
+
15
+ .bp-ticket-ops__title {
16
+ margin: 0;
17
+ }
18
+
19
+ .bp-ticket-detail {
20
+ display: grid;
21
+ gap: var(--sy-spacing-md);
22
+ }
23
+
24
+ .bp-ticket-detail__description {
25
+ padding: var(--sy-spacing-md);
26
+ border: 1px solid var(--sy-color-border-secondary);
27
+ border-radius: var(--sy-radius-md);
28
+ background: var(--sy-color-bg-container);
29
+ }
30
+
31
+ .bp-query-state {
32
+ display: grid;
33
+ min-height: 120px;
34
+ place-items: center;
35
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "version": "v3",
3
+ "nodes": [
4
+ { "id": "start", "type": "start", "data": { "label": "开始" } },
5
+ {
6
+ "id": "daily_digest",
7
+ "type": "js_code",
8
+ "data": {
9
+ "label": "生成每日工单摘要",
10
+ "runtimeMode": "trusted_node",
11
+ "sourceType": "file_snapshot",
12
+ "scriptCode": "daily_ticket_digest",
13
+ "sourceFile": {
14
+ "localPath": "src/js-code-nodes/daily_ticket_digest/index.ts"
15
+ },
16
+ "timeout": 30000
17
+ }
18
+ },
19
+ { "id": "end", "type": "end", "data": { "label": "结束" } }
20
+ ],
21
+ "edges": [
22
+ { "id": "e_start_digest", "source": "start", "target": "daily_digest" },
23
+ { "id": "e_digest_end", "source": "daily_digest", "target": "end" }
24
+ ]
25
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "type": "scheduled",
3
+ "appType": "APP_XXX",
4
+ "enabled": true,
5
+ "scheduleType": "fixed_time",
6
+ "fixedTime": {
7
+ "cronExpression": "0 0 9 * * *"
8
+ }
9
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "templates": [
3
+ {
4
+ "code": "daily_ticket_digest",
5
+ "name": "每日工单摘要",
6
+ "variables": ["title", "count", "riskCount"],
7
+ "channelsConfig": {
8
+ "inapp": {
9
+ "enabled": true,
10
+ "title": "{{title}}",
11
+ "content": "今日待处理 {{count}} 条,其中超时风险 {{riskCount}} 条"
12
+ }
13
+ }
14
+ }
15
+ ],
16
+ "typeConfigs": [
17
+ {
18
+ "notificationType": "daily_ticket_digest",
19
+ "templateCode": "daily_ticket_digest",
20
+ "enabled": true,
21
+ "priority": 0
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "formCode": "service-ticket",
3
+ "name": "学院管理员查看本学院工单",
4
+ "type": "view",
5
+ "roles": ["college_admin"],
6
+ "operations": ["view", "edit", "change_records"],
7
+ "dataPermission": {
8
+ "type": "condition",
9
+ "condition": {
10
+ "logic": "AND",
11
+ "rules": [
12
+ {
13
+ "field": "collegeId",
14
+ "componentType": "Text",
15
+ "op": "=",
16
+ "value": "${ROLE_SCOPE_COLLEGE_ID}"
17
+ }
18
+ ]
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "code": "app_admin",
4
+ "name": "应用管理员",
5
+ "description": "可以查看和处理全部业务数据"
6
+ },
7
+ {
8
+ "code": "college_admin",
9
+ "name": "学院管理员",
10
+ "description": "通过业务角色表动态维护,按 collegeId 隔离数据"
11
+ },
12
+ {
13
+ "code": "class_owner",
14
+ "name": "班级负责人",
15
+ "description": "通过业务角色表动态维护,按 classId 隔离数据"
16
+ }
17
+ ]
@@ -0,0 +1,48 @@
1
+ {
2
+ "version": "v3",
3
+ "nodes": [
4
+ { "id": "start", "type": "start", "data": { "label": "提交报销" } },
5
+ {
6
+ "id": "amount_branch",
7
+ "type": "condition",
8
+ "data": {
9
+ "label": "金额判断"
10
+ }
11
+ },
12
+ {
13
+ "id": "manager_approval",
14
+ "type": "approval",
15
+ "data": {
16
+ "label": "直属主管审批"
17
+ }
18
+ },
19
+ {
20
+ "id": "finance_approval",
21
+ "type": "approval",
22
+ "data": {
23
+ "label": "财务审批"
24
+ }
25
+ },
26
+ {
27
+ "id": "notify",
28
+ "type": "work_notification",
29
+ "data": {
30
+ "label": "审批结果通知"
31
+ }
32
+ },
33
+ { "id": "end", "type": "end", "data": { "label": "结束" } }
34
+ ],
35
+ "edges": [
36
+ { "id": "e_start_branch", "source": "start", "target": "amount_branch" },
37
+ { "id": "e_branch_manager", "source": "amount_branch", "target": "manager_approval" },
38
+ { "id": "e_manager_finance", "source": "manager_approval", "target": "finance_approval" },
39
+ { "id": "e_finance_notify", "source": "finance_approval", "target": "notify" },
40
+ { "id": "e_notify_end", "source": "notify", "target": "end" }
41
+ ],
42
+ "flowConfig": {
43
+ "manager_approval": [
44
+ { "fieldId": "amount", "fieldBehavior": "READONLY" },
45
+ { "fieldId": "remark", "fieldBehavior": "NORMAL" }
46
+ ]
47
+ }
48
+ }
@@ -0,0 +1,22 @@
1
+ import { Button, Popconfirm } from "antd";
2
+
3
+ export function ConfirmAction(props: {
4
+ label: string;
5
+ title?: string;
6
+ danger?: boolean;
7
+ loading?: boolean;
8
+ onConfirm: () => void;
9
+ }) {
10
+ return (
11
+ <Popconfirm
12
+ title={props.title || `确认${props.label}?`}
13
+ okText="确认"
14
+ cancelText="取消"
15
+ onConfirm={props.onConfirm}
16
+ >
17
+ <Button danger={props.danger} loading={props.loading} size="small">
18
+ {props.label}
19
+ </Button>
20
+ </Popconfirm>
21
+ );
22
+ }
@@ -0,0 +1,37 @@
1
+ import { Alert, Button, Empty, Spin } from "antd";
2
+
3
+ export function QueryState(props: {
4
+ loading?: boolean;
5
+ error?: string | null;
6
+ empty?: boolean;
7
+ onRetry?: () => void;
8
+ }) {
9
+ if (props.loading) {
10
+ return (
11
+ <div className="bp-query-state">
12
+ <Spin />
13
+ </div>
14
+ );
15
+ }
16
+ if (props.error) {
17
+ return (
18
+ <Alert
19
+ type="error"
20
+ showIcon
21
+ message="加载失败"
22
+ description={props.error}
23
+ action={
24
+ props.onRetry ? (
25
+ <Button size="small" onClick={props.onRetry}>
26
+ 重试
27
+ </Button>
28
+ ) : null
29
+ }
30
+ />
31
+ );
32
+ }
33
+ if (props.empty) {
34
+ return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无数据" />;
35
+ }
36
+ return null;
37
+ }
@@ -0,0 +1,20 @@
1
+ import { Tag } from "antd";
2
+
3
+ import {
4
+ ticketStatusLabels,
5
+ type TicketStatus,
6
+ } from "@/domain/service-ticket";
7
+
8
+ const statusColor: Record<TicketStatus, string> = {
9
+ new: "default",
10
+ accepted: "processing",
11
+ processing: "blue",
12
+ paused: "warning",
13
+ resolved: "success",
14
+ closed: "default",
15
+ cancelled: "error",
16
+ };
17
+
18
+ export function StatusTag({ status }: { status: TicketStatus }) {
19
+ return <Tag color={statusColor[status]}>{ticketStatusLabels[status]}</Tag>;
20
+ }
@@ -0,0 +1,96 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import type { PageSdk } from "openxiangda/runtime";
3
+
4
+ import {
5
+ getTicketUiPermissions,
6
+ type TicketAction,
7
+ type TicketOperator,
8
+ type TicketRecord,
9
+ type TicketSearchState,
10
+ } from "@/domain/service-ticket";
11
+ import {
12
+ queryTickets,
13
+ transitionTicket,
14
+ type TicketQueryResult,
15
+ } from "@/shared/services/service-ticket";
16
+
17
+ const initialResult: TicketQueryResult = { records: [], total: 0 };
18
+
19
+ export function useTicketOps(sdk: PageSdk, operator: TicketOperator) {
20
+ const [search, setSearch] = useState<TicketSearchState>({});
21
+ const [page, setPage] = useState(1);
22
+ const [pageSize, setPageSize] = useState(20);
23
+ const [result, setResult] = useState<TicketQueryResult>(initialResult);
24
+ const [loading, setLoading] = useState(false);
25
+ const [refreshing, setRefreshing] = useState(false);
26
+ const [submittingAction, setSubmittingAction] = useState<TicketAction | null>(null);
27
+ const [error, setError] = useState<string | null>(null);
28
+
29
+ const load = useCallback(
30
+ async (mode: "loading" | "refreshing" = "loading") => {
31
+ if (mode === "loading") setLoading(true);
32
+ else setRefreshing(true);
33
+ setError(null);
34
+ try {
35
+ const next = await queryTickets(sdk, {
36
+ currentPage: page,
37
+ pageSize,
38
+ search,
39
+ });
40
+ setResult(next);
41
+ } catch (nextError) {
42
+ setError(nextError instanceof Error ? nextError.message : "加载失败");
43
+ } finally {
44
+ setLoading(false);
45
+ setRefreshing(false);
46
+ }
47
+ },
48
+ [page, pageSize, sdk, search],
49
+ );
50
+
51
+ useEffect(() => {
52
+ void load("loading");
53
+ }, [load]);
54
+
55
+ const runAction = useCallback(
56
+ async (ticket: TicketRecord, action: TicketAction, comment?: string) => {
57
+ setSubmittingAction(action);
58
+ setError(null);
59
+ try {
60
+ await transitionTicket(sdk, ticket, action, operator, comment);
61
+ await load("refreshing");
62
+ } catch (nextError) {
63
+ setError(nextError instanceof Error ? nextError.message : "操作失败");
64
+ } finally {
65
+ setSubmittingAction(null);
66
+ }
67
+ },
68
+ [load, operator, sdk],
69
+ );
70
+
71
+ const recordsWithPermissions = useMemo(
72
+ () =>
73
+ result.records.map((ticket) => ({
74
+ ticket,
75
+ permissions: getTicketUiPermissions(ticket, operator),
76
+ })),
77
+ [operator, result.records],
78
+ );
79
+
80
+ return {
81
+ search,
82
+ setSearch,
83
+ page,
84
+ setPage,
85
+ pageSize,
86
+ setPageSize,
87
+ total: result.total,
88
+ recordsWithPermissions,
89
+ loading,
90
+ refreshing,
91
+ submittingAction,
92
+ error,
93
+ reload: () => load("refreshing"),
94
+ runAction,
95
+ };
96
+ }