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,35 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import { buildConditionDataPermission, buildRoleCode } from "./permissions";
5
+
6
+ describe("role-governance permission helpers", () => {
7
+ it("normalizes dynamic role codes", () => {
8
+ assert.equal(buildRoleCode({ roleCode: " College Admin " }), "college_admin");
9
+ assert.equal(buildRoleCode({ roleCode: "class advisor" }), "class_advisor");
10
+ });
11
+
12
+ it("builds condition-based data permissions from redundant fields", () => {
13
+ const permission = buildConditionDataPermission({
14
+ collegeId: "college_science",
15
+ classId: "",
16
+ ownerDeptId: "dept_lab",
17
+ });
18
+
19
+ assert.equal(permission.type, "condition");
20
+ assert.deepEqual(permission.condition.rules, [
21
+ {
22
+ field: "collegeId",
23
+ componentType: "Text",
24
+ op: "=",
25
+ value: "college_science",
26
+ },
27
+ {
28
+ field: "ownerDeptId",
29
+ componentType: "Text",
30
+ op: "=",
31
+ value: "dept_lab",
32
+ },
33
+ ]);
34
+ });
35
+ });
@@ -0,0 +1,24 @@
1
+ import type { AppRoleRecord, DataOwnershipFields } from "./types";
2
+
3
+ export function buildRoleCode(role: Pick<AppRoleRecord, "roleCode">) {
4
+ return role.roleCode.trim().replace(/\s+/g, "_").toLowerCase();
5
+ }
6
+
7
+ export function buildConditionDataPermission(scope: DataOwnershipFields) {
8
+ const rules = Object.entries(scope)
9
+ .filter(([, value]) => Boolean(value))
10
+ .map(([field, value]) => ({
11
+ field,
12
+ componentType: "Text",
13
+ op: "=",
14
+ value,
15
+ }));
16
+
17
+ return {
18
+ type: "condition" as const,
19
+ condition: {
20
+ logic: "AND" as const,
21
+ rules,
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,17 @@
1
+ export interface AppRoleRecord {
2
+ formInstanceId: string;
3
+ roleCode: string;
4
+ roleName: string;
5
+ members?: Array<{ label: string; value: string }>;
6
+ collegeId?: string;
7
+ classId?: string;
8
+ enabled?: { label: string; value: "enabled" | "disabled" };
9
+ lastSyncedAt?: string;
10
+ }
11
+
12
+ export interface DataOwnershipFields {
13
+ collegeId?: string;
14
+ classId?: string;
15
+ ownerDeptId?: string;
16
+ ownerUserId?: string;
17
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./permissions";
2
+ export * from "./state-machine";
3
+ export * from "./ticket-query";
4
+ export * from "./types";
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import { canViewTicket, getTicketUiPermissions } from "./permissions";
5
+ import type { TicketOperator, TicketRecord } from "./types";
6
+
7
+ const ticket: TicketRecord = {
8
+ formInstanceId: "ticket_001",
9
+ title: "实验室报修",
10
+ status: "accepted",
11
+ ownerUserId: "owner_001",
12
+ ownerDeptId: "dept_lab",
13
+ collegeId: "college_science",
14
+ };
15
+
16
+ describe("service-ticket permission helpers", () => {
17
+ it("allows admins and redundant ownership matches", () => {
18
+ assert.equal(canViewTicket(ticket, user({ roleCodes: ["app_admin"] })), true);
19
+ assert.equal(canViewTicket(ticket, user({ userId: "owner_001" })), true);
20
+ assert.equal(canViewTicket(ticket, user({ collegeIds: ["college_science"] })), true);
21
+ assert.equal(canViewTicket(ticket, user({ departmentIds: ["dept_lab"] })), true);
22
+ });
23
+
24
+ it("keeps action visibility tied to operation permission", () => {
25
+ const allowed = getTicketUiPermissions(ticket, user({ departmentIds: ["dept_lab"] }));
26
+ assert.equal(allowed.canView, true);
27
+ assert.deepEqual(allowed.actions, ["start", "cancel"]);
28
+
29
+ const denied = getTicketUiPermissions(ticket, user({ userId: "other" }));
30
+ assert.equal(denied.canView, false);
31
+ assert.deepEqual(denied.actions, []);
32
+ });
33
+ });
34
+
35
+ function user(input: Partial<TicketOperator>): TicketOperator {
36
+ return {
37
+ userId: "u_001",
38
+ roleCodes: [],
39
+ departmentIds: [],
40
+ ...input,
41
+ };
42
+ }
@@ -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
+ });