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,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
|
+
}
|
|
@@ -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,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,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
|
+
}
|
package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/hooks/useTicketOps.ts
ADDED
|
@@ -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
|
+
}
|