Prechádzať zdrojové kódy

📖 MALL:商品编辑的简化

YunaiV 1 rok pred
rodič
commit
f1c858b9af

+ 0 - 2
package.json

@@ -57,7 +57,6 @@
     "pinia": "^2.1.7",
     "qrcode": "^1.5.3",
     "qs": "^6.11.2",
-    "sortablejs": "^1.15.0",
     "steady-xml": "^0.1.0",
     "url": "^0.11.3",
     "video.js": "^7.21.5",
@@ -81,7 +80,6 @@
     "@types/nprogress": "^0.2.3",
     "@types/qrcode": "^1.5.5",
     "@types/qs": "^6.9.10",
-    "@types/sortablejs": "^1.15.5",
     "@typescript-eslint/eslint-plugin": "^6.11.0",
     "@typescript-eslint/parser": "^6.11.0",
     "@unocss/transformer-variant-group": "^0.57.4",

+ 0 - 6
src/api/mall/product/spu.ts

@@ -48,11 +48,6 @@ export interface Spu {
   sort?: number // 商品排序
   giveIntegral?: number // 赠送积分
   virtualSalesCount?: number // 虚拟销量
-  recommendHot?: boolean // 是否热卖
-  recommendBenefit?: boolean // 是否优惠
-  recommendBest?: boolean // 是否精品
-  recommendNew?: boolean // 是否新品
-  recommendGood?: boolean // 是否优品
   price?: number // 商品价格
   salesCount?: number // 商品销量
   marketPrice?: number // 市场价
@@ -60,7 +55,6 @@ export interface Spu {
   stock?: number // 商品库存
   createTime?: Date // 商品创建时间
   status?: number // 商品状态
-  activityOrders: number[] // 活动排序
 }
 
 // 获得 Spu 列表

+ 0 - 66
src/views/mall/product/spu/form/ActivityOrdersSort.vue

@@ -1,66 +0,0 @@
-<template>
-  <div ref="elTagWrappingRef">
-    <template v-if="activityOrders && activityOrders.length > 0">
-      <el-tag
-        v-for="activityType in activityOrders"
-        :key="activityType"
-        :type="promotionTypes.find((item) => item.value === activityType)?.colorType"
-        class="mr-[10px]"
-      >
-        {{ promotionTypes.find((item) => item.value === activityType)?.label }}
-      </el-tag>
-    </template>
-    <template v-else>
-      <el-tag
-        v-for="type in promotionTypes"
-        :key="type.value as number"
-        :type="type.colorType"
-        class="mr-[10px]"
-      >
-        {{ type.label }}
-      </el-tag>
-    </template>
-  </div>
-</template>
-<script lang="ts" setup>
-import Sortable from 'sortablejs'
-import type { DictDataType } from '@/utils/dict'
-
-defineOptions({ name: 'ActivityOrdersSort' })
-const props = defineProps<{
-  promotionTypes: DictDataType[]
-  activityOrders: number[]
-}>()
-const emit = defineEmits<{
-  (e: 'update:activityOrders', v: number[])
-}>()
-const elTagWrappingRef = ref() // elTag 容器 Ref
-
-const initSortable = () => {
-  new Sortable(elTagWrappingRef.value, {
-    swapThreshold: 1,
-    animation: 150,
-    onEnd: (el) => {
-      const innerText = el.to.innerText
-      // 将字符串按换行符分割成数组
-      const activityOrder = innerText.split('\n')
-      // 根据字符串中的顺序重新排序数组
-      const sortedActivityOrder = activityOrder.map((activityName) => {
-        return props.promotionTypes.find((item) => item.label === activityName)?.value
-      })
-      emit('update:activityOrders', sortedActivityOrder as number[])
-    }
-  })
-}
-onMounted(async () => {
-  await nextTick()
-  // 如果活动排序为空也就是新增的时候加入活动
-  if (props.activityOrders && props.activityOrders.length === 0) {
-    emit(
-      'update:activityOrders',
-      props.promotionTypes.map((item) => item.value as number)
-    )
-  }
-  initSortable()
-})
-</script>

+ 61 - 216
src/views/mall/product/spu/form/BasicInfoForm.vue

@@ -7,131 +7,62 @@
     :rules="rules"
     label-width="120px"
   >
-    <el-row>
-      <el-col :span="12">
-        <el-form-item label="商品名称" prop="name">
-          <el-input v-model="formData.name" placeholder="请输入商品名称" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品分类" prop="categoryId">
-          <el-cascader
-            v-model="formData.categoryId"
-            :options="categoryList"
-            :props="defaultProps"
-            class="w-1/1"
-            clearable
-            placeholder="请选择商品分类"
-            filterable
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品关键字" prop="keyword">
-          <el-input v-model="formData.keyword" placeholder="请输入商品关键字" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="单位" prop="unit">
-          <el-select v-model="formData.unit" class="w-1/1" placeholder="请选择单位">
-            <el-option
-              v-for="dict in getIntDictOptions(DICT_TYPE.PRODUCT_UNIT)"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品简介" prop="introduction">
-          <el-input
-            v-model="formData.introduction"
-            :rows="3"
-            placeholder="请输入商品简介"
-            type="textarea"
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品封面图" prop="picUrl">
-          <UploadImg v-model="formData.picUrl" height="80px" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="24">
-        <el-form-item label="商品轮播图" prop="sliderPicUrls">
-          <UploadImgs v-model:modelValue="formData.sliderPicUrls" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="运费模板" prop="deliveryTemplateId">
-          <el-select v-model="formData.deliveryTemplateId" placeholder="请选择">
-            <el-option
-              v-for="item in deliveryTemplateList"
-              :key="item.id"
-              :label="item.name"
-              :value="item.id"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="品牌" prop="brandId">
-          <el-select v-model="formData.brandId" placeholder="请选择">
-            <el-option
-              v-for="item in brandList"
-              :key="item.id"
-              :label="item.name"
-              :value="item.id"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品规格" props="specType">
-          <el-radio-group v-model="formData.specType" @change="onChangeSpec">
-            <el-radio :label="false" class="radio">单规格</el-radio>
-            <el-radio :label="true">多规格</el-radio>
-          </el-radio-group>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="分销类型" props="subCommissionType">
-          <el-radio-group v-model="formData.subCommissionType" @change="changeSubCommissionType">
-            <el-radio :label="false">默认设置</el-radio>
-            <el-radio :label="true" class="radio">单独设置</el-radio>
-          </el-radio-group>
-        </el-form-item>
-      </el-col>
-      <!-- 多规格添加-->
-      <el-col :span="24">
-        <el-form-item v-if="!formData.specType">
-          <SkuList
-            ref="skuListRef"
-            :prop-form-data="formData"
-            :propertyList="propertyList"
-            :rule-config="ruleConfig"
-          />
-        </el-form-item>
-        <el-form-item v-if="formData.specType" label="商品属性">
-          <el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">添加属性</el-button>
-          <ProductAttributes :propertyList="propertyList" @success="generateSkus" />
-        </el-form-item>
-        <template v-if="formData.specType && propertyList.length > 0">
-          <el-form-item label="批量设置">
-            <SkuList :is-batch="true" :prop-form-data="formData" :propertyList="propertyList" />
-          </el-form-item>
-          <el-form-item label="属性列表">
-            <SkuList
-              ref="skuListRef"
-              :prop-form-data="formData"
-              :propertyList="propertyList"
-              :rule-config="ruleConfig"
-            />
-          </el-form-item>
-        </template>
-      </el-col>
-    </el-row>
+    <!-- TODO 芋艿:宽度!! -->
+    <el-form-item label="商品名称" prop="name">
+      <el-input
+        v-model="formData.name"
+        placeholder="请输入商品名称"
+        type="textarea"
+        :autosize="{ minRows: 2, maxRows: 2 }"
+        maxlength="64"
+        :show-word-limit="true"
+        :clearable="true"
+      />
+    </el-form-item>
+    <el-form-item label="商品分类" prop="categoryId">
+      <el-cascader
+        v-model="formData.categoryId"
+        :options="categoryList"
+        :props="defaultProps"
+        class="w-1/1"
+        clearable
+        placeholder="请选择商品分类"
+        filterable
+      />
+    </el-form-item>
+    <el-form-item label="商品关键字" prop="keyword">
+      <el-input v-model="formData.keyword" placeholder="请输入商品关键字" />
+    </el-form-item>
+    <el-form-item label="商品简介" prop="introduction">
+      <el-input
+        v-model="formData.introduction"
+        :rows="3"
+        placeholder="请输入商品简介"
+        type="textarea"
+      />
+    </el-form-item>
+    <el-form-item label="商品封面图" prop="picUrl">
+      <UploadImg v-model="formData.picUrl" height="80px" />
+    </el-form-item>
+    <el-form-item label="商品轮播图" prop="sliderPicUrls">
+      <UploadImgs v-model:modelValue="formData.sliderPicUrls" />
+    </el-form-item>
+    <!-- TODO 芋艿:这里要挪出去 -->
+    <el-form-item label="运费模板" prop="deliveryTemplateId">
+      <el-select v-model="formData.deliveryTemplateId" placeholder="请选择">
+        <el-option
+          v-for="item in deliveryTemplateList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="品牌" prop="brandId">
+      <el-select v-model="formData.brandId" placeholder="请选择">
+        <el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id" />
+      </el-select>
+    </el-form-item>
   </el-form>
 
   <!-- 情况二:详情 -->
@@ -161,30 +92,15 @@
         @click="imagePreview(row.sliderPicUrls)"
       />
     </template>
-    <template #skus>
-      <SkuList
-        ref="skuDetailListRef"
-        :is-detail="isDetail"
-        :prop-form-data="formData"
-        :propertyList="propertyList"
-      />
-    </template>
   </Descriptions>
-
-  <!-- 商品属性添加 Form 表单 -->
-  <ProductPropertyAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
 </template>
 <script lang="ts" setup>
 import { PropType } from 'vue'
 import { isArray } from '@/utils/is'
 import { copyValueToTarget } from '@/utils'
 import { propTypes } from '@/utils/propTypes'
-import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree'
+import { defaultProps, handleTree, treeToString } from '@/utils/tree'
 import { createImageViewer } from '@/components/ImageViewer'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { getPropertyList, RuleConfig, SkuList } from '@/views/mall/product/spu/components/index.ts'
-import ProductAttributes from './ProductAttributes.vue'
-import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
 import { basicInfoSchema } from './spu.data'
 import type { Spu } from '@/api/mall/product/spu'
 import * as ProductCategoryApi from '@/api/mall/product/category'
@@ -193,30 +109,6 @@ import * as ExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
 
 defineOptions({ name: 'ProductSpuBasicInfoForm' })
 
-// sku 相关属性校验规则
-const ruleConfig: RuleConfig[] = [
-  {
-    name: 'stock',
-    rule: (arg) => arg >= 0,
-    message: '商品库存必须大于等于 1 !!!'
-  },
-  {
-    name: 'price',
-    rule: (arg) => arg >= 0.01,
-    message: '商品销售价格必须大于等于 0.01 元!!!'
-  },
-  {
-    name: 'marketPrice',
-    rule: (arg) => arg >= 0.01,
-    message: '商品市场价格必须大于等于 0.01 元!!!'
-  },
-  {
-    name: 'costPrice',
-    rule: (arg) => arg >= 0.01,
-    message: '商品成本价格必须大于等于 0.00 元!!!'
-  }
-]
-
 // ====== 商品详情相关操作 ======
 const { allSchemas } = useCrudSchemas(basicInfoSchema)
 /** 商品图预览 */
@@ -246,40 +138,26 @@ const props = defineProps({
   activeName: propTypes.string.def(''),
   isDetail: propTypes.bool.def(false) // 是否作为详情组件
 })
-const attributesAddFormRef = ref() // 添加商品属性表单
 const productSpuBasicInfoRef = ref() // 表单 Ref
-const propertyList = ref([]) // 商品属性列表
-const skuListRef = ref() // 商品属性列表Ref
-/** 调用 SkuList generateTableData 方法*/
-const generateSkus = (propertyList) => {
-  skuListRef.value.generateTableData(propertyList)
-}
 const formData = reactive<Spu>({
   name: '', // 商品名称
   categoryId: null, // 商品分类
   keyword: '', // 关键字
-  unit: null, // 单位
   picUrl: '', // 商品封面图
   sliderPicUrls: [], // 商品轮播图
   introduction: '', // 商品简介
   deliveryTemplateId: null, // 运费模版
-  brandId: null, // 商品品牌
-  specType: false, // 商品规格
-  subCommissionType: false, // 分销类型
-  skus: []
+  brandId: null // 商品品牌
 })
 const rules = reactive({
   name: [required],
   categoryId: [required],
   keyword: [required],
-  unit: [required],
   introduction: [required],
   picUrl: [required],
   sliderPicUrls: [required],
   deliveryTemplateId: [required],
-  brandId: [required],
-  specType: [required],
-  subCommissionType: [required]
+  brandId: [required]
 })
 
 /**
@@ -295,7 +173,6 @@ watch(
     formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
       url: item
     }))
-    propertyList.value = getPropertyList(data)
   },
   {
     immediate: true
@@ -307,8 +184,6 @@ watch(
  */
 const emit = defineEmits(['update:activeName'])
 const validate = async () => {
-  // 校验 sku
-  skuListRef.value.validateSku()
   // 校验表单
   if (!productSpuBasicInfoRef) return
   return await unref(productSpuBasicInfoRef).validate((valid) => {
@@ -325,39 +200,9 @@ const validate = async () => {
 }
 defineExpose({ validate })
 
-/** 分销类型 */
-const changeSubCommissionType = () => {
-  // 默认为零,类型切换后也要重置为零
-  for (const item of formData.skus) {
-    item.firstBrokeragePrice = 0
-    item.secondBrokeragePrice = 0
-  }
-}
-
-/** 选择规格 */
-const onChangeSpec = () => {
-  // 重置商品属性列表
-  propertyList.value = []
-  // 重置sku列表
-  formData.skus = [
-    {
-      price: 0,
-      marketPrice: 0,
-      costPrice: 0,
-      barCode: '',
-      picUrl: '',
-      stock: 0,
-      weight: 0,
-      volume: 0,
-      firstBrokeragePrice: 0,
-      secondBrokeragePrice: 0
-    }
-  ]
-}
-
-const categoryList = ref([]) // 分类树
 /** 获取分类的节点的完整结构 */
-const formatCategoryName = (categoryId) => {
+const categoryList = ref<any[]>([]) // 分类树
+const formatCategoryName = (categoryId: number) => {
   return treeToString(categoryList.value, categoryId)
 }
 

+ 11 - 124
src/views/mall/product/spu/form/OtherSettingsForm.vue

@@ -7,78 +7,19 @@
     :rules="rules"
     label-width="120px"
   >
-    <el-row>
-      <el-col :span="24">
-        <el-row :gutter="20">
-          <el-col :span="8">
-            <el-form-item label="商品排序" prop="sort">
-              <el-input-number v-model="formData.sort" :min="0" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item label="赠送积分" prop="giveIntegral">
-              <el-input-number v-model="formData.giveIntegral" :min="0" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item label="虚拟销量" prop="virtualSalesCount">
-              <el-input-number
-                v-model="formData.virtualSalesCount"
-                :min="0"
-                placeholder="请输入虚拟销量"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-col>
-      <el-col :span="24">
-        <el-form-item label="商品推荐">
-          <el-checkbox-group v-model="checkboxGroup" @change="onChangeGroup">
-            <el-checkbox v-for="(item, index) in recommendOptions" :key="index" :label="item.value">
-              {{ item.name }}
-            </el-checkbox>
-          </el-checkbox-group>
-        </el-form-item>
-      </el-col>
-      <el-col :span="24">
-        <el-form-item label="活动优先级">
-          <ActivityOrdersSort
-            v-model:activity-orders="formData.activityOrders"
-            :promotion-types="promotionTypes"
-          />
-        </el-form-item>
-      </el-col>
-    </el-row>
+    <el-form-item label="商品排序" prop="sort">
+      <el-input-number v-model="formData.sort" :min="0" />
+    </el-form-item>
+    <el-form-item label="赠送积分" prop="giveIntegral">
+      <el-input-number v-model="formData.giveIntegral" :min="0" />
+    </el-form-item>
+    <el-form-item label="虚拟销量" prop="virtualSalesCount">
+      <el-input-number v-model="formData.virtualSalesCount" :min="0" placeholder="请输入虚拟销量" />
+    </el-form-item>
   </el-form>
 
   <!-- 情况二:详情 -->
-  <Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
-    <template #recommendHot="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendHot" />
-    </template>
-    <template #recommendBenefit="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendBenefit" />
-    </template>
-    <template #recommendBest="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendBest" />
-    </template>
-    <template #recommendNew="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendNew" />
-    </template>
-    <template #recommendGood="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendGood" />
-    </template>
-    <template #activityOrders="{ row }">
-      <el-tag
-        v-for="activityType in row.activityOrders"
-        :key="activityType"
-        :type="promotionTypes.find((item) => item.value === activityType)?.colorType"
-        class="mr-[10px]"
-      >
-        {{ promotionTypes.find((item) => item.value === activityType)?.label }}
-      </el-tag>
-    </template>
-  </Descriptions>
+  <Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema" />
 </template>
 <script lang="ts" setup>
 import type { Spu } from '@/api/mall/product/spu'
@@ -86,8 +27,6 @@ import { PropType } from 'vue'
 import { propTypes } from '@/utils/propTypes'
 import { copyValueToTarget } from '@/utils'
 import { otherSettingsSchema } from './spu.data'
-import { DICT_TYPE, DictDataType } from '@/utils/dict'
-import ActivityOrdersSort from './ActivityOrdersSort.vue'
 
 defineOptions({ name: 'OtherSettingsForm' })
 
@@ -104,44 +43,12 @@ const props = defineProps({
   isDetail: propTypes.bool.def(false) // 是否作为详情组件
 })
 
-// TODO @puhui999:这个目前先写死;主要是,这个优惠类型不好用 promotion_type_enum;因为优惠劵、会员折扣都算
-// 活动优先级处理
-const promotionTypes = ref<DictDataType[]>([
-  {
-    dictType: 'promotionTypes',
-    label: '秒杀',
-    value: 1,
-    colorType: 'warning',
-    cssClass: ''
-  },
-  {
-    dictType: 'promotionTypes',
-    label: '砍价',
-    value: 2,
-    colorType: 'warning',
-    cssClass: ''
-  },
-  {
-    dictType: 'promotionTypes',
-    label: '拼团',
-    value: 3,
-    colorType: 'warning',
-    cssClass: ''
-  }
-])
-
 const otherSettingsFormRef = ref() // 表单Ref
 // 表单数据
 const formData = ref<Spu>({
   sort: 1, // 商品排序
   giveIntegral: 1, // 赠送积分
-  virtualSalesCount: 1, // 虚拟销量
-  recommendHot: false, // 是否热卖
-  recommendBenefit: false, // 是否优惠
-  recommendBest: false, // 是否精品
-  recommendNew: false, // 是否新品
-  recommendGood: false, // 是否优品
-  activityOrders: [] // 活动排序
+  virtualSalesCount: 1 // 虚拟销量
 })
 // 表单规则
 const rules = reactive({
@@ -149,21 +56,6 @@ const rules = reactive({
   giveIntegral: [required],
   virtualSalesCount: [required]
 })
-const recommendOptions = [
-  { name: '是否热卖', value: 'recommendHot' },
-  { name: '是否优惠', value: 'recommendBenefit' },
-  { name: '是否精品', value: 'recommendBest' },
-  { name: '是否新品', value: 'recommendNew' },
-  { name: '是否优品', value: 'recommendGood' }
-] // 商品推荐选项
-const checkboxGroup = ref<string[]>([]) // 选中的推荐选项
-
-/** 选择商品后赋值 */
-const onChangeGroup = () => {
-  recommendOptions.forEach(({ value }) => {
-    formData.value[value] = checkboxGroup.value.includes(value)
-  })
-}
 
 /**
  * 将传进来的值赋值给formData
@@ -175,11 +67,6 @@ watch(
       return
     }
     copyValueToTarget(formData.value, data)
-    recommendOptions.forEach(({ value }) => {
-      if (formData.value[value] && !checkboxGroup.value.includes(value)) {
-        checkboxGroup.value.push(value)
-      }
-    })
   },
   {
     immediate: true

+ 240 - 0
src/views/mall/product/spu/form/SkuForm.vue

@@ -0,0 +1,240 @@
+<template>
+  <!-- 情况一:添加/修改 -->
+  <el-form
+    v-if="!isDetail"
+    ref="productSpuSkuRef"
+    :model="formData"
+    :rules="rules"
+    label-width="120px"
+  >
+    <el-row>
+      <el-col :span="12">
+        <el-form-item label="商品规格" props="specType">
+          <el-radio-group v-model="formData.specType" @change="onChangeSpec">
+            <el-radio :label="false" class="radio">单规格</el-radio>
+            <el-radio :label="true">多规格</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="分销类型" props="subCommissionType">
+          <el-radio-group v-model="formData.subCommissionType" @change="changeSubCommissionType">
+            <el-radio :label="false">默认设置</el-radio>
+            <el-radio :label="true" class="radio">单独设置</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-col>
+      <!-- 多规格添加-->
+      <el-col :span="24">
+        <el-form-item v-if="!formData.specType">
+          <SkuList
+            ref="skuListRef"
+            :prop-form-data="formData"
+            :propertyList="propertyList"
+            :rule-config="ruleConfig"
+          />
+        </el-form-item>
+        <el-form-item v-if="formData.specType" label="商品属性">
+          <el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">添加属性</el-button>
+          <ProductAttributes :propertyList="propertyList" @success="generateSkus" />
+        </el-form-item>
+        <template v-if="formData.specType && propertyList.length > 0">
+          <el-form-item label="批量设置">
+            <SkuList :is-batch="true" :prop-form-data="formData" :propertyList="propertyList" />
+          </el-form-item>
+          <el-form-item label="属性列表">
+            <SkuList
+              ref="skuListRef"
+              :prop-form-data="formData"
+              :propertyList="propertyList"
+              :rule-config="ruleConfig"
+            />
+          </el-form-item>
+        </template>
+      </el-col>
+    </el-row>
+  </el-form>
+
+  <!-- 情况二:详情 -->
+  <Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
+    <template #specType="{ row }">
+      {{ row.specType ? '多规格' : '单规格' }}
+    </template>
+    <template #subCommissionType="{ row }">
+      {{ row.subCommissionType ? '单独设置' : '默认设置' }}
+    </template>
+    <template #sliderPicUrls="{ row }">
+      <el-image
+        v-for="(item, index) in row.sliderPicUrls"
+        :key="index"
+        :src="item.url"
+        class="mr-10px h-60px w-60px"
+        @click="imagePreview(row.sliderPicUrls)"
+      />
+    </template>
+    <template #skus>
+      <SkuList
+        ref="skuDetailListRef"
+        :is-detail="isDetail"
+        :prop-form-data="formData"
+        :propertyList="propertyList"
+      />
+    </template>
+  </Descriptions>
+
+  <!-- 商品属性添加 Form 表单 -->
+  <ProductPropertyAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
+</template>
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { isArray } from '@/utils/is'
+import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import { createImageViewer } from '@/components/ImageViewer'
+import { getPropertyList, RuleConfig, SkuList } from '@/views/mall/product/spu/components/index.ts'
+import ProductAttributes from './ProductAttributes.vue'
+import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
+import { basicInfoSchema } from './spu.data'
+import type { Spu } from '@/api/mall/product/spu'
+
+defineOptions({ name: 'ProductSpuSkuForm' })
+
+// sku 相关属性校验规则
+const ruleConfig: RuleConfig[] = [
+  {
+    name: 'stock',
+    rule: (arg) => arg >= 0,
+    message: '商品库存必须大于等于 1 !!!'
+  },
+  {
+    name: 'price',
+    rule: (arg) => arg >= 0.01,
+    message: '商品销售价格必须大于等于 0.01 元!!!'
+  },
+  {
+    name: 'marketPrice',
+    rule: (arg) => arg >= 0.01,
+    message: '商品市场价格必须大于等于 0.01 元!!!'
+  },
+  {
+    name: 'costPrice',
+    rule: (arg) => arg >= 0.01,
+    message: '商品成本价格必须大于等于 0.00 元!!!'
+  }
+]
+
+// ====== 商品详情相关操作 ======
+const { allSchemas } = useCrudSchemas(basicInfoSchema)
+/** 商品图预览 */
+const imagePreview = (args) => {
+  const urlList = []
+  if (isArray(args)) {
+    args.forEach((item) => {
+      urlList.push(item.url)
+    })
+  } else {
+    urlList.push(args)
+  }
+  createImageViewer({
+    urlList
+  })
+}
+
+// ====== end ======
+
+const message = useMessage() // 消息弹窗
+
+const props = defineProps({
+  propFormData: {
+    type: Object as PropType<Spu>,
+    default: () => {}
+  },
+  activeName: propTypes.string.def(''),
+  isDetail: propTypes.bool.def(false) // 是否作为详情组件
+})
+const attributesAddFormRef = ref() // 添加商品属性表单
+const productSpuSkuRef = ref() // 表单 Ref
+const propertyList = ref([]) // 商品属性列表
+const skuListRef = ref() // 商品属性列表Ref
+/** 调用 SkuList generateTableData 方法*/
+const generateSkus = (propertyList) => {
+  skuListRef.value.generateTableData(propertyList)
+}
+const formData = reactive<Spu>({
+  specType: false, // 商品规格
+  subCommissionType: false, // 分销类型
+  skus: []
+})
+const rules = reactive({
+  specType: [required],
+  subCommissionType: [required]
+})
+
+/**
+ * 将传进来的值赋值给 formData
+ */
+watch(
+  () => props.propFormData,
+  (data) => {
+    if (!data) {
+      return
+    }
+    copyValueToTarget(formData, data)
+    propertyList.value = getPropertyList(data)
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 表单校验 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+  // 校验 sku
+  skuListRef.value.validateSku()
+  // 校验表单
+  if (!productSpuSkuRef) return
+  return await unref(productSpuSkuRef).validate((valid) => {
+    if (!valid) {
+      message.warning('商品信息未完善!!')
+      emit('update:activeName', 'sku')
+      // 目的截断之后的校验
+      throw new Error('商品信息未完善!!')
+    } else {
+      // 校验通过更新数据
+      Object.assign(props.propFormData, formData)
+    }
+  })
+}
+defineExpose({ validate })
+
+/** 分销类型 */
+const changeSubCommissionType = () => {
+  // 默认为零,类型切换后也要重置为零
+  for (const item of formData.skus) {
+    item.firstBrokeragePrice = 0
+    item.secondBrokeragePrice = 0
+  }
+}
+
+/** 选择规格 */
+const onChangeSpec = () => {
+  // 重置商品属性列表
+  propertyList.value = []
+  // 重置sku列表
+  formData.skus = [
+    {
+      price: 0,
+      marketPrice: 0,
+      costPrice: 0,
+      barCode: '',
+      picUrl: '',
+      stock: 0,
+      weight: 0,
+      volume: 0,
+      firstBrokeragePrice: 0,
+      secondBrokeragePrice: 0
+    }
+  ]
+}
+</script>

+ 18 - 12
src/views/mall/product/spu/form/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap v-loading="formLoading">
     <el-tabs v-model="activeName">
-      <el-tab-pane label="商品信息" name="basicInfo">
+      <el-tab-pane label="基础信息" name="basicInfo">
         <BasicInfoForm
           ref="basicInfoRef"
           v-model:activeName="activeName"
@@ -9,6 +9,14 @@
           :propFormData="formData"
         />
       </el-tab-pane>
+      <el-tab-pane label="价格库存" name="sku">
+        <SkuForm
+          ref="skuRef"
+          v-model:activeName="activeName"
+          :is-detail="isDetail"
+          :propFormData="formData"
+        />
+      </el-tab-pane>
       <el-tab-pane label="商品详情" name="description">
         <DescriptionForm
           ref="descriptionRef"
@@ -17,6 +25,7 @@
           :propFormData="formData"
         />
       </el-tab-pane>
+      <!-- TODO 芋艿:物流设置 -->
       <el-tab-pane label="其他设置" name="otherSettings">
         <OtherSettingsForm
           ref="otherSettingsRef"
@@ -43,6 +52,7 @@ import * as ProductSpuApi from '@/api/mall/product/spu'
 import BasicInfoForm from './BasicInfoForm.vue'
 import DescriptionForm from './DescriptionForm.vue'
 import OtherSettingsForm from './OtherSettingsForm.vue'
+import SkuForm from './SkuForm.vue'
 import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
 
 defineOptions({ name: 'ProductSpuForm' })
@@ -56,15 +66,15 @@ const { delView } = useTagsViewStore() // 视图操作
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const activeName = ref('basicInfo') // Tag 激活的窗口
 const isDetail = ref(false) // 是否查看详情
-const basicInfoRef = ref() // 商品信息Ref
-const descriptionRef = ref() // 商品详情Ref
-const otherSettingsRef = ref() // 其他设置Ref
+const basicInfoRef = ref() // 商品信息 Ref
+const skuRef = ref() // 商品规格 Ref
+const descriptionRef = ref() // 商品详情 Ref
+const otherSettingsRef = ref() // 其他设置 Ref
 // spu 表单数据
 const formData = ref<ProductSpuApi.Spu>({
   name: '', // 商品名称
   categoryId: undefined, // 商品分类
   keyword: '', // 关键字
-  unit: undefined, // 单位
   picUrl: '', // 商品封面图
   sliderPicUrls: [], // 商品轮播图
   introduction: '', // 商品简介
@@ -89,13 +99,7 @@ const formData = ref<ProductSpuApi.Spu>({
   description: '', // 商品详情
   sort: 0, // 商品排序
   giveIntegral: 0, // 赠送积分
-  virtualSalesCount: 0, // 虚拟销量
-  recommendHot: false, // 是否热卖
-  recommendBenefit: false, // 是否优惠
-  recommendBest: false, // 是否精品
-  recommendNew: false, // 是否新品
-  recommendGood: false, // 是否优品
-  activityOrders: [] // 活动排序
+  virtualSalesCount: 0 // 虚拟销量
 })
 
 /** 获得详情 */
@@ -139,6 +143,7 @@ const submitForm = async () => {
   // 校验各表单
   try {
     await unref(basicInfoRef)?.validate()
+    await unref(skuRef)?.validate()
     await unref(descriptionRef)?.validate()
     await unref(otherSettingsRef)?.validate()
     // 深拷贝一份, 这样最终 server 端不满足,不需要恢复,
@@ -181,6 +186,7 @@ const close = () => {
   delView(unref(currentRoute))
   push({ name: 'ProductSpu' })
 }
+
 /** 初始化 */
 onMounted(async () => {
   await getDetail()

+ 0 - 29
src/views/mall/product/spu/form/spu.data.ts

@@ -33,11 +33,6 @@ export const basicInfoSchema = reactive<CrudSchema[]>([
     label: '商品视频',
     field: 'videoUrl'
   },
-  {
-    label: '单位',
-    field: 'unit',
-    dictType: DICT_TYPE.PRODUCT_UNIT
-  },
   {
     label: '规格类型',
     field: 'specType'
@@ -73,29 +68,5 @@ export const otherSettingsSchema = reactive<CrudSchema[]>([
   {
     label: '虚拟销量',
     field: 'virtualSalesCount'
-  },
-  {
-    label: '是否热卖推荐',
-    field: 'recommendHot'
-  },
-  {
-    label: '是否优惠推荐',
-    field: 'recommendBenefit'
-  },
-  {
-    label: '是否精品推荐',
-    field: 'recommendBest'
-  },
-  {
-    label: '是否新品推荐',
-    field: 'recommendNew'
-  },
-  {
-    label: '是否优品推荐',
-    field: 'recommendGood'
-  },
-  {
-    label: '活动显示排序',
-    field: 'activityOrders'
   }
 ])