优化了页面显示效果,实现了商品删除功能

This commit is contained in:
puzvv
2025-12-30 17:46:50 +08:00
parent b0c37cc6af
commit a9887b63df
7 changed files with 757 additions and 11 deletions

View File

@@ -18,6 +18,15 @@ export const getProductList = (params: ProductPageParams) => {
}); });
}; };
// 获取商家商品列表
export const getMerchantProductList = (params: ProductPageParams) => {
return http<ProductPageResponse>({
url: "/product/page/merchant",
method: "get",
params,
});
};
// 获取商品分类 // 获取商品分类
export const getProductCategories = () => { export const getProductCategories = () => {
return http<ProductCategory[]>({ return http<ProductCategory[]>({
@@ -51,6 +60,15 @@ export const saveProduct = (data: ProductDTO) => {
}); });
}; };
// 删除商品
export const deleteProductAPI = (id: number) => {
return http({
url: `/product`,
method: "delete",
params: { id },
});
};
// 更新商品 // 更新商品
export const updateProduct = (data: ProductDTO) => { export const updateProduct = (data: ProductDTO) => {
return http({ return http({

View File

@@ -13,6 +13,11 @@ const isLoggedIn = computed(() => {
return !!userStore.token; return !!userStore.token;
}); });
// 计算属性:判断用户是否为管理员
const isAdmin = computed(() => {
return userStore.userInfo?.isAdmin === 1;
});
// 计算属性:获取用户昵称 // 计算属性:获取用户昵称
const userNickname = computed(() => { const userNickname = computed(() => {
return userStore.userInfo?.nickname || userStore.userInfo?.username || "用户"; return userStore.userInfo?.nickname || userStore.userInfo?.username || "用户";
@@ -42,6 +47,24 @@ const goToProfile = () => {
router.push(`/user/profile/${userStore.userInfo?.id}`); router.push(`/user/profile/${userStore.userInfo?.id}`);
isMobileMenuOpen.value = false; isMobileMenuOpen.value = false;
}; };
// 跳转到管理界面
const goToAdmin = () => {
router.push("/admin");
isMobileMenuOpen.value = false;
};
// 跳转到地址管理
const goToAddress = () => {
router.push("/user/address");
isMobileMenuOpen.value = false;
};
// 跳转到商品管理界面
const goToProductManage = () => {
router.push("/user/product");
isMobileMenuOpen.value = false;
};
</script> </script>
<template> <template>
@@ -81,12 +104,14 @@ const goToProfile = () => {
@click.prevent="$router.push('/order/content')" @click.prevent="$router.push('/order/content')"
>我的订单</a >我的订单</a
> >
<!-- 管理员导航项 -->
<a <a
v-if="isAdmin"
href="#" href="#"
class="text-gray-600 hover:text-blue-600 transition-colors" class="text-gray-600 hover:text-blue-600 transition-colors"
@click.prevent="$router.push('/user/address')" @click.prevent="goToAdmin"
> >
地址管理 管理界面
</a> </a>
</nav> </nav>
@@ -105,6 +130,15 @@ const goToProfile = () => {
<el-dropdown-item @click="goToProfile" <el-dropdown-item @click="goToProfile"
>个人中心</el-dropdown-item >个人中心</el-dropdown-item
> >
<el-dropdown-item @click="goToProductManage"
>我的商品</el-dropdown-item
>
<el-dropdown-item @click="goToAddress"
>我的地址</el-dropdown-item
>
<el-dropdown-item v-if="isAdmin" @click="goToAdmin"
>管理界面</el-dropdown-item
>
<el-dropdown-item @click="handleLogout" <el-dropdown-item @click="handleLogout"
>退出登录</el-dropdown-item >退出登录</el-dropdown-item
> >
@@ -166,6 +200,14 @@ const goToProfile = () => {
class="py-2 text-gray-600 hover:text-blue-600 transition-colors" class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
>我的订单</a >我的订单</a
> >
<a
v-if="isAdmin"
href="#"
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
@click.prevent="goToAdmin"
>
管理界面
</a>
<div class="pt-2 border-t border-gray-100"> <div class="pt-2 border-t border-gray-100">
<template v-if="isLoggedIn"> <template v-if="isLoggedIn">
@@ -176,6 +218,25 @@ const goToProfile = () => {
> >
个人中心 个人中心
</button> </button>
<button
@click="goToProductManage"
class="w-full mb-2 px-4 py-2 text-left text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
我的商品
</button>
<button
@click="goToAddress"
class="w-full mb-2 px-4 py-2 text-left text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
我的地址
</button>
<button
v-if="isAdmin"
@click="goToAdmin"
class="w-full mb-2 px-4 py-2 text-left text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
管理界面
</button>
<button <button
@click="handleLogout" @click="handleLogout"
class="w-full px-4 py-2 text-left text-red-600 hover:bg-red-50 rounded-lg transition-colors" class="w-full px-4 py-2 text-left text-red-600 hover:bg-red-50 rounded-lg transition-colors"

View File

@@ -8,6 +8,15 @@
<nav class="flex-1 p-2"> <nav class="flex-1 p-2">
<ul class="space-y-1"> <ul class="space-y-1">
<li>
<router-link
to="/"
class="flex items-center p-3 rounded-lg hover:bg-gray-700 transition-colors"
>
<i class="fa-solid fa-home mr-3"></i>
<span>返回首页</span>
</router-link>
</li>
<li> <li>
<router-link <router-link
to="/admin/user" to="/admin/user"

View File

@@ -161,6 +161,7 @@ import {
saveProduct, saveProduct,
updateProduct, updateProduct,
updateProductStatus, updateProductStatus,
deleteProductAPI,
} from "@/apis/product"; } from "@/apis/product";
import { getUserList } from "@/apis/user"; // 导入获取用户列表的API import { getUserList } from "@/apis/user"; // 导入获取用户列表的API
import { ProductListItem, ProductCategory, ProductDTO } from "@/types/product"; import { ProductListItem, ProductCategory, ProductDTO } from "@/types/product";
@@ -365,7 +366,7 @@ const toggleProductStatus = async (product: ProductListItem) => {
} }
}; };
// 删除商品(这里我们暂时用下架代替删除) // 删除商品
const deleteProduct = async (product: ProductListItem) => { const deleteProduct = async (product: ProductListItem) => {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
@@ -378,15 +379,15 @@ const deleteProduct = async (product: ProductListItem) => {
}, },
); );
// 实际应用中应该调用删除API,这里我们使用下架来代替 // 调用删除商品API
const response = await updateProductStatus(product.id, 0); const response = await deleteProductAPI(product.id);
if (response.code === 200) { if (response.code === 200) {
// 从本地数据中过滤掉已下架的商品 // 从本地数据中删除商品
products.value = products.value.filter((p) => p.id !== product.id); products.value = products.value.filter((p) => p.id !== product.id);
ElMessage.success("操作成功"); ElMessage.success("删除成功");
} else { } else {
ElMessage.error(response.message || "操作失败"); ElMessage.error(response.message || "删除失败");
} }
} catch (error) { } catch (error) {
console.error("删除商品错误:", error); console.error("删除商品错误:", error);

View File

@@ -46,11 +46,16 @@ const fetchProductList = async () => {
size: pageSize.value, size: pageSize.value,
categoryId: activeCategoryId.value || undefined, categoryId: activeCategoryId.value || undefined,
name: searchKeyword.value || undefined, name: searchKeyword.value || undefined,
status: 1, // 只获取上架的商品
}); });
if (response.code === 200) { if (response.code === 200) {
productList.value = response.data.list; // 过滤掉下架商品
total.value = response.data.total; const activeProducts = response.data.list.filter(
(product) => product.status === 1,
);
productList.value = activeProducts;
total.value = activeProducts.length;
} else { } else {
productList.value = []; productList.value = [];
total.value = 0; total.value = 0;
@@ -397,7 +402,7 @@ onMounted(() => {
<el-col :span="24"> <el-col :span="24">
<!-- 商品列表 --> <!-- 商品列表 -->
<div class="product-list"> <div class="product-list">
<el-row :gutter="20" style="row-gap: 24px;"> <el-row :gutter="20" style="row-gap: 24px">
<template v-if="loading"> <template v-if="loading">
<el-col v-for="n in pageSize" :key="n" :span="8"> <el-col v-for="n in pageSize" :key="n" :span="8">
<el-card class="product-card"> <el-card class="product-card">

View File

@@ -0,0 +1,648 @@
<template>
<div class="product-manage-page">
<!-- 搜索和操作按钮 -->
<div class="mb-6 flex justify-between items-center">
<div class="flex items-center">
<el-input
v-model="searchKeyword"
placeholder="搜索商品名称或分类"
style="width: 300px; margin-right: 16px"
@keyup.enter="handleSearch"
>
<template #prefix>
<i class="fa fa-search"></i>
</template>
</el-input>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</div>
<el-button type="success" @click="showAddProductDialog = true">
<i class="fa fa-plus mr-1"></i> 添加商品
</el-button>
</div>
<!-- 商品列表 -->
<div class="product-list mb-6">
<el-row :gutter="20" style="row-gap: 24px">
<template v-if="loading">
<el-col v-for="n in pageSize" :key="n" :span="8">
<el-card class="product-card">
<el-skeleton :rows="3" animated />
</el-card>
</el-col>
</template>
<template v-else-if="paginatedProducts.length > 0">
<el-col
v-for="product in paginatedProducts"
:key="product.id"
:span="8"
>
<!-- 商品卡片 -->
<el-card class="product-card h-full" shadow="hover">
<div class="product-image mb-3 relative">
<el-image
:src="product.mainImage || '/placeholder.jpg'"
class="w-full h-48 object-cover rounded"
fit="cover"
lazy
>
<template #error>
<div class="image-slot">
<i
class="el-icon-picture-outline text-4xl text-gray-300"
></i>
</div>
</template>
</el-image>
<!-- 商品状态标签 -->
<div class="status-tag absolute top-2 right-2">
<el-tag :type="product.status === 1 ? 'success' : 'danger'">
{{ product.status === 1 ? "上架" : "下架" }}
</el-tag>
</div>
</div>
<div class="product-info">
<h3
class="product-title text-sm font-medium mb-2 line-clamp-2 h-12"
:title="product.name"
>
{{ product.name }}
</h3>
<div class="product-price text-red-600 font-bold mb-2">
¥{{ product.price }}
</div>
<div class="product-sales text-gray-500 text-xs mb-3">
销量: {{ product.sales }} | 库存: {{ product.stock || 0 }}
</div>
<!-- 操作按钮 -->
<div class="product-actions flex flex-wrap gap-2">
<el-button size="small" @click="editProduct(product)"
>编辑</el-button
>
<el-button
size="small"
:type="product.status === 1 ? 'warning' : 'success'"
@click="toggleProductStatus(product)"
>
{{ product.status === 1 ? "下架" : "上架" }}
</el-button>
<el-button
size="small"
type="danger"
@click="deleteProduct(product)"
>
删除
</el-button>
</div>
</div>
</el-card>
</el-col>
</template>
<template v-else>
<el-col :span="24">
<el-empty description="暂无商品数据" />
</el-col>
</template>
</el-row>
</div>
<!-- 分页 -->
<div class="mt-6 flex justify-center">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[6, 12, 24, 36]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="filteredProducts.length"
/>
</div>
<!-- 添加/编辑商品对话框 -->
<el-dialog
:title="isEditing ? '编辑商品' : '添加商品'"
v-model="showAddProductDialog"
width="600px"
>
<el-form
:model="productForm"
:rules="productFormRules"
ref="productFormRef"
label-width="100px"
>
<el-form-item label="商品名称" prop="name">
<el-input v-model="productForm.name" />
</el-form-item>
<el-form-item label="分类" prop="categoryId">
<el-select
v-model="selectedCategory"
placeholder="请选择分类或创建新分类"
@change="onCategoryChange"
style="width: 100%; margin-bottom: 10px"
>
<el-option
v-for="category in allCategories"
:key="category.id"
:label="category.name"
:value="category.id"
/>
</el-select>
<el-button @click="showCategoryDialog = true" type="primary" plain>
<i class="fa fa-plus mr-1"></i> 创建新分类
</el-button>
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<el-input v-model="productForm.subtitle" />
</el-form-item>
<el-form-item label="主图" prop="mainImage">
<el-input v-model="productForm.mainImage" placeholder="图片URL" />
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input-number
v-model="productForm.price"
:precision="2"
:min="0"
/>
</el-form-item>
<el-form-item label="库存" prop="stock">
<el-input-number v-model="productForm.stock" :min="0" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="productForm.description"
type="textarea"
:rows="3"
placeholder="商品描述"
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="productForm.sort" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="showAddProductDialog = false">取消</el-button>
<el-button type="primary" @click="submitProductForm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 创建分类对话框 -->
<el-dialog title="创建新分类" v-model="showCategoryDialog" width="400px">
<el-form
:model="categoryForm"
:rules="categoryFormRules"
ref="categoryFormRef"
label-width="100px"
>
<el-form-item label="分类名称" prop="name">
<el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="categoryForm.sort" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="showCategoryDialog = false">取消</el-button>
<el-button type="primary" @click="submitCategoryForm">创建</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from "vue";
import {
ElMessage,
ElMessageBox,
ElRow,
ElCol,
ElCard,
ElImage,
ElEmpty,
ElSkeleton,
ElTag,
} from "element-plus";
import {
getMerchantProductList,
getProductCategories,
saveProduct,
deleteProductAPI,
updateProduct,
updateProductStatus,
addProductCategory,
} from "@/apis/product";
import {
ProductListItem,
ProductCategory,
ProductDTO,
ProductCategoryDTO,
} from "@/types/product";
import { useRouter } from "vue-router";
import { useUserStore } from "@/stores/UserStore";
// 获取用户存储和路由
const userStore = useUserStore();
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 currentPage = ref(1);
const pageSize = ref(12); // 改为每页显示12个商品
// 商品表单
const productForm = ref<ProductDTO>({
id: undefined,
categoryId: 0, // 设置默认值为0
name: "",
subtitle: "",
mainImage: "",
description: "",
price: 0,
stock: 0,
sort: 0,
});
// 分类表单
const categoryForm = ref({
name: "",
sort: 0,
});
// 所有商品数据
const products = ref<ProductListItem[]>([]);
// 所有分类
const allCategories = ref<ProductCategory[]>([]);
// 当前选中的分类
const selectedCategory = ref<number | null>(null);
// 商品表单验证规则
const productFormRules = reactive({
name: [
{ required: true, message: "请输入商品名称", trigger: "blur" },
{
min: 2,
max: 50,
message: "商品名称长度应在2-50个字符之间",
trigger: "blur",
},
],
categoryId: [{ required: true, message: "请选择分类", trigger: "change" }],
price: [
{ required: true, message: "请输入价格", trigger: "blur" },
{ type: "number", min: 0, message: "价格不能小于0", trigger: "blur" },
],
stock: [
{ required: true, message: "请输入库存", trigger: "blur" },
{ type: "number", min: 0, message: "库存不能小于0", trigger: "blur" },
],
});
// 分类表单验证规则
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;
return products.value.filter(
(product) =>
product.name?.includes(searchKeyword.value) ||
product.categoryName?.includes(searchKeyword.value),
);
});
// 计算属性:当前页的商品数据
const paginatedProducts = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredProducts.value.slice(start, end);
});
// 获取用户商品列表
const fetchProducts = async () => {
loading.value = true;
try {
const response = await getMerchantProductList({
page: 1,
size: 1000, // 获取所有商品
currentUserId: userStore.userInfo?.id, // 获取当前用户商品
});
products.value = response.data.list || [];
} catch (error) {
console.error("获取商品列表错误:", error);
ElMessage.error("获取商品列表失败");
} finally {
loading.value = false;
}
};
// 获取商品分类
const fetchCategories = async () => {
try {
const response = await getProductCategories();
if (response.code === 200) {
allCategories.value = response.data || [];
}
} catch (error) {
console.error("获取商品分类错误:", error);
ElMessage.error("获取商品分类失败");
}
};
// 搜索处理
const handleSearch = () => {
currentPage.value = 1; // 重置到第一页
};
// 分页大小改变
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
};
// 当前页改变
const handleCurrentChange = (page: number) => {
currentPage.value = page;
};
// 编辑商品
const editProduct = (product: ProductListItem) => {
isEditing.value = true;
// 复制商品数据处理可能的undefined值
productForm.value = {
id: product.id,
categoryId: product.categoryId || 0, // 确保是number类型
name: product.name,
mainImage: product.mainImage,
price: product.price,
stock: product.stock || 0,
subtitle: product.subtitle || "",
description: product.description || "",
sort: product.sort || 0,
};
// 设置选中的分类
selectedCategory.value = product.categoryId || null;
showAddProductDialog.value = true;
};
// 切换商品状态(上下架)
const toggleProductStatus = async (product: ProductListItem) => {
try {
await ElMessageBox.confirm(
`确定要${product.status === 1 ? "下架" : "上架"}商品 "${product.name}" 吗?`,
"确认操作",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
},
);
// 调用更新商品状态API
const response = await updateProductStatus(
product.id,
product.status === 1 ? 0 : 1,
);
if (response.code === 200) {
// 更新本地数据
const index = products.value.findIndex((p) => p.id === product.id);
if (index !== -1) {
products.value[index] = {
...product,
status: product.status === 1 ? 0 : 1,
};
}
ElMessage.success(`${product.status === 1 ? "下架" : "上架"}成功`);
} else {
ElMessage.error(response.message || "操作失败");
}
} catch (error) {
console.error("切换商品状态错误:", error);
if (error !== "cancel") {
ElMessage.error("操作失败");
}
}
};
// 删除商品
const deleteProduct = async (product: ProductListItem) => {
try {
await ElMessageBox.confirm(
`确定要删除商品 "${product.name}" 吗?此操作不可恢复!`,
"警告",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
},
);
// 调用删除商品API
const response = await deleteProductAPI(product.id);
if (response.code === 200) {
// 从本地数据中删除商品
products.value = products.value.filter((p) => p.id !== product.id);
ElMessage.success("删除成功");
} else {
ElMessage.error(response.message || "删除失败");
}
} catch (error) {
console.error("删除商品错误:", error);
if (error !== "cancel") {
ElMessage.error("操作失败");
}
}
};
// 提交商品表单
const submitProductForm = async () => {
try {
// 验证表单
const form = productFormRef.value;
if (!form) return;
await form.validate();
// 设置分类ID
productForm.value.categoryId = selectedCategory.value || 0;
if (isEditing.value) {
// 更新商品
const response = await updateProduct(productForm.value);
if (response.code === 200) {
// 更新本地数据 - 重新获取商品列表以确保数据同步
fetchProducts();
ElMessage.success("更新成功");
} else {
ElMessage.error(response.message || "更新失败");
return;
}
} else {
// 添加新商品
const response = await saveProduct({
...productForm.value,
userId: userStore.userInfo?.id, // 当前用户为商家
});
if (response.code === 200) {
ElMessage.success("添加成功");
} else {
ElMessage.error(response.message || "添加失败");
return;
}
}
showAddProductDialog.value = false;
fetchProducts(); // 重新获取商品列表
} catch (error) {
console.error("提交商品表单错误:", error);
if (error) {
console.log(error);
} else {
ElMessage.error("提交失败");
}
}
};
// 提交分类表单
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: 0, // 顶级分类
level: 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,
);
if (newCategoryItem) {
selectedCategory.value = newCategoryItem.id;
}
// 清空表单并关闭对话框
categoryForm.value = {
name: "",
sort: 0,
};
showCategoryDialog.value = false;
} else {
ElMessage.error(response.message || "分类创建失败");
}
} catch (error) {
console.error("提交分类表单错误:", error);
ElMessage.error("提交失败");
}
};
// 分类选择改变事件
const onCategoryChange = (value: number | null) => {
if (value) {
productForm.value.categoryId = value;
}
};
// 初始化
onMounted(() => {
if (!userStore.token || !userStore.userInfo) {
ElMessage.error("请先登录!");
router.push("/user/login");
return;
}
fetchProducts();
fetchCategories();
});
</script>
<style scoped>
.product-manage-page {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.product-card {
border-radius: 12px;
overflow: hidden;
transition: all 0.3s;
height: 100%;
display: flex;
flex-direction: column;
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #f5f7fa;
color: #909399;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.dialog-footer {
text-align: right;
}
.status-tag {
z-index: 10;
}
</style>

View File

@@ -10,6 +10,9 @@ export interface ProductListItem {
categoryId?: number; categoryId?: number;
userId?: number; userId?: number;
stock?: number; stock?: number;
subtitle?: string;
description?: string;
sort?: number;
} }
// 商品详情 // 商品详情
@@ -63,6 +66,7 @@ export interface ProductPageParams {
categoryId?: number; categoryId?: number;
name?: string; name?: string;
status?: number; status?: number;
currentUserId?: number; // 添加商家ID参数
} }
// 商品分页响应数据 // 商品分页响应数据