实现了商品添加编辑时上传图片到oss存储桶中

This commit is contained in:
puzvv
2025-12-30 19:42:55 +08:00
parent a9887b63df
commit f89327b609
2 changed files with 221 additions and 26 deletions

View File

@@ -111,3 +111,18 @@ export const deleteProductCategory = (id: number) => {
method: "delete", 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",
},
});
};

View File

@@ -124,6 +124,7 @@
:title="isEditing ? '编辑商品' : '添加商品'" :title="isEditing ? '编辑商品' : '添加商品'"
v-model="showAddProductDialog" v-model="showAddProductDialog"
width="600px" width="600px"
@close="resetProductForm"
> >
<el-form <el-form
:model="productForm" :model="productForm"
@@ -135,19 +136,15 @@
<el-input v-model="productForm.name" /> <el-input v-model="productForm.name" />
</el-form-item> </el-form-item>
<el-form-item label="分类" prop="categoryId"> <el-form-item label="分类" prop="categoryId">
<el-select <el-cascader
v-model="selectedCategory" v-model="selectedCategoryPath"
placeholder="请选择分类或创建新分类" :options="categoriesTree"
@change="onCategoryChange" :props="{ value: 'id', label: 'name', children: 'children' }"
placeholder="请选择分类"
style="width: 100%; margin-bottom: 10px" style="width: 100%; margin-bottom: 10px"
> clearable
<el-option @change="onCategoryChange"
v-for="category in allCategories" />
:key="category.id"
:label="category.name"
:value="category.id"
/>
</el-select>
<el-button @click="showCategoryDialog = true" type="primary" plain> <el-button @click="showCategoryDialog = true" type="primary" plain>
<i class="fa fa-plus mr-1"></i> 创建新分类 <i class="fa fa-plus mr-1"></i> 创建新分类
</el-button> </el-button>
@@ -156,7 +153,21 @@
<el-input v-model="productForm.subtitle" /> <el-input v-model="productForm.subtitle" />
</el-form-item> </el-form-item>
<el-form-item label="主图" prop="mainImage"> <el-form-item label="主图" prop="mainImage">
<el-input v-model="productForm.mainImage" placeholder="图片URL" /> <el-upload
class="avatar-uploader"
:auto-upload="false"
:show-file-list="false"
:on-change="handleImageChange"
:before-upload="beforeImageUpload"
accept="image/*"
>
<template v-if="productForm.mainImage">
<img :src="productForm.mainImage" class="avatar" alt="主图预览" />
</template>
<template v-else>
<el-button type="primary" plain>选择图片</el-button>
</template>
</el-upload>
</el-form-item> </el-form-item>
<el-form-item label="价格" prop="price"> <el-form-item label="价格" prop="price">
<el-input-number <el-input-number
@@ -182,7 +193,7 @@
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="showAddProductDialog = false">取消</el-button> <el-button @click="cancelProductForm">取消</el-button>
<el-button type="primary" @click="submitProductForm">确定</el-button> <el-button type="primary" @click="submitProductForm">确定</el-button>
</div> </div>
</template> </template>
@@ -199,6 +210,16 @@
<el-form-item label="分类名称" prop="name"> <el-form-item label="分类名称" prop="name">
<el-input v-model="categoryForm.name" placeholder="请输入分类名称" /> <el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
</el-form-item> </el-form-item>
<el-form-item label="父分类">
<el-cascader
v-model="categoryForm.parentId"
:options="categoriesTree"
:props="{ value: 'id', label: 'name', children: 'children' }"
placeholder="选择父分类(不选则为一级分类)"
:clearable="true"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="排序"> <el-form-item label="排序">
<el-input-number v-model="categoryForm.sort" :min="0" /> <el-input-number v-model="categoryForm.sort" :min="0" />
</el-form-item> </el-form-item>
@@ -234,6 +255,8 @@ import {
updateProduct, updateProduct,
updateProductStatus, updateProductStatus,
addProductCategory, addProductCategory,
uploadFile,
getProductSubCategories,
} from "@/apis/product"; } from "@/apis/product";
import { import {
ProductListItem, ProductListItem,
@@ -256,6 +279,7 @@ const showCategoryDialog = ref(false);
const isEditing = ref(false); const isEditing = ref(false);
const productFormRef = ref(); const productFormRef = ref();
const categoryFormRef = ref(); const categoryFormRef = ref();
const pendingImageFile = ref<File | null>(null); // 新增:暂存待上传的图片文件
// 分页数据 // 分页数据
const currentPage = ref(1); const currentPage = ref(1);
@@ -277,6 +301,7 @@ const productForm = ref<ProductDTO>({
// 分类表单 // 分类表单
const categoryForm = ref({ const categoryForm = ref({
name: "", name: "",
parentId: 0, // 修改为数字类型,支持级联选择
sort: 0, sort: 0,
}); });
@@ -285,9 +310,13 @@ const products = ref<ProductListItem[]>([]);
// 所有分类 // 所有分类
const allCategories = ref<ProductCategory[]>([]); const allCategories = ref<ProductCategory[]>([]);
const categoriesTree = ref<
(ProductCategory & { children?: ProductCategory[] })[]
>([]); // 新增:树形结构的分类数据
// 当前选中的分类 // 当前选中的分类
const selectedCategory = ref<number | null>(null); const selectedCategory = ref<number | null>(null);
const selectedCategoryPath = ref<number[]>([]); // 新增:选中的分类路径(用于级联选择器)
// 商品表单验证规则 // 商品表单验证规则
const productFormRules = reactive({ const productFormRules = reactive({
@@ -363,9 +392,39 @@ const fetchProducts = async () => {
// 获取商品分类 // 获取商品分类
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
// 获取所有顶级分类
const response = await getProductCategories(); const response = await getProductCategories();
if (response.code === 200) { 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) { } catch (error) {
console.error("获取商品分类错误:", error); console.error("获取商品分类错误:", error);
@@ -389,6 +448,30 @@ const handleCurrentChange = (page: number) => {
currentPage.value = page; 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 = (product: ProductListItem) => {
isEditing.value = true; isEditing.value = true;
@@ -405,8 +488,15 @@ const editProduct = (product: ProductListItem) => {
sort: product.sort || 0, 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; showAddProductDialog.value = true;
}; };
@@ -491,11 +581,30 @@ const submitProductForm = async () => {
await form.validate(); await form.validate();
// 设置分类ID // 设置分类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) { if (isEditing.value) {
// 更新商品 // 更新商品
const response = await updateProduct(productForm.value); const response = await updateProduct(finalProductData);
if (response.code === 200) { if (response.code === 200) {
// 更新本地数据 - 重新获取商品列表以确保数据同步 // 更新本地数据 - 重新获取商品列表以确保数据同步
@@ -508,7 +617,7 @@ const submitProductForm = async () => {
} else { } else {
// 添加新商品 // 添加新商品
const response = await saveProduct({ const response = await saveProduct({
...productForm.value, ...finalProductData,
userId: userStore.userInfo?.id, // 当前用户为商家 userId: userStore.userInfo?.id, // 当前用户为商家
}); });
@@ -521,6 +630,7 @@ const submitProductForm = async () => {
} }
showAddProductDialog.value = false; showAddProductDialog.value = false;
resetProductForm(); // 关闭对话框后重置表单
fetchProducts(); // 重新获取商品列表 fetchProducts(); // 重新获取商品列表
} catch (error) { } catch (error) {
console.error("提交商品表单错误:", 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 () => { const submitCategoryForm = async () => {
try { try {
@@ -545,8 +680,8 @@ const submitCategoryForm = async () => {
const newCategory: ProductCategoryDTO = { const newCategory: ProductCategoryDTO = {
name: categoryForm.value.name, name: categoryForm.value.name,
sort: categoryForm.value.sort, sort: categoryForm.value.sort,
parentId: 0, // 顶级分类 parentId: categoryForm.value.parentId || 0, // 使用级联选择器的值
level: 1, // 顶级分类层级 level: 1, // 初始设置为1后端会根据父分类自动计算
}; };
const response = await addProductCategory(newCategory); const response = await addProductCategory(newCategory);
@@ -559,15 +694,20 @@ const submitCategoryForm = async () => {
// 选中新创建的分类 // 选中新创建的分类
const newCategoryItem = allCategories.value.find( 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) { if (newCategoryItem) {
selectedCategory.value = newCategoryItem.id; selectedCategory.value = newCategoryItem.id;
const path = findCategoryPath(categoriesTree.value, newCategoryItem.id);
selectedCategoryPath.value = path || [newCategoryItem.id];
} }
// 清空表单并关闭对话框 // 清空表单并关闭对话框
categoryForm.value = { categoryForm.value = {
name: "", name: "",
parentId: 0,
sort: 0, sort: 0,
}; };
showCategoryDialog.value = false; showCategoryDialog.value = false;
@@ -581,12 +721,45 @@ const submitCategoryForm = async () => {
}; };
// 分类选择改变事件 // 分类选择改变事件
const onCategoryChange = (value: number | null) => { const onCategoryChange = (value: number[]) => {
if (value) { if (value && value.length > 0) {
productForm.value.categoryId = value; // 获取路径中最后一个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(() => { onMounted(() => {
if (!userStore.token || !userStore.userInfo) { if (!userStore.token || !userStore.userInfo) {
@@ -645,4 +818,11 @@ onMounted(() => {
.status-tag { .status-tag {
z-index: 10; z-index: 10;
} }
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
object-fit: cover;
}
</style> </style>