@me-framework/me-sku-editor 1.0.1

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.
@@ -0,0 +1,1028 @@
1
+ # SkuEditor 使用文档
2
+
3
+ ## 目录
4
+
5
+ - [组件简介](#组件简介)
6
+ - [功能特性](#功能特性)
7
+ - [快速开始](#快速开始)
8
+ - [组件 API](#组件-api)
9
+ - [数据格式](#数据格式)
10
+ - [工具函数](#工具函数)
11
+ - [数据校验](#数据校验)
12
+ - [高级用法](#高级用法)
13
+ - [常见问题](#常见问题)
14
+
15
+ ---
16
+
17
+ ## 组件简介
18
+
19
+ SkuEditor 是一个功能强大的 Vue 3 商品规格 SKU 编辑器组件,支持多规格管理、智能 SKU 组合生成、数据导入导出等功能。
20
+
21
+ ### 核心特点
22
+
23
+ - 支持内部数据管理和外部数据控制两种模式
24
+ - 自动生成 SKU 组合并保留已有数据
25
+ - 支持 Excel 和 JSON 格式的导入导出
26
+ - 拖拽排序、批量编辑、列可见性自定义
27
+ - 内置完整的数据校验系统
28
+
29
+ ---
30
+
31
+ ## 功能特性
32
+
33
+ ### 1. 规格管理
34
+
35
+ - 支持三种规格类型:**文字**、**颜色**、**图片**
36
+ - 规格可拖拽排序
37
+ - 支持规格的增删改操作
38
+ - 每个规格可包含多个属性值
39
+
40
+ ### 2. 属性管理
41
+
42
+ - 属性支持名称、颜色值、图片值、扩展内容
43
+ - 属性可拖拽排序
44
+ - 颜色类型规格可直接选择颜色
45
+ - 图片类型规格支持图片预览
46
+
47
+ ### 3. SKU 表格
48
+
49
+ - 自动生成所有规格组合的 SKU
50
+ - 智能单元格合并显示
51
+ - 支持列筛选和自定义列可见性
52
+ - 支持单元格直接编辑
53
+ - 虚拟滚动支持大量数据
54
+ - 行高亮配置支持
55
+
56
+ ### 4. 数据导入导出
57
+
58
+ - 导出为 Excel 文件(含规格信息和 SKU 数据两个 Sheet)
59
+ - 导出为 JSON 文件(完整数据格式)
60
+ - 从 Excel 导入数据
61
+ - 从 JSON 导入数据
62
+ - 支持拖拽文件导入
63
+
64
+ ### 5. 批量编辑
65
+
66
+ - 统一设置多个 SKU 的属性
67
+ - 支持设置:销售价、市场价、成本价、重量、体积、状态、步长、起购数、限购数
68
+
69
+ ### 6. 数据校验
70
+
71
+ - **规格校验**:校验规格数据完整性
72
+ - **SKU 编号重复校验**:编辑时自动校验重复
73
+ - **必填字段校验**:校验关键字段
74
+ - **数值合理性校验**:校验数值范围
75
+ - **自定义校验**:支持自定义校验函数
76
+ - **错误和警告分级**:支持 error 和 warning 两种级别
77
+
78
+ ---
79
+
80
+ ## 快速开始
81
+
82
+ ### 安装依赖
83
+
84
+ ```bash
85
+ npm install
86
+ ```
87
+
88
+ ### 基础用法 - 内部管理模式
89
+
90
+ 组件内部自动管理数据,适合简单场景:
91
+
92
+ ```vue
93
+ <template>
94
+ <SkuEditor />
95
+ </template>
96
+
97
+ <script setup lang="ts">
98
+ import SkuEditor from './components/sku/SkuEditor.vue'
99
+ </script>
100
+ ```
101
+
102
+ ### 进阶用法 - 外部控制模式
103
+
104
+ 父组件控制数据,适合需要自定义数据处理的场景:
105
+
106
+ ```vue
107
+ <template>
108
+ <div>
109
+ <div class="demo-info">
110
+ <p>当前规格数量:{{ specs.length }}</p>
111
+ <p>当前SKU数量:{{ skuData.length }}</p>
112
+ </div>
113
+ <SkuEditor
114
+ v-model:specs="specs"
115
+ v-model:sku-data="skuData"
116
+ />
117
+ </div>
118
+ </template>
119
+
120
+ <script setup lang="ts">
121
+ import { ref } from 'vue'
122
+ import SkuEditor from './components/sku/SkuEditor.vue'
123
+ import type { Spec, SkuRow } from './types/sku'
124
+ import { generateSkuData, toInternalFormat } from './composables/sku/useSkuGenerator'
125
+
126
+ // 初始化规格数据
127
+ const specs = ref<Spec[]>([
128
+ {
129
+ id: 'spec-1',
130
+ name: '颜色',
131
+ type: 'color',
132
+ sort: 0,
133
+ attributes: [
134
+ { id: 'attr-1-1', name: '红色', color: '#ff0000', image: '', sort: 0, content: '' },
135
+ { id: 'attr-1-2', name: '蓝色', color: '#0000ff', image: '', sort: 1, content: '' }
136
+ ]
137
+ },
138
+ {
139
+ id: 'spec-2',
140
+ name: '尺码',
141
+ type: 'text',
142
+ sort: 1,
143
+ attributes: [
144
+ { id: 'attr-2-1', name: 'S', color: '', image: '', sort: 0, content: '' },
145
+ { id: 'attr-2-2', name: 'M', color: '', image: '', sort: 1, content: '' },
146
+ { id: 'attr-2-3', name: 'L', color: '', image: '', sort: 2, content: '' }
147
+ ]
148
+ }
149
+ ])
150
+
151
+ // 生成初始 SKU 数据
152
+ const skuData = ref<SkuRow[]>(generateSkuData(specs.value))
153
+ </script>
154
+ ```
155
+
156
+ ### 使用外部格式数据
157
+
158
+ 推荐使用 `spec_json` 格式(规格名称和属性名称),组件内部会自动转换:
159
+
160
+ ```vue
161
+ <template>
162
+ <SkuEditor
163
+ v-model:specs="specs"
164
+ v-model:sku-data="skuData"
165
+ />
166
+ </template>
167
+
168
+ <script setup lang="ts">
169
+ import { ref } from 'vue'
170
+ import SkuEditor from './components/sku/SkuEditor.vue'
171
+ import type { Spec, SkuRow } from './types/sku'
172
+ import { generateSkuData, toInternalFormat } from './composables/sku/useSkuGenerator'
173
+
174
+ // 规格数据
175
+ const specs = ref<Spec[]>([
176
+ {
177
+ id: 'spec-1',
178
+ name: '颜色',
179
+ type: 'color',
180
+ sort: 0,
181
+ attributes: [
182
+ { id: 'attr-1-1', name: '冠军金', color: '#FFD700', image: '', sort: 0, content: '' },
183
+ { id: 'attr-1-2', name: '荣耀红', color: '#DC143C', image: '', sort: 1, content: '' },
184
+ { id: 'attr-1-3', name: '星空蓝', color: '#1E90FF', image: '', sort: 2, content: '' }
185
+ ]
186
+ },
187
+ {
188
+ id: 'spec-2',
189
+ name: '尺码',
190
+ type: 'text',
191
+ sort: 1,
192
+ attributes: [
193
+ { id: 'attr-2-1', name: 'S', color: '', image: '', sort: 0, content: '' },
194
+ { id: 'attr-2-2', name: 'M', color: '', image: '', sort: 1, content: '' },
195
+ { id: 'attr-2-3', name: 'L', color: '', image: '', sort: 2, content: '' }
196
+ ]
197
+ },
198
+ {
199
+ id: 'spec-3',
200
+ name: '款式',
201
+ type: 'text',
202
+ sort: 2,
203
+ attributes: [
204
+ { id: 'attr-3-1', name: '球迷版', color: '', image: '', sort: 0, content: '' },
205
+ { id: 'attr-3-2', name: '球员版', color: '', image: '', sort: 1, content: '' }
206
+ ]
207
+ }
208
+ ])
209
+
210
+ // 使用 spec_json 格式的 SKU 数据(使用名称而非 ID,推荐用于存储)
211
+ const externalSkus: SkuRow[] = [
212
+ {
213
+ spec_json: { '颜色': '冠军金', '尺码': 'S', '款式': '球迷版' },
214
+ skuNo: 'WC2026-JER-GOLD-S-FAN',
215
+ skuName: '2026世界杯球衣 冠军金 S码 球迷版',
216
+ salePrice: 599,
217
+ marketPrice: 799,
218
+ costPrice: 299,
219
+ weight: 0.3,
220
+ status: 1,
221
+ sort: 1,
222
+ step: 1,
223
+ minBuyLimit: 1,
224
+ maxBuyLimit: 10
225
+ },
226
+ {
227
+ spec_json: { '颜色': '荣耀红', '尺码': 'M', '款式': '球员版' },
228
+ skuNo: 'WC2026-JER-RED-M-PRO',
229
+ skuName: '2026世界杯球衣 荣耀红 M码 球员版',
230
+ salePrice: 899,
231
+ marketPrice: 1199,
232
+ costPrice: 499,
233
+ weight: 0.25,
234
+ status: 1,
235
+ sort: 2,
236
+ step: 1,
237
+ minBuyLimit: 1,
238
+ maxBuyLimit: 5
239
+ }
240
+ ]
241
+
242
+ // 转换为内部格式并生成完整 SKU 数据
243
+ const internalSkus = toInternalFormat(externalSkus, specs.value)
244
+ const skuData = ref<SkuRow[]>(generateSkuData(specs.value, internalSkus))
245
+ </script>
246
+ ```
247
+
248
+ ---
249
+
250
+ ## 组件 API
251
+
252
+ ### SkuEditor Props
253
+
254
+ | 参数 | 说明 | 类型 | 默认值 |
255
+ |------|------|------|--------|
256
+ | specs | 规格列表(外部数据模式时使用) | `Spec[]` | - |
257
+ | skuData | SKU 数据列表(外部数据模式时使用,推荐使用 spec_json 格式) | `SkuRow[]` | - |
258
+ | validationRules | 校验规则配置 | `ValidationRules` | - |
259
+
260
+ ### SkuEditor Events
261
+
262
+ | 事件名 | 说明 | 回调参数 |
263
+ |--------|------|----------|
264
+ | update:specs | 规格数据更新时触发 | `(value: Spec[]) => void` |
265
+ | update:skuData | SKU 数据更新时触发 | `(value: SkuRow[]) => void` |
266
+
267
+ ### SkuEditor Exposed Methods
268
+
269
+ 通过 `ref` 可以调用组件的以下方法:
270
+
271
+ | 方法名 | 说明 | 参数 |
272
+ |--------|------|------|
273
+ | exportExcel | 导出 Excel 文件 | - |
274
+ | exportJson | 导出 JSON 文件 | - |
275
+ | confirmImport | 确认导入(会弹出文件选择器) | `(type: 'json' \| 'excel') => void` |
276
+ | confirmImportExcel | 确认导入 Excel | - |
277
+ | validateSkuData | 校验所有 SKU 数据 | `(rules?: ValidationRules) => ValidationResult` |
278
+
279
+ #### 示例 - 导出数据
280
+
281
+ ```vue
282
+ <template>
283
+ <div>
284
+ <SkuEditor ref="skuEditorRef" />
285
+ <button @click="exportData">导出数据</button>
286
+ </div>
287
+ </template>
288
+
289
+ <script setup lang="ts">
290
+ import { ref } from 'vue'
291
+ import SkuEditor from './components/sku/SkuEditor.vue'
292
+
293
+ const skuEditorRef = ref<InstanceType<typeof SkuEditor>>()
294
+
295
+ const exportData = () => {
296
+ skuEditorRef.value?.exportExcel()
297
+ }
298
+ </script>
299
+ ```
300
+
301
+ #### 示例 - 数据校验
302
+
303
+ ```vue
304
+ <template>
305
+ <div>
306
+ <SkuEditor
307
+ ref="skuEditorRef"
308
+ v-model:specs="specs"
309
+ v-model:sku-data="skuData"
310
+ :validation-rules="validationRules"
311
+ />
312
+ <div class="actions">
313
+ <button @click="validateAndSave" type="primary">保存</button>
314
+ <button @click="quickValidate">快速校验</button>
315
+ </div>
316
+ <div v-if="validationResults.length > 0" class="validation-results">
317
+ <h3>校验结果:</h3>
318
+ <div
319
+ v-for="(item, index) in validationResults"
320
+ :key="index"
321
+ class="validation-item"
322
+ :class="item.type"
323
+ >
324
+ <span>{{ item.message }}</span>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ </template>
329
+
330
+ <script setup lang="ts">
331
+ import { ref } from 'vue'
332
+ import { ElMessage } from 'element-plus'
333
+ import { CircleCheck, CircleClose, Warning } from '@element-plus/icons-vue'
334
+ import SkuEditor from './components/sku/SkuEditor.vue'
335
+ import type { Spec, SkuRow, ValidationItem, ValidationRules } from './types/sku'
336
+
337
+ const skuEditorRef = ref<InstanceType<typeof SkuEditor>>()
338
+ const specs = ref<Spec[]>([])
339
+ const skuData = ref<SkuRow[]>([])
340
+ const validationResults = ref<ValidationItem[]>([])
341
+
342
+ // 配置校验规则
343
+ const validationRules: ValidationRules = {
344
+ specs: true,
345
+ skuNoDuplication: true,
346
+ requiredFields: true,
347
+ numericValues: true,
348
+ custom: (_specs: Spec[], skuData: SkuRow[]) => {
349
+ const customItems: ValidationItem[] = []
350
+ skuData.forEach((row: SkuRow, index: number) => {
351
+ if (row.salePrice && row.costPrice && row.costPrice > row.salePrice * 0.9) {
352
+ customItems.push({
353
+ type: 'warning',
354
+ message: `第${index + 1}行:成本价接近销售价,利润空间较小`
355
+ })
356
+ }
357
+ })
358
+ return customItems
359
+ }
360
+ }
361
+
362
+ // 完整校验并保存
363
+ const validateAndSave = () => {
364
+ const result = skuEditorRef.value?.validateSkuData()
365
+
366
+ if (!result) {
367
+ ElMessage.error('无法获取校验结果')
368
+ return
369
+ }
370
+
371
+ validationResults.value = result.items
372
+
373
+ if (!result.valid) {
374
+ const errorCount = result.items.filter((item: ValidationItem) => item.type === 'error').length
375
+ const warningCount = result.items.filter((item: ValidationItem) => item.type === 'warning').length
376
+ ElMessage.error(`校验发现${errorCount}个错误${warningCount > 0 ? `,${warningCount}个警告` : ''}`)
377
+ return
378
+ }
379
+
380
+ if (result.items.length > 0) {
381
+ ElMessage.warning(`校验通过,但有${result.items.length}个警告`)
382
+ } else {
383
+ ElMessage.success('数据校验通过')
384
+ }
385
+
386
+ // 校验通过,执行保存逻辑
387
+ saveToBackend()
388
+ }
389
+
390
+ // 快速校验(不校验数值范围)
391
+ const quickValidate = () => {
392
+ const result = skuEditorRef.value?.validateSkuData({
393
+ specs: true,
394
+ skuNoDuplication: true,
395
+ requiredFields: true,
396
+ numericValues: false
397
+ })
398
+
399
+ if (!result) return
400
+
401
+ validationResults.value = result.items
402
+
403
+ if (result.valid) {
404
+ ElMessage.success('快速校验通过')
405
+ } else {
406
+ ElMessage.error('快速校验发现问题')
407
+ }
408
+ }
409
+
410
+ // 保存到后端
411
+ const saveToBackend = () => {
412
+ // 在这里实现保存逻辑
413
+ ElMessage.success('保存成功')
414
+ }
415
+ </script>
416
+
417
+ <style scoped>
418
+ .actions {
419
+ margin: 16px 0;
420
+ gap: 12px;
421
+ display: flex;
422
+ }
423
+
424
+ .validation-results {
425
+ margin-top: 16px;
426
+ }
427
+
428
+ .validation-item {
429
+ padding: 12px;
430
+ border-radius: 4px;
431
+ margin-bottom: 8px;
432
+ }
433
+
434
+ .validation-item.error {
435
+ background-color: #fef0f0;
436
+ border: 1px solid #fde2e2;
437
+ color: #f56c6c;
438
+ }
439
+
440
+ .validation-item.warning {
441
+ background-color: #fdf6ec;
442
+ border: 1px solid #faecd8;
443
+ color: #e6a23c;
444
+ }
445
+ </style>
446
+ ```
447
+
448
+ ---
449
+
450
+ ## 数据格式
451
+
452
+ ### 类型定义位置
453
+
454
+ - 类型定义统一在 `src/types/sku/` 目录下
455
+ - 主入口:`src/types/sku/index.ts`
456
+ - 规格类型:`src/types/sku/spec.ts`
457
+ - SKU 行类型:`src/types/sku/sku-row.ts`
458
+
459
+ ### Spec(规格)
460
+
461
+ ```typescript
462
+ interface Spec {
463
+ id: string // 规格唯一标识
464
+ name: string // 规格名称,如"颜色"、"尺码"
465
+ type: SpecType // 规格类型:'text' | 'color' | 'image'
466
+ sort: number // 排序序号
467
+ attributes: SpecAttribute[] // 属性列表
468
+ }
469
+ ```
470
+
471
+ ### SpecAttribute(规格属性)
472
+
473
+ ```typescript
474
+ interface SpecAttribute {
475
+ id: string // 属性唯一标识
476
+ name: string // 属性名称,如"红色"、"S"
477
+ color: string // 颜色值(颜色类型规格使用),如"#ff0000"
478
+ image: string // 图片 URL(图片类型规格使用)
479
+ sort: number // 排序序号
480
+ content: AttributeValue | string // 扩展内容,可以是字符串或对象
481
+ }
482
+ ```
483
+
484
+ ### SkuRow(SKU 行数据)
485
+
486
+ ```typescript
487
+ interface SkuRow {
488
+ id?: string
489
+ // 内部格式:规格 ID → 属性 ID(组件内部使用)
490
+ specs?: { [specId: string]: string }
491
+ // 外部格式:规格名称 → 属性名称(推荐用于存储和展示)
492
+ spec_json?: { [specName: string]: string }
493
+ skuNo?: string
494
+ skuName?: string
495
+ image?: string
496
+ salePrice?: number
497
+ marketPrice?: number
498
+ costPrice?: number
499
+ weight?: number
500
+ volume?: number
501
+ barcode?: string
502
+ status?: number
503
+ sort?: number
504
+ step?: number
505
+ minBuyLimit?: number
506
+ maxBuyLimit?: number
507
+ _X_ROW_KEY?: string
508
+ }
509
+ ```
510
+
511
+ **数据格式说明**:
512
+
513
+ - **外部格式(推荐)**:使用 `spec_json` 字段,键是规格名称,值是属性名称,适合存储和后端交互
514
+ - **内部格式**:使用 `specs` 字段,键是规格 ID,值是属性 ID,组件内部使用
515
+ - 组件会自动在两种格式之间转换,传入和传出都使用外部格式
516
+
517
+ ### ValidationRules(校验规则)
518
+
519
+ ```typescript
520
+ interface ValidationRules {
521
+ /** 是否校验规格数据 */
522
+ specs?: boolean
523
+ /** 是否校验SKU编号重复 */
524
+ skuNoDuplication?: boolean
525
+ /** 是否校验必填字段 */
526
+ requiredFields?: boolean
527
+ /** 是否校验数值合理性 */
528
+ numericValues?: boolean
529
+ /** 自定义校验函数 */
530
+ custom?: (specs: any[], skuData: any[]) => ValidationItem[]
531
+ }
532
+ ```
533
+
534
+ ### ValidationResult(校验结果)
535
+
536
+ ```typescript
537
+ interface ValidationResult {
538
+ valid: boolean
539
+ items: ValidationItem[]
540
+ }
541
+
542
+ interface ValidationItem {
543
+ type: 'error' | 'warning'
544
+ message: string
545
+ }
546
+ ```
547
+
548
+ ---
549
+
550
+ ## 工具函数
551
+
552
+ ### useSkuGenerator 相关函数
553
+
554
+ 位置:`src/composables/sku/useSkuGenerator.ts`
555
+
556
+ #### generateId()
557
+
558
+ 生成 UUID v4 格式的唯一 ID。
559
+
560
+ ```typescript
561
+ import { generateId } from './composables/sku/useSkuGenerator'
562
+
563
+ const id = generateId()
564
+ // 输出类似:"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
565
+ ```
566
+
567
+ #### generateSkuData(specs, existingData?)
568
+
569
+ 根据规格生成 SKU 组合数据,可保留已有数据。
570
+
571
+ ```typescript
572
+ import { generateSkuData } from './composables/sku/useSkuGenerator'
573
+
574
+ // 仅传入规格,生成全新的 SKU 数据
575
+ const skuData = generateSkuData(specs)
576
+
577
+ // 传入已有数据,保留匹配的 SKU 信息
578
+ const skuData = generateSkuData(specs, existingData)
579
+ ```
580
+
581
+ **参数**:
582
+ - `specs: Spec[]` - 规格列表
583
+ - `existingData?: SkuRow[]` - 已有 SKU 数据(可选)
584
+
585
+ **返回值**:
586
+ - `SkuRow[]` - 生成的完整 SKU 数据
587
+
588
+ #### toInternalFormat(externalSkus, specs)
589
+
590
+ 将外部格式(名称)转换为内部格式(ID)。
591
+
592
+ ```typescript
593
+ import { toInternalFormat } from './composables/sku/useSkuGenerator'
594
+
595
+ const internalSkus = toInternalFormat(externalSkus, specs)
596
+ ```
597
+
598
+ **参数**:
599
+ - `externalSkus: SkuRow[]` - 使用 spec_json 格式的 SKU 列表
600
+ - `specs: Spec[]` - 规格列表
601
+
602
+ **返回值**:
603
+ - `SkuRow[]` - 转换后的内部格式 SKU 列表
604
+
605
+ #### toExternalFormat(internalSkus, specs)
606
+
607
+ 将内部格式(ID)转换为外部格式(名称)。
608
+
609
+ ```typescript
610
+ import { toExternalFormat } from './composables/sku/useSkuGenerator'
611
+
612
+ const externalSkus = toExternalFormat(internalSkus, specs)
613
+ ```
614
+
615
+ **参数**:
616
+ - `internalSkus: SkuRow[]` - 内部格式 SKU 列表
617
+ - `specs: Spec[]` - 规格列表
618
+
619
+ **返回值**:
620
+ - `SkuRow[]` - 转换后的外部格式 SKU 列表(含 spec_json)
621
+
622
+ **兼容性别名**(仍可用,推荐使用新名称):
623
+ - `convertCompatibleToInternal` = `toInternalFormat`
624
+ - `convertInternalToCompatible` = `toExternalFormat`
625
+
626
+ ### useImportExport 相关函数
627
+
628
+ 位置:`src/composables/sku/useImportExport.ts`
629
+
630
+ 通过 `useImportExport` 可以获取导入导出功能:
631
+
632
+ ```typescript
633
+ import { useImportExport } from './composables/sku/useImportExport'
634
+
635
+ const {
636
+ exportExcel, // 导出 Excel
637
+ exportJson, // 导出 JSON
638
+ confirmImport, // 确认导入
639
+ handleDropFile // 处理拖拽文件
640
+ } = useImportExport(specs, skuData, skuGridRef, onDataUpdate)
641
+ ```
642
+
643
+ ### useColumnVisibility
644
+
645
+ 位置:`src/composables/sku/useColumnVisibility.ts`
646
+
647
+ 管理表格列的可见性状态:
648
+
649
+ ```typescript
650
+ import { useColumnVisibility, defaultColumnVisibility } from './composables/sku/useColumnVisibility'
651
+
652
+ const { columnVisibility, saveColumnVisibility } = useColumnVisibility(specs, skuGridRef)
653
+ ```
654
+
655
+ ### useMergeCells
656
+
657
+ 位置:`src/composables/sku/useMergeCells.ts`
658
+
659
+ 提供 SKU 表格的单元格合并功能:
660
+
661
+ ```typescript
662
+ import { useMergeCells } from './composables/sku/useMergeCells'
663
+
664
+ const { spanMethod } = useMergeCells(specs)
665
+ ```
666
+
667
+ ### useSortable
668
+
669
+ 位置:`src/composables/sku/useSortable.ts`
670
+
671
+ 提供规格和属性的拖拽排序功能:
672
+
673
+ ```typescript
674
+ import { useSortable } from './composables/sku/useSortable'
675
+
676
+ const { initSpecSortable, initAttrSortable } = useSortable(
677
+ specs,
678
+ specListRef,
679
+ attrListRefs,
680
+ (newSpecs) => {
681
+ // 规格变化时的回调
682
+ console.log('Specs updated:', newSpecs)
683
+ }
684
+ )
685
+ ```
686
+
687
+ ### useValidation
688
+
689
+ 位置:`src/composables/sku/useValidation.ts`
690
+
691
+ 提供数据校验功能:
692
+
693
+ ```typescript
694
+ import { useValidation } from './composables/sku/useValidation'
695
+
696
+ const { validateSkuData } = useValidation()
697
+
698
+ const result = validateSkuData(specs, skuData, rules)
699
+ console.log(result.valid) // 是否通过
700
+ console.log(result.items) // 校验结果列表
701
+ ```
702
+
703
+ ---
704
+
705
+ ## 数据校验
706
+
707
+ ### 内置校验项
708
+
709
+ 1. **规格校验**(`specs`)
710
+ - 检查规格是否有名称
711
+ - 检查规格的属性是否有名称
712
+
713
+ 2. **SKU 编号重复校验**(`skuNoDuplication`)
714
+ - 检查非空的 SKU 编号是否重复
715
+
716
+ 3. **必填字段校验**(`requiredFields`)
717
+ - 检查 SKU 名称是否为空
718
+
719
+ 4. **数值合理性校验**(`numericValues`)
720
+ - 检查销售价是否 ≥ 0
721
+ - 检查市场价是否 ≥ 0
722
+ - 检查成本价是否 ≥ 0
723
+ - 检查重量是否 ≥ 0
724
+ - 检查体积是否 ≥ 0
725
+ - 检查步长是否 ≥ 1
726
+ - 检查起购数是否 ≥ 1
727
+ - 检查限购数是否 ≥ 0
728
+
729
+ ### 自定义校验
730
+
731
+ 通过 `custom` 函数添加自定义校验:
732
+
733
+ ```typescript
734
+ const validationRules: ValidationRules = {
735
+ custom: (specs: Spec[], skuData: SkuRow[]) => {
736
+ const items: ValidationItem[] = []
737
+
738
+ skuData.forEach((row, index) => {
739
+ // 检查利润空间
740
+ if (row.salePrice && row.costPrice) {
741
+ const profit = row.salePrice - row.costPrice
742
+ if (profit < 10) {
743
+ items.push({
744
+ type: 'warning',
745
+ message: `第${index + 1}行:利润空间较小(${profit}元)`
746
+ })
747
+ }
748
+ }
749
+
750
+ // 检查状态
751
+ if (row.status !== 1 && row.salePrice > 0) {
752
+ items.push({
753
+ type: 'warning',
754
+ message: `第${index + 1}行:SKU已下架但设置了销售价`
755
+ })
756
+ }
757
+ })
758
+
759
+ return items
760
+ }
761
+ }
762
+ ```
763
+
764
+ ---
765
+
766
+ ## 高级用法
767
+
768
+ ### 监听数据变化
769
+
770
+ ```vue
771
+ <script setup lang="ts">
772
+ import { ref, watch } from 'vue'
773
+ import SkuEditor from './components/sku/SkuEditor.vue'
774
+ import type { Spec, SkuRow } from './types/sku'
775
+
776
+ const specs = ref<Spec[]>([])
777
+ const skuData = ref<SkuRow[]>([])
778
+
779
+ // 监听规格变化
780
+ watch(specs, (newSpecs) => {
781
+ console.log('规格数据已更新:', newSpecs)
782
+ // 可以在这里保存到后端
783
+ }, { deep: true })
784
+
785
+ // 监听 SKU 数据变化
786
+ watch(skuData, (newSkuData) => {
787
+ console.log('SKU 数据已更新:', newSkuData)
788
+ // 可以在这里保存到后端
789
+ }, { deep: true })
790
+ </script>
791
+
792
+ <template>
793
+ <SkuEditor
794
+ v-model:specs="specs"
795
+ v-model:sku-data="skuData"
796
+ />
797
+ </template>
798
+ ```
799
+
800
+ ### 保存到后端示例
801
+
802
+ ```vue
803
+ <script setup lang="ts">
804
+ import { ref, watch } from 'vue'
805
+ import { ElMessage } from 'element-plus'
806
+ import SkuEditor from './components/sku/SkuEditor.vue'
807
+ import type { Spec, SkuRow } from './types/sku'
808
+ import { generateSkuData, toExternalFormat } from './composables/sku/useSkuGenerator'
809
+
810
+ const specs = ref<Spec[]>([])
811
+ const skuData = ref<SkuRow[]>([])
812
+
813
+ // 防抖保存
814
+ let saveTimer: ReturnType<typeof setTimeout> | null = null
815
+ const saveToBackend = async () => {
816
+ if (saveTimer) clearTimeout(saveTimer)
817
+
818
+ saveTimer = setTimeout(async () => {
819
+ try {
820
+ // 转换为外部格式保存到后端
821
+ const externalSkus = toExternalFormat(skuData.value, specs.value)
822
+
823
+ // 发送到后端
824
+ await fetch('/api/save-sku', {
825
+ method: 'POST',
826
+ headers: { 'Content-Type': 'application/json' },
827
+ body: JSON.stringify({
828
+ specs: specs.value,
829
+ skus: externalSkus
830
+ })
831
+ })
832
+
833
+ ElMessage.success('保存成功')
834
+ } catch (error) {
835
+ ElMessage.error('保存失败')
836
+ }
837
+ }, 500)
838
+ }
839
+
840
+ // 监听数据变化自动保存
841
+ watch([specs, skuData], saveToBackend, { deep: true })
842
+ </script>
843
+
844
+ <template>
845
+ <SkuEditor
846
+ v-model:specs="specs"
847
+ v-model:sku-data="skuData"
848
+ />
849
+ </template>
850
+ ```
851
+
852
+ ### 从后端加载数据并初始化
853
+
854
+ ```vue
855
+ <script setup lang="ts">
856
+ import { ref, onMounted } from 'vue'
857
+ import SkuEditor from './components/sku/SkuEditor.vue'
858
+ import type { Spec, SkuRow } from './types/sku'
859
+ import { generateSkuData, toInternalFormat } from './composables/sku/useSkuGenerator'
860
+
861
+ const specs = ref<Spec[]>([])
862
+ const skuData = ref<SkuRow[]>([])
863
+
864
+ // 从后端加载数据
865
+ onMounted(async () => {
866
+ try {
867
+ const response = await fetch('/api/get-sku')
868
+ const data = await response.json()
869
+
870
+ specs.value = data.specs
871
+
872
+ // 将后端返回的外部格式(spec_json)转换为内部格式
873
+ const internalSkus = toInternalFormat(data.skus, data.specs)
874
+ // 生成完整的 SKU 数据(保留已有数据)
875
+ skuData.value = generateSkuData(data.specs, internalSkus)
876
+ } catch (error) {
877
+ console.error('加载数据失败:', error)
878
+ }
879
+ })
880
+ </script>
881
+
882
+ <template>
883
+ <SkuEditor
884
+ v-model:specs="specs"
885
+ v-model:sku-data="skuData"
886
+ />
887
+ </template>
888
+ ```
889
+
890
+ ---
891
+
892
+ ## Excel 文件格式
893
+
894
+ ### Sheet 1:规格信息
895
+
896
+ | 规格序号 | 规格名称 | 规格类型 | 属性序号 | 属性名称 | 颜色 | 图片 | 内容 |
897
+ |---------|---------|---------|---------|---------|------|------|------|
898
+ | 1 | 颜色 | 颜色 | 1 | 红色 | #ff0000 | | |
899
+ | 1 | 颜色 | 颜色 | 2 | 蓝色 | #0000ff | | |
900
+ | 2 | 尺码 | 文字 | 1 | S | | | |
901
+ | 2 | 尺码 | 文字 | 2 | M | | | |
902
+
903
+ ### Sheet 2:SKU 数据
904
+
905
+ | 颜色 | 尺码 | SKU编号 | 销售价 | 市场价 | 成本价 | 重量 | 体积 | 条形码 | 状态 | 步长 | 起购数 | 限购数 |
906
+ |------|------|---------|--------|--------|--------|------|------|--------|------|------|--------|--------|
907
+ | 红色 | S | SKU001 | 99 | 199 | 50 | 0.1 | | | 1 | 1 | 1 | 0 |
908
+ | 红色 | M | SKU002 | 99 | 199 | 50 | 0.12 | | | 1 | 1 | 1 | 0 |
909
+ | 蓝色 | S | SKU003 | 99 | 199 | 50 | 0.1 | | | 1 | 1 | 1 | 0 |
910
+
911
+ **注意**:
912
+ - 规格列名会根据实际的规格名称动态生成
913
+ - 系统保留字段名不能用作规格名称:`SKU编号`、`销售价`、`市场价`、`成本价`、`重量`、`体积`、`条形码`、`状态`、`步长`、`起购数`、`限购数`
914
+
915
+ ---
916
+
917
+ ## 常见问题
918
+
919
+ ### 1. 如何修改默认列可见性?
920
+
921
+ 修改 `src/composables/sku/useColumnVisibility.ts` 中的 `defaultColumnVisibility`:
922
+
923
+ ```typescript
924
+ export const defaultColumnVisibility: ColumnVisibilityState = {
925
+ specColumns: {},
926
+ fixedColumns: {
927
+ skuNo: true,
928
+ salePrice: true,
929
+ marketPrice: true,
930
+ costPrice: true,
931
+ weight: true, // 修改为 true 默认显示
932
+ volume: false,
933
+ barcode: false,
934
+ status: true, // 修改为 true 默认显示
935
+ step: false,
936
+ minBuyLimit: false,
937
+ maxBuyLimit: false
938
+ }
939
+ }
940
+ ```
941
+
942
+ ### 2. 添加新的规格时如何自动生成默认属性?
943
+
944
+ 在 `SkuEditor.vue` 的 `saveSpec` 方法中修改,或在添加规格后手动添加属性。
945
+
946
+ ### 3. 如何自定义 SKU 表格的高度?
947
+
948
+ 修改 `SkuGrid.vue` 中的表格配置,或通过 CSS 调整。
949
+
950
+ ### 4. 规格名称与保留字段冲突怎么办?
951
+
952
+ 系统会检查并提示规格名称不能使用保留字段,请使用其他名称。
953
+
954
+ 保留字段列表:`SKU编号`、`销售价`、`市场价`、`成本价`、`重量`、`体积`、`条形码`、`状态`、`步长`、`起购数`、`限购数`
955
+
956
+ 保留字段定义在 `src/constants/sku.ts`:
957
+
958
+ ```typescript
959
+ export const RESERVED_NAMES: string[] = [
960
+ 'SKU编号',
961
+ '销售价',
962
+ '市场价',
963
+ '成本价',
964
+ '重量',
965
+ '体积',
966
+ '条形码',
967
+ '状态',
968
+ '步长',
969
+ '起购数',
970
+ '限购数'
971
+ ]
972
+ ```
973
+
974
+ ### 5. 如何在导入时保留自定义的扩展字段?
975
+
976
+ 需要修改 `useImportExport.ts` 中的导入导出逻辑,在解析和生成数据时包含自定义字段。
977
+
978
+ ### 6. 修改规格时如何保留已编辑的 SKU 数据?
979
+
980
+ 组件已内置此功能。`generateSkuData` 函数会自动保留与新规格组合匹配的已有 SKU 数据。
981
+
982
+ ### 7. 支持多少个规格和属性?
983
+
984
+ 理论上没有硬性限制,但建议规格不超过 5 个,每个规格的属性不超过 10 个,否则 SKU 组合数量会呈指数级增长。
985
+
986
+ 例如:3个规格,每个有 5 个属性,会生成 5×5×5 = 125 个 SKU。
987
+
988
+ ### 8. 如何禁用拖拽排序功能?
989
+
990
+ 可以在 `SkuEditor.vue` 中注释掉 `useSortable` 相关的代码,或者移除模板中的拖拽手柄元素。
991
+
992
+ ### 9. specs 和 spec_json 有什么区别?
993
+
994
+ - **specs**:内部格式,使用规格 ID 和属性 ID,组件内部使用
995
+ - **spec_json**:外部格式,使用规格名称和属性名称,推荐用于存储和后端交互
996
+
997
+ 推荐始终使用 `spec_json` 格式与组件交互,组件会自动处理转换。
998
+
999
+ ### 10. 如何实现行高亮?
1000
+
1001
+ `SkuGrid` 组件已支持行配置属性,可以通过自定义逻辑实现高亮。需要修改 `SkuGrid.vue` 中的行样式逻辑。
1002
+
1003
+ ### 11. 拖拽排序后 sort 字段会自动更新吗?
1004
+
1005
+ 会的。无论是规格还是属性,拖拽排序后都会自动更新对应的 `sort` 字段值,确保数据与显示顺序一致。
1006
+
1007
+ ---
1008
+
1009
+ ## 技术栈
1010
+
1011
+ - **Vue 3** - 渐进式 JavaScript 框架
1012
+ - **TypeScript** - JavaScript 的超集,提供类型安全
1013
+ - **Element Plus** - Vue 3 组件库
1014
+ - **VXE Table** - 功能强大的 Vue 表格组件
1015
+ - **XLSX** - Excel 文件处理库
1016
+ - **Sortable.js** - 拖拽排序库
1017
+ - **Vite** - 下一代前端构建工具
1018
+
1019
+ ---
1020
+
1021
+ ## 相关链接
1022
+
1023
+ - [项目 README](../README.md)
1024
+ - [快速开始指南](./快速开始.md)
1025
+ - [开发指南](./开发指南.md)
1026
+ - [Vue 3 文档](https://cn.vuejs.org/)
1027
+ - [Element Plus 文档](https://element-plus.org/)
1028
+ - [VXE Table 文档](https://vxetable.cn/)