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,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
+ }
@@ -0,0 +1,48 @@
1
+ import type { PageSdk } from "openxiangda/runtime";
2
+
3
+ import { buildRoleCode, type AppRoleRecord } from "@/domain/role-governance";
4
+
5
+ export const APP_ROLE_FORM_UUID = "FORM_APP_ROLE";
6
+
7
+ export async function queryEnabledBusinessRoles(sdk: PageSdk) {
8
+ const response = await sdk.form.advancedSearch<AppRoleRecord>({
9
+ formUuid: APP_ROLE_FORM_UUID,
10
+ currentPage: 1,
11
+ pageSize: 100,
12
+ filters: {
13
+ id: "enabled_roles",
14
+ logic: "AND",
15
+ rules: [
16
+ {
17
+ id: "enabled_EQ",
18
+ key: "enabled",
19
+ componentName: "RadioField",
20
+ operator: "EQ",
21
+ value: "enabled",
22
+ },
23
+ ],
24
+ conditions: [],
25
+ } as never,
26
+ });
27
+ return response.result?.data || [];
28
+ }
29
+
30
+ export async function syncBusinessRoleToPlatform(sdk: PageSdk, role: AppRoleRecord) {
31
+ const roleCode = buildRoleCode(role);
32
+ const platformRole = await sdk.role.create({
33
+ code: roleCode,
34
+ name: role.roleName,
35
+ scope: "app",
36
+ description: "由业务角色维护表同步",
37
+ });
38
+ const roleId = platformRole.result?.id;
39
+ if (!roleId) return platformRole;
40
+
41
+ for (const member of role.members || []) {
42
+ await sdk.role.assignRoles({
43
+ userId: member.value,
44
+ roleIds: [roleId],
45
+ });
46
+ }
47
+ return platformRole;
48
+ }
@@ -0,0 +1,113 @@
1
+ import type { PageSdk } from "openxiangda/runtime";
2
+
3
+ import {
4
+ assertTicketTransition,
5
+ buildTicketFilterGroup,
6
+ type TicketAction,
7
+ type TicketActionLog,
8
+ type TicketOperator,
9
+ type TicketRecord,
10
+ type TicketSearchState,
11
+ } from "@/domain/service-ticket";
12
+
13
+ export const SERVICE_TICKET_FORM_UUID = "FORM_SERVICE_TICKET";
14
+ export const TICKET_ACTION_LOG_FORM_UUID = "FORM_TICKET_ACTION_LOG";
15
+
16
+ export interface TicketQueryInput {
17
+ currentPage: number;
18
+ pageSize: number;
19
+ search: TicketSearchState;
20
+ }
21
+
22
+ export interface TicketQueryResult {
23
+ records: TicketRecord[];
24
+ total: number;
25
+ }
26
+
27
+ export async function queryTickets(
28
+ sdk: PageSdk,
29
+ input: TicketQueryInput,
30
+ ): Promise<TicketQueryResult> {
31
+ const response = await sdk.form.advancedSearch<TicketRecord>({
32
+ formUuid: SERVICE_TICKET_FORM_UUID,
33
+ currentPage: input.currentPage,
34
+ pageSize: input.pageSize,
35
+ filters: buildTicketFilterGroup(input.search) as never,
36
+ order: [{ id: "modifiedTime", isAsc: "n" }],
37
+ });
38
+
39
+ return {
40
+ records: response.result?.data || [],
41
+ total: response.result?.totalCount || 0,
42
+ };
43
+ }
44
+
45
+ export async function transitionTicket(
46
+ sdk: PageSdk,
47
+ ticket: TicketRecord,
48
+ action: TicketAction,
49
+ operator: TicketOperator,
50
+ comment?: string,
51
+ ) {
52
+ const nextStatus = assertTicketTransition(ticket, action, operator);
53
+ const operatedAt = new Date().toISOString();
54
+
55
+ await sdk.form.update({
56
+ formUuid: SERVICE_TICKET_FORM_UUID,
57
+ formInstanceId: ticket.formInstanceId,
58
+ data: {
59
+ status: nextStatus,
60
+ lastActionAt: operatedAt,
61
+ ownerUserId: ticket.ownerUserId,
62
+ ownerDeptId: ticket.ownerDeptId,
63
+ collegeId: ticket.collegeId,
64
+ classId: ticket.classId,
65
+ },
66
+ } as never);
67
+
68
+ const log: TicketActionLog = {
69
+ ticketId: ticket.formInstanceId,
70
+ action,
71
+ fromStatus: ticket.status,
72
+ toStatus: nextStatus,
73
+ operatorId: operator.userId,
74
+ operatorName: operator.userName,
75
+ comment,
76
+ operatedAt,
77
+ };
78
+
79
+ await sdk.form.create({
80
+ formUuid: TICKET_ACTION_LOG_FORM_UUID,
81
+ data: log,
82
+ } as never);
83
+
84
+ return { nextStatus, operatedAt };
85
+ }
86
+
87
+ export async function queryTicketActionLogs(
88
+ sdk: PageSdk,
89
+ ticketId: string,
90
+ ): Promise<TicketActionLog[]> {
91
+ const response = await sdk.form.advancedSearch<TicketActionLog>({
92
+ formUuid: TICKET_ACTION_LOG_FORM_UUID,
93
+ currentPage: 1,
94
+ pageSize: 50,
95
+ filters: {
96
+ id: "ticket_log_filters",
97
+ logic: "AND",
98
+ rules: [
99
+ {
100
+ id: "ticketId_EQ",
101
+ key: "ticketId",
102
+ componentName: "TextField",
103
+ operator: "EQ",
104
+ value: ticketId,
105
+ },
106
+ ],
107
+ conditions: [],
108
+ } as never,
109
+ order: [{ id: "createdAt", isAsc: "n" }],
110
+ });
111
+
112
+ return response.result?.data || [];
113
+ }
@@ -12,11 +12,13 @@
12
12
  "build-js-code": "node scripts/build-js-code.mjs",
13
13
  "build:js-code": "node scripts/build-js-code.mjs",
14
14
  "typecheck:js-code": "tsc -p tsconfig.js-code-nodes.json --noEmit",
15
+ "examples:check": "tsc -p tsconfig.examples.json --noEmit",
15
16
  "sync-schema": "lowcode-workspace sync-schema",
16
17
  "publish:oss": "lowcode-workspace publish-oss",
17
18
  "register": "lowcode-workspace register",
18
19
  "register-bundle": "lowcode-workspace register",
19
20
  "publish:all": "lowcode-workspace publish-all",
21
+ "publish:changed": "lowcode-workspace publish-all --changed",
20
22
  "openxiangda:publish": "lowcode-workspace publish-all",
21
23
  "ai:update": "pnpm dlx openxiangda@latest lowcode-workspace update --channel latest",
22
24
  "ai:migrate": "pnpm dlx openxiangda@latest lowcode-workspace migrate",