adspecs 0.1.18 → 0.1.20

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 (88) hide show
  1. package/.adspecs/extensions/git/commands/adspecs.git.initialize.md +4 -4
  2. package/.adspecs/extensions/git/extension.yml +1 -1
  3. package/.adspecs/extensions/git/git-config.yml +17 -2
  4. package/.adspecs/extensions.yml +32 -0
  5. package/.adspecs/feature.json +16 -16
  6. package/.adspecs/feature.yml +28 -28
  7. package/.adspecs/paths.json +17 -0
  8. package/.adspecs/templates/02-/344/272/247/345/223/201/351/234/200/346/261/202/350/257/264/346/230/216/344/271/246/346/250/241/346/235/277.md +4 -0
  9. package/.adspecs/workflows/adspecs/workflow.yml +5 -4
  10. package/.claude-plugin/marketplace.json +23 -23
  11. package/.claude-plugin/plugin.json +18 -18
  12. package/.qoder-plugin/plugin.json +31 -31
  13. package/CLAUDE.md +1 -5
  14. package/README.md +395 -395
  15. package/bin/adspecs.js +129 -129
  16. package/hooks/commit-queue.js +245 -245
  17. package/hooks/hooks.json +63 -63
  18. package/hooks/session-start.js +44 -44
  19. package/hooks/wiki-queue.js +127 -127
  20. package/package.json +61 -61
  21. package/references/ant6-front-standard/index.md +99 -99
  22. package/references/antd-front-demo/.env.develop +12 -0
  23. package/references/antd-front-demo/.env.production +5 -0
  24. package/references/antd-front-demo/package-lock.json +2 -2
  25. package/references/antd-front-demo/package.json +1 -1
  26. package/references/antd-front-demo/public/mockServiceWorker.js +361 -361
  27. package/references/ecp-end-standard/index.md +63 -63
  28. package/references/python-end-standard/01-Python/345/220/216/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +372 -372
  29. package/references/python-end-standard/02-/346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/344/270/216/344/275/277/347/224/250/350/247/204/350/214/203.md +226 -226
  30. package/references/python-end-standard/03-Celery/345/274/202/346/255/245/344/273/273/345/212/241/350/247/204/350/214/203.md +237 -237
  31. package/references/python-end-standard/04-Redis/344/275/277/347/224/250/350/247/204/350/214/203.md +231 -231
  32. package/scripts/postinstall.js +107 -107
  33. package/scripts/sync-version.js +105 -105
  34. package/skills/.claude/.wiki-update-queue +26 -26
  35. package/skills/adspecs-constitution/SKILL.md +157 -0
  36. package/skills/adspecs-export-word/SKILL.md +498 -498
  37. package/skills/adspecs-export-word/references/md-to-docx.js +862 -862
  38. package/skills/adspecs-export-word/references/package-lock.json +220 -220
  39. package/skills/adspecs-export-word/references/package.json +10 -10
  40. package/skills/adspecs-front-prototype/SKILL.md +405 -0
  41. package/skills/adspecs-git-commit/SKILL.md +23 -8
  42. package/skills/adspecs-git-initialize/SKILL.md +54 -0
  43. package/skills/adspecs-plan/SKILL.md +59 -69
  44. package/skills/adspecs-prd/SKILL.md +13 -5
  45. package/skills/adspecs-prd-to-demo/SKILL.md +532 -0
  46. package/skills/adspecs-tasks/SKILL.md +175 -204
  47. package/skills/adspecs-update-status/SKILL.md +382 -382
  48. package/skills/adspecs-utest/SKILL.md +107 -116
  49. package/skills/grill-me/SKILL.md +7 -0
  50. package/skills/grill-me/agents/openai.yaml +5 -0
  51. package/skills/playwright-cli/SKILL.md +420 -0
  52. package/skills/playwright-cli/references/element-attributes.md +23 -0
  53. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  54. package/skills/playwright-cli/references/request-mocking.md +87 -0
  55. package/skills/playwright-cli/references/running-code.md +241 -0
  56. package/skills/playwright-cli/references/session-management.md +225 -0
  57. package/skills/playwright-cli/references/storage-state.md +275 -0
  58. package/skills/playwright-cli/references/test-generation.md +433 -0
  59. package/skills/playwright-cli/references/tracing.md +139 -0
  60. package/skills/playwright-cli/references/video-recording.md +143 -0
  61. package/skills/playwright-trace/SKILL.md +171 -0
  62. package/skills/project-init/references/front-demo/.claude/settings.local.json +9 -0
  63. package/skills/wiki-update/SKILL.md +232 -232
  64. package/src/commands/doctor.js +197 -197
  65. package/src/commands/init.js +83 -83
  66. package/src/commands/plugin.js +165 -165
  67. package/src/commands/update.js +87 -87
  68. package/src/lib/area-scanner.js +129 -129
  69. package/src/lib/copier.js +104 -104
  70. package/src/lib/dir-utils.js +133 -133
  71. package/src/lib/json-merge.js +114 -114
  72. package/src/lib/paths-defaults.js +37 -37
  73. package/src/lib/prompts.js +347 -345
  74. package/src/lib/readme-gen.js +143 -143
  75. package/src/lib/report.js +327 -327
  76. package/src/lib/scaffolder.js +518 -518
  77. package/src/lib/short-name.js +36 -36
  78. package/src/utils.js +80 -80
  79. package/references/antd-front-demo/.env +0 -15
  80. package/references/antd-front-demo/src/pages/aps/coil/components/CoilDrawer.tsx +0 -141
  81. package/references/antd-front-demo/src/pages/aps/coil/index.tsx +0 -209
  82. package/references/antd-front-demo/src/pages/aps/optimization/index.tsx +0 -192
  83. package/references/antd-front-demo/src/pages/aps/order/components/OrderDrawer.tsx +0 -251
  84. package/references/antd-front-demo/src/pages/aps/order/index.tsx +0 -273
  85. package/references/antd-front-demo/src/pages/aps/param/index.tsx +0 -153
  86. package/references/antd-front-demo/src/pages/aps/plan/components/CuttingPlanVisualization.tsx +0 -821
  87. package/references/antd-front-demo/src/pages/aps/plan/index.tsx +0 -254
  88. package/references/antd-front-demo/src/pages/aps/task/index.tsx +0 -190
