添加了个人中心界面的母版页,统一了个人中心界面风格

This commit is contained in:
puzvv
2025-12-30 21:06:32 +08:00
parent f89327b609
commit ec89dc43e6
7 changed files with 511 additions and 262 deletions

View File

@@ -98,12 +98,7 @@ const goToProductManage = () => {
@click.prevent="$router.push('/shoppingcart/content')"
>购物车</a
>
<a
href="#"
class="text-gray-600 hover:text-blue-600 transition-colors"
@click.prevent="$router.push('/order/content')"
>我的订单</a
>
<!-- 移除我的订单链接 -->
<!-- 管理员导航项 -->
<a
v-if="isAdmin"
@@ -136,6 +131,9 @@ const goToProductManage = () => {
<el-dropdown-item @click="goToAddress"
>我的地址</el-dropdown-item
>
<el-dropdown-item @click="$router.push('/order/content')"
>我的订单</el-dropdown-item
>
<el-dropdown-item v-if="isAdmin" @click="goToAdmin"
>管理界面</el-dropdown-item
>

166
src/layouts/UserLayout.vue Normal file
View File

@@ -0,0 +1,166 @@
<template>
<div
class="user-layout flex h-screen bg-gradient-to-br from-blue-50 to-indigo-100"
>
<!-- 侧边栏 -->
<aside
class="w-64 bg-white shadow-lg flex flex-col border-r border-gray-200"
>
<div class="p-5 border-b border-gray-100">
<div class="flex items-center">
<i class="fa-solid fa-user-circle text-3xl text-indigo-600 mr-3"></i>
<h1 class="text-xl font-bold text-gray-800">用户中心</h1>
</div>
</div>
<nav class="flex-1 p-3">
<ul class="space-y-1">
<li>
<router-link
to="/"
class="flex items-center p-3 rounded-lg hover:bg-indigo-50 text-gray-600 hover:text-indigo-700 transition-colors"
>
<i class="fa-solid fa-home mr-3 text-indigo-500"></i>
<span>返回首页</span>
</router-link>
</li>
<li>
<router-link
to="/user/profile"
class="flex items-center p-3 rounded-lg hover:bg-indigo-50 text-gray-600 hover:text-indigo-700 transition-colors"
active-class="bg-indigo-100 text-indigo-700 border-r-2 border-indigo-500"
>
<i class="fa-solid fa-user mr-3 text-indigo-500"></i>
<span>个人资料</span>
</router-link>
</li>
<li>
<router-link
to="/user/address"
class="flex items-center p-3 rounded-lg hover:bg-indigo-50 text-gray-600 hover:text-indigo-700 transition-colors"
active-class="bg-indigo-100 text-indigo-700 border-r-2 border-indigo-500"
>
<i class="fa-solid fa-address-book mr-3 text-indigo-500"></i>
<span>地址管理</span>
</router-link>
</li>
<li>
<router-link
to="/user/product"
class="flex items-center p-3 rounded-lg hover:bg-indigo-50 text-gray-600 hover:text-indigo-700 transition-colors"
active-class="bg-indigo-100 text-indigo-700 border-r-2 border-indigo-500"
>
<i class="fa-solid fa-box mr-3 text-indigo-500"></i>
<span>商品管理</span>
</router-link>
</li>
<li>
<router-link
to="/order/content"
class="flex items-center p-3 rounded-lg hover:bg-indigo-50 text-gray-600 hover:text-indigo-700 transition-colors"
active-class="bg-indigo-100 text-indigo-700 border-r-2 border-indigo-500"
>
<i class="fa-solid fa-file-invoice mr-3 text-indigo-500"></i>
<span>我的订单</span>
</router-link>
</li>
</ul>
</nav>
<div class="p-4 border-t border-gray-100">
<div class="flex items-center">
<div class="mr-3">
<div
class="w-12 h-12 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 flex items-center justify-center text-white font-bold"
>
{{ userInitial }}
</div>
</div>
<div>
<p class="text-sm font-medium text-gray-800">{{ userNickname }}</p>
<p class="text-xs text-gray-500">
{{ isAdmin ? "管理员" : "普通用户" }}
</p>
</div>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="flex-1 overflow-auto">
<header
class="bg-white shadow-sm p-5 flex justify-between items-center border-b border-gray-100"
>
<h2 class="text-xl 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 text-gray-600">
<i class="fa-solid fa-bell text-lg"></i>
</button>
<button
@click="handleLogout"
class="px-4 py-2 bg-gradient-to-r from-red-500 to-red-600 text-white rounded-lg hover:from-red-600 hover:to-red-700 transition-all shadow-sm"
>
退出登录
</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 userInitial = computed(() => {
const name =
userStore.userInfo?.nickname || userStore.userInfo?.username || "U";
return name.charAt(0).toUpperCase();
});
// 计算属性:判断是否为管理员
const isAdmin = computed(() => {
return userStore.userInfo?.isAdmin === 1;
});
// 计算属性:获取当前页面标题
const getCurrentPageTitle = computed(() => {
const path = route.path;
if (path.includes("/user/profile")) return "个人资料";
if (path.includes("/user/address")) return "地址管理";
if (path.includes("/user/product")) return "商品管理";
if (path.includes("/order/content")) return "我的订单";
return "用户中心";
});
// 用户退出登录
const handleLogout = () => {
userStore.clearUserInfo();
ElMessage.success("退出登录成功");
router.push("/user/login");
};
</script>
<style scoped>
.user-layout {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<MainLayout>
<UserLayout>
<div class="order-page">
<el-card class="order-card">
<template #header>
@@ -17,7 +17,11 @@
<div class="order-list">
<el-table :data="orderList" style="width: 100%" row-key="id">
<el-table-column prop="orderNo" label="订单号" min-width="200" />
<el-table-column prop="createTime" label="下单时间" min-width="180">
<el-table-column
prop="createTime"
label="下单时间"
min-width="180"
>
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
@@ -27,12 +31,20 @@
label="商品数量"
min-width="100"
/>
<el-table-column prop="totalAmount" label="订单总额" min-width="120">
<el-table-column
prop="totalAmount"
label="订单总额"
min-width="120"
>
<template #default="scope">
¥{{ scope.row.totalAmount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="statusText" label="订单状态" min-width="100" />
<el-table-column
prop="statusText"
label="订单状态"
min-width="100"
/>
<el-table-column label="操作" min-width="120">
<template #default="scope">
<el-button
@@ -150,10 +162,10 @@
</span>
</template>
</el-dialog>
</MainLayout>
</UserLayout>
</template>
<script setup lang="ts">
import MainLayout from "@/layouts/MainLayout.vue";
import UserLayout from "@/layouts/UserLayout.vue";
import { ref, onMounted } from "vue";
import { useUserStore } from "@/stores/UserStore";
import { ElMessage } from "element-plus";

View File

@@ -24,7 +24,7 @@ import {
setDefaultAddress,
} from "@/apis/user";
import type { UserAddress, UserAddressForm } from "@/types/address";
import MainLayout from "@/layouts/MainLayout.vue";
import UserLayout from "@/layouts/UserLayout.vue";
const userStore = useUserStore();
const addressList = ref<UserAddress[]>([]);
@@ -204,7 +204,7 @@ onMounted(() => {
</script>
<template>
<MainLayout>
<UserLayout>
<div class="address-page">
<div class="container mx-auto px-4 py-8">
<el-card class="address-card">
@@ -335,7 +335,7 @@ onMounted(() => {
</template>
</el-dialog>
</div>
</MainLayout>
</UserLayout>
</template>
<style scoped>

View File

@@ -1,4 +1,5 @@
<template>
<UserLayout>
<div class="product-manage-page">
<!-- 搜索和操作按钮 -->
<div class="mb-6 flex justify-between items-center">
@@ -162,7 +163,11 @@
accept="image/*"
>
<template v-if="productForm.mainImage">
<img :src="productForm.mainImage" class="avatar" alt="主图预览" />
<img
:src="productForm.mainImage"
class="avatar"
alt="主图预览"
/>
</template>
<template v-else>
<el-button type="primary" plain>选择图片</el-button>
@@ -194,7 +199,9 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelProductForm">取消</el-button>
<el-button type="primary" @click="submitProductForm">确定</el-button>
<el-button type="primary" @click="submitProductForm"
>确定</el-button
>
</div>
</template>
</el-dialog>
@@ -208,7 +215,10 @@
label-width="100px"
>
<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 label="父分类">
<el-cascader
@@ -227,11 +237,14 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="showCategoryDialog = false">取消</el-button>
<el-button type="primary" @click="submitCategoryForm">创建</el-button>
<el-button type="primary" @click="submitCategoryForm"
>创建</el-button
>
</div>
</template>
</el-dialog>
</div>
</UserLayout>
</template>
<script lang="ts" setup>
@@ -257,6 +270,7 @@ import {
addProductCategory,
uploadFile,
getProductSubCategories,
getProductDetail,
} from "@/apis/product";
import {
ProductListItem,
@@ -266,6 +280,7 @@ import {
} from "@/types/product";
import { useRouter } from "vue-router";
import { useUserStore } from "@/stores/UserStore";
import UserLayout from "@/layouts/UserLayout.vue";
// 获取用户存储和路由
const userStore = useUserStore();
@@ -473,26 +488,43 @@ const findCategoryPath = (
};
// 编辑商品
const editProduct = (product: ProductListItem) => {
const editProduct = async (product: ProductListItem) => {
isEditing.value = true;
// 首先尝试从列表数据获取如果categoryId为空则获取详细信息
let fullProduct = product;
if (!product.categoryId) {
try {
// 获取商品详细信息以获取完整的分类ID
const detailResponse = await getProductDetail(product.id);
if (detailResponse.code === 200) {
fullProduct = detailResponse.data as ProductListItem;
}
} catch (error) {
console.error("获取商品详情失败:", error);
ElMessage.error("获取商品详情失败");
}
}
// 复制商品数据处理可能的undefined值
productForm.value = {
id: product.id,
categoryId: product.categoryId || 0, // 确保是number类型
name: product.name,
mainImage: product.mainImage,
price: product.price,
stock: product.stock || 0,
subtitle: product.subtitle || "",
description: product.description || "",
sort: product.sort || 0,
id: fullProduct.id,
categoryId: fullProduct.categoryId || 0, // 确保是number类型
name: fullProduct.name,
mainImage: fullProduct.mainImage,
price: fullProduct.price,
stock: fullProduct.stock || 0,
subtitle: fullProduct.subtitle || "",
description: fullProduct.description || "",
sort: fullProduct.sort || 0,
};
// 设置选中的分类路径
if (product.categoryId) {
selectedCategory.value = product.categoryId;
const path = findCategoryPath(categoriesTree.value, product.categoryId);
selectedCategoryPath.value = path || [product.categoryId];
// 设置选中的分类路径 - 仿照分类管理页面的方式
if (fullProduct.categoryId) {
selectedCategory.value = fullProduct.categoryId;
// 根据商品的分类ID找到对应的分类路径
const path = findCategoryPath(categoriesTree.value, fullProduct.categoryId);
selectedCategoryPath.value = path || [fullProduct.categoryId];
} else {
selectedCategory.value = null;
selectedCategoryPath.value = [];

View File

@@ -29,7 +29,7 @@ import {
import { getUserProfile, updateUserProfile } from "@/apis/user";
import { useUserStore } from "@/stores/UserStore";
import type { UserInfo } from "@/types/user";
import MainLayout from "@/layouts/MainLayout.vue";
import UserLayout from "@/layouts/UserLayout.vue";
// 用户信息
const userInfo = ref<UserInfo | null>(null);
@@ -247,7 +247,7 @@ onMounted(() => {
</script>
<template>
<MainLayout>
<UserLayout>
<div class="profile-page">
<div class="container mx-auto px-4 py-8">
<el-card class="profile-card">
@@ -435,7 +435,7 @@ onMounted(() => {
</span>
</template>
</el-dialog>
</MainLayout>
</UserLayout>
</template>
<style scoped>

41
src/stores/ThemeStore.ts Normal file
View File

@@ -0,0 +1,41 @@
import { defineStore } from "pinia";
export const useThemeStore = defineStore("theme", {
state: () => ({
theme: "light" as "light" | "dark",
}),
actions: {
initTheme() {
// 从本地存储获取主题设置
const savedTheme = localStorage.getItem("theme") as
| "light"
| "dark"
| null;
if (savedTheme) {
this.theme = savedTheme;
this.applyTheme(savedTheme);
} else {
// 检测系统主题偏好
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
this.theme = prefersDark ? "dark" : "light";
this.applyTheme(this.theme);
}
},
toggleTheme() {
this.theme = this.theme === "light" ? "dark" : "light";
this.applyTheme(this.theme);
localStorage.setItem("theme", this.theme);
},
applyTheme(theme: "light" | "dark") {
// 移除所有主题类
document.documentElement.classList.remove("light", "dark");
// 添加当前主题类
document.documentElement.classList.add(theme);
// 同时设置 data-theme 属性,用于 CSS 选择器
document.documentElement.setAttribute("data-theme", theme);
},
},
});