From 2b61b2b6a742609a5d4c1f6efcf7d4c67979f912 Mon Sep 17 00:00:00 2001 From: dzl <786316265@qq.com> Date: Sat, 11 Oct 2025 17:32:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=8D=E7=B1=BB=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/api.js | 7 + src/views/ProductForm.vue | 855 ++++++++++++++++++++++++++++++++++++-- src/views/Products.vue | 48 ++- 3 files changed, 865 insertions(+), 45 deletions(-) diff --git a/src/utils/api.js b/src/utils/api.js index 4795d9d..a4fd879 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -252,6 +252,13 @@ const api = { createWithdraw: (data) => apiRequest.post('/withdrawals', data), }, + categories: { + getCategories: () => apiRequest.get('/category'), + createCategory: (data) => apiRequest.post('/category', data), + updateCategory: (id, data) => apiRequest.put(`/category/${id}`, data), + deleteCategory: (id) => apiRequest.delete(`/category/${id}`) + }, + // 为了向后兼容,添加直接的 get、post 等方法 diff --git a/src/views/ProductForm.vue b/src/views/ProductForm.vue index d454e4e..a18e173 100644 --- a/src/views/ProductForm.vue +++ b/src/views/ProductForm.vue @@ -39,7 +39,7 @@ - + - + + + + + + + + +
可选择多个二级分类,但必须在同一个一级分类下
+
+
+
@@ -283,6 +309,101 @@ + + + + + +
+
+

规格维度管理

+ + + 添加规格维度 + +
+ +
+ + {{ specName.name }} + ({{ getSpecValueCount(specName.id) }}个值) + +
+ + +
+ + +
+
+

规格组合

+ + + 生成规格组合 + +
+ +
+ + + + + + + + + + +
+ 还有 {{ combinations.length - 3 }} 个规格组合... +
+
+ + +
+
+ - +
+ + +
+ +
+ {{ form.free_shipping ? '该商品免运费' : '该商品需要支付运费' }} +
+
+
+
@@ -334,9 +472,192 @@ 重置表单 + + + 通过审核 +
+ + + +
+ +
+ + + + + + + + 添加 + + + +
+ + +
+

已有规格维度

+
+
+
+ {{ specName.name }} + ({{ getSpecValueCount(specName.id) }}个值) +
+
+ + 管理规格值 + + + 删除 + +
+
+
+ +
+
+ + +
+ + + +
+ +
+ + + + + + + + 添加 + + + +
+ + +
+

已有规格值

