From fe1fd57c82bdc614949fc695070893a7a4fb3224 Mon Sep 17 00:00:00 2001 From: puzvv <1@> Date: Tue, 30 Dec 2025 21:17:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E7=BC=96=E8=BE=91=E5=95=86=E5=93=81=E5=92=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=95=86=E5=93=81=E5=8A=9F=E8=83=BD=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/admin/ProductPage.vue | 382 +++++++++++++++++++++++++++++--- 1 file changed, 356 insertions(+), 26 deletions(-) diff --git a/src/pages/admin/ProductPage.vue b/src/pages/admin/ProductPage.vue index ad560f0..81aed6a 100644 --- a/src/pages/admin/ProductPage.vue +++ b/src/pages/admin/ProductPage.vue @@ -88,6 +88,7 @@ :title="isEditing ? '编辑商品' : '添加商品'" v-model="showAddProductDialog" width="600px" + @close="resetProductForm" > - - - + style="width: 100%; margin-bottom: 10px" + clearable + @change="onCategoryChange" + /> + + 创建新分类 + - + + + + + + + + + + + + + + + + + + + + @@ -162,9 +220,18 @@ import { updateProduct, updateProductStatus, deleteProductAPI, + addProductCategory, + uploadFile, + getProductSubCategories, + getProductDetail, } from "@/apis/product"; import { getUserList } from "@/apis/user"; // 导入获取用户列表的API -import { ProductListItem, ProductCategory, ProductDTO } from "@/types/product"; +import { + ProductListItem, + ProductCategory, + ProductDTO, + ProductCategoryDTO, +} from "@/types/product"; import { UserDTO } from "@/types/user"; // 导入用户类型 import ManageLayout from "@/layouts/ManageLayout.vue"; import { useRouter } from "vue-router"; @@ -178,8 +245,11 @@ const router = useRouter(); const loading = ref(false); const searchKeyword = ref(""); const showAddProductDialog = ref(false); +const showCategoryDialog = ref(false); const isEditing = ref(false); const productFormRef = ref(); +const categoryFormRef = ref(); +const pendingImageFile = ref(null); // 新增:暂存待上传的图片文件 // 分页数据 const currentPage = ref(1); @@ -198,11 +268,25 @@ const productForm = ref({ sort: 0, }); +// 分类表单 +const categoryForm = ref({ + name: "", + parentId: 0, // 修改为数字类型,支持级联选择 + sort: 0, +}); + // 所有商品数据 const products = ref([]); -// 商品分类 -const categories = ref([]); +// 所有分类 +const allCategories = ref([]); +const categoriesTree = ref< + (ProductCategory & { children?: ProductCategory[] })[] +>([]); // 新增:树形结构的分类数据 + +// 当前选中的分类 +const selectedCategory = ref(null); +const selectedCategoryPath = ref([]); // 新增:选中的分类路径(用于级联选择器) // 所有用户数据(用于获取商家名称) const users = ref([]); @@ -229,6 +313,19 @@ const productFormRules = reactive({ ], }); +// 分类表单验证规则 +const categoryFormRules = reactive({ + name: [ + { required: true, message: "请输入分类名称", trigger: "blur" }, + { + min: 1, + max: 20, + message: "分类名称长度应在1-20个字符之间", + trigger: "blur", + }, + ], +}); + // 计算属性:根据搜索关键词过滤商品 const filteredProducts = computed(() => { if (!searchKeyword.value) return products.value; @@ -272,9 +369,39 @@ const fetchProducts = async () => { // 获取商品分类 const fetchCategories = async () => { try { + // 获取所有顶级分类 const response = await getProductCategories(); if (response.code === 200) { - categories.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); @@ -311,18 +438,73 @@ 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) => { +const editProduct = async (product: ProductListItem) => { isEditing.value = true; + + // 首先尝试从列表数据获取,如果categoryId为空则获取详细信息 + let fullProduct = product; + if (!product.categoryId) { + try { + // 获取商品详细信息以获取完整的分类ID + const detailResponse = await getProductDetail(product.id); + if (detailResponse.code === 200) { + fullProduct = detailResponse.data as ProductListItem; + } + } catch (error) { + console.error("获取商品详情失败:", error); + ElMessage.error("获取商品详情失败"); + } + } + // 复制商品数据,处理可能的undefined值 productForm.value = { - id: product.id, - categoryId: product.categoryId || 0, // 确保是number类型 - name: product.name, - mainImage: product.mainImage, - price: product.price, - stock: product.stock || 0, + id: fullProduct.id, + categoryId: fullProduct.categoryId || 0, // 确保是number类型 + name: fullProduct.name, + mainImage: fullProduct.mainImage, + price: fullProduct.price, + stock: fullProduct.stock || 0, + subtitle: fullProduct.subtitle || "", + description: fullProduct.description || "", + sort: fullProduct.sort || 0, }; + + // 设置选中的分类路径 - 仿照分类管理页面的方式 + if (fullProduct.categoryId) { + selectedCategory.value = fullProduct.categoryId; + // 根据商品的分类ID找到对应的分类路径 + const path = findCategoryPath(categoriesTree.value, fullProduct.categoryId); + selectedCategoryPath.value = path || [fullProduct.categoryId]; + } else { + selectedCategory.value = null; + selectedCategoryPath.value = []; + } + showAddProductDialog.value = true; }; @@ -406,9 +588,31 @@ const submitProductForm = async () => { await form.validate(); + // 设置分类ID + 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) { // 更新本地数据 - 重新获取商品列表以确保数据同步 @@ -421,8 +625,8 @@ const submitProductForm = async () => { } else { // 添加新商品 const response = await saveProduct({ - ...productForm.value, - userId: userStore.userInfo?.id, // 假设当前用户为商家 + ...finalProductData, + userId: userStore.userInfo?.id, // 当前用户为商家 }); if (response.code === 200) { @@ -434,6 +638,7 @@ const submitProductForm = async () => { } showAddProductDialog.value = false; + resetProductForm(); // 关闭对话框后重置表单 fetchProducts(); // 重新获取商品列表 } catch (error) { console.error("提交商品表单错误:", error); @@ -445,6 +650,124 @@ 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 { + // 验证表单 + const form = categoryFormRef.value; + if (!form) return; + + await form.validate(); + + // 添加新分类 - 创建符合 ProductCategoryDTO 类型的对象 + const newCategory: ProductCategoryDTO = { + name: categoryForm.value.name, + sort: categoryForm.value.sort, + parentId: categoryForm.value.parentId || 0, // 使用级联选择器的值 + level: 1, // 初始设置为1,后端会根据父分类自动计算 + }; + + const response = await addProductCategory(newCategory); + + if (response.code === 200) { + ElMessage.success("分类创建成功"); + + // 重新获取分类列表 + await fetchCategories(); + + // 选中新创建的分类 + const newCategoryItem = allCategories.value.find( + (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; + } else { + ElMessage.error(response.message || "分类创建失败"); + } + } catch (error) { + console.error("提交分类表单错误:", error); + ElMessage.error("提交失败"); + } +}; + +// 分类选择改变事件 +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.userInfo?.isAdmin !== 1) { @@ -468,4 +791,11 @@ onMounted(() => { .dialog-footer { text-align: right; } + +.avatar-uploader .avatar { + width: 178px; + height: 178px; + display: block; + object-fit: cover; +}