实现了管理员用户登录校验,完成了用户管理界面和商品管理界面
This commit is contained in:
24
index.html
24
index.html
@@ -1,13 +1,17 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Hucky</title>
|
<title>Hucky</title>
|
||||||
</head>
|
<link
|
||||||
<body>
|
rel="stylesheet"
|
||||||
<div id="app"></div>
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||||
<script type="module" src="/src/main.ts"></script>
|
/>
|
||||||
</body>
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
// ProductListItem,
|
// ProductListItem,
|
||||||
ProductCategory,
|
ProductCategory,
|
||||||
ProductDetail,
|
ProductDetail,
|
||||||
|
ProductDTO,
|
||||||
ProductPageParams,
|
ProductPageParams,
|
||||||
ProductPageResponse,
|
ProductPageResponse,
|
||||||
} from "@/types/product";
|
} from "@/types/product";
|
||||||
@@ -39,3 +40,29 @@ export const getProductDetail = (id: number) => {
|
|||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 新增商品
|
||||||
|
export const saveProduct = (data: ProductDTO) => {
|
||||||
|
return http({
|
||||||
|
url: "/product",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新商品
|
||||||
|
export const updateProduct = (data: ProductDTO) => {
|
||||||
|
return http({
|
||||||
|
url: "/product",
|
||||||
|
method: "put",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新商品状态(上下架)
|
||||||
|
export const updateProductStatus = (id: number, status: number) => {
|
||||||
|
return http({
|
||||||
|
url: `/product/status/${id}/${status}`,
|
||||||
|
method: "put",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
LoginResponse,
|
LoginResponse,
|
||||||
RegisterRequest,
|
RegisterRequest,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
|
UserDTO,
|
||||||
} from "@/types/user.ts";
|
} from "@/types/user.ts";
|
||||||
import { UserAddress, UserAddressForm } from "@/types/address.ts";
|
import { UserAddress, UserAddressForm } from "@/types/address.ts";
|
||||||
|
|
||||||
@@ -51,6 +52,14 @@ export const getUserProfile = (id: number) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取用户列表(管理员)
|
||||||
|
export const getUserList = () => {
|
||||||
|
return http<UserDTO[]>({
|
||||||
|
url: "/user/admin",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
export const updateUserProfile = (data: Partial<UserInfo>) => {
|
export const updateUserProfile = (data: Partial<UserInfo>) => {
|
||||||
return http({
|
return http({
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
NEW_FILE_CODE
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// import { useRouter } from "vue-router";
|
// import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
|||||||
135
src/layouts/ManageLayout.vue
Normal file
135
src/layouts/ManageLayout.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-layout flex h-screen bg-gray-100">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<aside class="w-64 bg-gray-800 text-white flex flex-col">
|
||||||
|
<div class="p-4 border-b border-gray-700">
|
||||||
|
<h1 class="text-xl font-bold">管理员面板</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex-1 p-2">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li>
|
||||||
|
<router-link
|
||||||
|
to="/admin/user"
|
||||||
|
class="flex items-center p-3 rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-users mr-3"></i>
|
||||||
|
<span>用户管理</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<router-link
|
||||||
|
to="/admin/category"
|
||||||
|
class="flex items-center p-3 rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-tags mr-3"></i>
|
||||||
|
<span>分类管理</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<router-link
|
||||||
|
to="/admin/product"
|
||||||
|
class="flex items-center p-3 rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-box mr-3"></i>
|
||||||
|
<span>商品管理</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<router-link
|
||||||
|
to="/admin/orders"
|
||||||
|
class="flex items-center p-3 rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-file-invoice mr-3"></i>
|
||||||
|
<span>订单管理</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="p-4 border-t border-gray-700">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-3">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-full bg-gray-600 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-user-gear"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">{{ userNickname }}</p>
|
||||||
|
<p class="text-xs text-gray-400">管理员</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="flex-1 overflow-auto">
|
||||||
|
<header class="bg-white shadow-sm p-4 flex justify-between items-center">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800">
|
||||||
|
{{ getCurrentPageTitle }}
|
||||||
|
</h2>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<button class="p-2 rounded-full hover:bg-gray-100">
|
||||||
|
<i class="fa-solid fa-bell text-gray-600"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="handleLogout"
|
||||||
|
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
退出登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { useUserStore } from "@/stores/UserStore";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 计算属性:获取用户昵称
|
||||||
|
const userNickname = computed(() => {
|
||||||
|
return (
|
||||||
|
userStore.userInfo?.nickname || userStore.userInfo?.username || "管理员"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:获取当前页面标题
|
||||||
|
const getCurrentPageTitle = computed(() => {
|
||||||
|
const path = route.path;
|
||||||
|
if (path.includes("/admin/user")) return "用户管理";
|
||||||
|
if (path.includes("/admin/category")) return "分类管理";
|
||||||
|
if (path.includes("/admin/product")) return "商品管理";
|
||||||
|
if (path.includes("/admin/order")) return "订单管理";
|
||||||
|
return "管理员面板";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 用户退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
userStore.clearUserInfo();
|
||||||
|
ElMessage.success("退出登录成功");
|
||||||
|
router.push("/user/login");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-layout {
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
470
src/pages/admin/ProductPage.vue
Normal file
470
src/pages/admin/ProductPage.vue
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
<template>
|
||||||
|
<ManageLayout>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 商品表格 -->
|
||||||
|
<el-table
|
||||||
|
:data="paginatedProducts"
|
||||||
|
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="categoryName" label="分类" />
|
||||||
|
<el-table-column prop="price" label="价格" width="100">
|
||||||
|
<template #default="{ row }"> ¥{{ row.price }} </template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="stock" label="库存" width="100" />
|
||||||
|
<el-table-column prop="sales" label="销量" width="100" />
|
||||||
|
<el-table-column prop="userId" label="商家ID" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.userId }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商家名称" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getUserById(row.userId || 0)?.username || "未知商家" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ row.status === 1 ? "上架" : "下架" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="380">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="editProduct(row)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="row.status === 1 ? 'warning' : 'success'"
|
||||||
|
@click="toggleProductStatus(row)"
|
||||||
|
>
|
||||||
|
{{ row.status === 1 ? "下架" : "上架" }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="deleteProduct(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="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="productForm.categoryId"
|
||||||
|
placeholder="请选择分类"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
:label="category.name"
|
||||||
|
:value="category.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</ManageLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from "vue";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import {
|
||||||
|
getProductList,
|
||||||
|
getProductCategories,
|
||||||
|
saveProduct,
|
||||||
|
updateProduct,
|
||||||
|
updateProductStatus,
|
||||||
|
} from "@/apis/product";
|
||||||
|
import { getUserList } from "@/apis/user"; // 导入获取用户列表的API
|
||||||
|
import { ProductListItem, ProductCategory, ProductDTO } from "@/types/product";
|
||||||
|
import { UserDTO } from "@/types/user"; // 导入用户类型
|
||||||
|
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 showAddProductDialog = ref(false);
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const productFormRef = ref();
|
||||||
|
|
||||||
|
// 分页数据
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
|
||||||
|
// 商品表单
|
||||||
|
const productForm = ref<ProductDTO>({
|
||||||
|
id: undefined,
|
||||||
|
categoryId: 0, // 设置默认值为0
|
||||||
|
name: "",
|
||||||
|
subtitle: "",
|
||||||
|
mainImage: "",
|
||||||
|
description: "",
|
||||||
|
price: 0,
|
||||||
|
stock: 0,
|
||||||
|
sort: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 所有商品数据
|
||||||
|
const products = ref<ProductListItem[]>([]);
|
||||||
|
|
||||||
|
// 商品分类
|
||||||
|
const categories = ref<ProductCategory[]>([]);
|
||||||
|
|
||||||
|
// 所有用户数据(用于获取商家名称)
|
||||||
|
const users = ref<UserDTO[]>([]);
|
||||||
|
|
||||||
|
// 商品表单验证规则
|
||||||
|
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 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据用户ID获取用户信息
|
||||||
|
const getUserById = (userId: number) => {
|
||||||
|
return users.value.find((user) => user.id === userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取商品列表
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getProductList({
|
||||||
|
page: 1,
|
||||||
|
size: 1000, // 获取所有商品
|
||||||
|
});
|
||||||
|
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) {
|
||||||
|
categories.value = response.data || [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取商品分类错误:", error);
|
||||||
|
ElMessage.error("获取商品分类失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取用户列表(用于显示商家名称)
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getUserList();
|
||||||
|
if (response.code === 200) {
|
||||||
|
users.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,
|
||||||
|
};
|
||||||
|
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 updateProductStatus(product.id, 0);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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("提交失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
if (userStore.userInfo?.isAdmin !== 1) {
|
||||||
|
ElMessage.error("您没有管理员权限!");
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
fetchProducts();
|
||||||
|
fetchCategories();
|
||||||
|
fetchUsers(); // 获取用户列表
|
||||||
|
});
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
471
src/pages/admin/UserPage.vue
Normal file
471
src/pages/admin/UserPage.vue
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
<template>
|
||||||
|
<ManageLayout>
|
||||||
|
<div class="user-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="showAddUserDialog = true">
|
||||||
|
<i class="fa fa-plus mr-1"></i> 添加用户
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户表格 -->
|
||||||
|
<el-table
|
||||||
|
:data="paginatedUsers"
|
||||||
|
style="width: 100%"
|
||||||
|
v-loading="loading"
|
||||||
|
border
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="username" label="用户名" />
|
||||||
|
<el-table-column prop="nickname" label="昵称" />
|
||||||
|
<el-table-column prop="phone" label="手机号" />
|
||||||
|
<el-table-column prop="email" label="邮箱" />
|
||||||
|
<el-table-column prop="gender" label="性别" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.gender === 1 ? "男" : row.gender === 0 ? "女" : "未知" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ row.status === 1 ? "启用" : "禁用" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="isAdmin" label="管理员" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.isAdmin === 1 ? 'warning' : 'info'">
|
||||||
|
{{ row.isAdmin === 1 ? "是" : "否" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="380">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="editUser(row)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="row.status === 1 ? 'warning' : 'success'"
|
||||||
|
@click="toggleUserStatus(row)"
|
||||||
|
>
|
||||||
|
{{ row.status === 1 ? "禁用" : "启用" }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:type="row.isAdmin === 1 ? 'info' : 'primary'"
|
||||||
|
@click="toggleAdminStatus(row)"
|
||||||
|
>
|
||||||
|
{{ row.isAdmin === 1 ? "取消管理员" : "设为管理员" }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="deleteUser(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="filteredUsers.length"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加/编辑用户对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
:title="isEditing ? '编辑用户' : '添加用户'"
|
||||||
|
v-model="showAddUserDialog"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
:model="userForm"
|
||||||
|
:rules="userFormRules"
|
||||||
|
ref="userFormRef"
|
||||||
|
label-width="80px"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input v-model="userForm.username" :disabled="isEditing" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称" prop="nickname">
|
||||||
|
<el-input v-model="userForm.nickname" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="userForm.phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="userForm.email" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="性别" prop="gender">
|
||||||
|
<el-radio-group v-model="userForm.gender">
|
||||||
|
<el-radio :label="1">男</el-radio>
|
||||||
|
<el-radio :label="0">女</el-radio>
|
||||||
|
<el-radio :label="2">未知</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password" :required="!isEditing">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.password"
|
||||||
|
type="password"
|
||||||
|
:placeholder="isEditing ? '不填写则不修改密码' : '请输入密码'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="头像">
|
||||||
|
<el-input v-model="userForm.avatar" placeholder="头像URL" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="管理员">
|
||||||
|
<el-switch
|
||||||
|
v-model="userForm.isAdmin"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="showAddUserDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitUserForm">确定</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 { getUserList, updateUserProfile, userRegister } from "@/apis/user";
|
||||||
|
import { UserDTO } from "@/types/user";
|
||||||
|
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 showAddUserDialog = ref(false);
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const userFormRef = ref();
|
||||||
|
|
||||||
|
// 分页数据
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
|
||||||
|
// 用户表单
|
||||||
|
const userForm = ref<UserDTO>({
|
||||||
|
id: undefined,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
avatar: "",
|
||||||
|
nickname: "",
|
||||||
|
gender: 2,
|
||||||
|
status: 1,
|
||||||
|
isAdmin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 所有用户数据
|
||||||
|
const users = ref<UserDTO[]>([]);
|
||||||
|
|
||||||
|
// 用户表单验证规则
|
||||||
|
const userFormRules = reactive({
|
||||||
|
username: [
|
||||||
|
{ required: true, message: "请输入用户名", trigger: "blur" },
|
||||||
|
{
|
||||||
|
min: 3,
|
||||||
|
max: 20,
|
||||||
|
message: "用户名长度应在3-20个字符之间",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: !isEditing.value, message: "请输入密码", trigger: "blur" },
|
||||||
|
{ min: 6, max: 20, message: "密码长度应在6-20个字符之间", trigger: "blur" },
|
||||||
|
],
|
||||||
|
phone: [
|
||||||
|
{ required: true, message: "请输入手机号", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: /^1[3-9]\d{9}$/,
|
||||||
|
message: "请输入正确的手机号格式",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ required: true, message: "请输入邮箱", trigger: "blur" },
|
||||||
|
{ type: "email", message: "请输入正确的邮箱格式", trigger: "blur" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:根据搜索关键词过滤用户
|
||||||
|
const filteredUsers = computed(() => {
|
||||||
|
if (!searchKeyword.value) return users.value;
|
||||||
|
|
||||||
|
return users.value.filter(
|
||||||
|
(user) =>
|
||||||
|
user.username?.includes(searchKeyword.value) ||
|
||||||
|
user.email?.includes(searchKeyword.value) ||
|
||||||
|
user.phone?.includes(searchKeyword.value) ||
|
||||||
|
user.nickname?.includes(searchKeyword.value),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:当前页的用户数据
|
||||||
|
const paginatedUsers = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value;
|
||||||
|
const end = start + pageSize.value;
|
||||||
|
return filteredUsers.value.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getUserList();
|
||||||
|
users.value = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取用户列表错误:", error);
|
||||||
|
ElMessage.error("获取用户列表失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1; // 重置到第一页
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页大小改变
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pageSize.value = size;
|
||||||
|
currentPage.value = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前页改变
|
||||||
|
const handleCurrentChange = (page: number) => {
|
||||||
|
currentPage.value = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
const editUser = (user: UserDTO) => {
|
||||||
|
isEditing.value = true;
|
||||||
|
// 复制用户数据,但不包含密码字段(编辑时不需要密码)
|
||||||
|
userForm.value = {
|
||||||
|
...user,
|
||||||
|
password: "", // 编辑时不显示密码,需要时重新输入
|
||||||
|
};
|
||||||
|
showAddUserDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换用户状态(启用/禁用)
|
||||||
|
const toggleUserStatus = async (user: UserDTO) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要${user.status === 1 ? "禁用" : "启用"}用户 "${user.username}" 吗?`,
|
||||||
|
"确认操作",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 准备更新数据,只更新状态
|
||||||
|
const updateData = {
|
||||||
|
...user,
|
||||||
|
status: user.status === 1 ? 0 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用更新用户信息API
|
||||||
|
await updateUserProfile(updateData);
|
||||||
|
|
||||||
|
// 更新本地数据
|
||||||
|
const index = users.value.findIndex((u) => u.id === user.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
users.value[index] = { ...updateData };
|
||||||
|
}
|
||||||
|
ElMessage.success(`${user.status === 1 ? "禁用" : "启用"}成功`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("切换用户状态错误:", error);
|
||||||
|
if (error !== "cancel") {
|
||||||
|
ElMessage.error("操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换管理员状态
|
||||||
|
const toggleAdminStatus = async (user: UserDTO) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要${user.isAdmin === 1 ? "取消" : "设置"}用户 "${user.username}" 为管理员吗?`,
|
||||||
|
"确认操作",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: user.isAdmin === 1 ? "warning" : "primary",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 准备更新数据,只更新管理员状态
|
||||||
|
const updateData = {
|
||||||
|
...user,
|
||||||
|
isAdmin: user.isAdmin === 1 ? 0 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用更新用户信息API
|
||||||
|
await updateUserProfile(updateData);
|
||||||
|
|
||||||
|
// 更新本地数据
|
||||||
|
const index = users.value.findIndex((u) => u.id === user.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
users.value[index] = { ...updateData };
|
||||||
|
}
|
||||||
|
ElMessage.success(
|
||||||
|
`${user.isAdmin === 1 ? "取消管理员" : "设为管理员"}成功`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("切换管理员状态错误:", error);
|
||||||
|
if (error !== "cancel") {
|
||||||
|
ElMessage.error("操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const deleteUser = async (user: UserDTO) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除用户 "${user.username}" 吗?此操作不可恢复!`,
|
||||||
|
"警告",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 在实际应用中,这里应该调用删除API
|
||||||
|
// 但根据后端设计,我们使用更新状态的方式
|
||||||
|
const updateData = {
|
||||||
|
...user,
|
||||||
|
status: 0, // 设置为禁用状态
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateUserProfile(updateData);
|
||||||
|
|
||||||
|
// 从本地数据中过滤掉已禁用的用户(如果需要完全隐藏的话)
|
||||||
|
users.value = users.value.filter((u) => u.id !== user.id);
|
||||||
|
ElMessage.success("操作成功");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除用户错误:", error);
|
||||||
|
if (error !== "cancel") {
|
||||||
|
ElMessage.error("操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交用户表单
|
||||||
|
const submitUserForm = async () => {
|
||||||
|
try {
|
||||||
|
// 验证表单
|
||||||
|
const form = userFormRef.value;
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
await form.validate();
|
||||||
|
|
||||||
|
if (isEditing.value) {
|
||||||
|
// 更新用户
|
||||||
|
await updateUserProfile(userForm.value);
|
||||||
|
|
||||||
|
// 更新本地数据
|
||||||
|
const index = users.value.findIndex((u) => u.id === userForm.value.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
users.value[index] = { ...userForm.value };
|
||||||
|
}
|
||||||
|
ElMessage.success("更新成功");
|
||||||
|
} else {
|
||||||
|
// 添加新用户 - 使用专门的管理员添加用户API
|
||||||
|
// 如果后端没有专门的API,我们使用注册API,但需要确保密码字段存在
|
||||||
|
if (!userForm.value.password) {
|
||||||
|
ElMessage.error("添加用户时必须设置密码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用注册API来添加新用户
|
||||||
|
const response = await userRegister({
|
||||||
|
username: userForm.value.username,
|
||||||
|
password: userForm.value.password,
|
||||||
|
phone: userForm.value.phone,
|
||||||
|
email: userForm.value.email,
|
||||||
|
nickname: userForm.value.nickname || undefined,
|
||||||
|
gender: userForm.value.gender,
|
||||||
|
avatar: userForm.value.avatar || undefined,
|
||||||
|
isAdmin: userForm.value.isAdmin,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查响应
|
||||||
|
if (response.code === 200) {
|
||||||
|
ElMessage.success("添加成功");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || "添加失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAddUserDialog.value = false;
|
||||||
|
fetchUsers(); // 重新获取用户列表
|
||||||
|
} catch (error) {
|
||||||
|
console.error("提交用户表单错误:", error);
|
||||||
|
if (error) {
|
||||||
|
console.log(error);
|
||||||
|
} else {
|
||||||
|
ElMessage.error("提交失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
if (userStore.userInfo?.isAdmin !== 1) {
|
||||||
|
ElMessage.error("您没有管理员权限!");
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
fetchUsers();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-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>
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
setDefaultAddress,
|
setDefaultAddress,
|
||||||
} from "@/apis/user";
|
} from "@/apis/user";
|
||||||
import type { UserAddress, UserAddressForm } from "@/types/address";
|
import type { UserAddress, UserAddressForm } from "@/types/address";
|
||||||
|
import MainLayout from "@/layouts/MainLayout.vue";
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const addressList = ref<UserAddress[]>([]);
|
const addressList = ref<UserAddress[]>([]);
|
||||||
@@ -203,136 +204,138 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="address-page">
|
<MainLayout>
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="address-page">
|
||||||
<el-card class="address-card">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<template #header>
|
<el-card class="address-card">
|
||||||
<div class="flex justify-between items-center">
|
<template #header>
|
||||||
<span class="text-xl font-bold">地址管理</span>
|
<div class="flex justify-between items-center">
|
||||||
<el-button type="primary" @click="handleAddAddress">
|
<span class="text-xl font-bold">地址管理</span>
|
||||||
<el-icon><plus /></el-icon>
|
<el-button type="primary" @click="handleAddAddress">
|
||||||
新增收货地址
|
<el-icon><plus /></el-icon>
|
||||||
</el-button>
|
新增收货地址
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 地址列表 -->
|
||||||
|
<el-table
|
||||||
|
:data="addressList"
|
||||||
|
v-loading="loading"
|
||||||
|
empty-text="暂无地址信息"
|
||||||
|
stripe
|
||||||
|
>
|
||||||
|
<el-table-column prop="receiverName" label="收货人" width="100" />
|
||||||
|
<el-table-column prop="receiverPhone" label="手机号" width="120" />
|
||||||
|
<el-table-column label="地址">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatAddress(row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="默认地址" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.isDefault === 1" type="success">默认</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="handleEditAddress(row)"
|
||||||
|
>
|
||||||
|
<el-icon><edit /></el-icon>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
title="确定要删除这个地址吗?"
|
||||||
|
@confirm="handleDeleteAddress(row.id)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="danger" link>
|
||||||
|
<el-icon><delete /></el-icon>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
<el-button
|
||||||
|
v-if="row.isDefault !== 1"
|
||||||
|
size="small"
|
||||||
|
type="warning"
|
||||||
|
link
|
||||||
|
@click="handleSetDefault(row.id)"
|
||||||
|
>
|
||||||
|
<el-icon><star /></el-icon>
|
||||||
|
设为默认
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 地址编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="500px"
|
||||||
|
@close="formRef?.resetFields()"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="addressForm"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="收货人" prop="receiverName">
|
||||||
|
<el-input
|
||||||
|
v-model="addressForm.receiverName"
|
||||||
|
placeholder="请输入收货人姓名"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="receiverPhone">
|
||||||
|
<el-input
|
||||||
|
v-model="addressForm.receiverPhone"
|
||||||
|
placeholder="请输入收货人手机号"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="省份" prop="province">
|
||||||
|
<el-input v-model="addressForm.province" placeholder="请输入省份" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="城市" prop="city">
|
||||||
|
<el-input v-model="addressForm.city" placeholder="请输入城市" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="区县" prop="district">
|
||||||
|
<el-input v-model="addressForm.district" placeholder="请输入区县" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细地址" prop="detailAddress">
|
||||||
|
<el-input
|
||||||
|
v-model="addressForm.detailAddress"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入详细地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设为默认">
|
||||||
|
<el-switch
|
||||||
|
v-model="addressForm.isDefault"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</el-dialog>
|
||||||
<!-- 地址列表 -->
|
|
||||||
<el-table
|
|
||||||
:data="addressList"
|
|
||||||
v-loading="loading"
|
|
||||||
empty-text="暂无地址信息"
|
|
||||||
stripe
|
|
||||||
>
|
|
||||||
<el-table-column prop="receiverName" label="收货人" width="100" />
|
|
||||||
<el-table-column prop="receiverPhone" label="手机号" width="120" />
|
|
||||||
<el-table-column label="地址">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ formatAddress(row) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="默认地址" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag v-if="row.isDefault === 1" type="success">默认</el-tag>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
@click="handleEditAddress(row)"
|
|
||||||
>
|
|
||||||
<el-icon><edit /></el-icon>
|
|
||||||
编辑
|
|
||||||
</el-button>
|
|
||||||
<el-popconfirm
|
|
||||||
title="确定要删除这个地址吗?"
|
|
||||||
@confirm="handleDeleteAddress(row.id)"
|
|
||||||
>
|
|
||||||
<template #reference>
|
|
||||||
<el-button size="small" type="danger" link>
|
|
||||||
<el-icon><delete /></el-icon>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
<el-button
|
|
||||||
v-if="row.isDefault !== 1"
|
|
||||||
size="small"
|
|
||||||
type="warning"
|
|
||||||
link
|
|
||||||
@click="handleSetDefault(row.id)"
|
|
||||||
>
|
|
||||||
<el-icon><star /></el-icon>
|
|
||||||
设为默认
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
</div>
|
||||||
|
</MainLayout>
|
||||||
<!-- 地址编辑对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
:title="dialogTitle"
|
|
||||||
width="500px"
|
|
||||||
@close="formRef?.resetFields()"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="addressForm"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="100px"
|
|
||||||
>
|
|
||||||
<el-form-item label="收货人" prop="receiverName">
|
|
||||||
<el-input
|
|
||||||
v-model="addressForm.receiverName"
|
|
||||||
placeholder="请输入收货人姓名"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="手机号" prop="receiverPhone">
|
|
||||||
<el-input
|
|
||||||
v-model="addressForm.receiverPhone"
|
|
||||||
placeholder="请输入收货人手机号"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="省份" prop="province">
|
|
||||||
<el-input v-model="addressForm.province" placeholder="请输入省份" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="城市" prop="city">
|
|
||||||
<el-input v-model="addressForm.city" placeholder="请输入城市" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="区县" prop="district">
|
|
||||||
<el-input v-model="addressForm.district" placeholder="请输入区县" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="详细地址" prop="detailAddress">
|
|
||||||
<el-input
|
|
||||||
v-model="addressForm.detailAddress"
|
|
||||||
type="textarea"
|
|
||||||
placeholder="请输入详细地址"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="设为默认">
|
|
||||||
<el-switch
|
|
||||||
v-model="addressForm.isDefault"
|
|
||||||
:active-value="1"
|
|
||||||
:inactive-value="0"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const handleLogin = async () => {
|
|||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
username: response.data.username,
|
username: response.data.username,
|
||||||
nickname: response.data.nickname,
|
nickname: response.data.nickname,
|
||||||
|
isAdmin: response.data.isAdmin,
|
||||||
});
|
});
|
||||||
|
|
||||||
ElMessage.success("登录成功");
|
ElMessage.success("登录成功");
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
type RouteRecordRaw,
|
type RouteRecordRaw,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
|
|
||||||
|
import { useUserStore } from "@/stores/UserStore.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert PascalCase to kebab-case and remove 'Page' suffix
|
* Convert PascalCase to kebab-case and remove 'Page' suffix
|
||||||
* @param name PascalCase name (e.g., AboutMePage)
|
* @param name PascalCase name (e.g., AboutMePage)
|
||||||
@@ -104,6 +106,34 @@ const extraRoutes: RouteRecordRaw[] = [
|
|||||||
name: "ProductDetail",
|
name: "ProductDetail",
|
||||||
component: () => import("@/pages/product/DetailPage.vue"),
|
component: () => import("@/pages/product/DetailPage.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/admin",
|
||||||
|
redirect: "/admin/userManage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/admin/userManage",
|
||||||
|
name: "UserManage",
|
||||||
|
component: () => import("@/pages/admin/UserPage.vue"),
|
||||||
|
meta: { requiresAdmin: true }, // 添加管理员权限要求
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/admin/category",
|
||||||
|
name: "Categories",
|
||||||
|
component: () => import("@/pages/admin/CategoryPage.vue"),
|
||||||
|
meta: { requiresAdmin: true }, // 添加管理员权限要求
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/admin/product",
|
||||||
|
name: "Products",
|
||||||
|
component: () => import("@/pages/admin/ProductPage.vue"),
|
||||||
|
meta: { requiresAdmin: true }, // 添加管理员权限要求
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/admin/order",
|
||||||
|
name: "Orders",
|
||||||
|
component: () => import("@/pages/admin/OrderPage.vue"),
|
||||||
|
meta: { requiresAdmin: true }, // 添加管理员权限要求
|
||||||
|
},
|
||||||
// 可以在这里添加其他需要特殊配置的路由
|
// 可以在这里添加其他需要特殊配置的路由
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -113,7 +143,7 @@ const allRoutes = [...routes, ...extraRoutes];
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes: allRoutes,
|
routes: allRoutes,
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(_to, _from, savedPosition) {
|
||||||
// 如果有保存的位置(浏览器前进/后退),则使用保存的位置
|
// 如果有保存的位置(浏览器前进/后退),则使用保存的位置
|
||||||
if (savedPosition) {
|
if (savedPosition) {
|
||||||
return savedPosition;
|
return savedPosition;
|
||||||
@@ -123,4 +153,32 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 全局路由守卫 - 检查管理员权限
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
// 检查是否需要管理员权限
|
||||||
|
if (to.meta.requiresAdmin) {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 检查用户是否已登录
|
||||||
|
if (!userStore.token || !userStore.userInfo) {
|
||||||
|
// 未登录,跳转到登录页面
|
||||||
|
next({
|
||||||
|
name: "Login",
|
||||||
|
query: { redirect: to.fullPath }, // 保存重定向路径
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否为管理员
|
||||||
|
if (userStore.userInfo.isAdmin !== 1) {
|
||||||
|
// 不是管理员,跳转到首页或其他页面
|
||||||
|
alert("您没有管理员权限!");
|
||||||
|
next({ name: "Home" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next(); // 继续导航
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ export interface ProductListItem {
|
|||||||
sales: number;
|
sales: number;
|
||||||
status: number;
|
status: number;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
|
categoryId?: number;
|
||||||
|
userId?: number;
|
||||||
|
stock?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 商品详情
|
// 商品详情
|
||||||
export interface ProductDetail {
|
export interface ProductDetail {
|
||||||
id: number;
|
id: number;
|
||||||
|
userId?: number;
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -68,3 +72,20 @@ export interface ProductPageResponse {
|
|||||||
page: number;
|
page: number;
|
||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 商品DTO (用于创建和更新)
|
||||||
|
export interface ProductDTO {
|
||||||
|
id?: number; // 商品ID(修改时使用)
|
||||||
|
categoryId: number; // 分类ID
|
||||||
|
userId?: number; // 商家ID
|
||||||
|
name: string; // 商品名称
|
||||||
|
subtitle?: string; // 副标题
|
||||||
|
mainImage?: string; // 主图URL
|
||||||
|
subImages?: string; // 子图URL(逗号分隔)
|
||||||
|
description?: string; // 商品描述
|
||||||
|
price: number; // 价格
|
||||||
|
stock: number; // 库存
|
||||||
|
status?: number; // 状态 0-下架 1-上架
|
||||||
|
sort?: number; // 排序
|
||||||
|
skuList?: ProductSku[]; // 规格列表
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ export interface RegisterRequest {
|
|||||||
password: string;
|
password: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
avatar?: string;
|
||||||
|
gender?: number;
|
||||||
|
nickname?: string;
|
||||||
|
isAdmin?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录响应数据类型
|
// 登录响应数据类型
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
|
isAdmin: number;
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
@@ -35,4 +40,19 @@ export interface UserInfo {
|
|||||||
oldPassword?: string;
|
oldPassword?: string;
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
updateTime?: string;
|
updateTime?: string;
|
||||||
|
isAdmin?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户DTO类型(用于管理员操作)
|
||||||
|
export interface UserDTO {
|
||||||
|
id?: number;
|
||||||
|
username: string;
|
||||||
|
password?: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
avatar?: string;
|
||||||
|
nickname?: string;
|
||||||
|
gender?: number;
|
||||||
|
status?: number;
|
||||||
|
isAdmin?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user