Files
MTKJPAY-FRONT/src/views/ProductCreate.vue
qiube ed745ee6a5 feat(product): 新增商品创建页面
- 添加商品创建表单,支持商品基本信息录入
- 实现多图片上传功能,支持主图和SKU图片上传
- 添加SKU配置模块,支持多个SKU的添加和管理
- 实现销售地区选择功能,支持多地区价格设置
- 添加物流信息配置,包括重量和尺寸设置
- 实现批量编辑功能,支持批量设置价格和库存
- 添加表单验证和提交逻辑,确保数据完整性
- 集成API接口,实现商品创建功能
- 添加页面路由配置,支持从商品管理页面跳转
2025-12-22 11:37:35 +08:00

1425 lines
38 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">
点击上传主图支持jpgpnggif等最大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>