feat: 前端页面和组件全面完善
- 优化通用组件:导航栏、页脚、图片上传、搜索 - 完善业务组件:商品卡片、秒杀卡片 - 更新用户端页面:首页、商品、秒杀、订单、购物车、个人中心 - 新增用户收藏页面 - 完善管理后台:仪表盘、商品/订单/用户/秒杀管理 - 新增管理后台:收藏管理、评价管理、系统监控页面
This commit is contained in:
106
flash-sale-frontend/src/pages/user/favorites.vue
Normal file
106
flash-sale-frontend/src/pages/user/favorites.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="favorites-page">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2 flex items-center">
|
||||
<el-icon class="text-pink-500 mr-2"><StarFilled /></el-icon>
|
||||
我的收藏
|
||||
</h1>
|
||||
<p class="text-gray-600">收藏你感兴趣的商品,随时回来查看</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-center py-12">
|
||||
<el-icon :size="40" class="animate-spin"><Loading /></el-icon>
|
||||
<p class="mt-2 text-gray-500">加载中...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="favorites.length === 0" class="bg-white rounded-lg shadow-sm p-12">
|
||||
<el-empty description="暂无收藏商品">
|
||||
<el-button type="primary" @click="router.push('/products')">去逛逛</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<div v-for="item in favorites" :key="item.id" class="bg-white rounded-lg shadow-sm overflow-hidden hover:shadow-md transition-shadow">
|
||||
<SafeImage
|
||||
:src="item.productImageUrl"
|
||||
:alt="item.productName"
|
||||
wrapper-class="w-full h-48 bg-gray-100"
|
||||
img-class="w-full h-48 object-cover"
|
||||
:clickable="true"
|
||||
@click="router.push(`/product/${item.productId}`)"
|
||||
/>
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold mb-2 line-clamp-1">{{ item.productName }}</h3>
|
||||
<p class="text-sm text-gray-500 mb-3">{{ item.productCategory || '默认分类' }}</p>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-xl font-bold text-red-500">¥{{ item.productPrice }}</span>
|
||||
<span class="text-xs text-gray-400">收藏于 {{ formatTime(item.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button type="primary" class="flex-1" @click="addToCart(item.productId)">加入购物车</el-button>
|
||||
<el-button @click="toggleFavorite(item.productId)">取消收藏</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import dayjs from 'dayjs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { favoriteApi, type FavoriteItem } from '@/api/modules/favorite'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import SafeImage from '@/components/common/SafeImage.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const loading = ref(false)
|
||||
const favorites = ref<FavoriteItem[]>([])
|
||||
|
||||
const formatTime = (value: string) => dayjs(value).format('YYYY-MM-DD')
|
||||
|
||||
const loadFavorites = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await favoriteApi.getList()
|
||||
if (res.success) favorites.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addToCart = async (productId: number) => {
|
||||
await cartStore.addToCart(productId)
|
||||
}
|
||||
|
||||
const toggleFavorite = async (productId: number) => {
|
||||
const res = await favoriteApi.toggle(productId)
|
||||
if (res.success) {
|
||||
ElMessage.success(res.data.favorited ? '收藏成功' : '已取消收藏')
|
||||
loadFavorites()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFavorites()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.favorites-page {
|
||||
min-height: calc(100vh - 60px);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.line-clamp-1 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -86,8 +86,8 @@
|
||||
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
|
||||
<h3 class="font-semibold text-blue-900 mb-2">测试账号</h3>
|
||||
<div class="text-sm text-blue-700">
|
||||
<p>普通用户: user / 123456</p>
|
||||
<p>管理员: admin / 123456</p>
|
||||
<p>普通用户: demo1 / 123456</p>
|
||||
<p>管理员: admin / admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,13 +96,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElForm } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type { LoginParams } from '@/types/api'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
@@ -147,11 +144,11 @@ const handleLogin = async () => {
|
||||
// 快速登录
|
||||
const quickLogin = (type: 'user' | 'admin') => {
|
||||
if (type === 'user') {
|
||||
loginForm.username = 'user'
|
||||
loginForm.username = 'demo1'
|
||||
loginForm.password = '123456'
|
||||
} else {
|
||||
loginForm.username = 'admin'
|
||||
loginForm.password = '123456'
|
||||
loginForm.password = 'admin123'
|
||||
}
|
||||
loginForm.rememberMe = true
|
||||
handleLogin()
|
||||
@@ -162,4 +159,4 @@ const quickLogin = (type: 'user' | 'admin') => {
|
||||
.login-page {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,303 +1,325 @@
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat-card blue">
|
||||
<div class="stat-value">{{ profileStats.totalOrders }}</div>
|
||||
<div class="stat-label">总订单数</div>
|
||||
</div>
|
||||
<div class="stat-card green">
|
||||
<div class="stat-value">¥{{ Number(profileStats.totalAmount || 0).toFixed(2) }}</div>
|
||||
<div class="stat-label">累计消费</div>
|
||||
</div>
|
||||
<div class="stat-card orange">
|
||||
<div class="stat-value">{{ profileStats.flashSaleSuccess }}</div>
|
||||
<div class="stat-label">秒杀成功</div>
|
||||
</div>
|
||||
<div class="stat-card purple">
|
||||
<div class="stat-value">{{ profileStats.favoriteCount }}</div>
|
||||
<div class="stat-label">收藏商品</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<!-- 左侧菜单 -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<!-- 用户头像 -->
|
||||
<div class="text-center mb-6">
|
||||
<el-avatar :size="80" :src="userStore.user?.avatar">
|
||||
{{ userStore.username[0] }}
|
||||
<el-avatar :size="80" :src="infoForm.avatar">
|
||||
{{ (userStore.username || 'U')[0] }}
|
||||
</el-avatar>
|
||||
<h3 class="mt-3 font-semibold">{{ userStore.username }}</h3>
|
||||
<p class="text-sm text-gray-500">{{ userStore.user?.email }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<el-menu-item index="info">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>基本信息</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="security">
|
||||
<el-icon><Lock /></el-icon>
|
||||
<span>账号安全</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="address">
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>收货地址</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="orders">
|
||||
<el-icon><List /></el-icon>
|
||||
<span>我的订单</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu :default-active="activeMenu" @select="handleMenuSelect">
|
||||
<el-menu-item index="info"><el-icon><User /></el-icon><span>基本信息</span></el-menu-item>
|
||||
<el-menu-item index="security"><el-icon><Lock /></el-icon><span>账号安全</span></el-menu-item>
|
||||
<el-menu-item index="address"><el-icon><Location /></el-icon><span>收货地址</span></el-menu-item>
|
||||
<el-menu-item index="orders"><el-icon><List /></el-icon><span>我的订单</span></el-menu-item>
|
||||
<el-menu-item index="favorites"><el-icon><Star /></el-icon><span>我的收藏</span></el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
|
||||
<div class="lg:col-span-3">
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<!-- 基本信息 -->
|
||||
<div v-if="activeMenu === 'info'">
|
||||
<h2 class="text-xl font-semibold mb-6">基本信息</h2>
|
||||
|
||||
<el-form
|
||||
ref="infoFormRef"
|
||||
:model="infoForm"
|
||||
:rules="infoRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="infoForm.username" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="infoForm.email" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="infoForm.phone" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form ref="infoFormRef" :model="infoForm" :rules="infoRules" label-width="100px">
|
||||
<el-form-item label="用户名"><el-input v-model="infoForm.username" disabled /></el-form-item>
|
||||
<el-form-item label="邮箱" prop="email"><el-input v-model="infoForm.email" /></el-form-item>
|
||||
<el-form-item label="手机号" prop="phone"><el-input v-model="infoForm.phone" /></el-form-item>
|
||||
<el-form-item label="头像">
|
||||
<div class="flex items-center gap-4">
|
||||
<el-avatar :size="60" :src="infoForm.avatar">
|
||||
{{ infoForm.username[0] }}
|
||||
</el-avatar>
|
||||
<el-button>更换头像</el-button>
|
||||
<el-avatar :size="60" :src="infoForm.avatar">{{ (infoForm.username || 'U')[0] }}</el-avatar>
|
||||
<el-button @click="handleUpdateAvatar">更换头像</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveInfo">
|
||||
保存修改
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="handleSaveInfo">保存修改</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 账号安全 -->
|
||||
|
||||
<div v-else-if="activeMenu === 'security'">
|
||||
<h2 class="text-xl font-semibold mb-6">账号安全</h2>
|
||||
|
||||
<el-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="原密码" prop="oldPassword">
|
||||
<el-input
|
||||
v-model="passwordForm.oldPassword"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleChangePassword">
|
||||
修改密码
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="100px">
|
||||
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="passwordForm.oldPassword" type="password" show-password /></el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword"><el-input v-model="passwordForm.newPassword" type="password" show-password /></el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword"><el-input v-model="passwordForm.confirmPassword" type="password" show-password /></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="handleChangePassword">修改密码</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 收货地址 -->
|
||||
|
||||
<div v-else-if="activeMenu === 'address'">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-semibold">收货地址</h2>
|
||||
<el-button type="primary" @click="handleAddAddress">
|
||||
<el-icon class="mr-1"><Plus /></el-icon>
|
||||
添加地址
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openAddressDialog()"><el-icon class="mr-1"><Plus /></el-icon>添加地址</el-button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="addr in addresses"
|
||||
:key="addr.id"
|
||||
class="border rounded-lg p-4"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<div v-for="addr in addresses" :key="addr.id" class="border rounded-lg p-4">
|
||||
<div class="flex justify-between items-start gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<span class="font-semibold">{{ addr.name }}</span>
|
||||
<span class="text-gray-500">{{ addr.phone }}</span>
|
||||
<el-tag v-if="addr.isDefault" type="primary" size="small">
|
||||
默认
|
||||
</el-tag>
|
||||
<el-tag v-if="addr.isDefault" type="primary" size="small">默认</el-tag>
|
||||
</div>
|
||||
<p class="text-gray-600">
|
||||
{{ addr.province }} {{ addr.city }} {{ addr.district }} {{ addr.address }}
|
||||
</p>
|
||||
<p class="text-gray-600">{{ addr.province }} {{ addr.city }} {{ addr.district }} {{ addr.address }}</p>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<el-button text type="primary" size="small">编辑</el-button>
|
||||
<el-button text type="danger" size="small">删除</el-button>
|
||||
<div class="space-x-2 whitespace-nowrap">
|
||||
<el-button v-if="!addr.isDefault" text type="success" size="small" @click="setDefaultAddress(addr.id)">设为默认</el-button>
|
||||
<el-button text type="primary" size="small" @click="openAddressDialog(addr)">编辑</el-button>
|
||||
<el-button text type="danger" size="small" @click="removeAddress(addr.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="addresses.length === 0" description="暂无收货地址" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 我的订单 -->
|
||||
|
||||
<div v-else-if="activeMenu === 'orders'">
|
||||
<h2 class="text-xl font-semibold mb-6">我的订单</h2>
|
||||
<p class="text-gray-500">请访问 <router-link to="/orders" class="text-primary-500">订单页面</router-link> 查看详细订单信息</p>
|
||||
<p class="text-gray-500">正在跳转到订单列表页...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeMenu === 'favorites'">
|
||||
<h2 class="text-xl font-semibold mb-6">我的收藏</h2>
|
||||
<p class="text-gray-500">正在跳转到收藏页...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="addressDialogVisible" :title="editingAddressId ? '编辑地址' : '新增地址'" width="620px">
|
||||
<el-form ref="addressFormRef" :model="addressForm" :rules="addressRules" label-width="90px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="收货人" prop="name"><el-input v-model="addressForm.name" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="手机号" prop="phone"><el-input v-model="addressForm.phone" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="省份" prop="province"><el-input v-model="addressForm.province" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="城市" prop="city"><el-input v-model="addressForm.city" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="区县" prop="district"><el-input v-model="addressForm.district" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="详细地址" prop="address"><el-input v-model="addressForm.address" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item><el-checkbox v-model="addressForm.isDefault">设为默认地址</el-checkbox></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addressDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveAddress">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElForm } from 'element-plus'
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { userApi } from '@/api/modules/user'
|
||||
import { addressApi, type AddressItem } from '@/api/modules/address'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeMenu = ref('info')
|
||||
const infoFormRef = ref<FormInstance>()
|
||||
const passwordFormRef = ref<FormInstance>()
|
||||
const addressFormRef = ref<FormInstance>()
|
||||
const addressDialogVisible = ref(false)
|
||||
const editingAddressId = ref<number | null>(null)
|
||||
const addresses = ref<AddressItem[]>([])
|
||||
const profileStats = reactive({ totalOrders: 0, totalAmount: 0, flashSaleSuccess: 0, favoriteCount: 0 })
|
||||
|
||||
// 基本信息表单
|
||||
const infoForm = reactive({
|
||||
username: userStore.user?.username || '',
|
||||
email: userStore.user?.email || '',
|
||||
phone: userStore.user?.phone || '',
|
||||
avatar: userStore.user?.avatar || ''
|
||||
})
|
||||
const infoForm = reactive({ username: '', email: '', phone: '', avatar: '' })
|
||||
const passwordForm = reactive({ oldPassword: '', newPassword: '', confirmPassword: '' })
|
||||
const addressForm = reactive<AddressItem>({ id: 0, name: '', phone: '', province: '', city: '', district: '', address: '', isDefault: false })
|
||||
|
||||
const infoRules: FormRules = {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
]
|
||||
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }, { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
|
||||
phone: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
// 修改密码表单
|
||||
const passwordForm = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const validatePassword = (rule: any, value: any, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== passwordForm.newPassword) {
|
||||
callback(new Error('两次输入密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
const validatePassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value) callback(new Error('请再次输入密码'))
|
||||
else if (value !== passwordForm.newPassword) callback(new Error('两次输入密码不一致'))
|
||||
else callback()
|
||||
}
|
||||
|
||||
const passwordRules: FormRules = {
|
||||
oldPassword: [
|
||||
{ required: true, message: '请输入原密码', trigger: 'blur' }
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, validator: validatePassword, trigger: 'blur' }
|
||||
]
|
||||
oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
|
||||
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }, { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }],
|
||||
confirmPassword: [{ required: true, validator: validatePassword, trigger: 'blur' }],
|
||||
}
|
||||
|
||||
// 收货地址
|
||||
const addresses = ref<any[]>([])
|
||||
const addressRules: FormRules = {
|
||||
name: [{ required: true, message: '请输入收货人', trigger: 'blur' }],
|
||||
phone: [{ required: true, pattern: /^1[3-9]\d{9}$/, message: '请输入正确手机号', trigger: 'blur' }],
|
||||
province: [{ required: true, message: '请输入省份', trigger: 'blur' }],
|
||||
city: [{ required: true, message: '请输入城市', trigger: 'blur' }],
|
||||
district: [{ required: true, message: '请输入区县', trigger: 'blur' }],
|
||||
address: [{ required: true, message: '请输入详细地址', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
// 菜单选择
|
||||
const handleMenuSelect = (index: string) => {
|
||||
activeMenu.value = index
|
||||
if (index === 'orders') {
|
||||
router.push('/orders')
|
||||
const syncInfoForm = () => {
|
||||
infoForm.username = userStore.user?.username || ''
|
||||
infoForm.email = userStore.user?.email || ''
|
||||
infoForm.phone = userStore.user?.phone || ''
|
||||
infoForm.avatar = userStore.user?.avatar || ''
|
||||
}
|
||||
|
||||
const syncActiveMenu = () => {
|
||||
if (route.path.endsWith('/addresses')) activeMenu.value = 'address'
|
||||
else activeMenu.value = (route.query.tab as string) || 'info'
|
||||
}
|
||||
|
||||
const loadAddresses = async () => {
|
||||
try {
|
||||
const res = await addressApi.getList()
|
||||
if (res.success) addresses.value = res.data || []
|
||||
} catch (error) {
|
||||
console.error('加载地址失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存基本信息
|
||||
const loadProfileStats = async () => {
|
||||
try {
|
||||
const res = await userApi.getProfileStats()
|
||||
if (res.success) Object.assign(profileStats, res.data)
|
||||
} catch (error) {
|
||||
console.error('加载个人中心统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const resetAddressForm = () => {
|
||||
editingAddressId.value = null
|
||||
Object.assign(addressForm, { id: 0, name: '', phone: '', province: '', city: '', district: '', address: '', isDefault: false })
|
||||
}
|
||||
|
||||
const handleMenuSelect = (index: string) => {
|
||||
activeMenu.value = index
|
||||
if (index === 'orders') router.push('/orders')
|
||||
else if (index === 'favorites') router.push('/favorites')
|
||||
else if (index === 'address') router.push('/addresses')
|
||||
else router.push({ path: '/profile', query: { tab: index } })
|
||||
}
|
||||
|
||||
const handleSaveInfo = async () => {
|
||||
if (!infoFormRef.value) return
|
||||
|
||||
await infoFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await userApi.updateInfo(infoForm)
|
||||
if (res.success) {
|
||||
userStore.updateUserInfo(infoForm)
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
if (!valid) return
|
||||
try {
|
||||
const res = await userApi.updateInfo({ email: infoForm.email, phone: infoForm.phone, avatar: infoForm.avatar })
|
||||
if (res.success) {
|
||||
userStore.updateUserInfo(res.data)
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
const handleUpdateAvatar = async () => {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('请输入新的头像图片 URL', '更换头像', { inputValue: infoForm.avatar, confirmButtonText: '保存', cancelButtonText: '取消' })
|
||||
infoForm.avatar = value || ''
|
||||
userStore.updateUserInfo({ avatar: infoForm.avatar })
|
||||
ElMessage.success('头像已更新')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
if (!passwordFormRef.value) return
|
||||
|
||||
await passwordFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await userApi.changePassword({
|
||||
oldPassword: passwordForm.oldPassword,
|
||||
newPassword: passwordForm.newPassword
|
||||
})
|
||||
if (res.success) {
|
||||
ElMessage.success('密码修改成功,请重新登录')
|
||||
userStore.logout()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error)
|
||||
if (!valid) return
|
||||
try {
|
||||
const res = await userApi.changePassword({ oldPassword: passwordForm.oldPassword, newPassword: passwordForm.newPassword, confirmPassword: passwordForm.confirmPassword })
|
||||
if (res.success) {
|
||||
ElMessage.success('密码修改成功,请重新登录')
|
||||
await userStore.logout()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加地址
|
||||
const handleAddAddress = () => {
|
||||
ElMessage.info('功能开发中...')
|
||||
const openAddressDialog = (item?: AddressItem) => {
|
||||
resetAddressForm()
|
||||
if (item) {
|
||||
editingAddressId.value = item.id
|
||||
Object.assign(addressForm, item)
|
||||
}
|
||||
addressDialogVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 加载用户信息
|
||||
userStore.getUserInfo()
|
||||
const saveAddress = async () => {
|
||||
if (!addressFormRef.value) return
|
||||
await addressFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const payload = { name: addressForm.name, phone: addressForm.phone, province: addressForm.province, city: addressForm.city, district: addressForm.district, address: addressForm.address, isDefault: addressForm.isDefault }
|
||||
try {
|
||||
if (editingAddressId.value) {
|
||||
await addressApi.update(editingAddressId.value, payload)
|
||||
ElMessage.success('地址已更新')
|
||||
} else {
|
||||
await addressApi.create(payload)
|
||||
ElMessage.success('地址已新增')
|
||||
}
|
||||
addressDialogVisible.value = false
|
||||
loadAddresses()
|
||||
} catch (error) {
|
||||
console.error('保存地址失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setDefaultAddress = async (id: number) => {
|
||||
await addressApi.setDefault(id)
|
||||
ElMessage.success('默认地址已更新')
|
||||
loadAddresses()
|
||||
}
|
||||
|
||||
const removeAddress = async (id: number) => {
|
||||
await ElMessageBox.confirm('确定删除这个地址吗?', '提示', { type: 'warning' })
|
||||
await addressApi.delete(id)
|
||||
ElMessage.success('地址已删除')
|
||||
loadAddresses()
|
||||
}
|
||||
|
||||
watch(() => userStore.user, syncInfoForm, { immediate: true })
|
||||
watch(() => route.fullPath, syncActiveMenu, { immediate: true })
|
||||
|
||||
onMounted(async () => {
|
||||
if (userStore.token) await userStore.getUserInfo()
|
||||
syncInfoForm()
|
||||
syncActiveMenu()
|
||||
loadAddresses()
|
||||
loadProfileStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -306,4 +328,21 @@ onMounted(() => {
|
||||
min-height: calc(100vh - 60px);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
|
||||
.stat-card {
|
||||
@apply rounded-lg p-5 text-white shadow-sm;
|
||||
|
||||
&.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); }
|
||||
&.green { background: linear-gradient(135deg, #10b981, #059669); }
|
||||
&.orange { background: linear-gradient(135deg, #f59e0b, #ea580c); }
|
||||
&.purple { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@apply text-2xl font-bold;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@apply text-sm mt-2 opacity-90;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user