openxiangda 1.0.39 → 1.0.41

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 (58) hide show
  1. package/README.md +2 -2
  2. package/lib/cli.js +9 -1
  3. package/openxiangda-skills/SKILL.md +6 -3
  4. package/openxiangda-skills/references/best-practices.md +29 -5
  5. package/openxiangda-skills/references/component-guide.md +79 -45
  6. package/openxiangda-skills/references/forms/component-registry.md +33 -1
  7. package/openxiangda-skills/references/openxiangda-api.md +7 -0
  8. package/openxiangda-skills/references/permissions-settings.md +27 -4
  9. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -2
  10. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +9 -2
  11. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +5 -3
  12. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +5 -0
  13. package/package.json +1 -1
  14. package/packages/sdk/dist/runtime/index.cjs +546 -0
  15. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  16. package/packages/sdk/dist/runtime/index.d.mts +13 -1
  17. package/packages/sdk/dist/runtime/index.d.ts +13 -1
  18. package/packages/sdk/dist/runtime/index.mjs +546 -0
  19. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  20. package/packages/sdk/src/build-source/scripts/build-forms.mjs +5 -1
  21. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-form.mdc +4 -0
  22. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-page.mdc +2 -0
  23. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +3 -0
  24. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-form.md +4 -0
  25. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-page.md +2 -0
  26. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +3 -0
  27. package/templates/sy-lowcode-app-workspace/AGENTS.md +5 -0
  28. package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +8 -0
  29. package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +32 -0
  30. package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +20 -0
  31. package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +48 -0
  32. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/App.tsx +8 -0
  33. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/GlassHomeDashboard.tsx +232 -0
  34. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/index.tsx +10 -0
  35. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/page.config.ts +14 -0
  36. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/glass-home-dashboard/styles.css +196 -0
  37. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/App.tsx +8 -0
  38. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/MintAnalyticsDashboard.tsx +279 -0
  39. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/index.tsx +10 -0
  40. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/page.config.ts +14 -0
  41. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mint-analytics-dashboard/styles.css +163 -0
  42. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/App.tsx +8 -0
  43. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/OpsMonitorDashboard.tsx +306 -0
  44. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/index.tsx +10 -0
  45. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/page.config.ts +14 -0
  46. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/ops-monitor-dashboard/styles.css +248 -0
  47. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/App.tsx +8 -0
  48. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/WorkOrderListDrawerPage.tsx +371 -0
  49. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/index.tsx +10 -0
  50. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/page.config.ts +14 -0
  51. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/work-order-list-drawer/styles.css +182 -0
  52. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/DashboardPrimitives.tsx +832 -0
  53. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/chartOptions.ts +140 -0
  54. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/sampleData.ts +466 -0
  55. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/styles.css +874 -0
  56. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/shared/components/admin-ui-templates/types.ts +150 -0
  57. package/templates/sy-lowcode-app-workspace/src/forms/README.md +4 -0
  58. package/templates/sy-lowcode-app-workspace/src/main.tsx +4 -0