@@ -1,821 +0,0 @@
1
- import { useMemo } from 'react';
2
- import { Card, Tag, Space, Typography, Row, Col, Statistic } from 'antd';
3
- import { useTranslation } from 'react-i18next';
4
- import type { PlanDetailVO, SmallCoilVO } from '@/types/aps/plan';
5
-
6
- const { Text } = Typography;
7
-
8
- interface CuttingPlanVisualizationProps {
9
- plan: PlanDetailVO;
10
- }
11
-
12
- interface Region {
13
- index: number;
14
- startY: number; // 沿母卷长度方向(Y 轴)的起始位置
15
- endY: number; // 沿母卷长度方向(Y 轴)的结束位置
16
- length: number;
17
- verticalCuts: number[];
18
- strips: Strip[];
19
- }
20
-
21
- interface Strip {
22
- x: number; // 沿母卷宽度方向(X 轴)的起始位置
23
- width: number; // 窄条宽度(沿 X 轴)
24
- smallCoil?: SmallCoilVO;
25
- isWasteEdge: boolean;
26
- }
27
-
28
- // 订单颜色映射(全局缓存)
29
- const ORDER_COLORS: Record<string, string> = {};
30
- const COLOR_PALETTE = [
31
- '#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1',
32
- '#13c2c2', '#eb2f96', '#fa8c16', '#a0d911', '#2f54eb',
33
- ];
34
- let colorIndex = 0;
35
-
36
- function getOrderColor(orderId: string | undefined): string {
37
- if (!orderId) return '#bfbfbf';
38
- if (!ORDER_COLORS[orderId]) {
39
- ORDER_COLORS[orderId] = COLOR_PALETTE[colorIndex % COLOR_PALETTE.length];
40
- colorIndex++;
41
- }
42
- return ORDER_COLORS[orderId];
43
- }
44
-
45
- // 区域背景色(浅色)
46
- const REGION_BG_COLORS = [
47
- 'rgba(24, 144, 255, 0.06)',
48
- 'rgba(82, 196, 26, 0.06)',
49
- 'rgba(250, 173, 20, 0.06)',
50
- 'rgba(114, 46, 209, 0.06)',
51
- 'rgba(19, 194, 194, 0.06)',
52
- ];
53
-
54
- const CuttingPlanVisualization: React.FC<CuttingPlanVisualizationProps> = ({ plan }) => {
55
- const { t } = useTranslation('aps/plan');
56
-
57
- // 必须在 useMemo 之前声明,因为 useMemo 内部引用了 coilWidth
58
- const coilWidth = plan.coilWidth || 1500;
59
-
60
- const visualizationData = useMemo(() => {
61
- // 解析竖切切点(每个区域一组)—— 竖切沿母卷宽度方向切割
62
- const verticalCutsByRegion = plan.verticalCuts.split(';').map(s =>
63
- s.split(',').map(n => parseFloat(n.trim())).filter(n => !isNaN(n))
64
- );
65
-
66
- // 解析横切切点 —— 横切沿母卷长度方向切割,将母卷分为多个区域
67
- const horizontalCuts = plan.horizontalCuts
68
- ? plan.horizontalCuts.split(',').map(n => parseFloat(n.trim())).filter(n => !isNaN(n))
69
- : [];
70
-
71
- // 工艺参数(假设值)
72
- const headWaste = 100; // 废头 mm
73
- const tailWaste = 100; // 废尾 mm
74
- const cutHeadLength = 200; // 切头 mm
75
-
76
- // 构建区域数据(竖向布局:区域沿 Y 轴排列)
77
- const regions: Region[] = [];
78
- let currentY = headWaste; // 从废头之后开始
79
-
80
- for (let i = 0; i < verticalCutsByRegion.length; i++) {
81
- const verticalCuts = verticalCutsByRegion[i];
82
-
83
- // 计算区域结束位置(长度方向)
84
- let regionEndY: number;
85
- if (i < horizontalCuts.length) {
86
- regionEndY = horizontalCuts[i];
87
- } else {
88
- // 最后一个区域,根据小卷长度估算
89
- const regionCoils = plan.smallCoils.filter(sc => sc.region === i + 1);
90
- const avgLength = regionCoils.length > 0
91
- ? regionCoils.reduce((sum, sc) => sum + sc.length, 0) / regionCoils.length
92
- : 100000;
93
- regionEndY = currentY + avgLength;
94
- }
95
-
96
- const regionLength = regionEndY - currentY;
97
-
98
- // 构建窄条(竖切沿宽度方向,窄条在 X 轴上排列)
99
- const strips: Strip[] = [];
100
-
101
- // 左废边:从母卷左边缘(0)到第一个竖切切点
102
- const leftWasteWidth = verticalCuts[0];
103
- strips.push({
104
- x: 0,
105
- width: leftWasteWidth,
106
- isWasteEdge: true,
107
- });
108
-
109
- // 有效窄条:相邻竖切切点之间的所有窄条
110
- for (let j = 0; j < verticalCuts.length - 1; j++) {
111
- const x = verticalCuts[j];
112
- const stripWidth = verticalCuts[j + 1] - x;
113
-
114
- // 查找对应的小卷
115
- const smallCoil = plan.smallCoils.find(sc =>
116
- sc.region === i + 1 &&
117
- Math.abs(sc.width - stripWidth) < 1
118
- );
119
-
120
- strips.push({ x, width: stripWidth, smallCoil, isWasteEdge: false });
121
- }
122
-
123
- // 右侧区域:从最后一个竖切切点到母卷右边缘
124
- const lastCutPos = verticalCuts[verticalCuts.length - 1];
125
- const rightAreaWidth = coilWidth - lastCutPos;
126
-
127
- // 判断右侧是废边还是未分配区域
128
- // 如果宽度 <= 参数A(假设 10mm),则认为是右废边
129
- const EDGE_WASTE_THRESHOLD = 15; // 废边宽度阈值
130
- const isRightWaste = rightAreaWidth <= EDGE_WASTE_THRESHOLD;
131
-
132
- if (isRightWaste) {
133
- // 右废边
134
- strips.push({
135
- x: lastCutPos,
136
- width: rightAreaWidth,
137
- isWasteEdge: true,
138
- });
139
- } else {
140
- // 未分配区域(右侧剩余宽度)
141
- strips.push({
142
- x: lastCutPos,
143
- width: rightAreaWidth,
144
- smallCoil: undefined,
145
- isWasteEdge: false,
146
- });
147
- }
148
-
149
- regions.push({
150
- index: i,
151
- startY: currentY,
152
- endY: regionEndY,
153
- length: regionLength,
154
- verticalCuts,
155
- strips,
156
- });
157
-
158
- currentY = regionEndY;
159
-
160
- // 添加切头(除最后一个区域外)
161
- if (i < verticalCutsByRegion.length - 1) {
162
- currentY += cutHeadLength;
163
- }
164
- }
165
-
166
- const totalLength = currentY + tailWaste;
167
-
168
- return {
169
- regions,
170
- horizontalCuts,
171
- headWaste,
172
- tailWaste,
173
- cutHeadLength,
174
- totalLength,
175
- };
176
- }, [plan, coilWidth]);
177
-
178
- // SVG 参数 —— 竖向布局
179
-
180
- // 竖向布局:X 轴 = 母卷宽度方向,Y 轴 = 母卷长度方向
181
- const containerWidth = 800; // viewBox 宽度
182
- const padding = { top: 50, right: 80, bottom: 50, left: 50 };
183
-
184
- // X 方向缩放(宽度方向)
185
- const availableWidth = containerWidth - padding.left - padding.right;
186
- const scaleX = availableWidth / coilWidth;
187
- const drawingWidth = coilWidth * scaleX;
188
-
189
- // Y 方向缩放(长度方向)—— 使用较小的缩放比,保持合理高度
190
- // 目标:总长度映射到约 500px
191
- const targetDrawingHeight = 500;
192
- const scaleY = targetDrawingHeight / visualizationData.totalLength;
193
- const drawingHeight = visualizationData.totalLength * scaleY;
194
-
195
- const svgHeight = drawingHeight + padding.top + padding.bottom;
196
- const offsetX = padding.left;
197
- const offsetY = padding.top;
198
-
199
- // 获取订单列表(用于图例)
200
- const orderList = useMemo(() => {
201
- const orders = new Set<string>();
202
- plan.smallCoils.forEach(sc => {
203
- if (sc.orderId) orders.add(sc.orderId);
204
- });
205
- return Array.from(orders).sort();
206
- }, [plan.smallCoils]);
207
-
208
- // 统计信息
209
- const stats = useMemo(() => {
210
- const assignedCoils = plan.smallCoils.filter(sc => sc.orderId);
211
- const unassignedCoils = plan.smallCoils.filter(sc => !sc.orderId);
212
- const totalWeight = plan.smallCoils.reduce((sum, sc) => sum + sc.weight, 0);
213
- const assignedWeight = assignedCoils.reduce((sum, sc) => sum + sc.weight, 0);
214
-
215
- return {
216
- totalCoils: plan.smallCoils.length,
217
- assignedCoils: assignedCoils.length,
218
- unassignedCoils: unassignedCoils.length,
219
- totalWeight,
220
- assignedWeight,
221
- assignmentRate: totalWeight > 0 ? (assignedWeight / totalWeight * 100) : 0,
222
- };
223
- }, [plan.smallCoils]);
224
-
225
- return (
226
- <Card title={t('visualization')} style={{ marginTop: 16 }}>
227
- <Space direction="vertical" style={{ width: '100%' }} size="large">
228
- {/* 统计信息 */}
229
- <Row gutter={16}>
230
- <Col span={4}>
231
- <Statistic title={t('coilId')} value={plan.coilId} styles={{ content: { fontSize: 16 } }} />
232
- </Col>
233
- <Col span={4}>
234
- <Statistic title={t('coilWidth')} value={coilWidth} suffix="mm" styles={{ content: { fontSize: 16 } }} />
235
- </Col>
236
- <Col span={4}>
237
- <Statistic title={t('coilWeight')} value={plan.coilWeight} suffix="吨" styles={{ content: { fontSize: 16 } }} />
238
- </Col>
239
- <Col span={3}>
240
- <Statistic title={t('totalCuts')} value={plan.totalCuts} suffix="刀" styles={{ content: { fontSize: 16 } }} />
241
- </Col>
242
- <Col span={3}>
243
- <Statistic
244
- title={t('theoreticalUtilization')}
245
- value={plan.theoreticalUtilization}
246
- suffix="%"
247
- precision={2}
248
- styles={{ content: { fontSize: 16, color: '#52c41a' } }}
249
- />
250
- </Col>
251
- <Col span={3}>
252
- <Statistic
253
- title="分配率"
254
- value={stats.assignmentRate}
255
- suffix="%"
256
- precision={1}
257
- styles={{ content: { fontSize: 16, color: stats.assignmentRate >= 90 ? '#52c41a' : '#faad14' } }}
258
- />
259
- </Col>
260
- <Col span={3}>
261
- <Statistic
262
- title="小卷数"
263
- value={stats.totalCoils}
264
- suffix={`(${stats.assignedCoils}/${stats.unassignedCoils})`}
265
- styles={{ content: { fontSize: 16 } }}
266
- />
267
- </Col>
268
- </Row>
269
-
270
- {/* SVG 竖向平面图 */}
271
- <div style={{
272
- width: '96%',
273
- margin: '0 auto',
274
- overflowY: 'auto',
275
- background: '#fafafa',
276
- padding: 16,
277
- borderRadius: 4,
278
- border: '1px solid #f0f0f0',
279
- }}>
280
- <svg
281
- viewBox={`0 0 ${containerWidth} ${svgHeight}`}
282
- width="100%"
283
- style={{ display: 'block' }}
284
- >
285
- {/* 定义箭头标记 */}
286
- <defs>
287
- <marker id="arrowL" markerWidth="8" markerHeight="8" refX="0" refY="3" orient="auto">
288
- <path d="M8,0 L0,3 L8,6" fill="#666" />
289
- </marker>
290
- <marker id="arrowR" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
291
- <path d="M0,0 L8,3 L0,6" fill="#666" />
292
- </marker>
293
- <marker id="arrowU" markerWidth="8" markerHeight="8" refX="3" refY="0" orient="auto">
294
- <path d="M0,8 L3,0 L6,8" fill="#666" />
295
- </marker>
296
- <marker id="arrowD" markerWidth="8" markerHeight="8" refX="3" refY="8" orient="auto">
297
- <path d="M0,0 L3,8 L6,0" fill="#666" />
298
- </marker>
299
- </defs>
300
-
301
- {/* 母卷轮廓 */}
302
- <rect
303
- x={offsetX}
304
- y={offsetY}
305
- width={drawingWidth}
306
- height={drawingHeight}
307
- fill="#fff"
308
- stroke="#333"
309
- strokeWidth={2}
310
- />
311
-
312
- {/* ===== 废头区域(顶部) ===== */}
313
- <rect
314
- x={offsetX}
315
- y={offsetY}
316
- width={drawingWidth}
317
- height={visualizationData.headWaste * scaleY}
318
- fill="#f0f0f0"
319
- stroke="#999"
320
- strokeWidth={1}
321
- strokeDasharray="5,5"
322
- />
323
- <text
324
- x={offsetX + drawingWidth / 2}
325
- y={offsetY + (visualizationData.headWaste * scaleY) / 2 - 5}
326
- textAnchor="middle"
327
- dominantBaseline="middle"
328
- fontSize={10}
329
- fill="#666"
330
- fontWeight="bold"
331
- >
332
- 废头
333
- </text>
334
- <text
335
- x={offsetX + drawingWidth / 2}
336
- y={offsetY + (visualizationData.headWaste * scaleY) / 2 + 8}
337
- textAnchor="middle"
338
- dominantBaseline="middle"
339
- fontSize={9}
340
- fill="#999"
341
- >
342
- {visualizationData.headWaste}mm
343
- </text>
344
-
345
- {/* ===== 废尾区域(底部) ===== */}
346
- <rect
347
- x={offsetX}
348
- y={offsetY + drawingHeight - visualizationData.tailWaste * scaleY}
349
- width={drawingWidth}
350
- height={visualizationData.tailWaste * scaleY}
351
- fill="#f0f0f0"
352
- stroke="#999"
353
- strokeWidth={1}
354
- strokeDasharray="5,5"
355
- />
356
- <text
357
- x={offsetX + drawingWidth / 2}
358
- y={offsetY + drawingHeight - (visualizationData.tailWaste * scaleY) / 2 - 5}
359
- textAnchor="middle"
360
- dominantBaseline="middle"
361
- fontSize={10}
362
- fill="#666"
363
- fontWeight="bold"
364
- >
365
- 废尾
366
- </text>
367
- <text
368
- x={offsetX + drawingWidth / 2}
369
- y={offsetY + drawingHeight - (visualizationData.tailWaste * scaleY) / 2 + 8}
370
- textAnchor="middle"
371
- dominantBaseline="middle"
372
- fontSize={9}
373
- fill="#999"
374
- >
375
- {visualizationData.tailWaste}mm
376
- </text>
377
-
378
- {/* ===== 绘制各区域 ===== */}
379
- {visualizationData.regions.map((region) => {
380
- const regionY = offsetY + region.startY * scaleY;
381
- const regionH = region.length * scaleY;
382
- const bgColor = REGION_BG_COLORS[region.index % REGION_BG_COLORS.length];
383
-
384
- return (
385
- <g key={region.index}>
386
- {/* 区域背景 */}
387
- <rect
388
- x={offsetX}
389
- y={regionY}
390
- width={drawingWidth}
391
- height={regionH}
392
- fill={bgColor}
393
- stroke="none"
394
- />
395
-
396
- {/* 区域标签(左侧) */}
397
- <rect
398
- x={offsetX - 36}
399
- y={regionY + regionH / 2 - 10}
400
- width={30}
401
- height={20}
402
- fill="#1890ff"
403
- rx={3}
404
- />
405
- <text
406
- x={offsetX - 21}
407
- y={regionY + regionH / 2}
408
- textAnchor="middle"
409
- dominantBaseline="middle"
410
- fontSize={11}
411
- fill="#fff"
412
- fontWeight="bold"
413
- >
414
- R{region.index + 1}
415
- </text>
416
-
417
- {/* 区域长度标注(右侧) */}
418
- <text
419
- x={offsetX + drawingWidth + 8}
420
- y={regionY + regionH / 2}
421
- textAnchor="start"
422
- dominantBaseline="middle"
423
- fontSize={9}
424
- fill="#666"
425
- >
426
- {Math.round(region.length)}mm
427
- </text>
428
-
429
- {/* ===== 绘制窄条(沿 X 轴排列) ===== */}
430
- {region.strips.map((strip, stripIndex) => {
431
- const stripX = offsetX + strip.x * scaleX;
432
- const stripW = strip.width * scaleX;
433
-
434
- if (strip.isWasteEdge) {
435
- // 废边(左/右两侧)
436
- return (
437
- <g key={`waste-${stripIndex}`}>
438
- <rect
439
- x={stripX}
440
- y={regionY}
441
- width={stripW}
442
- height={regionH}
443
- fill="#e8e8e8"
444
- stroke="#bbb"
445
- strokeWidth={0.5}
446
- strokeDasharray="3,3"
447
- />
448
- {stripW > 12 && (
449
- <text
450
- x={stripX + stripW / 2}
451
- y={regionY + regionH / 2}
452
- textAnchor="middle"
453
- dominantBaseline="middle"
454
- fontSize={8}
455
- fill="#999"
456
- transform={`rotate(-90, ${stripX + stripW / 2}, ${regionY + regionH / 2})`}
457
- >
458
- 废边 {strip.width}mm
459
- </text>
460
- )}
461
- </g>
462
- );
463
- } else {
464
- // 有效窄条
465
- const hasOrder = !!strip.smallCoil?.orderId;
466
- const color = hasOrder ? getOrderColor(strip.smallCoil!.orderId) : '#91caff';
467
- const showLabel = stripW > 20;
468
- const showDetail = stripW > 40 && regionH > 30;
469
-
470
- return (
471
- <g key={`strip-${stripIndex}`}>
472
- <rect
473
- x={stripX}
474
- y={regionY}
475
- width={stripW}
476
- height={regionH}
477
- fill={color}
478
- fillOpacity={hasOrder ? 0.35 : 0.25}
479
- stroke={color}
480
- strokeWidth={hasOrder ? 1.5 : 1}
481
- strokeDasharray={hasOrder ? 'none' : '4,2'}
482
- />
483
- {/* 窄条宽度标注 */}
484
- {showLabel && (
485
- <text
486
- x={stripX + stripW / 2}
487
- y={regionY + regionH / 2 - (showDetail ? 6 : 0)}
488
- textAnchor="middle"
489
- dominantBaseline="middle"
490
- fontSize={9}
491
- fill="#333"
492
- fontWeight="bold"
493
- >
494
- {strip.width}mm
495
- </text>
496
- )}
497
- {/* 订单标注 */}
498
- {hasOrder && showDetail && (
499
- <text
500
- x={stripX + stripW / 2}
501
- y={regionY + regionH / 2 + 8}
502
- textAnchor="middle"
503
- dominantBaseline="middle"
504
- fontSize={8}
505
- fill="#333"
506
- >
507
- {strip.smallCoil!.orderId}
508
- </text>
509
- )}
510
- </g>
511
- );
512
- }
513
- })}
514
-
515
- {/* ===== 竖切切点标注(沿 X 轴,垂直线) ===== */}
516
- {region.verticalCuts.map((cutPos, cutIndex) => {
517
- const cutX = offsetX + cutPos * scaleX;
518
- return (
519
- <g key={`vcut-${cutIndex}`}>
520
- {/* 竖切线(垂直线) */}
521
- <line
522
- x1={cutX}
523
- y1={regionY}
524
- x2={cutX}
525
- y2={regionY + regionH}
526
- stroke="#1890ff"
527
- strokeWidth={1}
528
- strokeDasharray="4,2"
529
- opacity={0.6}
530
- />
531
- </g>
532
- );
533
- })}
534
-
535
- {/* ===== 横切线(区域分隔线,水平线) ===== */}
536
- {region.index > 0 && (
537
- <g>
538
- <line
539
- x1={offsetX - 6}
540
- y1={regionY}
541
- x2={offsetX + drawingWidth + 6}
542
- y2={regionY}
543
- stroke="#f5222d"
544
- strokeWidth={2.5}
545
- strokeDasharray="6,3"
546
- />
547
- {/* 横切标记 */}
548
- <circle cx={offsetX - 6} cy={regionY} r={3} fill="#f5222d" />
549
- <circle cx={offsetX + drawingWidth + 6} cy={regionY} r={3} fill="#f5222d" />
550
- {/* 横切位置标注 */}
551
- {visualizationData.horizontalCuts[region.index - 1] !== undefined && (
552
- <text
553
- x={offsetX + drawingWidth + 45}
554
- y={regionY}
555
- textAnchor="start"
556
- dominantBaseline="middle"
557
- fontSize={8}
558
- fill="#f5222d"
559
- fontWeight="bold"
560
- >
561
- H{region.index}:{visualizationData.horizontalCuts[region.index - 1]}
562
- </text>
563
- )}
564
- {/* 切头标注 */}
565
- {visualizationData.cutHeadLength > 0 && (
566
- <>
567
- <rect
568
- x={offsetX}
569
- y={regionY - visualizationData.cutHeadLength * scaleY}
570
- width={drawingWidth}
571
- height={visualizationData.cutHeadLength * scaleY}
572
- fill="#fff1f0"
573
- stroke="#ffa39e"
574
- strokeWidth={0.5}
575
- strokeDasharray="3,3"
576
- />
577
- {visualizationData.cutHeadLength * scaleY > 12 && (
578
- <text
579
- x={offsetX + drawingWidth / 2}
580
- y={regionY - (visualizationData.cutHeadLength * scaleY) / 2}
581
- textAnchor="middle"
582
- dominantBaseline="middle"
583
- fontSize={8}
584
- fill="#f5222d"
585
- >
586
- 切头 {visualizationData.cutHeadLength}mm
587
- </text>
588
- )}
589
- </>
590
- )}
591
- </g>
592
- )}
593
- </g>
594
- );
595
- })}
596
-
597
- {/* ===== 宽度尺寸标注(X 轴方向,底部) ===== */}
598
- <line
599
- x1={offsetX}
600
- y1={offsetY + drawingHeight + 25}
601
- x2={offsetX + drawingWidth}
602
- y2={offsetY + drawingHeight + 25}
603
- stroke="#666"
604
- strokeWidth={1.5}
605
- markerStart="url(#arrowL)"
606
- markerEnd="url(#arrowR)"
607
- />
608
- <line
609
- x1={offsetX}
610
- y1={offsetY + drawingHeight + 20}
611
- x2={offsetX}
612
- y2={offsetY + drawingHeight + 30}
613
- stroke="#666"
614
- strokeWidth={1.5}
615
- />
616
- <line
617
- x1={offsetX + drawingWidth}
618
- y1={offsetY + drawingHeight + 20}
619
- x2={offsetX + drawingWidth}
620
- y2={offsetY + drawingHeight + 30}
621
- stroke="#666"
622
- strokeWidth={1.5}
623
- />
624
- <text
625
- x={offsetX + drawingWidth / 2}
626
- y={offsetY + drawingHeight + 38}
627
- textAnchor="middle"
628
- fontSize={11}
629
- fill="#333"
630
- fontWeight="bold"
631
- >
632
- 宽度 {coilWidth} mm
633
- </text>
634
-
635
- {/* ===== 长度尺寸标注(Y 轴方向,右侧) ===== */}
636
- <line
637
- x1={offsetX + drawingWidth + 20}
638
- y1={offsetY}
639
- x2={offsetX + drawingWidth + 20}
640
- y2={offsetY + drawingHeight}
641
- stroke="#666"
642
- strokeWidth={1.5}
643
- markerStart="url(#arrowU)"
644
- markerEnd="url(#arrowD)"
645
- />
646
- <line
647
- x1={offsetX + drawingWidth + 15}
648
- y1={offsetY}
649
- x2={offsetX + drawingWidth + 25}
650
- y2={offsetY}
651
- stroke="#666"
652
- strokeWidth={1.5}
653
- />
654
- <line
655
- x1={offsetX + drawingWidth + 15}
656
- y1={offsetY + drawingHeight}
657
- x2={offsetX + drawingWidth + 25}
658
- y2={offsetY + drawingHeight}
659
- stroke="#666"
660
- strokeWidth={1.5}
661
- />
662
- <text
663
- x={offsetX + drawingWidth + 35}
664
- y={offsetY + drawingHeight / 2}
665
- textAnchor="middle"
666
- dominantBaseline="middle"
667
- fontSize={11}
668
- fill="#333"
669
- fontWeight="bold"
670
- transform={`rotate(90, ${offsetX + drawingWidth + 35}, ${offsetY + drawingHeight / 2})`}
671
- >
672
- 长度 {Math.round(visualizationData.totalLength)} mm
673
- </text>
674
-
675
- {/* ===== 方向标注 ===== */}
676
- <text
677
- x={offsetX + drawingWidth / 2}
678
- y={offsetY - 15}
679
- textAnchor="middle"
680
- fontSize={10}
681
- fill="#999"
682
- >
683
- ← 母卷宽度方向 →
684
- </text>
685
- <text
686
- x={offsetX + drawingWidth + 55}
687
- y={offsetY + drawingHeight / 2}
688
- textAnchor="middle"
689
- fontSize={10}
690
- fill="#999"
691
- transform={`rotate(90, ${offsetX + drawingWidth + 55}, ${offsetY + drawingHeight / 2})`}
692
- >
693
- ← 母卷长度方向 →
694
- </text>
695
- </svg>
696
- </div>
697
-
698
- {/* 图例 */}
699
- <Card size="small" title="图例">
700
- <Row gutter={[16, 8]}>
701
- <Col span={6}>
702
- <Space size={4}>
703
- <svg width={24} height={14}>
704
- <line x1={0} y1={7} x2={24} y2={7} stroke="#f5222d" strokeWidth={2.5} strokeDasharray="6,3" />
705
- </svg>
706
- <Text>横切线(区域分隔)</Text>
707
- </Space>
708
- </Col>
709
- <Col span={6}>
710
- <Space size={4}>
711
- <svg width={24} height={14}>
712
- <line x1={12} y1={0} x2={12} y2={14} stroke="#1890ff" strokeWidth={1} strokeDasharray="4,2" />
713
- </svg>
714
- <Text>竖切线(窄条分隔)</Text>
715
- </Space>
716
- </Col>
717
- <Col span={6}>
718
- <Space size={4}>
719
- <div style={{ width: 24, height: 14, background: '#e8e8e8', border: '1px dashed #999' }} />
720
- <Text>废边</Text>
721
- </Space>
722
- </Col>
723
- <Col span={6}>
724
- <Space size={4}>
725
- <div style={{ width: 24, height: 14, background: '#f0f0f0', border: '1px dashed #999' }} />
726
- <Text>废头 / 废尾</Text>
727
- </Space>
728
- </Col>
729
- <Col span={6}>
730
- <Space size={4}>
731
- <div style={{ width: 24, height: 14, background: '#fff1f0', border: '1px dashed #ffa39e' }} />
732
- <Text>切头</Text>
733
- </Space>
734
- </Col>
735
- {orderList.map(orderId => (
736
- <Col span={6} key={orderId}>
737
- <Space size={4}>
738
- <div
739
- style={{
740
- width: 24,
741
- height: 14,
742
- background: getOrderColor(orderId),
743
- opacity: 0.35,
744
- border: `1.5px solid ${getOrderColor(orderId)}`,
745
- }}
746
- />
747
- <Text>{orderId}</Text>
748
- </Space>
749
- </Col>
750
- ))}
751
- </Row>
752
- </Card>
753
-
754
- {/* 订单分配详情 */}
755
- <Card size="small" title="订单分配详情">
756
- <Space wrap>
757
- {orderList.map(orderId => {
758
- const orderCoils = plan.smallCoils.filter(sc => sc.orderId === orderId);
759
- const totalWeight = orderCoils.reduce((sum, sc) => sum + sc.weight, 0);
760
- const regions = Array.from(new Set(orderCoils.map(sc => sc.region))).sort();
761
- return (
762
- <Tag key={orderId} color={getOrderColor(orderId)} style={{ padding: '4px 8px' }}>
763
- <Space size={4}>
764
- <span style={{ fontWeight: 'bold' }}>{orderId}</span>
765
- <span>{orderCoils.length}条</span>
766
- <span>{totalWeight.toFixed(2)}吨</span>
767
- <span style={{ fontSize: 11, color: '#666' }}>
768
- (R{regions.join(',R')})
769
- </span>
770
- </Space>
771
- </Tag>
772
- );
773
- })}
774
- {stats.unassignedCoils > 0 && (
775
- <Tag style={{ padding: '4px 8px' }}>
776
- <Space size={4}>
777
- <span style={{ fontWeight: 'bold' }}>未分配</span>
778
- <span>{stats.unassignedCoils}条</span>
779
- <span>
780
- {plan.smallCoils.filter(sc => !sc.orderId).reduce((sum, sc) => sum + sc.weight, 0).toFixed(2)}吨
781
- </span>
782
- </Space>
783
- </Tag>
784
- )}
785
- </Space>
786
- </Card>
787
-
788
- {/* 切点汇总 */}
789
- <Row gutter={16}>
790
- <Col span={12}>
791
- <Card size="small" title={`竖切切点 (共 ${plan.verticalCuts.split(';').reduce((sum, s) => sum + s.split(',').length, 0)} 个)`}>
792
- <Space wrap size={[8, 8]}>
793
- {visualizationData.regions.map((region, idx) => (
794
- <Tag key={idx} color="blue">
795
- R{idx + 1}: [{region.verticalCuts.join(', ')}]
796
- </Tag>
797
- ))}
798
- </Space>
799
- </Card>
800
- </Col>
801
- <Col span={12}>
802
- <Card size="small" title={`横切切点 (共 ${visualizationData.horizontalCuts.length} 个)`}>
803
- <Space wrap size={[8, 8]}>
804
- {visualizationData.horizontalCuts.map((cut, idx) => (
805
- <Tag key={idx} color="red">
806
- H{idx + 1}: {cut}mm
807
- </Tag>
808
- ))}
809
- {visualizationData.horizontalCuts.length === 0 && (
810
- <Text type="secondary">无横切</Text>
811
- )}
812
- </Space>
813
- </Card>
814
- </Col>
815
- </Row>
816
- </Space>
817
- </Card>
818
- );
819
- };
820
-
821
- export default CuttingPlanVisualization;