From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/views/mall/product/spu/components/SkuList.vue |  583 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 583 insertions(+), 0 deletions(-)

diff --git a/src/views/mall/product/spu/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue
new file mode 100644
index 0000000..c41da4b
--- /dev/null
+++ b/src/views/mall/product/spu/components/SkuList.vue
@@ -0,0 +1,583 @@
+<template>
+  <!-- 鎯呭喌涓�锛氭坊鍔�/淇敼 -->
+  <el-table
+    v-if="!isDetail && !isActivityComponent"
+    :data="isBatch ? skuList : formData!.skus!"
+    border
+    class="tabNumWidth"
+    max-height="500"
+    size="small"
+  >
+    <el-table-column align="center" label="鍥剧墖" min-width="65">
+      <template #default="{ row }">
+        <UploadImg v-model="row.picUrl" height="50px" width="50px" />
+      </template>
+    </el-table-column>
+    <template v-if="formData!.specType && !isBatch">
+      <!--  鏍规嵁鍟嗗搧灞炴�у姩鎬佹坊鍔� -->
+      <el-table-column
+        v-for="(item, index) in tableHeaders"
+        :key="index"
+        :label="item.label"
+        align="center"
+        min-width="120"
+      >
+        <template #default="{ row }">
+          <span style="font-weight: bold; color: #40aaff">
+            {{ row.properties?.[index]?.valueName }}
+          </span>
+        </template>
+      </el-table-column>
+    </template>
+    <el-table-column align="center" label="鍟嗗搧鏉$爜" min-width="168">
+      <template #default="{ row }">
+        <el-input v-model="row.barCode" class="w-100%" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="閿�鍞环" min-width="168">
+      <template #default="{ row }">
+        <el-input-number
+          v-model="row.price"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+          controls-position="right"
+        />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="甯傚満浠�" min-width="168">
+      <template #default="{ row }">
+        <el-input-number
+          v-model="row.marketPrice"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+          controls-position="right"
+        />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="鎴愭湰浠�" min-width="168">
+      <template #default="{ row }">
+        <el-input-number
+          v-model="row.costPrice"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+          controls-position="right"
+        />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="搴撳瓨" min-width="168">
+      <template #default="{ row }">
+        <el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="閲嶉噺(kg)" min-width="168">
+      <template #default="{ row }">
+        <el-input-number
+          v-model="row.weight"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+          controls-position="right"
+        />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="浣撶Н(m^3)" min-width="168">
+      <template #default="{ row }">
+        <el-input-number
+          v-model="row.volume"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+          controls-position="right"
+        />
+      </template>
+    </el-table-column>
+    <template v-if="formData!.subCommissionType">
+      <el-table-column align="center" label="涓�绾ц繑浣�(鍏�)" min-width="168">
+        <template #default="{ row }">
+          <el-input-number
+            v-model="row.firstBrokeragePrice"
+            :min="0"
+            :precision="2"
+            :step="0.1"
+            class="w-100%"
+            controls-position="right"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="浜岀骇杩斾剑(鍏�)" min-width="168">
+        <template #default="{ row }">
+          <el-input-number
+            v-model="row.secondBrokeragePrice"
+            :min="0"
+            :precision="2"
+            :step="0.1"
+            class="w-100%"
+            controls-position="right"
+          />
+        </template>
+      </el-table-column>
+    </template>
+    <el-table-column v-if="formData?.specType" align="center" fixed="right" label="鎿嶄綔" width="80">
+      <template #default="{ row }">
+        <el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
+          鎵归噺娣诲姞
+        </el-button>
+        <el-button v-else link size="small" type="primary" @click="deleteSku(row)">鍒犻櫎</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+
+  <!-- 鎯呭喌浜岋細璇︽儏 -->
+  <el-table
+    v-if="isDetail"
+    ref="activitySkuListRef"
+    :data="formData!.skus!"
+    border
+    max-height="500"
+    size="small"
+    style="width: 99%"
+    @selection-change="handleSelectionChange"
+  >
+    <el-table-column v-if="isComponent" type="selection" width="45" />
+    <el-table-column align="center" label="鍥剧墖" min-width="80">
+      <template #default="{ row }">
+        <el-image
+          v-if="row.picUrl"
+          :src="row.picUrl"
+          class="h-50px w-50px"
+          @click="imagePreview(row.picUrl)"
+        />
+      </template>
+    </el-table-column>
+    <template v-if="formData!.specType && !isBatch">
+      <!--  鏍规嵁鍟嗗搧灞炴�у姩鎬佹坊鍔� -->
+      <el-table-column
+        v-for="(item, index) in tableHeaders"
+        :key="index"
+        :label="item.label"
+        align="center"
+        min-width="80"
+      >
+        <template #default="{ row }">
+          <span style="font-weight: bold; color: #40aaff">
+            {{ row.properties?.[index]?.valueName }}
+          </span>
+        </template>
+      </el-table-column>
+    </template>
+    <el-table-column align="center" label="鍟嗗搧鏉$爜" min-width="100">
+      <template #default="{ row }">
+        {{ row.barCode }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="閿�鍞环(鍏�)" min-width="80">
+      <template #default="{ row }">
+        {{ row.price }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="甯傚満浠�(鍏�)" min-width="80">
+      <template #default="{ row }">
+        {{ row.marketPrice }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="鎴愭湰浠�(鍏�)" min-width="80">
+      <template #default="{ row }">
+        {{ row.costPrice }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="搴撳瓨" min-width="80">
+      <template #default="{ row }">
+        {{ row.stock }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="閲嶉噺(kg)" min-width="80">
+      <template #default="{ row }">
+        {{ row.weight }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="浣撶Н(m^3)" min-width="80">
+      <template #default="{ row }">
+        {{ row.volume }}
+      </template>
+    </el-table-column>
+    <template v-if="formData!.subCommissionType">
+      <el-table-column align="center" label="涓�绾ц繑浣�(鍏�)" min-width="80">
+        <template #default="{ row }">
+          {{ row.firstBrokeragePrice }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="浜岀骇杩斾剑(鍏�)" min-width="80">
+        <template #default="{ row }">
+          {{ row.secondBrokeragePrice }}
+        </template>
+      </el-table-column>
+    </template>
+  </el-table>
+
+  <!-- 鎯呭喌涓夛細浣滀负娲诲姩缁勪欢 -->
+  <el-table
+    v-if="isActivityComponent"
+    :data="formData!.skus!"
+    border
+    max-height="500"
+    size="small"
+    style="width: 99%"
+  >
+    <el-table-column v-if="isComponent" type="selection" width="45" />
+    <el-table-column align="center" label="鍥剧墖" min-width="80">
+      <template #default="{ row }">
+        <el-image :src="row.picUrl" class="h-60px w-60px" @click="imagePreview(row.picUrl)" />
+      </template>
+    </el-table-column>
+    <template v-if="formData!.specType">
+      <!--  鏍规嵁鍟嗗搧灞炴�у姩鎬佹坊鍔� -->
+      <el-table-column
+        v-for="(item, index) in tableHeaders"
+        :key="index"
+        :label="item.label"
+        align="center"
+        min-width="80"
+      >
+        <template #default="{ row }">
+          <span style="font-weight: bold; color: #40aaff">
+            {{ row.properties?.[index]?.valueName }}
+          </span>
+        </template>
+      </el-table-column>
+    </template>
+    <el-table-column align="center" label="鍟嗗搧鏉$爜" min-width="100">
+      <template #default="{ row }">
+        {{ row.barCode }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="閿�鍞环(鍏�)" min-width="80">
+      <template #default="{ row }">
+        {{ formatToFraction(row.price) }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="甯傚満浠�(鍏�)" min-width="80">
+      <template #default="{ row }">
+        {{ formatToFraction(row.marketPrice) }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="鎴愭湰浠�(鍏�)" min-width="80">
+      <template #default="{ row }">
+        {{ formatToFraction(row.costPrice) }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="搴撳瓨" min-width="80">
+      <template #default="{ row }">
+        {{ row.stock }}
+      </template>
+    </el-table-column>
+    <!--  鏂逛究鎵╁睍姣忎釜娲诲姩閰嶇疆鐨勫睘鎬т笉涓�鏍�  -->
+    <slot name="extension"></slot>
+  </el-table>
+</template>
+<script lang="ts" setup>
+import { PropType, Ref } from 'vue'
+import { copyValueToTarget, formatToFraction } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import { UploadImg } from '@/components/UploadFile'
+import type { Property, Sku, Spu } from '@/api/mall/product/spu'
+import { createImageViewer } from '@/components/ImageViewer'
+import { RuleConfig } from '@/views/mall/product/spu/components/index'
+import { PropertyAndValues } from './index'
+import { ElTable } from 'element-plus'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'SkuList' })
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps({
+  propFormData: {
+    type: Object as PropType<Spu>,
+    default: () => {}
+  },
+  propertyList: {
+    type: Array as PropType<PropertyAndValues[]>,
+    default: () => []
+  },
+  ruleConfig: {
+    type: Array as PropType<RuleConfig[]>,
+    default: () => []
+  },
+  isBatch: propTypes.bool.def(false), // 鏄惁浣滀负鎵归噺鎿嶄綔缁勪欢
+  isDetail: propTypes.bool.def(false), // 鏄惁浣滀负 sku 璇︽儏缁勪欢
+  isComponent: propTypes.bool.def(false), // 鏄惁浣滀负 sku 閫夋嫨缁勪欢
+  isActivityComponent: propTypes.bool.def(false) // 鏄惁浣滀负 sku 娲诲姩閰嶇疆缁勪欢
+})
+const formData: Ref<Spu | undefined> = ref<Spu>() // 琛ㄥ崟鏁版嵁
+const skuList = ref<Sku[]>([
+  {
+    price: 0, // 鍟嗗搧浠锋牸
+    marketPrice: 0, // 甯傚満浠�
+    costPrice: 0, // 鎴愭湰浠�
+    barCode: '', // 鍟嗗搧鏉$爜
+    picUrl: '', // 鍥剧墖鍦板潃
+    stock: 0, // 搴撳瓨
+    weight: 0, // 鍟嗗搧閲嶉噺
+    volume: 0, // 鍟嗗搧浣撶Н
+    firstBrokeragePrice: 0, // 涓�绾у垎閿�鐨勪剑閲�
+    secondBrokeragePrice: 0 // 浜岀骇鍒嗛攢鐨勪剑閲�
+  }
+]) // 鎵归噺娣诲姞鏃剁殑涓存椂鏁版嵁
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    zIndex: 9999999,
+    urlList: [imgUrl]
+  })
+}
+
+/** 鎵归噺娣诲姞 */
+const batchAdd = () => {
+  validateProperty()
+  formData.value!.skus!.forEach((item) => {
+    copyValueToTarget(item, skuList.value[0])
+  })
+}
+/** 鏍¢獙鍟嗗搧灞炴�у睘鎬у�� */
+const validateProperty = () => {
+  // 鏍¢獙鍟嗗搧灞炴�у睘鎬у�兼槸鍚︿负绌猴紝鏈変竴涓负绌洪兘涓嶇粰杩�
+  const warningInfo = '瀛樺湪灞炴�у睘鎬у�间负绌猴紝璇峰厛妫�鏌ュ畬鍠勫睘鎬у�煎悗閲嶈瘯锛侊紒锛�'
+  for (const item of props.propertyList) {
+    if (!item.values || isEmpty(item.values)) {
+      message.warning(warningInfo)
+      throw new Error(warningInfo)
+    }
+  }
+}
+/** 鍒犻櫎 sku */
+const deleteSku = (row) => {
+  const index = formData.value!.skus!.findIndex(
+    // 鐩存帴鎶婂垪琛ㄨ浆鎴愬瓧绗︿覆姣旇緝
+    (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
+  )
+  formData.value!.skus!.splice(index, 1)
+}
+const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 澶氬睘鎬ц〃澶�
+/**
+ * 淇濆瓨鏃讹紝姣忎釜鍟嗗搧瑙勬牸鐨勮〃鍗曡鏍¢獙涓嬨�備緥濡傝锛岄攢鍞噾棰濇渶浣庢槸 0.01 杩欑銆�
+ */
+const validateSku = () => {
+  validateProperty()
+  let warningInfo = '璇锋鏌ュ晢鍝佸悇琛岀浉鍏冲睘鎬ч厤缃紝'
+  let validate = true // 榛樿閫氳繃
+  for (const sku of formData.value!.skus!) {
+    // 浣滀负娲诲姩缁勪欢鐨勬牎楠�
+    for (const rule of props?.ruleConfig) {
+      const arg = getValue(sku, rule.name)
+      if (!rule.rule(arg)) {
+        validate = false // 鍙鏈変竴涓笉閫氳繃鍒欑洿鎺ヤ笉閫氳繃
+        warningInfo += rule.message
+        break
+      }
+    }
+    // 鍙鏈変竴涓笉閫氳繃鍒欑粨鏉熷悗缁殑鏍¢獙
+    if (!validate) {
+      message.warning(warningInfo)
+      throw new Error(warningInfo)
+    }
+  }
+}
+const getValue = (obj, arg) => {
+  const keys = arg.split('.')
+  let value = obj
+  for (const key of keys) {
+    if (value && typeof value === 'object' && key in value) {
+      value = value[key]
+    } else {
+      value = undefined
+      break
+    }
+  }
+  return value
+}
+
+const emit = defineEmits<{
+  (e: 'selectionChange', value: Sku[]): void
+}>()
+/**
+ * 閫夋嫨鏃惰Е鍙�
+ * @param Sku 浼犻�掕繃鏉ョ殑閫変腑鐨� sku 鏄竴涓暟缁�
+ */
+const handleSelectionChange = (val: Sku[]) => {
+  emit('selectionChange', val)
+}
+
+/**
+ * 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 skuList
+ */
+watch(
+  () => props.propFormData,
+  (data) => {
+    if (!data) return
+    formData.value = data
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+
+/** 鐢熸垚琛ㄦ暟鎹� */
+const generateTableData = (propertyList: any[]) => {
+  // 鏋勫缓鏁版嵁缁撴瀯
+  const propertyValues = propertyList.map((item) =>
+    item.values.map((v: any) => ({
+      propertyId: item.id,
+      propertyName: item.name,
+      valueId: v.id,
+      valueName: v.name
+    }))
+  )
+  const buildSkuList = build(propertyValues)
+  // 濡傛灉鍥炴樉鐨� sku 灞炴�у拰娣诲姞鐨勫睘鎬т笉涓�鑷村垯閲嶇疆 skus 鍒楄〃
+  if (!validateData(propertyList)) {
+    // 濡傛灉涓嶄竴鑷村垯閲嶇疆琛ㄦ暟鎹紝榛樿娣诲姞鏂扮殑灞炴�ч噸鏂扮敓鎴� sku 鍒楄〃
+    formData.value!.skus = []
+  }
+  for (const item of buildSkuList) {
+    const row = {
+      properties: Array.isArray(item) ? item : [item], // 濡傛灉鍙湁涓�涓睘鎬х殑璇濊繑鍥炵殑鏄竴涓� property 瀵硅薄
+      price: 0,
+      marketPrice: 0,
+      costPrice: 0,
+      barCode: '',
+      picUrl: '',
+      stock: 0,
+      weight: 0,
+      volume: 0,
+      firstBrokeragePrice: 0,
+      secondBrokeragePrice: 0
+    }
+    // 濡傛灉瀛樺湪灞炴�х浉鍚岀殑 sku 鍒欎笉鍋氬鐞�
+    const index = formData.value!.skus!.findIndex(
+      (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
+    )
+    if (index !== -1) {
+      continue
+    }
+    formData.value!.skus!.push(row)
+  }
+}
+
+/**
+ * 鐢熸垚 skus 鍓嶇疆鏍¢獙
+ */
+const validateData = (propertyList: any[]) => {
+  const skuPropertyIds: number[] = []
+  formData.value!.skus!.forEach((sku) =>
+    sku.properties
+      ?.map((property) => property.propertyId)
+      ?.forEach((propertyId) => {
+        if (skuPropertyIds.indexOf(propertyId!) === -1) {
+          skuPropertyIds.push(propertyId!)
+        }
+      })
+  )
+  const propertyIds = propertyList.map((item) => item.id)
+  return skuPropertyIds.length === propertyIds.length
+}
+
+/** 鏋勫缓鎵�鏈夋帓鍒楃粍鍚� */
+const build = (propertyValuesList: Property[][]) => {
+  if (propertyValuesList.length === 0) {
+    return []
+  } else if (propertyValuesList.length === 1) {
+    return propertyValuesList[0]
+  } else {
+    const result: Property[][] = []
+    const rest = build(propertyValuesList.slice(1))
+    for (let i = 0; i < propertyValuesList[0].length; i++) {
+      for (let j = 0; j < rest.length; j++) {
+        // 绗竴娆′笉鏄暟缁勭粨鏋勶紝鍚庨潰鐨勯兘鏄暟缁勭粨鏋�
+        if (Array.isArray(rest[j])) {
+          result.push([propertyValuesList[0][i], ...rest[j]])
+        } else {
+          result.push([propertyValuesList[0][i], rest[j]])
+        }
+      }
+    }
+    return result
+  }
+}
+
+/** 鐩戝惉灞炴�у垪琛紝鐢熸垚鐩稿叧鍙傛暟鍜岃〃澶� */
+watch(
+  () => props.propertyList,
+  (propertyList: PropertyAndValues[]) => {
+    // 濡傛灉涓嶆槸澶氳鏍煎垯缁撴潫
+    if (!formData.value!.specType) {
+      return
+    }
+    // 濡傛灉褰撳墠缁勪欢浣滀负鎵归噺娣诲姞鏁版嵁浣跨敤锛屽垯閲嶇疆琛ㄦ暟鎹�
+    if (props.isBatch) {
+      skuList.value = [
+        {
+          price: 0,
+          marketPrice: 0,
+          costPrice: 0,
+          barCode: '',
+          picUrl: '',
+          stock: 0,
+          weight: 0,
+          volume: 0,
+          firstBrokeragePrice: 0,
+          secondBrokeragePrice: 0
+        }
+      ]
+    }
+
+    // 鍒ゆ柇浠g悊瀵硅薄鏄惁涓虹┖
+    if (JSON.stringify(propertyList) === '[]') {
+      return
+    }
+    // 閲嶇疆琛ㄥご
+    tableHeaders.value = []
+    // 鐢熸垚琛ㄥご
+    propertyList.forEach((item, index) => {
+      // name鍔犲睘鎬ч」index鍖哄垎灞炴�у��
+      tableHeaders.value.push({ prop: `name${index}`, label: item.name })
+    })
+    // 濡傛灉鍥炴樉鐨� sku 灞炴�у拰娣诲姞鐨勫睘鎬т竴鑷村垯涓嶅鐞�
+    if (validateData(propertyList)) {
+      return
+    }
+    // 娣诲姞鏂板睘鎬ф病鏈夊睘鎬у�间篃涓嶅仛澶勭悊
+    if (propertyList.some((item) => !item.values || isEmpty(item.values))) {
+      return
+    }
+    // 鐢熸垚 table 鏁版嵁锛屽嵆 sku 鍒楄〃
+    generateTableData(propertyList)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+const activitySkuListRef = ref<InstanceType<typeof ElTable>>()
+
+const getSkuTableRef = () => {
+  return activitySkuListRef.value
+}
+// 鏆撮湶鍑虹敓鎴� sku 鏂规硶锛岀粰娣诲姞灞炴�ф垚鍔熸椂璋冪敤
+defineExpose({ generateTableData, validateSku, getSkuTableRef })
+</script>
+<style>
+// 閬垮厤婊氬姩鏉¢伄鎸℃渶鍚庝竴琛屾暟鎹�
+/*noinspection CssUnusedSymbol*/
+.el-table.tabNumWidth .el-scrollbar {
+  padding-bottom: 10px;
+}
+</style>

--
Gitblit v1.8.0