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,23 @@
1
+ import type { TicketOperator, TicketRecord } from "./types";
2
+ import { getAvailableTicketActions } from "./state-machine";
3
+
4
+ export function canViewTicket(ticket: TicketRecord, operator: TicketOperator) {
5
+ if (operator.roleCodes.includes("app_admin")) return true;
6
+ if (ticket.ownerUserId === operator.userId) return true;
7
+ if (ticket.collegeId && operator.collegeIds?.includes(ticket.collegeId)) return true;
8
+ if (ticket.classId && operator.classIds?.includes(ticket.classId)) return true;
9
+ if (
10
+ ticket.ownerDeptId &&
11
+ operator.departmentIds?.includes(ticket.ownerDeptId)
12
+ ) {
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+
18
+ export function getTicketUiPermissions(ticket: TicketRecord, operator: TicketOperator) {
19
+ return {
20
+ canView: canViewTicket(ticket, operator),
21
+ actions: getAvailableTicketActions(ticket, operator),
22
+ };
23
+ }
@@ -0,0 +1,63 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import {
5
+ assertTicketTransition,
6
+ getAvailableTicketActions,
7
+ getNextTicketStatus,
8
+ } from "./state-machine";
9
+ import type { TicketOperator, TicketRecord } from "./types";
10
+
11
+ const operator: TicketOperator = {
12
+ userId: "u_001",
13
+ roleCodes: ["ticket_operator"],
14
+ departmentIds: ["dept_lab"],
15
+ };
16
+
17
+ const ticket: TicketRecord = {
18
+ formInstanceId: "ticket_001",
19
+ title: "设备故障",
20
+ status: "new",
21
+ ownerDeptId: "dept_lab",
22
+ };
23
+
24
+ describe("service-ticket state machine", () => {
25
+ it("returns deterministic next status", () => {
26
+ assert.equal(getNextTicketStatus("new", "accept"), "accepted");
27
+ assert.equal(getNextTicketStatus("resolved", "close"), "closed");
28
+ assert.equal(getNextTicketStatus("closed", "start"), null);
29
+ });
30
+
31
+ it("filters available actions by status and operator permission", () => {
32
+ assert.deepEqual(getAvailableTicketActions(ticket, operator), [
33
+ "accept",
34
+ "cancel",
35
+ ]);
36
+
37
+ assert.deepEqual(
38
+ getAvailableTicketActions(ticket, {
39
+ userId: "u_002",
40
+ roleCodes: ["student"],
41
+ departmentIds: ["dept_other"],
42
+ }),
43
+ [],
44
+ );
45
+ });
46
+
47
+ it("throws clear errors for unauthorized or invalid transitions", () => {
48
+ assert.throws(
49
+ () =>
50
+ assertTicketTransition(ticket, "accept", {
51
+ userId: "u_002",
52
+ roleCodes: [],
53
+ departmentIds: [],
54
+ }),
55
+ /无权操作/,
56
+ );
57
+
58
+ assert.throws(
59
+ () => assertTicketTransition({ ...ticket, status: "closed" }, "start", operator),
60
+ /不允许执行动作/,
61
+ );
62
+ });
63
+ });
@@ -0,0 +1,73 @@
1
+ import type { TicketAction, TicketOperator, TicketRecord, TicketStatus } from "./types";
2
+
3
+ export const ticketStatusLabels: Record<TicketStatus, string> = {
4
+ new: "新建",
5
+ accepted: "已受理",
6
+ processing: "处理中",
7
+ paused: "挂起",
8
+ resolved: "已解决",
9
+ closed: "已关闭",
10
+ cancelled: "已取消",
11
+ };
12
+
13
+ export const ticketActionLabels: Record<TicketAction, string> = {
14
+ accept: "受理",
15
+ start: "开始处理",
16
+ pause: "挂起",
17
+ resume: "恢复",
18
+ resolve: "解决",
19
+ close: "关闭",
20
+ cancel: "取消",
21
+ };
22
+
23
+ const transitionTable: Record<TicketStatus, Partial<Record<TicketAction, TicketStatus>>> = {
24
+ new: { accept: "accepted", cancel: "cancelled" },
25
+ accepted: { start: "processing", cancel: "cancelled" },
26
+ processing: { pause: "paused", resolve: "resolved" },
27
+ paused: { resume: "processing", cancel: "cancelled" },
28
+ resolved: { close: "closed" },
29
+ closed: {},
30
+ cancelled: {},
31
+ };
32
+
33
+ export function getNextTicketStatus(
34
+ status: TicketStatus,
35
+ action: TicketAction,
36
+ ): TicketStatus | null {
37
+ return transitionTable[status]?.[action] || null;
38
+ }
39
+
40
+ export function canOperateTicket(ticket: TicketRecord, operator: TicketOperator) {
41
+ if (operator.roleCodes.includes("app_admin")) return true;
42
+ if (ticket.ownerUserId && ticket.ownerUserId === operator.userId) return true;
43
+ if (
44
+ ticket.ownerDeptId &&
45
+ operator.departmentIds?.includes(ticket.ownerDeptId)
46
+ ) {
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ export function getAvailableTicketActions(
53
+ ticket: TicketRecord,
54
+ operator: TicketOperator,
55
+ ): TicketAction[] {
56
+ if (!canOperateTicket(ticket, operator)) return [];
57
+ return Object.keys(transitionTable[ticket.status] || {}) as TicketAction[];
58
+ }
59
+
60
+ export function assertTicketTransition(
61
+ ticket: TicketRecord,
62
+ action: TicketAction,
63
+ operator: TicketOperator,
64
+ ) {
65
+ if (!canOperateTicket(ticket, operator)) {
66
+ throw new Error("当前用户无权操作该工单");
67
+ }
68
+ const nextStatus = getNextTicketStatus(ticket.status, action);
69
+ if (!nextStatus) {
70
+ throw new Error(`状态 ${ticket.status} 不允许执行动作 ${action}`);
71
+ }
72
+ return nextStatus;
73
+ }
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import { buildTicketFilterGroup } from "./ticket-query";
5
+
6
+ describe("service-ticket query builder", () => {
7
+ it("builds structured filters instead of browser-side filtering", () => {
8
+ const group = buildTicketFilterGroup({
9
+ keyword: "离心机",
10
+ statuses: ["new", "processing"],
11
+ priorities: ["urgent"],
12
+ collegeId: "college_science",
13
+ });
14
+
15
+ assert.equal(group.logic, "AND");
16
+ assert.deepEqual(
17
+ group.rules.map((item) => item.key),
18
+ ["status", "priority", "collegeId"],
19
+ );
20
+ assert.equal(group.conditions.length, 1);
21
+ assert.equal(group.conditions[0]?.logic, "OR");
22
+ assert.deepEqual(
23
+ group.conditions[0]?.rules.map((item) => item.key),
24
+ ["title", "description"],
25
+ );
26
+ });
27
+
28
+ it("omits empty search fields", () => {
29
+ const group = buildTicketFilterGroup({ keyword: " " });
30
+
31
+ assert.deepEqual(group.rules, []);
32
+ assert.deepEqual(group.conditions, []);
33
+ });
34
+ });
@@ -0,0 +1,73 @@
1
+ import type { TicketSearchState } from "./types";
2
+
3
+ export type FilterLogic = "AND" | "OR";
4
+
5
+ export interface FilterRule {
6
+ id: string;
7
+ key: string;
8
+ componentName: string;
9
+ operator: string;
10
+ value: unknown;
11
+ }
12
+
13
+ export interface FilterGroup {
14
+ id: string;
15
+ logic: FilterLogic;
16
+ rules: FilterRule[];
17
+ conditions: FilterGroup[];
18
+ }
19
+
20
+ const rule = (
21
+ key: string,
22
+ componentName: string,
23
+ operator: string,
24
+ value: unknown,
25
+ ): FilterRule => ({
26
+ id: `${key}_${operator}`,
27
+ key,
28
+ componentName,
29
+ operator,
30
+ value,
31
+ });
32
+
33
+ export function buildTicketFilterGroup(search: TicketSearchState): FilterGroup {
34
+ const group: FilterGroup = {
35
+ id: "ticket_filters",
36
+ logic: "AND",
37
+ rules: [],
38
+ conditions: [],
39
+ };
40
+
41
+ if (search.statuses?.length) {
42
+ group.rules.push(rule("status", "SelectField", "IN", search.statuses));
43
+ }
44
+ if (search.priorities?.length) {
45
+ group.rules.push(rule("priority", "SelectField", "IN", search.priorities));
46
+ }
47
+ if (search.ownerUserId) {
48
+ group.rules.push(rule("ownerUserId", "TextField", "EQ", search.ownerUserId));
49
+ }
50
+ if (search.ownerDeptId) {
51
+ group.rules.push(rule("ownerDeptId", "TextField", "EQ", search.ownerDeptId));
52
+ }
53
+ if (search.collegeId) {
54
+ group.rules.push(rule("collegeId", "TextField", "EQ", search.collegeId));
55
+ }
56
+ if (search.classId) {
57
+ group.rules.push(rule("classId", "TextField", "EQ", search.classId));
58
+ }
59
+ if (search.keyword?.trim()) {
60
+ const keyword = search.keyword.trim();
61
+ group.conditions.push({
62
+ id: "keyword_or",
63
+ logic: "OR",
64
+ rules: [
65
+ rule("title", "TextField", "CONTAINS", keyword),
66
+ rule("description", "TextAreaField", "CONTAINS", keyword),
67
+ ],
68
+ conditions: [],
69
+ });
70
+ }
71
+
72
+ return group;
73
+ }
@@ -0,0 +1,64 @@
1
+ export type TicketStatus =
2
+ | "new"
3
+ | "accepted"
4
+ | "processing"
5
+ | "paused"
6
+ | "resolved"
7
+ | "closed"
8
+ | "cancelled";
9
+
10
+ export type TicketAction =
11
+ | "accept"
12
+ | "start"
13
+ | "pause"
14
+ | "resume"
15
+ | "resolve"
16
+ | "close"
17
+ | "cancel";
18
+
19
+ export type TicketPriority = "urgent" | "high" | "normal" | "low";
20
+
21
+ export interface TicketRecord {
22
+ formInstanceId: string;
23
+ title: string;
24
+ status: TicketStatus;
25
+ priority?: TicketPriority;
26
+ ownerUserId?: string;
27
+ ownerDeptId?: string;
28
+ collegeId?: string;
29
+ classId?: string;
30
+ requester?: { label: string; value: string };
31
+ currentOwner?: { label: string; value: string };
32
+ description?: string;
33
+ lastActionAt?: string;
34
+ }
35
+
36
+ export interface TicketActionLog {
37
+ ticketId: string;
38
+ action: TicketAction;
39
+ fromStatus?: TicketStatus;
40
+ toStatus: TicketStatus;
41
+ operatorId: string;
42
+ operatorName?: string;
43
+ comment?: string;
44
+ operatedAt: string;
45
+ }
46
+
47
+ export interface TicketOperator {
48
+ userId: string;
49
+ userName?: string;
50
+ roleCodes: string[];
51
+ collegeIds?: string[];
52
+ classIds?: string[];
53
+ departmentIds?: string[];
54
+ }
55
+
56
+ export interface TicketSearchState {
57
+ keyword?: string;
58
+ statuses?: TicketStatus[];
59
+ priorities?: TicketPriority[];
60
+ ownerUserId?: string;
61
+ ownerDeptId?: string;
62
+ collegeId?: string;
63
+ classId?: string;
64
+ }
@@ -0,0 +1 @@
1
+ export { default } from "./schema";
@@ -0,0 +1,57 @@
1
+ import { createFormSchema } from "@/shared/form-schema";
2
+
3
+ export default createFormSchema({
4
+ formMeta: {
5
+ formUuid: "",
6
+ appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
7
+ title: "应用角色维护",
8
+ },
9
+ fields: [
10
+ {
11
+ fieldId: "roleCode",
12
+ componentName: "TextField",
13
+ label: "角色编码",
14
+ required: true,
15
+ placeholder: "如 college_admin",
16
+ },
17
+ {
18
+ fieldId: "roleName",
19
+ componentName: "TextField",
20
+ label: "角色名称",
21
+ required: true,
22
+ },
23
+ {
24
+ fieldId: "members",
25
+ componentName: "UserSelectField",
26
+ label: "成员",
27
+ multiple: true,
28
+ },
29
+ {
30
+ fieldId: "collegeId",
31
+ componentName: "TextField",
32
+ label: "学院 ID",
33
+ tips: "与业务表冗余字段配合,用于条件式数据权限。",
34
+ },
35
+ {
36
+ fieldId: "classId",
37
+ componentName: "TextField",
38
+ label: "班级 ID",
39
+ tips: "与业务表冗余字段配合,用于条件式数据权限。",
40
+ },
41
+ {
42
+ fieldId: "enabled",
43
+ componentName: "RadioField",
44
+ label: "启用状态",
45
+ defaultValue: { label: "启用", value: "enabled" },
46
+ options: [
47
+ { label: "启用", value: "enabled" },
48
+ { label: "停用", value: "disabled" },
49
+ ],
50
+ },
51
+ {
52
+ fieldId: "lastSyncedAt",
53
+ componentName: "DateField",
54
+ label: "最近同步时间",
55
+ },
56
+ ],
57
+ });
@@ -0,0 +1,83 @@
1
+ import { createFormSchema } from "@/shared/form-schema";
2
+
3
+ export default createFormSchema({
4
+ formMeta: {
5
+ formUuid: "",
6
+ appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
7
+ title: "客户档案",
8
+ },
9
+ fields: [
10
+ {
11
+ fieldId: "customerName",
12
+ componentName: "TextField",
13
+ label: "客户名称",
14
+ required: true,
15
+ placeholder: "请输入客户或组织名称",
16
+ },
17
+ {
18
+ fieldId: "customerLevel",
19
+ componentName: "SelectField",
20
+ label: "客户等级",
21
+ required: true,
22
+ options: [
23
+ { label: "重点客户", value: "key" },
24
+ { label: "普通客户", value: "normal" },
25
+ { label: "潜在客户", value: "prospect" },
26
+ ],
27
+ },
28
+ {
29
+ fieldId: "ownerUser",
30
+ componentName: "UserSelectField",
31
+ label: "负责人",
32
+ required: true,
33
+ },
34
+ {
35
+ fieldId: "ownerDept",
36
+ componentName: "DepartmentSelectField",
37
+ label: "负责部门",
38
+ required: true,
39
+ },
40
+ {
41
+ fieldId: "contactPhone",
42
+ componentName: "TextField",
43
+ label: "联系电话",
44
+ rules: [{ preset: "phone", message: "请输入有效手机号" }],
45
+ },
46
+ {
47
+ fieldId: "attachments",
48
+ componentName: "AttachmentField",
49
+ label: "附件",
50
+ maxCount: 5,
51
+ },
52
+ {
53
+ fieldId: "contacts",
54
+ componentName: "SubFormField",
55
+ label: "联系人",
56
+ columns: [
57
+ {
58
+ fieldId: "contactName",
59
+ componentName: "TextField",
60
+ label: "姓名",
61
+ required: true,
62
+ },
63
+ {
64
+ fieldId: "contactRole",
65
+ componentName: "TextField",
66
+ label: "角色",
67
+ },
68
+ {
69
+ fieldId: "contactMobile",
70
+ componentName: "TextField",
71
+ label: "手机",
72
+ rules: [{ preset: "phone", message: "请输入有效手机号" }],
73
+ },
74
+ ],
75
+ },
76
+ {
77
+ fieldId: "remark",
78
+ componentName: "TextAreaField",
79
+ label: "备注",
80
+ placeholder: "记录客户背景、跟进偏好或风险点",
81
+ },
82
+ ],
83
+ });
@@ -0,0 +1,97 @@
1
+ import { createFormSchema } from "@/shared/form-schema";
2
+
3
+ export default createFormSchema({
4
+ formMeta: {
5
+ formUuid: "",
6
+ appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
7
+ title: "服务工单",
8
+ },
9
+ fields: [
10
+ {
11
+ fieldId: "title",
12
+ componentName: "TextField",
13
+ label: "标题",
14
+ required: true,
15
+ },
16
+ {
17
+ fieldId: "status",
18
+ componentName: "SelectField",
19
+ label: "状态",
20
+ required: true,
21
+ defaultValue: { label: "新建", value: "new" },
22
+ options: [
23
+ { label: "新建", value: "new" },
24
+ { label: "已受理", value: "accepted" },
25
+ { label: "处理中", value: "processing" },
26
+ { label: "挂起", value: "paused" },
27
+ { label: "已解决", value: "resolved" },
28
+ { label: "已关闭", value: "closed" },
29
+ { label: "已取消", value: "cancelled" },
30
+ ],
31
+ },
32
+ {
33
+ fieldId: "priority",
34
+ componentName: "SelectField",
35
+ label: "优先级",
36
+ defaultValue: { label: "普通", value: "normal" },
37
+ options: [
38
+ { label: "紧急", value: "urgent" },
39
+ { label: "高", value: "high" },
40
+ { label: "普通", value: "normal" },
41
+ { label: "低", value: "low" },
42
+ ],
43
+ },
44
+ {
45
+ fieldId: "ownerUserId",
46
+ componentName: "TextField",
47
+ label: "负责人 ID",
48
+ tips: "权限隔离冗余字段,服务层从人员字段同步写入。",
49
+ },
50
+ {
51
+ fieldId: "ownerDeptId",
52
+ componentName: "TextField",
53
+ label: "负责部门 ID",
54
+ tips: "权限隔离冗余字段,权限组按此字段配置条件。",
55
+ },
56
+ {
57
+ fieldId: "collegeId",
58
+ componentName: "TextField",
59
+ label: "学院 ID",
60
+ tips: "示例冗余字段,可换成业务组织维度。",
61
+ },
62
+ {
63
+ fieldId: "classId",
64
+ componentName: "TextField",
65
+ label: "班级 ID",
66
+ tips: "示例冗余字段,可换成业务组织维度。",
67
+ },
68
+ {
69
+ fieldId: "currentOwner",
70
+ componentName: "UserSelectField",
71
+ label: "当前处理人",
72
+ },
73
+ {
74
+ fieldId: "requester",
75
+ componentName: "UserSelectField",
76
+ label: "提交人",
77
+ required: true,
78
+ },
79
+ {
80
+ fieldId: "description",
81
+ componentName: "TextAreaField",
82
+ label: "问题描述",
83
+ required: true,
84
+ },
85
+ {
86
+ fieldId: "lastActionAt",
87
+ componentName: "DateField",
88
+ label: "最近操作时间",
89
+ },
90
+ {
91
+ fieldId: "attachments",
92
+ componentName: "AttachmentField",
93
+ label: "附件",
94
+ maxCount: 8,
95
+ },
96
+ ],
97
+ });
@@ -0,0 +1,65 @@
1
+ import { createFormSchema } from "@/shared/form-schema";
2
+
3
+ export default createFormSchema({
4
+ formMeta: {
5
+ formUuid: "",
6
+ appType: process.env.OPENXIANGDA_APP_TYPE || "APP_XXXX",
7
+ title: "工单操作日志",
8
+ },
9
+ fields: [
10
+ {
11
+ fieldId: "ticketId",
12
+ componentName: "TextField",
13
+ label: "工单实例 ID",
14
+ required: true,
15
+ },
16
+ {
17
+ fieldId: "action",
18
+ componentName: "SelectField",
19
+ label: "动作",
20
+ required: true,
21
+ options: [
22
+ { label: "受理", value: "accept" },
23
+ { label: "开始处理", value: "start" },
24
+ { label: "挂起", value: "pause" },
25
+ { label: "恢复", value: "resume" },
26
+ { label: "解决", value: "resolve" },
27
+ { label: "关闭", value: "close" },
28
+ { label: "取消", value: "cancel" },
29
+ ],
30
+ },
31
+ {
32
+ fieldId: "fromStatus",
33
+ componentName: "TextField",
34
+ label: "原状态",
35
+ },
36
+ {
37
+ fieldId: "toStatus",
38
+ componentName: "TextField",
39
+ label: "新状态",
40
+ required: true,
41
+ },
42
+ {
43
+ fieldId: "operatorId",
44
+ componentName: "TextField",
45
+ label: "操作者 ID",
46
+ required: true,
47
+ },
48
+ {
49
+ fieldId: "operatorName",
50
+ componentName: "TextField",
51
+ label: "操作者",
52
+ },
53
+ {
54
+ fieldId: "comment",
55
+ componentName: "TextAreaField",
56
+ label: "说明",
57
+ },
58
+ {
59
+ fieldId: "operatedAt",
60
+ componentName: "DateField",
61
+ label: "操作时间",
62
+ required: true,
63
+ },
64
+ ],
65
+ });
@@ -0,0 +1,44 @@
1
+ export default async function dailyTicketDigest(ctx: any) {
2
+ const appType = ctx.app.appType;
3
+ const pageSize = 50;
4
+ let currentPage = 1;
5
+ let total = 0;
6
+ let riskCount = 0;
7
+
8
+ while (currentPage <= 20) {
9
+ const result = await ctx.methods.queryManyData(appType, "FORM_SERVICE_TICKET", {
10
+ currentPage,
11
+ pageSize,
12
+ filters: {
13
+ logic: "AND",
14
+ rules: [
15
+ {
16
+ key: "status",
17
+ componentName: "SelectField",
18
+ operator: "IN",
19
+ value: ["new", "accepted", "processing", "paused"],
20
+ },
21
+ ],
22
+ },
23
+ });
24
+ const rows = result?.data || result?.list || [];
25
+ total += rows.length;
26
+ riskCount += rows.filter((row: any) => row.priority?.value === "urgent").length;
27
+ if (rows.length < pageSize) break;
28
+ currentPage += 1;
29
+ }
30
+
31
+ if (total > 0 && ctx.operator?.userId) {
32
+ await ctx.notification.sendByType({
33
+ notificationType: "daily_ticket_digest",
34
+ recipientId: ctx.operator.userId,
35
+ payload: {
36
+ title: "每日工单摘要",
37
+ count: total,
38
+ riskCount,
39
+ },
40
+ });
41
+ }
42
+
43
+ return { total, riskCount };
44
+ }