添加了个人中心界面的母版页,统一了个人中心界面风格
This commit is contained in:
@@ -98,12 +98,7 @@ const goToProductManage = () => {
|
|||||||
@click.prevent="$router.push('/shoppingcart/content')"
|
@click.prevent="$router.push('/shoppingcart/content')"
|
||||||
>购物车</a
|
>购物车</a
|
||||||
>
|
>
|
||||||
<a
|
<!-- 移除我的订单链接 -->
|
||||||
href="#"
|
|
||||||
class="text-gray-600 hover:text-blue-600 transition-colors"
|
|
||||||
@click.prevent="$router.push('/order/content')"
|
|
||||||
>我的订单</a
|
|
||||||
>
|
|
||||||
<!-- 管理员导航项 -->
|
<!-- 管理员导航项 -->
|
||||||
<a
|
<a
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
@@ -136,6 +131,9 @@ const goToProductManage = () => {
|
|||||||
<el-dropdown-item @click="goToAddress"
|
<el-dropdown-item @click="goToAddress"
|
||||||
>我的地址</el-dropdown-item
|
>我的地址</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 v-if="isAdmin" @click="goToAdmin"
|
||||||
>管理界面</el-dropdown-item
|
>管理界面</el-dropdown-item
|
||||||
>
|
>
|
||||||
|
|||||||
166
src/layouts/UserLayout.vue
Normal file
166
src/layouts/UserLayout.vue
Normal 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>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<MainLayout>
|
<UserLayout>
|
||||||
<div class="order-page">
|
<div class="order-page">
|
||||||
<el-card class="order-card">
|
<el-card class="order-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -17,7 +17,11 @@
|
|||||||
<div class="order-list">
|
<div class="order-list">
|
||||||
<el-table :data="orderList" style="width: 100%" row-key="id">
|
<el-table :data="orderList" style="width: 100%" row-key="id">
|
||||||
<el-table-column prop="orderNo" label="订单号" min-width="200" />
|
<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">
|
<template #default="scope">
|
||||||
{{ formatDate(scope.row.createTime) }}
|
{{ formatDate(scope.row.createTime) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -27,12 +31,20 @@
|
|||||||
label="商品数量"
|
label="商品数量"
|
||||||
min-width="100"
|
min-width="100"
|
||||||
/>
|
/>
|
||||||
<el-table-column prop="totalAmount" label="订单总额" min-width="120">
|
<el-table-column
|
||||||
|
prop="totalAmount"
|
||||||
|
label="订单总额"
|
||||||
|
min-width="120"
|
||||||
|
>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
¥{{ scope.row.totalAmount.toFixed(2) }}
|
¥{{ scope.row.totalAmount.toFixed(2) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<el-table-column label="操作" min-width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
@@ -150,10 +162,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</MainLayout>
|
</UserLayout>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MainLayout from "@/layouts/MainLayout.vue";
|
import UserLayout from "@/layouts/UserLayout.vue";
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { useUserStore } from "@/stores/UserStore";
|
import { useUserStore } from "@/stores/UserStore";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
|||||||
@@ -24,7 +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";
|
import UserLayout from "@/layouts/UserLayout.vue";
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const addressList = ref<UserAddress[]>([]);
|
const addressList = ref<UserAddress[]>([]);
|
||||||
@@ -204,7 +204,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MainLayout>
|
<UserLayout>
|
||||||
<div class="address-page">
|
<div class="address-page">
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<el-card class="address-card">
|
<el-card class="address-card">
|
||||||
@@ -335,7 +335,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</UserLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,237 +1,250 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="product-manage-page">
|
<UserLayout>
|
||||||
<!-- 搜索和操作按钮 -->
|
<div class="product-manage-page">
|
||||||
<div class="mb-6 flex justify-between items-center">
|
<!-- 搜索和操作按钮 -->
|
||||||
<div class="flex items-center">
|
<div class="mb-6 flex justify-between items-center">
|
||||||
<el-input
|
<div class="flex items-center">
|
||||||
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"
|
|
||||||
@close="resetProductForm"
|
|
||||||
>
|
|
||||||
<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-cascader
|
|
||||||
v-model="selectedCategoryPath"
|
|
||||||
:options="categoriesTree"
|
|
||||||
:props="{ value: 'id', label: 'name', children: 'children' }"
|
|
||||||
placeholder="请选择分类"
|
|
||||||
style="width: 100%; margin-bottom: 10px"
|
|
||||||
clearable
|
|
||||||
@change="onCategoryChange"
|
|
||||||
/>
|
|
||||||
<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-upload
|
|
||||||
class="avatar-uploader"
|
|
||||||
:auto-upload="false"
|
|
||||||
:show-file-list="false"
|
|
||||||
:on-change="handleImageChange"
|
|
||||||
:before-upload="beforeImageUpload"
|
|
||||||
accept="image/*"
|
|
||||||
>
|
|
||||||
<template v-if="productForm.mainImage">
|
|
||||||
<img :src="productForm.mainImage" class="avatar" alt="主图预览" />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<el-button type="primary" plain>选择图片</el-button>
|
|
||||||
</template>
|
|
||||||
</el-upload>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item 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
|
<el-input
|
||||||
v-model="productForm.description"
|
v-model="searchKeyword"
|
||||||
type="textarea"
|
placeholder="搜索商品名称或分类"
|
||||||
:rows="3"
|
style="width: 300px; margin-right: 16px"
|
||||||
placeholder="商品描述"
|
@keyup.enter="handleSearch"
|
||||||
/>
|
>
|
||||||
</el-form-item>
|
<template #prefix>
|
||||||
<el-form-item label="排序" prop="sort">
|
<i class="fa fa-search"></i>
|
||||||
<el-input-number v-model="productForm.sort" :min="0" />
|
</template>
|
||||||
</el-form-item>
|
</el-input>
|
||||||
</el-form>
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="cancelProductForm">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitProductForm">确定</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<el-button type="success" @click="showAddProductDialog = true">
|
||||||
</el-dialog>
|
<i class="fa fa-plus mr-1"></i> 添加商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 创建分类对话框 -->
|
<!-- 商品列表 -->
|
||||||
<el-dialog title="创建新分类" v-model="showCategoryDialog" width="400px">
|
<div class="product-list mb-6">
|
||||||
<el-form
|
<el-row :gutter="20" style="row-gap: 24px">
|
||||||
:model="categoryForm"
|
<template v-if="loading">
|
||||||
:rules="categoryFormRules"
|
<el-col v-for="n in pageSize" :key="n" :span="8">
|
||||||
ref="categoryFormRef"
|
<el-card class="product-card">
|
||||||
label-width="100px"
|
<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"
|
||||||
|
@close="resetProductForm"
|
||||||
>
|
>
|
||||||
<el-form-item label="分类名称" prop="name">
|
<el-form
|
||||||
<el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
|
:model="productForm"
|
||||||
</el-form-item>
|
:rules="productFormRules"
|
||||||
<el-form-item label="父分类">
|
ref="productFormRef"
|
||||||
<el-cascader
|
label-width="100px"
|
||||||
v-model="categoryForm.parentId"
|
>
|
||||||
:options="categoriesTree"
|
<el-form-item label="商品名称" prop="name">
|
||||||
:props="{ value: 'id', label: 'name', children: 'children' }"
|
<el-input v-model="productForm.name" />
|
||||||
placeholder="选择父分类(不选则为一级分类)"
|
</el-form-item>
|
||||||
:clearable="true"
|
<el-form-item label="分类" prop="categoryId">
|
||||||
style="width: 100%"
|
<el-cascader
|
||||||
/>
|
v-model="selectedCategoryPath"
|
||||||
</el-form-item>
|
:options="categoriesTree"
|
||||||
<el-form-item label="排序">
|
:props="{ value: 'id', label: 'name', children: 'children' }"
|
||||||
<el-input-number v-model="categoryForm.sort" :min="0" />
|
placeholder="请选择分类"
|
||||||
</el-form-item>
|
style="width: 100%; margin-bottom: 10px"
|
||||||
</el-form>
|
clearable
|
||||||
<template #footer>
|
@change="onCategoryChange"
|
||||||
<div class="dialog-footer">
|
/>
|
||||||
<el-button @click="showCategoryDialog = false">取消</el-button>
|
<el-button @click="showCategoryDialog = true" type="primary" plain>
|
||||||
<el-button type="primary" @click="submitCategoryForm">创建</el-button>
|
<i class="fa fa-plus mr-1"></i> 创建新分类
|
||||||
</div>
|
</el-button>
|
||||||
</template>
|
</el-form-item>
|
||||||
</el-dialog>
|
<el-form-item label="副标题" prop="subtitle">
|
||||||
</div>
|
<el-input v-model="productForm.subtitle" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="主图" prop="mainImage">
|
||||||
|
<el-upload
|
||||||
|
class="avatar-uploader"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="handleImageChange"
|
||||||
|
:before-upload="beforeImageUpload"
|
||||||
|
accept="image/*"
|
||||||
|
>
|
||||||
|
<template v-if="productForm.mainImage">
|
||||||
|
<img
|
||||||
|
:src="productForm.mainImage"
|
||||||
|
class="avatar"
|
||||||
|
alt="主图预览"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-button type="primary" plain>选择图片</el-button>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item 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="cancelProductForm">取消</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-cascader
|
||||||
|
v-model="categoryForm.parentId"
|
||||||
|
:options="categoriesTree"
|
||||||
|
:props="{ value: 'id', label: 'name', children: 'children' }"
|
||||||
|
placeholder="选择父分类(不选则为一级分类)"
|
||||||
|
:clearable="true"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-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>
|
||||||
|
</UserLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -257,6 +270,7 @@ import {
|
|||||||
addProductCategory,
|
addProductCategory,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
getProductSubCategories,
|
getProductSubCategories,
|
||||||
|
getProductDetail,
|
||||||
} from "@/apis/product";
|
} from "@/apis/product";
|
||||||
import {
|
import {
|
||||||
ProductListItem,
|
ProductListItem,
|
||||||
@@ -266,6 +280,7 @@ import {
|
|||||||
} from "@/types/product";
|
} from "@/types/product";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useUserStore } from "@/stores/UserStore";
|
import { useUserStore } from "@/stores/UserStore";
|
||||||
|
import UserLayout from "@/layouts/UserLayout.vue";
|
||||||
|
|
||||||
// 获取用户存储和路由
|
// 获取用户存储和路由
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -473,26 +488,43 @@ const findCategoryPath = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 编辑商品
|
// 编辑商品
|
||||||
const editProduct = (product: ProductListItem) => {
|
const editProduct = async (product: ProductListItem) => {
|
||||||
isEditing.value = true;
|
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值
|
// 复制商品数据,处理可能的undefined值
|
||||||
productForm.value = {
|
productForm.value = {
|
||||||
id: product.id,
|
id: fullProduct.id,
|
||||||
categoryId: product.categoryId || 0, // 确保是number类型
|
categoryId: fullProduct.categoryId || 0, // 确保是number类型
|
||||||
name: product.name,
|
name: fullProduct.name,
|
||||||
mainImage: product.mainImage,
|
mainImage: fullProduct.mainImage,
|
||||||
price: product.price,
|
price: fullProduct.price,
|
||||||
stock: product.stock || 0,
|
stock: fullProduct.stock || 0,
|
||||||
subtitle: product.subtitle || "",
|
subtitle: fullProduct.subtitle || "",
|
||||||
description: product.description || "",
|
description: fullProduct.description || "",
|
||||||
sort: product.sort || 0,
|
sort: fullProduct.sort || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置选中的分类路径
|
// 设置选中的分类路径 - 仿照分类管理页面的方式
|
||||||
if (product.categoryId) {
|
if (fullProduct.categoryId) {
|
||||||
selectedCategory.value = product.categoryId;
|
selectedCategory.value = fullProduct.categoryId;
|
||||||
const path = findCategoryPath(categoriesTree.value, product.categoryId);
|
// 根据商品的分类ID找到对应的分类路径
|
||||||
selectedCategoryPath.value = path || [product.categoryId];
|
const path = findCategoryPath(categoriesTree.value, fullProduct.categoryId);
|
||||||
|
selectedCategoryPath.value = path || [fullProduct.categoryId];
|
||||||
} else {
|
} else {
|
||||||
selectedCategory.value = null;
|
selectedCategory.value = null;
|
||||||
selectedCategoryPath.value = [];
|
selectedCategoryPath.value = [];
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
import { getUserProfile, updateUserProfile } from "@/apis/user";
|
import { getUserProfile, updateUserProfile } from "@/apis/user";
|
||||||
import { useUserStore } from "@/stores/UserStore";
|
import { useUserStore } from "@/stores/UserStore";
|
||||||
import type { UserInfo } from "@/types/user";
|
import type { UserInfo } from "@/types/user";
|
||||||
import MainLayout from "@/layouts/MainLayout.vue";
|
import UserLayout from "@/layouts/UserLayout.vue";
|
||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
const userInfo = ref<UserInfo | null>(null);
|
const userInfo = ref<UserInfo | null>(null);
|
||||||
@@ -247,7 +247,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MainLayout>
|
<UserLayout>
|
||||||
<div class="profile-page">
|
<div class="profile-page">
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<el-card class="profile-card">
|
<el-card class="profile-card">
|
||||||
@@ -435,7 +435,7 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</MainLayout>
|
</UserLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
41
src/stores/ThemeStore.ts
Normal file
41
src/stores/ThemeStore.ts
Normal 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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user