+
+ + {{ specValue.value }} + +
+ +
+
+ + +
+ + + +
+ + + + + {{ specName.name }} ({{ getSpecValueCount(specName.id) }}个值) + + + + + + + + + +
+ +
+
+ + +
@@ -356,7 +677,9 @@ import { Coin, CreditCard, Check, - Refresh + Refresh, + Grid, + Plus } from '@element-plus/icons-vue' import api from '@/utils/api' import ImageUpload from '@/components/ImageUpload.vue' @@ -368,7 +691,24 @@ const route = useRoute() const router = useRouter() const formRef = ref() const loading = ref(false) -const categories = ref(['数码产品', '生活用品', '食品饮料', '图书文具', '服装配饰', '其他']) +const categories = ref([]) + +// 计算属性:一级分类 +const primaryCategories = computed(() => { + return categories.value.filter(cat => cat.level === 1) +}) + +// 计算属性:当前选中一级分类下的二级分类 +const secondaryCategories = computed(() => { + if (!form.primary_category) return [] + return categories.value.filter(cat => cat.level === 2 && cat.parent_id === form.primary_category) +}) + +// 处理一级分类变化 +const handlePrimaryCategoryChange = (value) => { + // 清空二级分类选择 + form.secondary_categories = [] +} const isEdit = computed(() => !!route.params.id) @@ -387,7 +727,8 @@ const handleStatusChange = (value) => { const form = reactive({ name: '', - category: '', + primary_category: null, + secondary_categories: [], price: null, points: null, rongdou_price: null, @@ -400,7 +741,30 @@ const form = reactive({ payment_methods: ['points'], description: '', details: '', - status: 'active' + status: 'active', + free_shipping: false +}) + +// 规格管理相关数据 +const specNames = ref([]) +const specValues = ref([]) +const combinations = ref([]) +const specNamesDialogVisible = ref(false) +const specValuesDialogVisible = ref(false) +const generateCombinationsDialogVisible = ref(false) +const newSpecName = ref('') +const newSpecValue = ref('') +const selectedSpecNames = ref([]) +const specValuesMap = ref({}) +const currentSpecName = ref(null) +const currentSpecValues = ref([]) +const defaultStock = ref(0) + +// 计算可用的规格名称(有规格值的) +const availableSpecNames = computed(() => { + return specNames.value.filter(specName => + specValues.value.some(value => value.spec_name_id === specName.id) + ) }) const rules = { @@ -408,8 +772,26 @@ const rules = { { required: true, message: '请输入商品名称', trigger: 'blur' }, { min: 2, max: 100, message: '商品名称长度在 2 到 100 个字符', trigger: 'blur' } ], - category: [ - { required: true, message: '请选择商品分类', trigger: 'change' } + primary_category: [ + { required: true, message: '请选择一级分类', trigger: 'change' } + ], + secondary_categories: [ + { + validator: (rule, value, callback) => { + // 如果选择了一级分类但没有对应的二级分类,则不需要选择二级分类 + if (form.primary_category && secondaryCategories.value.length === 0) { + callback() + return + } + // 如果有二级分类可选,但没有选择任何二级分类,则验证失败 + // if (form.primary_category && secondaryCategories.value.length > 0 && (!value || value.length === 0)) { + // callback(new Error('请至少选择一个二级分类')) + // return + // } + callback() + }, + trigger: 'change' + } ], price: [ { required: true, message: '请输入商品原价', trigger: 'blur' }, @@ -453,9 +835,207 @@ const rules = { ], status: [ { required: true, message: '请选择商品状态', trigger: 'change' } + ], + free_shipping: [ + { required: true, message: '请选择是否包邮', trigger: 'change' } ] } +// 规格管理相关函数 +const getSpecValueCount = (specNameId) => { + return specValues.value.filter(value => value.spec_name_id === specNameId).length +} + +const getTagType = (index) => { + const types = ['primary', 'success', 'info', 'warning', 'danger'] + return types[index % types.length] +} + +const showSpecNamesDialog = () => { + specNamesDialogVisible.value = true +} + +const showGenerateCombinationsDialog = () => { + generateCombinationsDialogVisible.value = true +} + +const deleteSpecName = async (specName) => { + try { + await api.specifications.deleteSpecName(specName.id) + await loadSpecNames() + await loadSpecValues() + ElMessage.success('删除规格维度成功') + } catch (error) { + ElMessage.error('删除规格维度失败') + } +} + +// 添加规格名称 +const addSpecName = async () => { + if (!newSpecName.value.trim()) { + ElMessage.warning('请输入规格名称') + return + } + + try { + await api.specifications.createSpecName({ name: newSpecName.value.trim(), display_name: newSpecName.value.trim() }) + newSpecName.value = '' + await loadSpecNames() + ElMessage.success('添加规格维度成功') + } catch (error) { + ElMessage.error('添加规格维度失败') + } +} + +// 管理规格值 +const manageSpecValues = async (specName) => { + currentSpecName.value = specName + await loadCurrentSpecValues(specName.id) + specValuesDialogVisible.value = true +} + +// 加载当前规格名称的规格值 +const loadCurrentSpecValues = async (specNameId) => { + try { + const response = await api.specifications.getSpecValues(specNameId) + currentSpecValues.value = response.data || [] + } catch (error) { + console.error('加载规格值失败:', error) + currentSpecValues.value = [] + } +} + +// 添加规格值 +const addSpecValue = async () => { + if (!newSpecValue.value.trim()) { + ElMessage.warning('请输入规格值') + return + } + + if (!currentSpecName.value) { + ElMessage.error('未选择规格名称') + return + } + + try { + await api.specifications.createSpecValue({ + spec_name_id: currentSpecName.value.id, + value: newSpecValue.value.trim(), + display_value: newSpecValue.value.trim() + }) + newSpecValue.value = '' + await loadCurrentSpecValues(currentSpecName.value.id) + await loadSpecValues() // 重新加载所有规格值 + ElMessage.success('添加规格值成功') + } catch (error) { + ElMessage.error('添加规格值失败') + } +} + +// 删除规格值 +const deleteSpecValue = async (specValue) => { + try { + await api.specifications.deleteSpecValue(specValue.id) + await loadCurrentSpecValues(currentSpecName.value.id) + await loadSpecValues() // 重新加载所有规格值 + ElMessage.success('删除规格值成功') + } catch (error) { + ElMessage.error('删除规格值失败') + } +} + +// 计算组合数量 +const calculateCombinationsCount = () => { + if (selectedSpecNames.value.length === 0) return 0 + + let count = 1 + selectedSpecNames.value.forEach(specNameId => { + const valueCount = getSpecValueCount(specNameId) + count *= valueCount + }) + return count +} + +// 生成规格组合 +const generateCombinations = async () => { + if (selectedSpecNames.value.length === 0) { + ElMessage.warning('请选择至少一个规格维度') + return + } + + try { + const productId = route.params.id || 'new' // 如果是新建商品,使用 'new' + await api.specifications.generateCombinations({ + product_id: productId, + spec_name_ids: selectedSpecNames.value, + default_stock: defaultStock.value + }) + + await loadCombinations() + generateCombinationsDialogVisible.value = false + selectedSpecNames.value = [] + defaultStock.value = 0 + ElMessage.success('生成规格组合成功') + } catch (error) { + ElMessage.error('生成规格组合失败') + } +} + +// 加载规格名称 +const loadSpecNames = async () => { + try { + const { data } = await api.specifications.getSpecNames() + specNames.value = data.data || [] + } catch (error) { + console.error('加载规格名称失败:', error) + specNames.value = [] + } +} + +// 加载规格值 +const loadSpecValues = async () => { + try { + // 获取所有规格值,与 ProductSpecCombinations.vue 保持一致 + const { data } = await api.specifications.getSpecValues() + specValues.value = data.data || [] + } catch (error) { + console.error('加载规格值失败:', error) + specValues.value = [] + } +} + +// 加载规格组合 +const loadCombinations = async () => { + if (!isEdit.value) return + + try { + const { data } = await api.specifications.getCombinations(route.params.id) + combinations.value = data.data || [] + } catch (error) { + console.error('加载规格组合失败:', error) + combinations.value = [] + } +} + +// 验证规格要求 +const validateSpecs = () => { + if (specNames.value.length === 0) { + ElMessage.error('请至少添加一个规格维度') + return false + } + + const hasValues = specNames.value.some(specName => + specValues.value.some(value => value.spec_name_id === specName.id) + ) + + if (!hasValues) { + ElMessage.error('请为规格维度添加规格值') + return false + } + + return true +} + // 加载商品数据(编辑模式) const loadProduct = async () => { if (!isEdit.value) return @@ -473,12 +1053,11 @@ const loadProduct = async () => { return } - // 将后端字段映射到前端字段名 - console.log(form,product); - + // 将前端字段映射到后端字段名 Object.assign(form, { name: product.name || '', - category: product.category || '', + primary_category: null, + secondary_categories: [], price: product.price || 0, points: product.points_price || 0, // 后端 points_price -> 前端 points rongdou_price: product.rongdou_price || 0, @@ -491,29 +1070,68 @@ const loadProduct = async () => { payment_methods: product.payment_methods || [], description: product.description || '', details: product.details || '', - status: product.status || 'active' + status: product.status || 'active', + free_shipping: product.free_shipping || false }) + + // 处理分类数据(API返回为对象数组,需提取category_id并匹配本地分类) + if (product.category && Array.isArray(product.category) && product.category.length > 0) { + // 统一使用字符串比较,避免类型不一致(数字/字符串)导致匹配失败 + const categoryIdStrs = product.category.map(c => String(c.category_id)) + const selectedCategories = categories.value.filter(c => categoryIdStrs.includes(String(c.id))) + + // 分离一级/二级分类 + const primaryCats = selectedCategories.filter(c => c.level === 1) + const secondaryCats = selectedCategories.filter(c => c.level === 2) + + if (primaryCats.length > 0) { + form.primary_category = primaryCats[0].id + if (secondaryCats.length > 0) { + form.secondary_categories = secondaryCats.map(c => c.id) + } + } else if (secondaryCats.length > 0) { + const parentId = secondaryCats[0].parent_id + form.primary_category = parentId + form.secondary_categories = secondaryCats.map(c => c.id) + } + } + console.log('表单数据:', form); } catch (error) { - ElMessage.error('加载商品信息失败'); - console.error('加载商品信息失败:', error); + ElMessage.error('加载商品信息失败',error) // router.back() } } +const loadCategories = async () => { + try { + const res = await api.categories.getCategories() + categories.value = res.data.data.categories + } catch (error) { + ElMessage.error(error.message) + } +} + // 提交表单 const submitForm = async () => { if (!formRef.value) return try { await formRef.value.validate() + + // 验证规格要求(仅在创建模式下) + if (!isEdit.value && !validateSpecs()) { + return + } + loading.value = true // 将前端字段映射到后端期望的字段名 const submitData = { name: form.name, - category: form.category, + category: form.secondary_categories.length > 0 ? form.secondary_categories : [form.primary_category], + level: form.secondary_categories.length > 0 ? 2 : 1, // 根据选择的分类类型设置level price: form.price, points_price: form.points, // 前端 points -> 后端 points_price rongdou_price: form.rongdou_price || 0, @@ -526,7 +1144,8 @@ const submitForm = async () => { payment_methods: JSON.stringify(form.payment_methods), description: form.description, details: form.details, - status: form.status + status: form.status, + free_shipping: form.free_shipping } console.log(submitData); @@ -541,6 +1160,7 @@ const submitForm = async () => { router.push('/products') } catch (error) { + console.log(error); if (error.response?.data?.message) { ElMessage.error(error.response.data.message) } else { @@ -612,8 +1232,20 @@ const handleDetailsChange = (content) => { } } -onMounted(() => { - loadProduct() +// 查看商品审核资质 +const checkQualification = () => { + console.log(route.params.id) +} + +onMounted(async () => { + // 先加载分类,确保编辑模式下分类默认值能正确匹配 + await loadCategories() + await loadProduct() + await loadSpecNames() + await loadSpecValues() + if (isEdit.value) { + await loadCombinations() + } }) @@ -768,7 +1400,8 @@ onMounted(() => { } /* 发布设置样式优化 - 开关组件 */ -.status-switch-container { +.status-switch-container, +.shipping-switch-container { display: flex; flex-direction: column; gap: 12px; @@ -778,7 +1411,8 @@ onMounted(() => { border-radius: 6px; } -.status-switch-container .status-desc { +.status-switch-container .status-desc, +.shipping-switch-container .shipping-desc { font-size: 12px; color: #6c757d; line-height: 1.4; @@ -1100,4 +1734,169 @@ onMounted(() => { margin-right: 4px; font-weight: 600; } + +/* 规格管理样式 */ +.spec-management-section { + margin-bottom: 24px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.section-header h4 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #495057; +} + +.spec-names-list { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 16px; +} + +.spec-name-tag { + padding: 8px 12px; + font-size: 14px; + border-radius: 6px; + background: #e3f2fd; + border: 1px solid #bbdefb; + color: #1976d2; +} + +.spec-name-count { + margin-left: 8px; + font-size: 12px; + color: #666; + font-weight: normal; +} + +.spec-combinations-section { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid #e9ecef; +} + +.combinations-preview { + margin-top: 16px; +} + +.spec-tag { + margin-right: 8px; + margin-bottom: 4px; +} + +.more-combinations { + text-align: center; + padding: 12px; + background: #f8f9fa; + border-radius: 6px; + margin-top: 8px; +} + +.required-mark { + color: #dc3545; + font-weight: 600; + margin-left: 4px; +} + +/* 对话框样式 */ +.spec-names-dialog { + padding: 16px 0; +} + +.add-spec-name { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid #eee; +} + +.existing-spec-names h4 { + margin: 0 0 16px 0; + color: #333; + font-size: 16px; + font-weight: 600; +} + +.spec-names-grid { + display: grid; + gap: 12px; +} + +.spec-name-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.spec-name-info { + display: flex; + align-items: center; + gap: 8px; +} + +.spec-name-text { + font-weight: 500; + color: #333; +} + +.spec-name-count { + color: #6c757d; + font-size: 12px; +} + +.spec-name-actions { + display: flex; + gap: 8px; +} + +.spec-values-dialog { + padding: 16px 0; +} + +.add-spec-value { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid #eee; +} + +.existing-spec-values h4 { + margin: 0 0 16px 0; + color: #333; + font-size: 16px; + font-weight: 600; +} + +.spec-values-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.spec-value-tag { + margin: 0; +} + +.generate-combinations-dialog { + padding: 16px 0; +} + +.spec-name-checkbox { + display: block; + margin-bottom: 12px; +} + +.preview-info { + margin-top: 16px; +} \ No newline at end of file diff --git a/src/views/Products.vue b/src/views/Products.vue index 0ef7014..2e48f23 100644 --- a/src/views/Products.vue +++ b/src/views/Products.vue @@ -23,8 +23,8 @@
@@ -32,6 +32,7 @@ + @@ -56,9 +57,9 @@ - + @@ -70,16 +71,23 @@ - + + + + @@ -88,7 +96,7 @@ {{ formatDate(row.created_at) }} - +