From f89327b60970afd44df4160b0c35425883c5e3a5 Mon Sep 17 00:00:00 2001 From: puzvv <1@> Date: Tue, 30 Dec 2025 19:42:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=E5=95=86=E5=93=81?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BC=96=E8=BE=91=E6=97=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=88=B0oss=E5=AD=98=E5=82=A8=E6=A1=B6?= =?UTF-8?q?=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/product.ts | 15 +++ src/pages/user/ProductPage.vue | 232 +++++++++++++++++++++++++++++---- 2 files changed, 221 insertions(+), 26 deletions(-) diff --git a/src/apis/product.ts b/src/apis/product.ts index 613d7fc..69066bc 100644 --- a/src/apis/product.ts +++ b/src/apis/product.ts @@ -111,3 +111,18 @@ export const deleteProductCategory = (id: number) => { method: "delete", }); }; + +// 上传文件 +export const uploadFile = (file: File) => { + const formData = new FormData(); + formData.append("file", file); + + return http<{ fileUrl: string }>({ + url: "/api/upload/file", + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); +}; diff --git a/src/pages/user/ProductPage.vue b/src/pages/user/ProductPage.vue index 3608a4c..34cd0c3 100644 --- a/src/pages/user/ProductPage.vue +++ b/src/pages/user/ProductPage.vue @@ -124,6 +124,7 @@ :title="isEditing ? '编辑商品' : '添加商品'" v-model="showAddProductDialog" width="600px" + @close="resetProductForm" > - - - + clearable + @change="onCategoryChange" + /> 创建新分类 @@ -156,7 +153,21 @@ - + + + + @@ -199,6 +210,16 @@ + + + @@ -234,6 +255,8 @@ import { updateProduct, updateProductStatus, addProductCategory, + uploadFile, + getProductSubCategories, } from "@/apis/product"; import { ProductListItem, @@ -256,6 +279,7 @@ const showCategoryDialog = ref(false); const isEditing = ref(false); const productFormRef = ref(); const categoryFormRef = ref(); +const pendingImageFile = ref(null); // 新增:暂存待上传的图片文件 // 分页数据 const currentPage = ref(1); @@ -277,6 +301,7 @@ const productForm = ref({ // 分类表单 const categoryForm = ref({ name: "", + parentId: 0, // 修改为数字类型,支持级联选择 sort: 0, }); @@ -285,9 +310,13 @@ const products = ref([]); // 所有分类 const allCategories = ref([]); +const categoriesTree = ref< + (ProductCategory & { children?: ProductCategory[] })[] +>([]); // 新增:树形结构的分类数据 // 当前选中的分类 const selectedCategory = ref(null); +const selectedCategoryPath = ref([]); // 新增:选中的分类路径(用于级联选择器) // 商品表单验证规则 const productFormRules = reactive({ @@ -363,9 +392,39 @@ const fetchProducts = async () => { // 获取商品分类 const fetchCategories = async () => { try { + // 获取所有顶级分类 const response = await getProductCategories(); if (response.code === 200) { - allCategories.value = response.data || []; + const topLevelCategories = response.data || []; + + // 递归获取子分类 + const allCategoryData: ProductCategory[] = [...topLevelCategories]; + const treeData = [...topLevelCategories] as CategoryTreeNode[]; + + const fetchSubCategories = async (categories: CategoryTreeNode[]) => { + for (const category of categories) { + try { + const subResponse = await getProductSubCategories(category.id); + if (subResponse.code === 200) { + const subCategories = subResponse.data || []; + if (subCategories.length > 0) { + allCategoryData.push(...subCategories); + + // 将子分类添加到父分类的子分类数组中 + category.children = subCategories as CategoryTreeNode[]; + + await fetchSubCategories(category.children); // 递归获取子分类 + } + } + } catch (error) { + console.error(`获取分类 ${category.id} 的子分类失败:`, error); + } + } + }; + + await fetchSubCategories(treeData); + allCategories.value = [...allCategoryData]; + categoriesTree.value = [...treeData]; // 用于树形选择器 } } catch (error) { console.error("获取商品分类错误:", error); @@ -389,6 +448,30 @@ const handleCurrentChange = (page: number) => { currentPage.value = page; }; +// 为树形结构定义类型 +type CategoryTreeNode = ProductCategory & { children?: CategoryTreeNode[] }; + +// 递归查找分类路径 +const findCategoryPath = ( + categories: CategoryTreeNode[], + targetId: number, + path: number[] = [], +): number[] | null => { + for (const category of categories) { + const currentPath = [...path, category.id]; + if (category.id === targetId) { + return currentPath; + } + if (category.children && category.children.length > 0) { + const result = findCategoryPath(category.children, targetId, currentPath); + if (result) { + return result; + } + } + } + return null; +}; + // 编辑商品 const editProduct = (product: ProductListItem) => { isEditing.value = true; @@ -405,8 +488,15 @@ const editProduct = (product: ProductListItem) => { sort: product.sort || 0, }; - // 设置选中的分类 - selectedCategory.value = product.categoryId || null; + // 设置选中的分类路径 + if (product.categoryId) { + selectedCategory.value = product.categoryId; + const path = findCategoryPath(categoriesTree.value, product.categoryId); + selectedCategoryPath.value = path || [product.categoryId]; + } else { + selectedCategory.value = null; + selectedCategoryPath.value = []; + } showAddProductDialog.value = true; }; @@ -491,11 +581,30 @@ const submitProductForm = async () => { await form.validate(); // 设置分类ID - productForm.value.categoryId = selectedCategory.value || 0; + const finalProductData = { ...productForm.value }; + finalProductData.categoryId = selectedCategory.value || 0; + + // 如果有待上传的图片文件,先上传 + if (pendingImageFile.value) { + try { + const response = await uploadFile(pendingImageFile.value); + if (response.code === 200) { + finalProductData.mainImage = response.data.fileUrl; + ElMessage.success("图片上传成功"); + } else { + ElMessage.error(response.message || "图片上传失败"); + return; + } + } catch (error) { + console.error("图片上传错误:", error); + ElMessage.error("图片上传失败"); + return; + } + } if (isEditing.value) { // 更新商品 - const response = await updateProduct(productForm.value); + const response = await updateProduct(finalProductData); if (response.code === 200) { // 更新本地数据 - 重新获取商品列表以确保数据同步 @@ -508,7 +617,7 @@ const submitProductForm = async () => { } else { // 添加新商品 const response = await saveProduct({ - ...productForm.value, + ...finalProductData, userId: userStore.userInfo?.id, // 当前用户为商家 }); @@ -521,6 +630,7 @@ const submitProductForm = async () => { } showAddProductDialog.value = false; + resetProductForm(); // 关闭对话框后重置表单 fetchProducts(); // 重新获取商品列表 } catch (error) { console.error("提交商品表单错误:", error); @@ -532,6 +642,31 @@ const submitProductForm = async () => { } }; +// 重置商品表单 +const resetProductForm = () => { + productForm.value = { + id: undefined, + categoryId: 0, + name: "", + subtitle: "", + mainImage: "", + description: "", + price: 0, + stock: 0, + sort: 0, + }; + selectedCategory.value = null; + selectedCategoryPath.value = []; // 重置路径 + isEditing.value = false; + pendingImageFile.value = null; // 清除暂存的文件 +}; + +// 取消添加/编辑商品 +const cancelProductForm = () => { + showAddProductDialog.value = false; + resetProductForm(); // 取消时也重置表单 +}; + // 提交分类表单 const submitCategoryForm = async () => { try { @@ -545,8 +680,8 @@ const submitCategoryForm = async () => { const newCategory: ProductCategoryDTO = { name: categoryForm.value.name, sort: categoryForm.value.sort, - parentId: 0, // 顶级分类 - level: 1, // 顶级分类层级 + parentId: categoryForm.value.parentId || 0, // 使用级联选择器的值 + level: 1, // 初始设置为1,后端会根据父分类自动计算 }; const response = await addProductCategory(newCategory); @@ -559,15 +694,20 @@ const submitCategoryForm = async () => { // 选中新创建的分类 const newCategoryItem = allCategories.value.find( - (cat) => cat.name === categoryForm.value.name, + (cat) => + cat.name === categoryForm.value.name && + cat.parentId === (categoryForm.value.parentId || 0), ); if (newCategoryItem) { selectedCategory.value = newCategoryItem.id; + const path = findCategoryPath(categoriesTree.value, newCategoryItem.id); + selectedCategoryPath.value = path || [newCategoryItem.id]; } // 清空表单并关闭对话框 categoryForm.value = { name: "", + parentId: 0, sort: 0, }; showCategoryDialog.value = false; @@ -581,12 +721,45 @@ const submitCategoryForm = async () => { }; // 分类选择改变事件 -const onCategoryChange = (value: number | null) => { - if (value) { - productForm.value.categoryId = value; +const onCategoryChange = (value: number[]) => { + if (value && value.length > 0) { + // 获取路径中最后一个ID作为实际选中的分类ID + selectedCategory.value = value[value.length - 1]; + productForm.value.categoryId = selectedCategory.value; + } else { + selectedCategory.value = null; + productForm.value.categoryId = 0; } }; +// 上传图片前的钩子 +const beforeImageUpload = (file: File) => { + const isImage = file.type.startsWith("image/"); + const isLt2M = file.size / 1024 / 1024 < 2; + + if (!isImage) { + ElMessage.error("上传图片只能是图片格式!"); + } + if (!isLt2M) { + ElMessage.error("上传图片大小不能超过 2MB!"); + } + return isImage && isLt2M; +}; + +// 处理图片选择变化 +const handleImageChange = async (file: { raw: File }) => { + if (!beforeImageUpload(file.raw)) { + return; + } + + // 暂存文件,等待提交时上传 + pendingImageFile.value = file.raw; + + // 创建预览URL用于显示 + const previewUrl = URL.createObjectURL(file.raw); + productForm.value.mainImage = previewUrl; +}; + // 初始化 onMounted(() => { if (!userStore.token || !userStore.userInfo) { @@ -645,4 +818,11 @@ onMounted(() => { .status-tag { z-index: 10; } + +.avatar-uploader .avatar { + width: 178px; + height: 178px; + display: block; + object-fit: cover; +}