在线商城

This commit is contained in:
zjf committed 2025-06-20 18:20:45 +08:00
1 parent 40e16b7d3b
commit 3ab25b57d0
1 file changed
+389 -104
+389 -104
View File
@@ -1,101 +1,199 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible"
:close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="货号" prop="productCode">
<el-input v-model="form.productCode" placeholder="请输入货号"/>
</el-form-item>
</el-col>
:close-on-click-modal="false" draggable
width="1100px">
<el-tabs v-model="activeTab" type="card" style="margin-bottom: 24px;" :before-leave="handleTabLeave">
<el-tab-pane label="基本信息" name="base">
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row :gutter="24">
<!-- 基本信息必填项 -->
<el-col :span="12" class="mb20">
<el-form-item label="货号" prop="productCode" required>
<el-input v-model="form.productCode" placeholder="请输入货号"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="商品名称" prop="productName" required>
<el-input v-model="form.productName" placeholder="请输入商品名称"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="商品分类" prop="categoryId" required>
<el-cascader
v-model="form.categoryId"
:options="categoryOptions"
:props="{ value: 'id', label: 'name', children: 'children', checkStrictly: true }"
clearable
placeholder="请选择商品分类"
/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="商品封面图" prop="picUrl" required>
<upload-img v-model:imageUrl="form.picUrl"></upload-img>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="商品名称" prop="productName">
<el-input v-model="form.productName" placeholder="请输入商品名称"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="商品分类" prop="categoryId">
<el-select v-model="form.categoryId" placeholder="请选择商品分类">
<el-option label="请选择">0</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="商品封面图" prop="picUrl">
<upload-img v-model="form.picUrl"></upload-img>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="商品轮播图" prop="sliderPicUrls">
<upload-img v-model="form.sliderPicUrls"></upload-img>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="排序" prop="sort">
<el-input-number :min="1" :max="1000" v-model="form.sort" placeholder="请输入排序字段"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="商品状态" prop="status">
<el-input v-model="form.status" placeholder="请输入商品状态"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="库存" prop="stock">
<el-input-number :min="0" :max="1000" v-model="form.stock" placeholder="请输入库存"></el-input-number>
</el-form-item>
</el-col>
<!-- <el-col :span="12" class="mb20">-->
<!-- <el-form-item label="销量" prop="salesCount">-->
<!-- <el-input v-model="form.salesCount" placeholder="请输入销量"/>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12" class="mb20">
<el-form-item label="单人限购数" prop="limitNum">
<el-input-number :min="1" :max="1000" v-model="form.limitNum"
placeholder="请输入单人限购数量"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="在销商户" prop="merchantRelationId">
<el-input v-model="form.merchantRelationId" placeholder="请输入商户关系表"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="商品详情" prop="description">
<editor v-if="visible" v-model:get-html="form.description" placeholder="请输入商品详情"></editor>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-col :span="12" class="mb20">
<el-form-item label="库存" prop="stock">
<el-input-number :min="0" :max="1000" v-model="form.stock" placeholder="请输入库存"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="单人限购数" prop="limitNum">
<el-input-number :min="1" :max="1000" v-model="form.limitNum" placeholder="请输入单人限购数量"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="商品详情" prop="description">
<editor v-if="visible" v-model:get-html="form.description" placeholder="请输入商品详情"></editor>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="SKU信息" name="sku">
<el-divider>SKU 列表</el-divider>
<el-table :data="form.skuList" style="width: 100%; min-width: 1000px; margin-bottom: 16px;" :header-cell-style="tableHeaderStyle">
<el-table-column prop="barCode" width="120">
<template #header>
<span class="required">*</span> 条码
</template>
<template #default="scope">
<el-form-item
:prop="`skuList.${scope.$index}.barCode`"
:rules="[{ required: true, message: '条码不能为空', trigger: 'blur' }]"
>
<el-input v-model="scope.row.barCode" placeholder="请输入条码" clearable />
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="spec" width="100">
<template #header>规格</template>
<template #default="scope">
<el-input v-model="scope.row.spec" placeholder="请输入规格" />
</template>
</el-table-column>
<el-table-column prop="unit" width="120">
<template #header>
<span class="required">*</span> 单位
</template>
<template #default="scope">
<el-form-item
:prop="`skuList.${scope.$index}.unit`"
:rules="[{ required: true, message: '单位不能为空', trigger: 'blur' }]"
>
<el-input v-model="scope.row.unit" placeholder="请输入单位" clearable />
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="status" width="100">
<template #header>状态</template>
<template #default="scope">
<el-select v-model="scope.row.status" placeholder="请选择">
<el-option label="正常" :value="0" />
<el-option label="停销" :value="1" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="retailPrice" width="140">
<template #header>
<span class="required">*</span> 零售价
</template>
<template #default="scope">
<el-form-item
:prop="`skuList.${scope.$index}.retailPrice`"
:rules="[{ required: true, message: '零售价不能为空', trigger: 'blur' }]"
>
<el-input v-model="scope.row.retailPrice" placeholder="零售价/单位分" clearable style="width: 80px;" />
<span style="margin-left: 4px; color: #888;">元</span>
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="purchasePrice" width="100">
<template #header>进价</template>
<template #default="scope">
<el-input v-model="scope.row.purchasePrice" placeholder="进价/单位分" />
</template>
</el-table-column>
<el-table-column prop="promotionPrice" width="140">
<template #header>促销价</template>
<template #default="scope">
<el-input v-model="scope.row.promotionPrice" placeholder="促销价" clearable style="width: 80px;" />
<span style="margin-left: 4px; color: #888;">元</span>
</template>
</el-table-column>
<el-table-column prop="stock" width="120">
<template #header>
<span class="required">*</span> 库存
</template>
<template #default="scope">
<el-form-item
:prop="`skuList.${scope.$index}.stock`"
:rules="[{ required: true, message: '库存不能为空', trigger: 'blur' }]"
>
<el-input v-model="scope.row.stock" placeholder="库存" clearable />
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="attribute" width="120">
<template #header>SKU属性</template>
<template #default="scope">
<el-input v-model="scope.row.attribute" placeholder="SKU属性" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #header>操作</template>
<template #default="scope">
<el-button type="danger" size="small" @click="removeSku(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-button type="primary" @click="addSku">新增SKU</el-button>
</el-tab-pane>
<el-tab-pane label="在销商户" name="merchant">
<el-button type="primary" @click="showMerchantDialog = true">选择在销商户</el-button>
<el-table :data="selectedMerchants" style="margin-top: 16px;">
<el-table-column prop="name" label="商户名称" />
<el-table-column prop="deptName" label="部门" />
<el-table-column label="操作" width="80">
<template #default="scope">
<el-button type="danger" size="small" @click="removeMerchant(scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="showMerchantDialog" title="选择在销商户" width="700px">
<el-input v-model="merchantSearch" placeholder="搜索商户名称" style="margin-bottom: 12px;" @input="fetchMerchantList" />
<el-table :data="merchantList" @selection-change="handleMerchantSelect" :row-key="(row: Record<string, any>) => row.id" height="400">
<el-table-column type="selection" width="40" />
<el-table-column prop="name" label="商户名称" />
<el-table-column prop="deptName" label="部门" />
</el-table>
<template #footer>
<el-button @click="showMerchantDialog = false">取消</el-button>
<el-button type="primary" @click="confirmMerchantSelect">确定</el-button>
</template>
</el-dialog>
</el-tab-pane>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
<el-button v-if="activeTab === 'sku'" @click="activeTab = 'base'">上一步</el-button>
<el-button v-if="activeTab === 'merchant'" @click="activeTab = 'sku'">上一步</el-button>
<el-button v-if="activeTab === 'base'" type="primary" @click="handleNextTab">下一步</el-button>
<el-button v-else-if="activeTab === 'sku'" type="primary" @click="handleNextTab">下一步</el-button>
<el-button v-else-if="activeTab === 'merchant'" type="primary" @click="onSubmit" :disabled="loading">完成</el-button>
<el-button @click="visible = false">取消</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="ShopSpuDialog">
import {useDict} from '/src/hooks/dict';
import {useMessage} from "/src/hooks/message";
import {getObj, addObj, putObj} from '/src/api/supermarket/shopSpu'
import {rule} from '/src/utils/validate';
import { useDict } from '/@/hooks/dict';
import { useMessage } from "/@/hooks/message";
import { getObj, addObj, putObj } from '/@/api/supermarket/shopSpu';
import { rule } from '/@/utils/validate';
import { tree as fetchCategoryTree } from '/@/api/supermarket/shopCategory';
import { select as fetchMerchantSelect } from '/@/api/wallet/mch-manage';
const emit = defineEmits(['refresh']);
@@ -124,41 +222,94 @@ const form = reactive({
shopSkuId: '',
salesCount: '',
limitNum: 0,
skuList: [] as any[],
});
// 定义校验规则
const dataRules = ref({
productCode: [{ required: true, message: '货号不能为空', trigger: 'blur' }],
productName: [{ required: true, message: '商品名称不能为空', trigger: 'blur' }],
categoryId: [{ required: true, message: '商品分类不能为空', trigger: 'change' }],
picUrl: [{ required: true, message: '商品封面图不能为空', trigger: 'change' }],
sort: [{validator: rule.number, trigger: 'blur'}],
stock: [{validator: rule.number, trigger: 'blur'}],
limitNum: [{validator: rule.number, trigger: 'blur'}],
})
const categoryOptions = ref<any[]>([]);
const loadCategoryTree = async () => {
const res = await fetchCategoryTree();
categoryOptions.value = res.data || [];
};
onMounted(() => {
loadCategoryTree();
});
const activeTab = ref('base');
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true
form.id = ''
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取shopSpu信息
if (id) {
form.id = id
getshopSpuData(id)
activeTab.value = 'base';
if (!id) {
// 新增时彻底重置所有表单数据
Object.assign(form, {
id: '',
productCode: '',
productName: '',
description: '',
categoryId: '',
picUrl: '',
sliderPicUrls: '',
sort: 0,
status: '',
stock: 0,
giveIntegral: '',
merchantRelationId: '',
deptId: '',
shopSkuId: '',
salesCount: '',
limitNum: 0,
skuList: []
});
selectedMerchants.value = [];
selectedMerchantIds.value = [];
// skuList至少有一条
addSku();
nextTick(() => {
dataFormRef.value?.resetFields();
});
} else {
form.id = id;
nextTick(() => {
dataFormRef.value?.resetFields();
});
getshopSpuData(id);
}
// 重新加载分类树
loadCategoryTree();
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {
});
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
if (!selectedMerchants.value.length) {
useMessage().error('请至少选择一个在销商户');
activeTab.value = 'merchant';
return false;
}
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
const merchantIds = selectedMerchants.value.map(m => m.id);
const submitForm = {
...form,
categoryId: Array.isArray(form.categoryId) ? form.categoryId.slice(-1)[0] : form.categoryId,
merchantIds
};
form.id ? await putObj(submitForm) : await addObj(submitForm);
useMessage().success(form.id ? '修改成功' : '添加成功');
visible.value = false;
emit('refresh');
@@ -176,13 +327,147 @@ const getshopSpuData = (id: string) => {
loading.value = true
getObj(id).then((res: any) => {
Object.assign(form, res.data)
form.skuList = res.data.skuList || []
// skuList至少有一条
if (!form.skuList || form.skuList.length === 0) {
addSku();
}
}).finally(() => {
loading.value = false
})
};
function addSku() {
form.skuList.push({
barCode: '',
spec: '',
unit: '',
status: 0,
purchasePrice: '',
retailPrice: '',
promotionPrice: '',
stock: '',
attribute: ''
});
}
function removeSku(index: number) {
form.skuList.splice(index, 1);
}
// 分步校验tab切换
const handleTabLeave = async (newTab: string, oldTab: string) => {
// 基本信息tab校验
if (oldTab === 'base' && newTab !== 'base') {
// 校验基本信息必填
const valid = await dataFormRef.value.validateField([
'productCode', 'productName', 'categoryId', 'picUrl'
]).catch(() => false);
return !!valid;
}
// SKU信息tab校验
if (oldTab === 'sku' && newTab === 'merchant') {
// 校验SKU表格必填
let valid = true;
for (let i = 0; i < form.skuList.length; i++) {
const sku = form.skuList[i];
if (!sku.barCode || !sku.unit || !sku.retailPrice || !sku.stock) {
useMessage().error(`第${i + 1}行SKU必填项未填写`);
valid = false;
break;
}
}
return valid;
}
return true;
};
const tabOrder = ['base', 'sku', 'merchant'];
function handleNextTab() {
if (activeTab.value === 'base') {
dataFormRef.value.validateField(['productCode', 'productName', 'categoryId', 'picUrl']).then(() => {
activeTab.value = 'sku';
});
} else if (activeTab.value === 'sku') {
// 校验SKU表格必填
let valid = true;
for (let i = 0; i < form.skuList.length; i++) {
const sku = form.skuList[i];
if (!sku.barCode || !sku.unit || !sku.retailPrice || !sku.stock) {
useMessage().error(`第${i + 1}行SKU必填项未填写`);
valid = false;
break;
}
}
if (valid) {
activeTab.value = 'merchant';
}
}
}
function requiredLabel(label: string) {
return `<span class='required'>*</span> ${label}`;
}
const tableHeaderStyle = () => ({ 'font-weight': 'bold' });
// 暴露变量
defineExpose({
openDialog
});
const showMerchantDialog = ref(false);
const merchantList = ref<any[]>([]);
const merchantSearch = ref('');
const selectedMerchants = ref<any[]>([]);
const selectedMerchantIds = ref<any[]>([]);
const fetchMerchantList = async () => {
const res = await fetchMerchantSelect({
name: merchantSearch.value,
current: 1,
size: 9999,
types: 3
});
merchantList.value = res.data?.records || [];
};
const handleMerchantSelect = (rows: any[]) => {
selectedMerchantIds.value = rows.map((row: any) => (row as any).id);
};
const confirmMerchantSelect = () => {
selectedMerchants.value = merchantList.value.filter(m => selectedMerchantIds.value.includes(m.id));
showMerchantDialog.value = false;
};
const removeMerchant = (idx: number) => {
selectedMerchants.value.splice(idx, 1);
selectedMerchantIds.value = selectedMerchants.value.map(m => m.id);
};
// 打开弹窗时自动加载商户
watch(showMerchantDialog, (val) => { if (val) fetchMerchantList(); });
</script>
<style scoped>
.required {
color: #f56c6c;
margin-right: 2px;
}
.el-table .el-form-item {
margin-bottom: 0;
}
.el-table .el-form-item__error {
position: absolute;
top: 100%;
left: 0;
font-size: 12px;
color: #f56c6c;
line-height: 1.2;
white-space: nowrap;
}
.el-table .el-table__cell {
vertical-align: top;
position: relative;
}
</style>