完成了分类管理界面编写

This commit is contained in:
puzvv
2025-12-29 18:38:40 +08:00
parent 39c1dd34b1
commit 70f2e96e2b
3 changed files with 541 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ import type {
ProductDTO,
ProductPageParams,
ProductPageResponse,
ProductCategoryDTO,
} from "@/types/product";
// 获取商品列表
@@ -66,3 +67,29 @@ export const updateProductStatus = (id: number, status: number) => {
method: "put",
});
};
// 新增商品分类
export const addProductCategory = (data: ProductCategoryDTO) => {
return http({
url: "/product/category",
method: "post",
data,
});
};
// 更新商品分类
export const updateProductCategory = (data: ProductCategoryDTO) => {
return http({
url: "/product/category",
method: "put",
data,
});
};
// 删除商品分类
export const deleteProductCategory = (id: number) => {
return http({
url: `/product/category/${id}`,
method: "delete",
});
};

View File

@@ -0,0 +1,502 @@
<template>
<ManageLayout>
<div class="category-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="showAddCategoryDialog = true">
<i class="fa fa-plus mr-1"></i> 添加分类
</el-button>
</div>
<!-- 分类表格 -->
<el-table
:data="paginatedCategories"
style="width: 100%"
v-loading="loading"
border
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="分类名称" />
<el-table-column prop="level" label="层级" width="100">
<template #default="{ row }">
<el-tag :type="getLevelType(row.level)">
{{ getLevelText(row.level) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="parentId" label="父分类ID" width="120">
<template #default="{ row }">
{{ row.parentId === 0 ? "无(一级分类)" : row.parentId }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="300">
<template #default="{ row }">
<el-button size="small" @click="editCategory(row)">编辑</el-button>
<el-button
size="small"
type="primary"
@click="addChildCategory(row)"
>
添加子分类
</el-button>
<el-button size="small" type="danger" @click="deleteCategory(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt-6 flex justify-center">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="filteredCategories.length"
/>
</div>
<!-- 添加/编辑分类对话框 -->
<el-dialog
:title="
isEditing ? '编辑分类' : isAddingChild ? '添加子分类' : '添加分类'
"
v-model="showAddCategoryDialog"
width="600px"
@close="handleDialogClose"
>
<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="父分类" prop="parentId">
<el-select
v-model="categoryForm.parentId"
placeholder="选择父分类(不选则为一级分类)"
:disabled="isAddingChild"
>
<el-option label="无(一级分类)" :value="0" />
<el-option
v-for="category in allCategories"
:key="category.id"
:label="category.name"
:value="category.id"
/>
</el-select>
</el-form-item>
<el-form-item label="图标URL">
<el-input
v-model="categoryForm.icon"
placeholder="分类图标URL可选"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="showAddCategoryDialog = false">取消</el-button>
<el-button type="primary" @click="submitCategoryForm"
>确定</el-button
>
</div>
</template>
</el-dialog>
</div>
</ManageLayout>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getProductCategories,
addProductCategory,
updateProductCategory,
deleteProductCategory,
getProductSubCategories,
} from "@/apis/product";
import { ProductCategory, ProductCategoryDTO } from "@/types/product";
import ManageLayout from "@/layouts/ManageLayout.vue";
import { useRouter } from "vue-router";
import { useUserStore } from "@/stores/UserStore";
// 获取用户存储和路由
const userStore = useUserStore();
const router = useRouter();
// 响应式数据
const loading = ref(false);
const searchKeyword = ref("");
const showAddCategoryDialog = ref(false);
const isEditing = ref(false);
const isAddingChild = ref(false);
const categoryFormRef = ref();
// 分页数据
const currentPage = ref(1);
const pageSize = ref(10);
// 分类表单
const categoryForm = ref<ProductCategoryDTO>({
id: undefined,
parentId: 0,
name: "",
sort: 0,
icon: "",
level: 1,
});
// 所有分类数据
const allCategories = ref<ProductCategory[]>([]);
const categories = ref<ProductCategory[]>([]);
// 分类表单验证规则
const categoryFormRules = reactive({
name: [
{ required: true, message: "请输入分类名称", trigger: "blur" },
{
min: 2,
max: 20,
message: "分类名称长度应在2-20个字符之间",
trigger: "blur",
},
],
parentId: [{ required: false, message: "请选择父分类", trigger: "change" }],
});
// 计算属性:根据搜索关键词过滤分类
const filteredCategories = computed(() => {
if (!searchKeyword.value) return categories.value;
return categories.value.filter((category) =>
category.name?.includes(searchKeyword.value),
);
});
// 计算属性:当前页的分类数据
const paginatedCategories = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredCategories.value.slice(start, end);
});
// 添加关闭对话框的处理函数
const handleDialogClose = () => {
// 关闭对话框时重置表单为初始状态
resetCategoryForm();
isEditing.value = false;
isAddingChild.value = false;
};
// 重置分类表单为初始状态
const resetCategoryForm = () => {
categoryForm.value = {
id: undefined,
parentId: 0,
name: "",
sort: 0,
icon: "",
level: 1,
};
};
// 获取分类列表(包括子分类)
const fetchCategories = async () => {
loading.value = true;
try {
// 获取所有顶级分类
const response = await getProductCategories();
if (response.code === 200) {
const topLevelCategories = response.data || [];
// 递归获取子分类
const allCategoryData: ProductCategory[] = [...topLevelCategories];
const fetchSubCategories = async (categories: ProductCategory[]) => {
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);
await fetchSubCategories(subCategories); // 递归获取子分类
}
}
} catch (error) {
console.error(`获取分类 ${category.id} 的子分类失败:`, error);
}
}
};
await fetchSubCategories(topLevelCategories);
categories.value = allCategoryData;
allCategories.value = [...allCategoryData]; // 用于下拉框选择
}
} catch (error) {
console.error("获取分类列表错误:", error);
ElMessage.error("获取分类列表失败");
} finally {
loading.value = false;
}
};
// 获取层级类型
const getLevelType = (level: number) => {
switch (level) {
case 1:
return "primary";
case 2:
return "success";
case 3:
return "warning";
default:
return "info";
}
};
// 获取层级文本
const getLevelText = (level: number) => {
switch (level) {
case 1:
return "一级";
case 2:
return "二级";
case 3:
return "三级";
default:
return "未知";
}
};
// 格式化日期
const formatDate = (dateString: string | (string | number)[]) => {
if (!dateString) return "";
try {
let date: Date;
// 如果是数组格式 [2025, 12, 20, 16, 41, 46]
if (Array.isArray(dateString)) {
const [year, month, day, hours = 0, minutes = 0, seconds = 0] =
dateString as number[];
date = new Date(year, month - 1, day, hours, minutes, seconds); // 月份需要减1
} else {
// 如果是字符串格式
date = new Date(dateString);
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (e) {
console.error("日期格式化错误:", e);
return dateString as string;
}
};
// 搜索处理
const handleSearch = () => {
currentPage.value = 1; // 重置到第一页
};
// 分页大小改变
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
};
// 当前页改变
const handleCurrentChange = (page: number) => {
currentPage.value = page;
};
// 编辑分类
const editCategory = (category: ProductCategory) => {
isEditing.value = true;
isAddingChild.value = false;
// 复制分类数据但排除level和sort字段的自动计算
categoryForm.value = {
...category,
level: category.level, // 保留编辑时的层级
sort: category.sort, // 保留编辑时的排序
};
showAddCategoryDialog.value = true;
};
// 添加子分类
const addChildCategory = (parentCategory: ProductCategory) => {
isEditing.value = false;
isAddingChild.value = true;
// 初始化子分类表单
categoryForm.value = {
id: undefined,
parentId: parentCategory.id,
name: "",
icon: "",
level: parentCategory.level + 1 > 3 ? 3 : parentCategory.level + 1, // 自动计算层级最大为3级
sort: 0, // 默认排序值
};
showAddCategoryDialog.value = true;
};
// 删除分类
const deleteCategory = async (category: ProductCategory) => {
try {
await ElMessageBox.confirm(
`确定要删除分类 "${category.name}" 吗?此操作不可恢复!`,
"警告",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
},
);
// 调用删除分类API
const response = await deleteProductCategory(category.id);
if (response.code === 200) {
// 从本地数据中移除已删除的分类
categories.value = categories.value.filter((c) => c.id !== category.id);
allCategories.value = allCategories.value.filter(
(c) => c.id !== category.id,
);
ElMessage.success("删除成功");
} else {
ElMessage.error(response.message || "删除失败");
}
} catch (error) {
console.error("删除分类错误:", error);
if (error !== "cancel") {
ElMessage.error("操作失败");
}
}
};
// 提交分类表单
const submitCategoryForm = async () => {
try {
// 验证表单
const form = categoryFormRef.value;
if (!form) return;
await form.validate();
// 自动计算层级和排序
if (!isEditing.value) {
// 添加新分类时自动计算
// 计算层级如果parentId为0则为1级否则根据父分类的层级+1
if (categoryForm.value.parentId === 0) {
categoryForm.value.level = 1;
} else {
const parentCategory = allCategories.value.find(
(cat) => cat.id === categoryForm.value.parentId,
);
if (parentCategory) {
categoryForm.value.level =
parentCategory.level + 1 > 3 ? 3 : parentCategory.level + 1;
} else {
categoryForm.value.level = 1; // 默认为1级
}
}
// 计算排序:获取同级分类的最大排序值+1
const siblingCategories = categories.value.filter(
(cat) => cat.parentId === categoryForm.value.parentId,
);
categoryForm.value.sort =
siblingCategories.length > 0
? Math.max(...siblingCategories.map((cat) => cat.sort)) + 1
: 1;
}
if (isEditing.value) {
// 更新分类
const response = await updateProductCategory(categoryForm.value);
if (response.code === 200) {
// 更新本地数据 - 重新获取分类列表以确保数据同步
fetchCategories();
ElMessage.success("更新成功");
} else {
ElMessage.error(response.message || "更新失败");
return;
}
} else {
// 添加新分类
const response = await addProductCategory(categoryForm.value);
if (response.code === 200) {
ElMessage.success("添加成功");
} else {
ElMessage.error(response.message || "添加失败");
return;
}
}
showAddCategoryDialog.value = false;
// 注意:这里不重置表单,因为对话框关闭事件会处理重置
fetchCategories(); // 重新获取分类列表
} catch (error) {
console.error("提交分类表单错误:", error);
if (error) {
console.log(error);
} else {
ElMessage.error("提交失败");
}
}
};
// 初始化
onMounted(() => {
if (userStore.userInfo?.isAdmin !== 1) {
ElMessage.error("您没有管理员权限!");
router.push("/");
}
fetchCategories();
});
</script>
<style scoped>
.category-manage-page {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.dialog-footer {
text-align: right;
}
</style>

View File

@@ -15,7 +15,7 @@ export interface ProductListItem {
// 商品详情
export interface ProductDetail {
id: number;
userId?: number;
userId?: number; //商家ID
categoryId: number;
categoryName: string;
name: string;
@@ -89,3 +89,14 @@ export interface ProductDTO {
sort?: number; // 排序
skuList?: ProductSku[]; // 规格列表
}
// 商品分类DTO (用于创建和更新)
export interface ProductCategoryDTO {
id?: number; // 分类ID修改时使用
parentId: number; // 父分类ID
name: string; // 分类名称
sort: number; // 排序
icon?: string; // 图标URL
level: number; // 层级
status?: number; // 状态
}