- 添加商品创建表单,支持商品基本信息录入 - 实现多图片上传功能,支持主图和SKU图片上传 - 添加SKU配置模块,支持多个SKU的添加和管理 - 实现销售地区选择功能,支持多地区价格设置 - 添加物流信息配置,包括重量和尺寸设置 - 实现批量编辑功能,支持批量设置价格和库存 - 添加表单验证和提交逻辑,确保数据完整性 - 集成API接口,实现商品创建功能 - 添加页面路由配置,支持从商品管理页面跳转
1425 lines
38 KiB
Vue
1425 lines
38 KiB
Vue
<template>
|
||
<div class="product-create">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>新增商品</span>
|
||
<el-button @click="goBack">返回</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
label-width="120px"
|
||
label-position="left"
|
||
>
|
||
<el-form-item label="商品名称" prop="name">
|
||
<el-input v-model="form.name" placeholder="请输入商品名称" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="商品价格" prop="price">
|
||
<el-input-number
|
||
v-model="form.price"
|
||
:precision="2"
|
||
:min="0.01"
|
||
:max="999999.99"
|
||
placeholder="请输入商品价格"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="主图" prop="mainImages">
|
||
<div class="image-upload-section">
|
||
<!-- 多图片上传 -->
|
||
<el-upload
|
||
class="image-uploader-multiple"
|
||
:http-request="handleMainImagesUpload"
|
||
:on-remove="handleMainImageRemove"
|
||
:file-list="mainImageFileList"
|
||
:before-upload="beforeUpload"
|
||
multiple
|
||
list-type="picture-card"
|
||
:limit="10"
|
||
accept="image/*"
|
||
>
|
||
<el-icon class="uploader-icon-multiple"><Plus /></el-icon>
|
||
</el-upload>
|
||
<div class="upload-tip">
|
||
点击上传主图(支持jpg、png、gif等,最大10MB,最多10张)
|
||
</div>
|
||
<!-- 图片预览列表 -->
|
||
<div v-if="form.mainImages && form.mainImages.length > 0" class="image-preview-list">
|
||
<div
|
||
v-for="(image, index) in form.mainImages"
|
||
:key="index"
|
||
class="image-preview-item"
|
||
>
|
||
<el-image
|
||
:src="image"
|
||
class="preview-image"
|
||
fit="cover"
|
||
:preview-src-list="form.mainImages"
|
||
:initial-index="index"
|
||
/>
|
||
<div class="image-actions">
|
||
<el-button
|
||
type="danger"
|
||
:icon="Delete"
|
||
circle
|
||
size="small"
|
||
@click="removeMainImage(index)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="店铺ID" prop="shopId">
|
||
<el-input-number
|
||
v-model="form.shopId"
|
||
:min="1"
|
||
placeholder="请输入店铺ID"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="商品状态" prop="status">
|
||
<el-radio-group v-model="form.status">
|
||
<el-radio label="ACTIVE">上架</el-radio>
|
||
<el-radio label="INACTIVE">下架</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="销售地区" prop="salesRegions">
|
||
<el-checkbox-group v-model="form.salesRegions">
|
||
<el-checkbox label="MY">马来西亚 (MYR)</el-checkbox>
|
||
<el-checkbox label="PH">菲律宾 (PHP)</el-checkbox>
|
||
<el-checkbox label="TH">泰国 (THB)</el-checkbox>
|
||
<el-checkbox label="VN">越南 (VND)</el-checkbox>
|
||
<el-checkbox label="SG">新加坡 (SGD)</el-checkbox>
|
||
<el-checkbox label="US">美国 (USD)</el-checkbox>
|
||
<el-checkbox label="CN">中国 (CNY)</el-checkbox>
|
||
<el-checkbox label="EU">欧洲 (EUR)</el-checkbox>
|
||
<el-checkbox label="GB">英国 (GBP)</el-checkbox>
|
||
</el-checkbox-group>
|
||
<div class="form-tip">
|
||
提示:选择该商品将在哪些地区销售,批量设置价格时会显示对应地区的价格输入框
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<!-- 物流信息(商品级别,所有SKU共享) -->
|
||
<el-divider content-position="left">
|
||
<span style="color: #f56c6c">*</span>
|
||
<span>物流信息</span>
|
||
</el-divider>
|
||
|
||
<el-form-item label="包裹重量" required>
|
||
<el-row :gutter="10" style="width: 100%">
|
||
<el-col :span="16">
|
||
<el-input-number
|
||
v-model="form.weightValue"
|
||
:precision="2"
|
||
:min="0"
|
||
placeholder="请输入重量"
|
||
style="width: 100%"
|
||
/>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-select v-model="form.weightUnit" style="width: 100%">
|
||
<el-option label="克(g)" value="g" />
|
||
<el-option label="千克(kg)" value="kg" />
|
||
<el-option label="磅(lb)" value="lb" />
|
||
</el-select>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="包裹尺寸" required>
|
||
<el-row :gutter="10" style="width: 100%">
|
||
<el-col :span="6">
|
||
<el-input-number
|
||
v-model="form.sizeLength"
|
||
:precision="2"
|
||
:min="0"
|
||
placeholder="长"
|
||
style="width: 100%"
|
||
/>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-input-number
|
||
v-model="form.sizeWidth"
|
||
:precision="2"
|
||
:min="0"
|
||
placeholder="宽"
|
||
style="width: 100%"
|
||
/>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-input-number
|
||
v-model="form.sizeHeight"
|
||
:precision="2"
|
||
:min="0"
|
||
placeholder="高"
|
||
style="width: 100%"
|
||
/>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-select v-model="form.sizeUnit" style="width: 100%">
|
||
<el-option label="厘米(cm)" value="cm" />
|
||
<el-option label="米(m)" value="m" />
|
||
<el-option label="英寸(in)" value="in" />
|
||
</el-select>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form-item>
|
||
|
||
<el-divider>SKU信息</el-divider>
|
||
|
||
<!-- SKU新增模块 -->
|
||
<el-card shadow="never" class="sku-add-module-card" style="margin-bottom: 20px">
|
||
<template #header>
|
||
<div class="sku-module-header">
|
||
<span class="module-title">SKU配置</span>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- SKU配置列表 -->
|
||
<div class="sku-config-list">
|
||
<div
|
||
v-for="(skuItem, skuIndex) in form.skus"
|
||
:key="skuIndex"
|
||
class="sku-config-item"
|
||
>
|
||
<div class="sku-config-content">
|
||
<!-- SKU描述编辑 -->
|
||
<div class="sku-description-section">
|
||
<el-input
|
||
v-model="skuItem.sku"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder='例如:【✅性价比首选】浅水蓝+浅水蓝+米白色【⭐️毛巾实惠3条装】'
|
||
maxlength="2000"
|
||
show-word-limit
|
||
class="sku-description-input"
|
||
/>
|
||
</div>
|
||
|
||
<!-- SKU图片关联 -->
|
||
<div class="sku-images-section">
|
||
<div class="section-label">SKU图片关联</div>
|
||
<div class="sku-image-wrapper">
|
||
<!-- 已上传的图片 -->
|
||
<div
|
||
v-if="skuItem.skuImage"
|
||
class="sku-image-item"
|
||
>
|
||
<el-image
|
||
:src="skuItem.skuImage"
|
||
fit="cover"
|
||
class="sku-preview-image"
|
||
>
|
||
<template #error>
|
||
<div class="image-slot">
|
||
<el-icon><Picture /></el-icon>
|
||
</div>
|
||
</template>
|
||
</el-image>
|
||
<div class="image-actions">
|
||
<el-button
|
||
type="danger"
|
||
:icon="Delete"
|
||
circle
|
||
size="small"
|
||
@click="removeSkuImage(skuIndex)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上传按钮 -->
|
||
<div
|
||
v-else
|
||
class="sku-upload-item"
|
||
>
|
||
<el-upload
|
||
:action="''"
|
||
:http-request="(options) => handleSkuImageUpload(options, skuIndex)"
|
||
:show-file-list="false"
|
||
:before-upload="beforeUpload"
|
||
accept="image/*"
|
||
class="sku-image-uploader"
|
||
>
|
||
<div class="upload-placeholder">
|
||
<el-icon class="upload-icon"><Plus /></el-icon>
|
||
<div class="upload-text">上传图片</div>
|
||
</div>
|
||
</el-upload>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 删除按钮 -->
|
||
<div class="sku-actions">
|
||
<el-button
|
||
type="danger"
|
||
:icon="Delete"
|
||
circle
|
||
@click="removeSku(skuIndex)"
|
||
:disabled="form.skus.length <= 1"
|
||
title="删除SKU"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加SKU按钮 -->
|
||
<div class="add-sku-button-wrapper">
|
||
<el-button
|
||
type="primary"
|
||
:icon="Plus"
|
||
@click="addSku"
|
||
class="add-sku-btn"
|
||
>
|
||
添加选项
|
||
</el-button>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-form-item label="SKU列表" prop="skus">
|
||
<!-- 批量编辑按钮 -->
|
||
<div class="sku-list-header" v-if="form.salesRegions && form.salesRegions.length > 0">
|
||
<el-button
|
||
type="primary"
|
||
@click="showBatchEdit = !showBatchEdit"
|
||
class="batch-edit-toggle-btn"
|
||
>
|
||
<el-icon style="margin-right: 5px">
|
||
<ArrowDown v-if="!showBatchEdit" />
|
||
<ArrowUp v-else />
|
||
</el-icon>
|
||
{{ showBatchEdit ? '收起批量编辑' : '批量编辑' }}
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 批量操作区域(可折叠) -->
|
||
<el-collapse-transition>
|
||
<el-card
|
||
v-show="showBatchEdit && form.salesRegions && form.salesRegions.length > 0"
|
||
shadow="never"
|
||
class="batch-operation-card"
|
||
style="margin-bottom: 20px"
|
||
>
|
||
<div class="batch-operation-content">
|
||
<div class="batch-regions-grid">
|
||
<div
|
||
v-for="region in selectedSalesRegions"
|
||
:key="region.code"
|
||
class="batch-region-item"
|
||
>
|
||
<div class="region-label-wrapper">
|
||
<span class="region-code-badge">{{ region.code }}</span>
|
||
<span class="region-name-text">{{ region.name }}</span>
|
||
</div>
|
||
<el-input-number
|
||
v-model="batchRegionPrices[region.code]"
|
||
:precision="2"
|
||
:min="0"
|
||
placeholder="价格"
|
||
style="width: 100%"
|
||
size="small"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="batch-actions-row">
|
||
<div class="batch-stock-wrapper">
|
||
<span class="batch-label">批量设置库存:</span>
|
||
<el-input-number
|
||
v-model="batchStock"
|
||
:min="0"
|
||
placeholder="库存数量"
|
||
style="width: 180px"
|
||
/>
|
||
</div>
|
||
|
||
<el-button
|
||
type="primary"
|
||
size="default"
|
||
@click="applyAllBatchSettings"
|
||
class="batch-apply-btn"
|
||
>
|
||
应用批量设置
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-collapse-transition>
|
||
|
||
<el-alert
|
||
v-if="!form.salesRegions || form.salesRegions.length === 0"
|
||
type="info"
|
||
:closable="false"
|
||
style="margin-bottom: 20px"
|
||
>
|
||
请先在商品信息中选择销售地区,批量设置功能将根据选择的地区显示对应的价格输入框
|
||
</el-alert>
|
||
<div class="sku-table-wrapper">
|
||
<div class="sku-table-container">
|
||
<el-table
|
||
:data="form.skus"
|
||
border
|
||
class="sku-table"
|
||
>
|
||
<el-table-column label="SKU" min-width="300" fixed="left">
|
||
<template #default="{ row, $index }">
|
||
<el-input
|
||
v-model="row.sku"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder='例如:【✅性价比首选】浅水蓝+浅水蓝+米白色【⭐️毛巾实惠3条装】'
|
||
maxlength="2000"
|
||
show-word-limit
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column
|
||
v-for="region in selectedSalesRegions"
|
||
:key="region.code"
|
||
:label="`*${region.code} 定价`"
|
||
width="150"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.regionPrices[region.code]"
|
||
:precision="2"
|
||
:min="0"
|
||
style="width: 100%"
|
||
placeholder="0.00"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="*商品库存" width="150" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-input-number
|
||
v-model="row.stock"
|
||
:min="0"
|
||
style="width: 100%"
|
||
placeholder="库存"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||
<template #default="{ $index }">
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
@click="removeSku($index)"
|
||
:disabled="form.skus.length <= 1"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button type="primary" @click="submitForm" :loading="submitting">
|
||
创建商品
|
||
</el-button>
|
||
<el-button @click="resetForm">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, watch } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Plus, Delete, ArrowDown, ArrowUp, Picture } from '@element-plus/icons-vue'
|
||
import request from '../api/request'
|
||
|
||
const router = useRouter()
|
||
const submitting = ref(false)
|
||
const formRef = ref()
|
||
|
||
// 主图文件列表(用于el-upload组件)
|
||
const mainImageFileList = ref([])
|
||
|
||
// 表单数据
|
||
const form = reactive({
|
||
name: '',
|
||
price: null,
|
||
mainImage: '', // 兼容字段,取mainImages的第一个
|
||
mainImages: [], // 主图列表
|
||
status: 'ACTIVE',
|
||
shopId: null,
|
||
salesRegions: [], // 销售地区列表,如:['MY', 'PH', 'TH', 'VN']
|
||
// 物流信息(商品级别,所有SKU共享)
|
||
weightValue: null,
|
||
weightUnit: 'g',
|
||
sizeLength: null,
|
||
sizeWidth: null,
|
||
sizeHeight: null,
|
||
sizeUnit: 'cm',
|
||
skus: [
|
||
{
|
||
sku: '', // SKU描述
|
||
regionPrices: {}, // 按地区存储价格,如:{ 'MY': 100, 'PH': 200 }
|
||
stock: 0,
|
||
skuImage: '', // SKU图片
|
||
status: 'ACTIVE'
|
||
}
|
||
]
|
||
})
|
||
|
||
// 地区到货币的映射
|
||
const regionCurrencyMap = {
|
||
'MY': { code: 'MY', name: '马来西亚', currency: 'MYR' },
|
||
'PH': { code: 'PH', name: '菲律宾', currency: 'PHP' },
|
||
'TH': { code: 'TH', name: '泰国', currency: 'THB' },
|
||
'VN': { code: 'VN', name: '越南', currency: 'VND' },
|
||
'SG': { code: 'SG', name: '新加坡', currency: 'SGD' },
|
||
'US': { code: 'US', name: '美国', currency: 'USD' },
|
||
'CN': { code: 'CN', name: '中国', currency: 'CNY' },
|
||
'EU': { code: 'EU', name: '欧洲', currency: 'EUR' },
|
||
'GB': { code: 'GB', name: '英国', currency: 'GBP' }
|
||
}
|
||
|
||
// 批量操作相关
|
||
const batchStock = ref(null)
|
||
const batchRegionPrices = ref({}) // 按地区存储批量价格,如:{ 'MY': 100, 'PH': 200 }
|
||
const showBatchEdit = ref(false) // 控制批量编辑模块的显示/隐藏
|
||
|
||
// 计算已选择的销售地区信息
|
||
const selectedSalesRegions = computed(() => {
|
||
if (!form.salesRegions || form.salesRegions.length === 0) {
|
||
return []
|
||
}
|
||
return form.salesRegions
|
||
.map(regionCode => regionCurrencyMap[regionCode])
|
||
.filter(Boolean)
|
||
})
|
||
|
||
// 监听销售地区变化,初始化批量价格对象和SKU的regionPrices
|
||
watch(() => form.salesRegions, (newRegions, oldRegions) => {
|
||
// 清空之前的批量价格
|
||
batchRegionPrices.value = {}
|
||
// 为每个新选择的地区初始化价格
|
||
if (newRegions && newRegions.length > 0) {
|
||
newRegions.forEach(regionCode => {
|
||
if (regionCurrencyMap[regionCode]) {
|
||
batchRegionPrices.value[regionCode] = null
|
||
}
|
||
})
|
||
}
|
||
|
||
// 更新所有SKU的regionPrices结构
|
||
form.skus.forEach(sku => {
|
||
if (!sku.regionPrices) {
|
||
sku.regionPrices = {}
|
||
}
|
||
// 移除已取消选择的地区
|
||
if (oldRegions && oldRegions.length > 0) {
|
||
oldRegions.forEach(oldRegion => {
|
||
if (!newRegions || !newRegions.includes(oldRegion)) {
|
||
delete sku.regionPrices[oldRegion]
|
||
}
|
||
})
|
||
}
|
||
// 为新选择的地区初始化价格
|
||
if (newRegions && newRegions.length > 0) {
|
||
newRegions.forEach(regionCode => {
|
||
if (sku.regionPrices[regionCode] === undefined) {
|
||
sku.regionPrices[regionCode] = null
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}, { immediate: true })
|
||
|
||
// 应用所有批量设置(价格、库存和SKU后缀)
|
||
const applyAllBatchSettings = () => {
|
||
let hasPriceUpdate = false
|
||
let hasStockUpdate = false
|
||
const updatedRegions = []
|
||
|
||
// 批量应用地区价格
|
||
Object.keys(batchRegionPrices.value).forEach(regionCode => {
|
||
const price = batchRegionPrices.value[regionCode]
|
||
if (price !== null && price > 0) {
|
||
form.skus.forEach(sku => {
|
||
if (!sku.regionPrices) {
|
||
sku.regionPrices = {}
|
||
}
|
||
sku.regionPrices[regionCode] = price
|
||
})
|
||
hasPriceUpdate = true
|
||
updatedRegions.push(regionCode)
|
||
// 清空已应用的价格
|
||
batchRegionPrices.value[regionCode] = null
|
||
}
|
||
})
|
||
|
||
// 批量应用库存
|
||
let stockValue = null
|
||
if (batchStock.value !== null && batchStock.value >= 0) {
|
||
stockValue = batchStock.value
|
||
form.skus.forEach(sku => {
|
||
sku.stock = batchStock.value
|
||
})
|
||
hasStockUpdate = true
|
||
batchStock.value = null
|
||
}
|
||
|
||
// 显示成功消息
|
||
const messages = []
|
||
if (hasPriceUpdate) {
|
||
const regionNames = updatedRegions.join('、')
|
||
messages.push(`价格(${regionNames})`)
|
||
}
|
||
if (hasStockUpdate) {
|
||
messages.push(`库存(${stockValue})`)
|
||
}
|
||
|
||
if (messages.length > 0) {
|
||
ElMessage.success(`已批量更新 ${form.skus.length} 个SKU的 ${messages.join('、')}`)
|
||
} else {
|
||
ElMessage.warning('请填写要批量设置的价格或库存')
|
||
}
|
||
}
|
||
|
||
// 表单验证规则
|
||
const rules = {
|
||
name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
|
||
price: [{ required: true, message: '请输入商品价格', trigger: 'blur' }],
|
||
shopId: [{ required: true, message: '请输入店铺ID', trigger: 'blur' }],
|
||
skus: [
|
||
{ required: true, message: '至少需要一个SKU', trigger: 'change' },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (!value || value.length === 0) {
|
||
callback(new Error('至少需要一个SKU'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
}
|
||
|
||
// 添加SKU
|
||
const addSku = () => {
|
||
const newSku = {
|
||
sku: '', // SKU描述
|
||
regionPrices: {}, // 按地区存储价格
|
||
stock: 0,
|
||
skuImage: '', // SKU图片(单张)
|
||
status: 'ACTIVE'
|
||
}
|
||
|
||
// 为已选择的销售地区初始化价格
|
||
if (form.salesRegions && form.salesRegions.length > 0) {
|
||
form.salesRegions.forEach(regionCode => {
|
||
newSku.regionPrices[regionCode] = null
|
||
})
|
||
}
|
||
|
||
form.skus.push(newSku)
|
||
}
|
||
|
||
// 删除SKU
|
||
const removeSku = (index) => {
|
||
if (form.skus.length > 1) {
|
||
form.skus.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
// 自定义多图片上传(单个文件上传)
|
||
const handleMainImagesUpload = async (options) => {
|
||
const { file, onSuccess, onError } = options
|
||
|
||
try {
|
||
// 验证文件
|
||
const isValid = beforeUpload(file)
|
||
if (!isValid) {
|
||
onError(new Error('文件验证失败'))
|
||
return
|
||
}
|
||
|
||
// 检查是否超过限制
|
||
if (form.mainImages.length >= 10) {
|
||
ElMessage.warning('最多只能上传10张图片')
|
||
onError(new Error('超过图片数量限制'))
|
||
return
|
||
}
|
||
|
||
// 创建FormData(单个文件)
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
|
||
// 上传文件(使用相对路径,baseURL已包含/api)
|
||
const response = await request.post('/product/upload/image', formData)
|
||
|
||
if (response.code === '0000' && response.data) {
|
||
const uploadedUrl = response.data.url || response.data.fileName
|
||
|
||
if (uploadedUrl) {
|
||
form.mainImages.push(uploadedUrl)
|
||
|
||
// 更新mainImage为第一张图片(兼容)
|
||
if (form.mainImages.length === 1) {
|
||
form.mainImage = uploadedUrl
|
||
}
|
||
|
||
// 更新文件列表
|
||
mainImageFileList.value.push({
|
||
name: file.name,
|
||
url: uploadedUrl,
|
||
status: 'success',
|
||
response: response
|
||
})
|
||
|
||
onSuccess(response, file)
|
||
ElMessage.success('图片上传成功')
|
||
} else {
|
||
onError(new Error('未获取到图片URL'))
|
||
ElMessage.error('图片上传失败:未获取到图片URL')
|
||
}
|
||
} else {
|
||
onError(new Error(response.message || '图片上传失败'))
|
||
ElMessage.error(response.message || '图片上传失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('图片上传失败:', error)
|
||
onError(error)
|
||
ElMessage.error('图片上传失败: ' + (error.message || '未知错误'))
|
||
}
|
||
}
|
||
|
||
// SKU图片上传
|
||
const handleSkuImageUpload = async (options, skuIndex) => {
|
||
const { file, onSuccess, onError } = options
|
||
|
||
try {
|
||
// 验证文件
|
||
const isValid = beforeUpload(file)
|
||
if (!isValid) {
|
||
onError(new Error('文件验证失败'))
|
||
return
|
||
}
|
||
|
||
// 创建FormData(单个文件)
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
|
||
// 上传文件(使用相对路径,baseURL已包含/api)
|
||
const response = await request.post('/product/upload/image', formData)
|
||
|
||
if (response.code === '0000' && response.data) {
|
||
const uploadedUrl = response.data.url || response.data.fileName
|
||
|
||
if (uploadedUrl) {
|
||
form.skus[skuIndex].skuImage = uploadedUrl
|
||
onSuccess(response, file)
|
||
ElMessage.success('图片上传成功')
|
||
} else {
|
||
onError(new Error('未获取到图片URL'))
|
||
ElMessage.error('图片上传失败:未获取到图片URL')
|
||
}
|
||
} else {
|
||
onError(new Error(response.message || '图片上传失败'))
|
||
ElMessage.error(response.message || '图片上传失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('SKU图片上传失败:', error)
|
||
onError(error)
|
||
ElMessage.error('图片上传失败: ' + (error.message || '未知错误'))
|
||
}
|
||
}
|
||
|
||
// 删除SKU图片
|
||
const removeSkuImage = (skuIndex) => {
|
||
form.skus[skuIndex].skuImage = ''
|
||
ElMessage.success('图片已删除')
|
||
}
|
||
|
||
// 删除主图
|
||
const handleMainImageRemove = (file, fileList) => {
|
||
// 从mainImages中移除对应的URL
|
||
if (file.url) {
|
||
const index = form.mainImages.indexOf(file.url)
|
||
if (index > -1) {
|
||
form.mainImages.splice(index, 1)
|
||
}
|
||
} else if (file.response && file.response.data && file.response.data.url) {
|
||
// 处理上传成功的文件
|
||
const url = file.response.data.url
|
||
const index = form.mainImages.indexOf(url)
|
||
if (index > -1) {
|
||
form.mainImages.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
// 更新mainImage
|
||
if (form.mainImages.length > 0) {
|
||
form.mainImage = form.mainImages[0]
|
||
} else {
|
||
form.mainImage = ''
|
||
}
|
||
|
||
// 更新文件列表
|
||
mainImageFileList.value = fileList
|
||
}
|
||
|
||
// 手动删除主图
|
||
const removeMainImage = (index) => {
|
||
form.mainImages.splice(index, 1)
|
||
// 更新mainImage
|
||
if (form.mainImages.length > 0) {
|
||
form.mainImage = form.mainImages[0]
|
||
} else {
|
||
form.mainImage = ''
|
||
}
|
||
// 更新文件列表
|
||
mainImageFileList.value = mainImageFileList.value.filter((_, i) => i !== index)
|
||
}
|
||
|
||
// 上传前验证
|
||
const beforeUpload = (file) => {
|
||
const isImage = file.type.startsWith('image/')
|
||
const isLt10M = file.size / 1024 / 1024 < 10
|
||
|
||
if (!isImage) {
|
||
ElMessage.error('只能上传图片文件!')
|
||
return false
|
||
}
|
||
if (!isLt10M) {
|
||
ElMessage.error('图片大小不能超过10MB!')
|
||
return false
|
||
}
|
||
|
||
// 检查是否超过限制
|
||
if (form.mainImages.length >= 10) {
|
||
ElMessage.warning('最多只能上传10张图片')
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// 提交表单
|
||
const submitForm = async () => {
|
||
if (!formRef.value) return
|
||
|
||
await formRef.value.validate(async (valid) => {
|
||
if (!valid) {
|
||
ElMessage.error('请填写完整信息')
|
||
return
|
||
}
|
||
|
||
submitting.value = true
|
||
|
||
try {
|
||
// 验证物流信息(商品级别)
|
||
if (!form.weightValue || form.weightValue <= 0) {
|
||
ElMessage.error('请填写包裹重量')
|
||
submitting.value = false
|
||
return
|
||
}
|
||
if (!form.sizeLength || !form.sizeWidth || !form.sizeHeight ||
|
||
form.sizeLength <= 0 || form.sizeWidth <= 0 || form.sizeHeight <= 0) {
|
||
ElMessage.error('请填写完整的包裹尺寸(长、宽、高)')
|
||
submitting.value = false
|
||
return
|
||
}
|
||
|
||
// 转换重量:统一转换为克(g)
|
||
let weightInGrams = null
|
||
if (form.weightValue != null && form.weightValue > 0) {
|
||
if (form.weightUnit === 'g') {
|
||
weightInGrams = form.weightValue
|
||
} else if (form.weightUnit === 'kg') {
|
||
weightInGrams = form.weightValue * 1000
|
||
} else if (form.weightUnit === 'lb') {
|
||
weightInGrams = form.weightValue * 453.592
|
||
}
|
||
}
|
||
|
||
// 转换尺寸:统一转换为厘米(cm),并生成JSON
|
||
let sizeJson = null
|
||
if (form.sizeLength != null && form.sizeWidth != null && form.sizeHeight != null) {
|
||
let length = form.sizeLength
|
||
let width = form.sizeWidth
|
||
let height = form.sizeHeight
|
||
|
||
// 转换为厘米
|
||
if (form.sizeUnit === 'm') {
|
||
length = length * 100
|
||
width = width * 100
|
||
height = height * 100
|
||
} else if (form.sizeUnit === 'in') {
|
||
length = length * 2.54
|
||
width = width * 2.54
|
||
height = height * 2.54
|
||
}
|
||
|
||
sizeJson = JSON.stringify({
|
||
length: parseFloat(length.toFixed(2)),
|
||
width: parseFloat(width.toFixed(2)),
|
||
height: parseFloat(height.toFixed(2)),
|
||
unit: 'cm'
|
||
})
|
||
}
|
||
|
||
// 构建请求数据
|
||
const requestData = {
|
||
name: form.name,
|
||
price: form.price,
|
||
// 优先使用mainImages,如果没有则使用mainImage(兼容)
|
||
mainImages: form.mainImages.length > 0 ? form.mainImages : (form.mainImage ? [form.mainImage] : []),
|
||
mainImage: form.mainImages.length > 0 ? form.mainImages[0] : form.mainImage, // 兼容字段
|
||
status: form.status,
|
||
shopId: form.shopId,
|
||
skus: form.skus.flatMap(sku => {
|
||
// 为每个有价格的地区创建一个SKU记录
|
||
const skuRecords = []
|
||
|
||
if (sku.regionPrices && Object.keys(sku.regionPrices).length > 0) {
|
||
Object.keys(sku.regionPrices).forEach(regionCode => {
|
||
const price = sku.regionPrices[regionCode]
|
||
if (price !== null && price > 0) {
|
||
const region = regionCurrencyMap[regionCode]
|
||
if (region) {
|
||
skuRecords.push({
|
||
sku: sku.sku, // SKU描述
|
||
price: price,
|
||
currency: region.currency,
|
||
stock: sku.stock,
|
||
skuImage: sku.skuImage || null, // SKU图片
|
||
// 所有SKU共享商品级别的重量和尺寸
|
||
weight: weightInGrams,
|
||
size: sizeJson,
|
||
// 销售属性和规格描述设为null(简化SKU,不需要这些字段)
|
||
salesAttrs: null,
|
||
specification: null,
|
||
status: sku.status
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 如果没有地区价格,至少创建一个默认SKU
|
||
if (skuRecords.length === 0) {
|
||
skuRecords.push({
|
||
sku: sku.sku,
|
||
price: 0,
|
||
currency: form.salesRegions && form.salesRegions.length > 0
|
||
? regionCurrencyMap[form.salesRegions[0]].currency
|
||
: 'USD',
|
||
stock: sku.stock,
|
||
skuImage: sku.skuImage || null, // SKU图片
|
||
weight: weightInGrams,
|
||
size: sizeJson,
|
||
salesAttrs: null,
|
||
specification: null,
|
||
status: sku.status
|
||
})
|
||
}
|
||
|
||
return skuRecords
|
||
})
|
||
}
|
||
|
||
const response = await request.post('/product', requestData)
|
||
|
||
if (response.code === '0000') {
|
||
ElMessage.success('商品创建成功')
|
||
router.push('/manage/product')
|
||
} else {
|
||
ElMessage.error(response.message || '商品创建失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('创建商品失败:', error)
|
||
ElMessage.error(error.response?.data?.message || '商品创建失败,请稍后重试')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// 重置表单
|
||
const resetForm = () => {
|
||
if (formRef.value) {
|
||
formRef.value.resetFields()
|
||
}
|
||
form.name = ''
|
||
form.price = null
|
||
form.mainImage = ''
|
||
form.mainImages = []
|
||
form.status = 'ACTIVE'
|
||
form.shopId = null
|
||
form.salesRegions = []
|
||
// 重置物流信息
|
||
form.weightValue = null
|
||
form.weightUnit = 'g'
|
||
form.sizeLength = null
|
||
form.sizeWidth = null
|
||
form.sizeHeight = null
|
||
form.sizeUnit = 'cm'
|
||
form.skus = [
|
||
{
|
||
sku: '', // SKU描述
|
||
regionPrices: {}, // 按地区存储价格
|
||
stock: 0,
|
||
skuImage: '', // SKU图片
|
||
status: 'ACTIVE'
|
||
}
|
||
]
|
||
|
||
// 为SKU初始化regionPrices
|
||
if (form.salesRegions && form.salesRegions.length > 0) {
|
||
form.skus[0].regionPrices = {}
|
||
form.salesRegions.forEach(regionCode => {
|
||
form.skus[0].regionPrices[regionCode] = null
|
||
})
|
||
}
|
||
|
||
mainImageFileList.value = []
|
||
|
||
// 重置批量操作状态
|
||
batchStock.value = null
|
||
batchRegionPrices.value = {}
|
||
showBatchEdit.value = false
|
||
}
|
||
|
||
// 返回
|
||
const goBack = () => {
|
||
router.push('/manage/product')
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.product-create {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 图片上传 */
|
||
.image-upload-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.upload-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.form-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 5px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 多图片上传样式 */
|
||
.image-uploader-multiple {
|
||
width: 100%;
|
||
}
|
||
|
||
.image-uploader-multiple :deep(.el-upload-list--picture-card) {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.image-uploader-multiple :deep(.el-upload--picture-card) {
|
||
width: 120px;
|
||
height: 120px;
|
||
line-height: 120px;
|
||
border: 1px dashed #d9d9d9;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.uploader-icon-multiple {
|
||
font-size: 28px;
|
||
color: #8c939d;
|
||
}
|
||
|
||
/* 图片预览列表 */
|
||
.image-preview-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.image-preview-item {
|
||
position: relative;
|
||
width: 120px;
|
||
height: 120px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.preview-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.image-actions {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 5px;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
.image-preview-item:hover .image-actions {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 批量操作区域 */
|
||
.batch-operation-card {
|
||
background: linear-gradient(135deg, #f0f9ff 0%, #e6f4ff 100%);
|
||
border: 1px solid #b3d8ff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
||
}
|
||
|
||
/* SKU列表头部 */
|
||
.sku-list-header {
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.batch-edit-toggle-btn {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.batch-operation-content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.batch-regions-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 20px;
|
||
border-bottom: 1px solid #d4e8ff;
|
||
}
|
||
|
||
.batch-region-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.region-label-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.region-code-badge {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
||
color: #fff;
|
||
border-radius: 4px;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
min-width: 40px;
|
||
text-align: center;
|
||
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
|
||
}
|
||
|
||
.region-name-text {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.batch-actions-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-top: 15px;
|
||
}
|
||
|
||
.batch-stock-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.batch-label {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.batch-apply-btn {
|
||
padding: 10px 24px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* SKU新增模块样式 */
|
||
.sku-add-module-card {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.sku-module-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.module-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.sku-config-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.sku-config-item {
|
||
display: flex;
|
||
gap: 15px;
|
||
padding: 20px;
|
||
background: #fafafa;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 8px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.sku-config-item:hover {
|
||
border-color: #409eff;
|
||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
||
}
|
||
|
||
.sku-config-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.sku-description-section {
|
||
width: 100%;
|
||
}
|
||
|
||
.sku-description-input {
|
||
width: 100%;
|
||
}
|
||
|
||
.sku-images-section {
|
||
width: 100%;
|
||
}
|
||
|
||
.section-label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #606266;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.sku-image-wrapper {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.sku-image-item {
|
||
position: relative;
|
||
width: 120px;
|
||
height: 120px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.sku-image-item:hover {
|
||
border-color: #409eff;
|
||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||
}
|
||
|
||
.sku-preview-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.image-slot {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: #f5f7fa;
|
||
color: #909399;
|
||
}
|
||
|
||
.sku-image-item .image-actions {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 5px;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
.sku-image-item:hover .image-actions {
|
||
opacity: 1;
|
||
}
|
||
|
||
.sku-upload-item {
|
||
width: 120px;
|
||
height: 120px;
|
||
}
|
||
|
||
.sku-image-uploader {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.sku-image-uploader :deep(.el-upload) {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 1px dashed #d9d9d9;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.sku-image-uploader :deep(.el-upload:hover) {
|
||
border-color: #409eff;
|
||
}
|
||
|
||
.upload-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background: #fafafa;
|
||
color: #8c939d;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 28px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 12px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
|
||
.sku-actions {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding-top: 5px;
|
||
}
|
||
|
||
.add-sku-button-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding-top: 10px;
|
||
border-top: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.add-sku-btn {
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* SKU表格样式 */
|
||
.sku-table-wrapper {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
overflow-y: visible;
|
||
position: relative;
|
||
}
|
||
|
||
.sku-table-container {
|
||
margin-top: 10px;
|
||
min-width: 100%;
|
||
}
|
||
|
||
.sku-table {
|
||
width: 100%;
|
||
min-width: 800px;
|
||
table-layout: auto;
|
||
}
|
||
|
||
.sku-table :deep(.el-table__cell) {
|
||
padding: 10px;
|
||
}
|
||
|
||
.sku-table :deep(.el-textarea__inner) {
|
||
border: none;
|
||
padding: 0;
|
||
resize: none;
|
||
}
|
||
|
||
/* 横向滚动条样式优化 */
|
||
.sku-table-wrapper::-webkit-scrollbar {
|
||
height: 8px;
|
||
}
|
||
|
||
.sku-table-wrapper::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.sku-table-wrapper::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.sku-table-wrapper::-webkit-scrollbar-thumb:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
</style>
|
||
|