@@ -0,0 +1,832 @@
1
+ import {
2
+ AlertOutlined,
3
+ AppstoreOutlined,
4
+ AuditOutlined,
5
+ BarChartOutlined,
6
+ BellOutlined,
7
+ CheckCircleOutlined,
8
+ CloudServerOutlined,
9
+ DatabaseOutlined,
10
+ DownloadOutlined,
11
+ FileDoneOutlined,
12
+ FileTextOutlined,
13
+ HomeOutlined,
14
+ InboxOutlined,
15
+ LineChartOutlined,
16
+ LogoutOutlined,
17
+ MenuFoldOutlined,
18
+ PlusOutlined,
19
+ ProductOutlined,
20
+ ProjectOutlined,
21
+ QuestionCircleOutlined,
22
+ SearchOutlined,
23
+ SettingOutlined,
24
+ ShopOutlined,
25
+ ShoppingCartOutlined,
26
+ SolutionOutlined,
27
+ TeamOutlined,
28
+ ThunderboltOutlined,
29
+ ToolOutlined,
30
+ UploadOutlined,
31
+ UserOutlined,
32
+ WalletOutlined,
33
+ } from "@ant-design/icons";
34
+ import ReactECharts from "echarts-for-react";
35
+ import {
36
+ Avatar,
37
+ Badge,
38
+ Button,
39
+ DatePicker,
40
+ Descriptions,
41
+ Dropdown,
42
+ Drawer,
43
+ Form,
44
+ Input,
45
+ Progress,
46
+ Select,
47
+ Space,
48
+ Tag,
49
+ } from "antd";
50
+ import type { MenuProps } from "antd";
51
+ import type { ReactNode } from "react";
52
+ import { useEffect } from "react";
53
+
54
+ import type {
55
+ ActivityItem,
56
+ DrawerMode,
57
+ IconName,
58
+ MetricItem,
59
+ NavItem,
60
+ NoticeItem,
61
+ QuickAction,
62
+ RankingItem,
63
+ TaskProgressItem,
64
+ TemplateTone,
65
+ TodoItem,
66
+ WorkOrderPriority,
67
+ WorkOrderRecord,
68
+ WorkOrderStatus,
69
+ } from "./types";
70
+
71
+ const iconMap: Record<IconName, ReactNode> = {
72
+ home: <HomeOutlined />,
73
+ analytics: <LineChartOutlined />,
74
+ business: <ProjectOutlined />,
75
+ customer: <TeamOutlined />,
76
+ order: <ShoppingCartOutlined />,
77
+ product: <ProductOutlined />,
78
+ finance: <WalletOutlined />,
79
+ supply: <CloudServerOutlined />,
80
+ hr: <SolutionOutlined />,
81
+ system: <SettingOutlined />,
82
+ app: <AppstoreOutlined />,
83
+ ticket: <FileTextOutlined />,
84
+ task: <CheckCircleOutlined />,
85
+ monitor: <BarChartOutlined />,
86
+ alert: <AlertOutlined />,
87
+ device: <DatabaseOutlined />,
88
+ report: <FileDoneOutlined />,
89
+ setting: <SettingOutlined />,
90
+ import: <UploadOutlined />,
91
+ export: <DownloadOutlined />,
92
+ help: <QuestionCircleOutlined />,
93
+ user: <UserOutlined />,
94
+ cart: <ShoppingCartOutlined />,
95
+ approval: <AuditOutlined />,
96
+ contract: <InboxOutlined />,
97
+ dashboard: <ThunderboltOutlined />,
98
+ };
99
+
100
+ const toneHex: Record<TemplateTone, string> = {
101
+ blue: "#3b82f6",
102
+ cyan: "#06b6d4",
103
+ green: "#16a34a",
104
+ mint: "#10b981",
105
+ purple: "#7c3aed",
106
+ orange: "#f97316",
107
+ red: "#ef4444",
108
+ slate: "#64748b",
109
+ };
110
+
111
+ const toneGradient: Record<TemplateTone, string> = {
112
+ blue: "linear-gradient(135deg, #60a5fa 0%, #2563eb 100%)",
113
+ cyan: "linear-gradient(135deg, #22d3ee 0%, #0891b2 100%)",
114
+ green: "linear-gradient(135deg, #34d399 0%, #059669 100%)",
115
+ mint: "linear-gradient(135deg, #5eead4 0%, #10b981 100%)",
116
+ purple: "linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%)",
117
+ orange: "linear-gradient(135deg, #fdba74 0%, #f97316 100%)",
118
+ red: "linear-gradient(135deg, #fb7185 0%, #ef4444 100%)",
119
+ slate: "linear-gradient(135deg, #cbd5e1 0%, #64748b 100%)",
120
+ };
121
+
122
+ const workOrderStatusMeta: Record<
123
+ WorkOrderStatus,
124
+ { label: string; color: string }
125
+ > = {
126
+ pending: { label: "待处理", color: "gold" },
127
+ processing: { label: "处理中", color: "blue" },
128
+ done: { label: "已完成", color: "green" },
129
+ cancelled: { label: "已取消", color: "default" },
130
+ };
131
+
132
+ const priorityMeta: Record<WorkOrderPriority, { label: string; color: string }> = {
133
+ high: { label: "高", color: "red" },
134
+ medium: { label: "中", color: "gold" },
135
+ low: { label: "低", color: "green" },
136
+ };
137
+
138
+ function toneClass(tone: TemplateTone) {
139
+ return `bp-admin-tone-${tone}`;
140
+ }
141
+
142
+ export function IconGlyph(props: {
143
+ name: IconName;
144
+ tone?: TemplateTone;
145
+ className?: string;
146
+ }) {
147
+ return (
148
+ <span
149
+ className={`bp-admin-icon ${props.tone ? toneClass(props.tone) : ""} ${
150
+ props.className || ""
151
+ }`}
152
+ >
153
+ {iconMap[props.name]}
154
+ </span>
155
+ );
156
+ }
157
+
158
+ export function AdminShell(props: {
159
+ variant: "glass" | "mint" | "ops" | "work";
160
+ activeKey: string;
161
+ navItems: NavItem[];
162
+ brandTitle: string;
163
+ brandSubtitle: string;
164
+ topbar: ReactNode;
165
+ children: ReactNode;
166
+ sidebarFooter?: ReactNode;
167
+ onNavChange?: (key: string) => void;
168
+ }) {
169
+ return (
170
+ <main className={`bp-admin-shell bp-admin-shell--${props.variant}`}>
171
+ <aside className="bp-admin-sidebar">
172
+ <div className="bp-admin-brand">
173
+ <span className="bp-admin-brand__mark">A</span>
174
+ <div>
175
+ <strong>{props.brandTitle}</strong>
176
+ <small>{props.brandSubtitle}</small>
177
+ </div>
178
+ {props.variant === "ops" ? (
179
+ <Button
180
+ className="bp-admin-collapse"
181
+ shape="circle"
182
+ icon={<MenuFoldOutlined />}
183
+ />
184
+ ) : null}
185
+ </div>
186
+ <nav className="bp-admin-nav">
187
+ {props.navItems.map((item) => (
188
+ <div key={item.key}>
189
+ <button
190
+ type="button"
191
+ className={`bp-admin-nav__item ${
192
+ props.activeKey === item.key ? "is-active" : ""
193
+ }`}
194
+ onClick={() => props.onNavChange?.(item.key)}
195
+ >
196
+ <IconGlyph name={item.icon} />
197
+ <span>{item.label}</span>
198
+ </button>
199
+ {item.children ? (
200
+ <div className="bp-admin-nav__children">
201
+ {item.children.map((child) => (
202
+ <button
203
+ type="button"
204
+ key={child.key}
205
+ className={`bp-admin-nav__child ${
206
+ props.activeKey === child.key ? "is-active" : ""
207
+ }`}
208
+ onClick={() => props.onNavChange?.(child.key)}
209
+ >
210
+ {child.label}
211
+ </button>
212
+ ))}
213
+ </div>
214
+ ) : null}
215
+ </div>
216
+ ))}
217
+ </nav>
218
+ {props.sidebarFooter ? (
219
+ <div className="bp-admin-sidebar__footer">{props.sidebarFooter}</div>
220
+ ) : null}
221
+ </aside>
222
+ <section className="bp-admin-main">
223
+ {props.topbar}
224
+ <div className="bp-admin-content">{props.children}</div>
225
+ </section>
226
+ </main>
227
+ );
228
+ }
229
+
230
+ export function AdminTopbar(props: {
231
+ title: string;
232
+ subtitle?: string;
233
+ searchPlaceholder?: string;
234
+ extra?: ReactNode;
235
+ userName?: string;
236
+ }) {
237
+ const displayName = props.userName || "管理员";
238
+ const userMenuItems: MenuProps["items"] = [
239
+ {
240
+ key: "profile",
241
+ icon: <UserOutlined />,
242
+ label: "个人信息",
243
+ },
244
+ {
245
+ key: "logout",
246
+ icon: <LogoutOutlined />,
247
+ label: "退出登录",
248
+ danger: true,
249
+ },
250
+ ];
251
+
252
+ return (
253
+ <header className="bp-admin-topbar">
254
+ <div className="bp-admin-topbar__title">
255
+ <strong>{props.title}</strong>
256
+ {props.subtitle ? <span>{props.subtitle}</span> : null}
257
+ </div>
258
+ <div className="bp-admin-topbar__actions">
259
+ <Input
260
+ className="bp-admin-search"
261
+ prefix={<SearchOutlined />}
262
+ placeholder={props.searchPlaceholder || "搜索功能、数据、报表"}
263
+ allowClear
264
+ />
265
+ {props.extra}
266
+ <Badge count={12} size="small">
267
+ <Button shape="circle" icon={<BellOutlined />} />
268
+ </Badge>
269
+ <Dropdown
270
+ menu={{ items: userMenuItems }}
271
+ placement="bottomRight"
272
+ trigger={["click"]}
273
+ >
274
+ <Button type="text" className="bp-admin-user-menu" aria-label="用户菜单">
275
+ <Avatar>{displayName.slice(0, 1)}</Avatar>
276
+ <span className="bp-admin-user-menu__name">{displayName}</span>
277
+ </Button>
278
+ </Dropdown>
279
+ </div>
280
+ </header>
281
+ );
282
+ }
283
+
284
+ export function Panel(props: {
285
+ title?: string;
286
+ extra?: ReactNode;
287
+ className?: string;
288
+ children: ReactNode;
289
+ }) {
290
+ return (
291
+ <section className={`bp-admin-panel ${props.className || ""}`}>
292
+ {props.title || props.extra ? (
293
+ <div className="bp-admin-panel__header">
294
+ {props.title ? <h3>{props.title}</h3> : <span />}
295
+ {props.extra}
296
+ </div>
297
+ ) : null}
298
+ {props.children}
299
+ </section>
300
+ );
301
+ }
302
+
303
+ export function Sparkline(props: {
304
+ values: number[];
305
+ tone: TemplateTone;
306
+ filled?: boolean;
307
+ }) {
308
+ const min = Math.min(...props.values);
309
+ const max = Math.max(...props.values);
310
+ const span = Math.max(max - min, 1);
311
+ const points = props.values
312
+ .map((value, index) => {
313
+ const x = (index / Math.max(props.values.length - 1, 1)) * 120;
314
+ const y = 42 - ((value - min) / span) * 34;
315
+ return `${x},${y}`;
316
+ })
317
+ .join(" ");
318
+ const fillPoints = `0,48 ${points} 120,48`;
319
+
320
+ return (
321
+ <svg className="bp-admin-sparkline" viewBox="0 0 120 52" aria-hidden="true">
322
+ {props.filled ? (
323
+ <polygon
324
+ points={fillPoints}
325
+ fill={toneHex[props.tone]}
326
+ opacity="0.12"
327
+ />
328
+ ) : null}
329
+ <polyline
330
+ points={points}
331
+ fill="none"
332
+ stroke={toneHex[props.tone]}
333
+ strokeLinecap="round"
334
+ strokeLinejoin="round"
335
+ strokeWidth="3"
336
+ />
337
+ </svg>
338
+ );
339
+ }
340
+
341
+ export function MetricCard({ metric }: { metric: MetricItem }) {
342
+ const positive = metric.direction !== "down";
343
+
344
+ return (
345
+ <article className="bp-admin-metric">
346
+ <div className="bp-admin-metric__head">
347
+ <span>{metric.label}</span>
348
+ <IconGlyph name={metric.icon} tone={metric.tone} />
349
+ </div>
350
+ <strong>{metric.value}</strong>
351
+ <div className="bp-admin-metric__delta">
352
+ <span>较昨日</span>
353
+ <b className={positive ? "is-up" : "is-down"}>
354
+ {positive ? "↑" : "↓"} {metric.delta}
355
+ </b>
356
+ </div>
357
+ <Sparkline values={metric.trend} tone={metric.tone} filled />
358
+ </article>
359
+ );
360
+ }
361
+
362
+ export function QuickActionGrid(props: {
363
+ actions: QuickAction[];
364
+ onAction?: (action: QuickAction) => void;
365
+ compact?: boolean;
366
+ }) {
367
+ return (
368
+ <div
369
+ className={`bp-admin-quick-grid ${
370
+ props.compact ? "bp-admin-quick-grid--compact" : ""
371
+ }`}
372
+ >
373
+ {props.actions.map((action) => (
374
+ <button
375
+ key={action.id}
376
+ type="button"
377
+ className="bp-admin-quick-action"
378
+ onClick={() => props.onAction?.(action)}
379
+ >
380
+ <span
381
+ className="bp-admin-quick-action__icon"
382
+ style={{ background: toneGradient[action.tone] }}
383
+ >
384
+ {iconMap[action.icon]}
385
+ </span>
386
+ <span>{action.label}</span>
387
+ </button>
388
+ ))}
389
+ </div>
390
+ );
391
+ }
392
+
393
+ export function RankingList({ items }: { items: RankingItem[] }) {
394
+ return (
395
+ <ol className="bp-admin-ranking">
396
+ {items.map((item) => (
397
+ <li key={item.id}>
398
+ <span className={`bp-admin-ranking__rank rank-${item.rank}`}>
399
+ {item.rank}
400
+ </span>
401
+ <span>{item.name}</span>
402
+ <strong>{item.amount}</strong>
403
+ </li>
404
+ ))}
405
+ </ol>
406
+ );
407
+ }
408
+
409
+ export function ActivityList({ items }: { items: ActivityItem[] }) {
410
+ return (
411
+ <div className="bp-admin-activity">
412
+ {items.map((item) => (
413
+ <div key={item.id} className="bp-admin-activity__item">
414
+ <i className={toneClass(item.tone)} />
415
+ <span>{item.content}</span>
416
+ <time>{item.time}</time>
417
+ </div>
418
+ ))}
419
+ </div>
420
+ );
421
+ }
422
+
423
+ export function NoticeList({ items }: { items: NoticeItem[] }) {
424
+ return (
425
+ <div className="bp-admin-notices">
426
+ {items.map((item) => (
427
+ <div key={item.id} className="bp-admin-notice">
428
+ <IconGlyph name="app" tone={item.tone} />
429
+ <div>
430
+ <strong>{item.title}</strong>
431
+ <span>{item.description}</span>
432
+ </div>
433
+ <time>{item.time}</time>
434
+ </div>
435
+ ))}
436
+ </div>
437
+ );
438
+ }
439
+
440
+ export function TodoList({ items }: { items: TodoItem[] }) {
441
+ return (
442
+ <div className="bp-admin-todo">
443
+ {items.map((item) => (
444
+ <label key={item.id} className="bp-admin-todo__item">
445
+ <input type="checkbox" defaultChecked={item.done} />
446
+ <span>{item.title}</span>
447
+ <Tag color={toneHex[item.tone]}>{item.owner}</Tag>
448
+ <time>{item.due}</time>
449
+ </label>
450
+ ))}
451
+ </div>
452
+ );
453
+ }
454
+
455
+ export function TaskProgressList({ items }: { items: TaskProgressItem[] }) {
456
+ return (
457
+ <div className="bp-admin-task-progress">
458
+ {items.map((item) => (
459
+ <div key={item.id} className="bp-admin-task-progress__item">
460
+ <div>
461
+ <span>{item.title}</span>
462
+ <b>{item.percent}%</b>
463
+ <small>{item.status}</small>
464
+ </div>
465
+ <Progress
466
+ percent={item.percent}
467
+ showInfo={false}
468
+ strokeColor={toneHex[item.tone]}
469
+ railColor="#e2e8f0"
470
+ />
471
+ </div>
472
+ ))}
473
+ </div>
474
+ );
475
+ }
476
+
477
+ export function HealthScore(props: {
478
+ value: number;
479
+ title?: string;
480
+ tone?: TemplateTone;
481
+ }) {
482
+ const tone = props.tone || "mint";
483
+
484
+ return (
485
+ <div className="bp-admin-health">
486
+ <Progress
487
+ type="circle"
488
+ percent={props.value}
489
+ strokeColor={toneHex[tone]}
490
+ railColor="#e8eef7"
491
+ format={() => (
492
+ <span className="bp-admin-health__score">
493
+ {props.value}
494
+ <small>{props.title || "健康"}</small>
495
+ </span>
496
+ )}
497
+ />
498
+ <div className="bp-admin-health__checks">
499
+ {["服务器状态", "接口服务", "数据库状态", "存储空间"].map((item) => (
500
+ <span key={item}>
501
+ <CheckCircleOutlined /> {item}
502
+ <b>正常</b>
503
+ </span>
504
+ ))}
505
+ </div>
506
+ </div>
507
+ );
508
+ }
509
+
510
+ export function DashboardChart(props: {
511
+ option: Record<string, unknown>;
512
+ height?: number;
513
+ }) {
514
+ return (
515
+ <ReactECharts
516
+ option={props.option}
517
+ style={{ height: props.height || 260, width: "100%" }}
518
+ opts={{ renderer: "svg" }}
519
+ notMerge
520
+ lazyUpdate
521
+ />
522
+ );
523
+ }
524
+
525
+ export function TemplateActionDrawer(props: {
526
+ open: boolean;
527
+ title: string;
528
+ mode: DrawerMode;
529
+ onClose: () => void;
530
+ onSubmit: () => void;
531
+ }) {
532
+ if (!props.open) {
533
+ return null;
534
+ }
535
+ return <TemplateActionDrawerInner {...props} />;
536
+ }
537
+
538
+ function TemplateActionDrawerInner(props: {
539
+ open: boolean;
540
+ title: string;
541
+ mode: DrawerMode;
542
+ onClose: () => void;
543
+ onSubmit: () => void;
544
+ }) {
545
+ const [form] = Form.useForm();
546
+
547
+ useEffect(() => {
548
+ if (props.open) {
549
+ form.setFieldsValue({
550
+ subject: props.title,
551
+ owner: "张伟",
552
+ priority: "medium",
553
+ department: "运营部",
554
+ });
555
+ } else {
556
+ form.resetFields();
557
+ }
558
+ }, [form, props.open, props.title]);
559
+
560
+ return (
561
+ <Drawer
562
+ title={props.title}
563
+ open={props.open}
564
+ size={560}
565
+ push={false}
566
+ destroyOnHidden
567
+ onClose={props.onClose}
568
+ footer={
569
+ <div className="bp-admin-drawer-footer">
570
+ <Button onClick={props.onClose}>取消</Button>
571
+ <Button type="primary" onClick={() => form.submit()}>
572
+ 保存
573
+ </Button>
574
+ </div>
575
+ }
576
+ >
577
+ <Form
578
+ form={form}
579
+ layout="vertical"
580
+ className="bp-admin-drawer-form"
581
+ onFinish={props.onSubmit}
582
+ >
583
+ <Form.Item
584
+ name="subject"
585
+ label="事项名称"
586
+ rules={[{ required: true, message: "请输入事项名称" }]}
587
+ >
588
+ <Input placeholder="请输入事项名称" />
589
+ </Form.Item>
590
+ <Form.Item
591
+ name="owner"
592
+ label="负责人"
593
+ rules={[{ required: true, message: "请选择负责人" }]}
594
+ >
595
+ <Select
596
+ options={[
597
+ { value: "张伟", label: "张伟" },
598
+ { value: "李明", label: "李明" },
599
+ { value: "王芳", label: "王芳" },
600
+ ]}
601
+ />
602
+ </Form.Item>
603
+ <Form.Item name="department" label="所属部门">
604
+ <Select
605
+ options={[
606
+ { value: "运营部", label: "运营部" },
607
+ { value: "技术部", label: "技术部" },
608
+ { value: "产品部", label: "产品部" },
609
+ ]}
610
+ />
611
+ </Form.Item>
612
+ <Form.Item name="priority" label="优先级">
613
+ <Select
614
+ options={[
615
+ { value: "high", label: "高" },
616
+ { value: "medium", label: "中" },
617
+ { value: "low", label: "低" },
618
+ ]}
619
+ />
620
+ </Form.Item>
621
+ <Form.Item name="dueDate" label="截止时间">
622
+ <DatePicker className="bp-admin-full" showTime />
623
+ </Form.Item>
624
+ <Form.Item name="description" label="说明">
625
+ <Input.TextArea rows={5} placeholder="补充业务背景、处理要求或备注" />
626
+ </Form.Item>
627
+ </Form>
628
+ </Drawer>
629
+ );
630
+ }
631
+
632
+ export function WorkOrderStatusTag({ status }: { status: WorkOrderStatus }) {
633
+ const meta = workOrderStatusMeta[status];
634
+ return <Tag color={meta.color}>{meta.label}</Tag>;
635
+ }
636
+
637
+ export function PriorityTag({ priority }: { priority: WorkOrderPriority }) {
638
+ const meta = priorityMeta[priority];
639
+ return <Tag color={meta.color}>{meta.label}</Tag>;
640
+ }
641
+
642
+ export function WorkOrderDrawer(props: {
643
+ mode: DrawerMode | null;
644
+ record: WorkOrderRecord | null;
645
+ onClose: () => void;
646
+ onSubmit: () => void;
647
+ onSwitchMode?: (mode: DrawerMode) => void;
648
+ }) {
649
+ if (!props.mode) {
650
+ return null;
651
+ }
652
+ return <WorkOrderDrawerInner {...props} mode={props.mode} />;
653
+ }
654
+
655
+ function WorkOrderDrawerInner(props: {
656
+ mode: DrawerMode;
657
+ record: WorkOrderRecord | null;
658
+ onClose: () => void;
659
+ onSubmit: () => void;
660
+ onSwitchMode?: (mode: DrawerMode) => void;
661
+ }) {
662
+ const [form] = Form.useForm();
663
+ const open = true;
664
+ const isDetail = props.mode === "detail";
665
+ const record = props.record;
666
+ const title =
667
+ props.mode === "create"
668
+ ? "新建工单"
669
+ : props.mode === "edit"
670
+ ? "编辑工单"
671
+ : props.mode === "process"
672
+ ? "处理工单"
673
+ : "工单详情";
674
+
675
+ useEffect(() => {
676
+ if (open && record) {
677
+ form.setFieldsValue({
678
+ title: record.title,
679
+ owner: record.owner,
680
+ department: record.department,
681
+ priority: record.priority,
682
+ status: record.status,
683
+ description: record.description,
684
+ });
685
+ }
686
+ if (open && !record) {
687
+ form.setFieldsValue({
688
+ owner: "李明",
689
+ department: "运营部",
690
+ priority: "medium",
691
+ status: "pending",
692
+ });
693
+ }
694
+ if (!open) {
695
+ form.resetFields();
696
+ }
697
+ }, [form, open, record]);
698
+
699
+ return (
700
+ <Drawer
701
+ title={title}
702
+ open={open}
703
+ size={560}
704
+ push={false}
705
+ destroyOnHidden
706
+ onClose={props.onClose}
707
+ footer={
708
+ <div className="bp-admin-drawer-footer">
709
+ <Button onClick={props.onClose}>{isDetail ? "关闭" : "取消"}</Button>
710
+ {isDetail ? (
711
+ <>
712
+ <Button onClick={() => props.onSwitchMode?.("edit")}>编辑</Button>
713
+ <Button type="primary" onClick={() => props.onSwitchMode?.("process")}>
714
+ 处理工单
715
+ </Button>
716
+ </>
717
+ ) : (
718
+ <Button type="primary" onClick={() => form.submit()}>
719
+ {props.mode === "process" ? "提交处理" : "保存"}
720
+ </Button>
721
+ )}
722
+ </div>
723
+ }
724
+ >
725
+ {isDetail && record ? (
726
+ <>
727
+ <Form form={form} component={false} onFinish={props.onSubmit} />
728
+ <div className="bp-admin-drawer-detail">
729
+ <div className="bp-admin-drawer-detail__title">
730
+ <WorkOrderStatusTag status={record.status} />
731
+ <strong>{record.title}</strong>
732
+ <span>工单编号:{record.id}</span>
733
+ </div>
734
+ <Descriptions column={1} size="small">
735
+ <Descriptions.Item label="负责人">
736
+ <Avatar size="small">{record.avatar}</Avatar> {record.owner}
737
+ </Descriptions.Item>
738
+ <Descriptions.Item label="所属部门">{record.department}</Descriptions.Item>
739
+ <Descriptions.Item label="优先级">
740
+ <PriorityTag priority={record.priority} />
741
+ </Descriptions.Item>
742
+ <Descriptions.Item label="创建时间">{record.createdAt}</Descriptions.Item>
743
+ <Descriptions.Item label="更新时间">{record.updatedAt}</Descriptions.Item>
744
+ <Descriptions.Item label="来源">{record.source}</Descriptions.Item>
745
+ <Descriptions.Item label="联系方式">{record.contact}</Descriptions.Item>
746
+ <Descriptions.Item label="相关系统">{record.relatedSystem}</Descriptions.Item>
747
+ </Descriptions>
748
+ <div className="bp-admin-drawer-block">
749
+ <h4>工单描述</h4>
750
+ <p>{record.description}</p>
751
+ </div>
752
+ <div className="bp-admin-drawer-attachment">
753
+ <FileTextOutlined />
754
+ <span>{record.attachmentName}</span>
755
+ <small>2.34 MB</small>
756
+ </div>
757
+ <div className="bp-admin-drawer-block">
758
+ <h4>操作时间线</h4>
759
+ <div className="bp-admin-drawer-timeline">
760
+ {record.timeline.map((item) => (
761
+ <div key={item.id}>
762
+ <i />
763
+ <strong>{item.title}</strong>
764
+ <span>
765
+ {item.operator} · {item.time}
766
+ </span>
767
+ </div>
768
+ ))}
769
+ </div>
770
+ </div>
771
+ </div>
772
+ </>
773
+ ) : (
774
+ <Form
775
+ form={form}
776
+ layout="vertical"
777
+ className="bp-admin-drawer-form"
778
+ onFinish={props.onSubmit}
779
+ >
780
+ <Form.Item
781
+ name="title"
782
+ label="工单标题"
783
+ rules={[{ required: true, message: "请输入工单标题" }]}
784
+ >
785
+ <Input placeholder="请输入工单标题" />
786
+ </Form.Item>
787
+ <Form.Item name="status" label="状态">
788
+ <Select
789
+ options={[
790
+ { value: "pending", label: "待处理" },
791
+ { value: "processing", label: "处理中" },
792
+ { value: "done", label: "已完成" },
793
+ { value: "cancelled", label: "已取消" },
794
+ ]}
795
+ />
796
+ </Form.Item>
797
+ <Form.Item name="priority" label="优先级">
798
+ <Select
799
+ options={[
800
+ { value: "high", label: "高" },
801
+ { value: "medium", label: "中" },
802
+ { value: "low", label: "低" },
803
+ ]}
804
+ />
805
+ </Form.Item>
806
+ <Form.Item name="owner" label="负责人">
807
+ <Select
808
+ options={[
809
+ { value: "李明", label: "李明" },
810
+ { value: "王芳", label: "王芳" },
811
+ { value: "张伟", label: "张伟" },
812
+ { value: "陈强", label: "陈强" },
813
+ ]}
814
+ />
815
+ </Form.Item>
816
+ <Form.Item name="department" label="所属部门">
817
+ <Select
818
+ options={[
819
+ { value: "运营部", label: "运营部" },
820
+ { value: "技术部", label: "技术部" },
821
+ { value: "产品部", label: "产品部" },
822
+ ]}
823
+ />
824
+ </Form.Item>
825
+ <Form.Item name="description" label="工单描述">
826
+ <Input.TextArea rows={6} placeholder="请输入问题描述和处理要求" />
827
+ </Form.Item>
828
+ </Form>
829
+ )}
830
+ </Drawer>
831
+ );
832
+ }