1660 lines
44 KiB
Vue
1660 lines
44 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 个人中心
|
||
*
|
||
* 侧边栏 + 内容区布局,覆盖:
|
||
* - 个人资料(展示 + 编辑/改密占位表单,后端接口开发中)
|
||
* - 我的订单(按状态 Tab 筛选 + 支付/取消/确认收货)
|
||
* - 收货地址(增删改 / 设默认)
|
||
* - 我的收藏(列表 + 取消收藏 + 跳商品详情)
|
||
* - 领券中心(可领取优惠券列表 + 一键领取)
|
||
* - 我的优惠券(按状态筛选)
|
||
*/
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
User as UserIcon,
|
||
Goods,
|
||
Location,
|
||
Star,
|
||
Ticket,
|
||
Present,
|
||
SwitchButton,
|
||
Plus,
|
||
Edit,
|
||
Delete,
|
||
Check
|
||
} from '@element-plus/icons-vue'
|
||
import AppHeader from '@/components/layout/AppHeader.vue'
|
||
import AppFooter from '@/components/layout/AppFooter.vue'
|
||
import EmptyState from '@/components/common/EmptyState.vue'
|
||
import { useUserStore } from '@/stores/modules/user'
|
||
import {
|
||
getOrderPageApi,
|
||
getOrderPayUrlApi,
|
||
cancelOrderApi,
|
||
receiveOrderApi
|
||
} from '@/api/order'
|
||
import {
|
||
getAddressListApi,
|
||
createAddressApi,
|
||
updateAddressApi,
|
||
deleteAddressApi,
|
||
setDefaultAddressApi
|
||
} from '@/api/address'
|
||
import { getFavoriteListApi, cancelFavoriteApi } from '@/api/favorite'
|
||
import { getReceivableCouponsApi, receiveCouponApi, getMyCouponsApi } from '@/api/coupon'
|
||
import type { PageResult } from '@/types/common'
|
||
import type { OrderDetail } from '@/types/order'
|
||
import type { Address, AddressSaveReq } from '@/types/address'
|
||
import type { FavoriteItem } from '@/types/favorite'
|
||
import type { ClaimableCoupon, UserCoupon } from '@/types/coupon'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const userStore = useUserStore()
|
||
|
||
// ==================== 侧边栏 ====================
|
||
type MenuKey = 'profile' | 'orders' | 'addresses' | 'favorites' | 'claim' | 'coupons'
|
||
|
||
const activeMenu = ref<MenuKey>('profile')
|
||
|
||
const menuItems: { key: MenuKey; label: string; icon: typeof UserIcon }[] = [
|
||
{ key: 'profile', label: '个人资料', icon: UserIcon },
|
||
{ key: 'orders', label: '我的订单', icon: Goods },
|
||
{ key: 'addresses', label: '收货地址', icon: Location },
|
||
{ key: 'favorites', label: '我的收藏', icon: Star },
|
||
{ key: 'claim', label: '领券中心', icon: Present },
|
||
{ key: 'coupons', label: '我的优惠券', icon: Ticket }
|
||
]
|
||
|
||
const avatarText = computed(() => {
|
||
const name = userStore.displayName || '用户'
|
||
return name.slice(0, 1)
|
||
})
|
||
|
||
function switchMenu(key: MenuKey) {
|
||
activeMenu.value = key
|
||
if (key === 'orders' && !ordersLoaded.value) loadOrders()
|
||
if (key === 'favorites' && !favoritesLoaded.value) loadFavorites()
|
||
}
|
||
|
||
async function handleLogout() {
|
||
try {
|
||
await ElMessageBox.confirm('确定退出登录吗?', '提示', {
|
||
confirmButtonText: '退出',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
} catch {
|
||
return
|
||
}
|
||
userStore.logout()
|
||
ElMessage.success('已退出登录')
|
||
router.push('/')
|
||
}
|
||
|
||
// ==================== 个人资料 ====================
|
||
const profileDialog = ref(false)
|
||
const pwdDialog = ref(false)
|
||
const profileForm = reactive({
|
||
nickname: userStore.nickname,
|
||
phone: '',
|
||
gender: 0,
|
||
birthday: ''
|
||
})
|
||
const pwdForm = reactive({
|
||
oldPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
})
|
||
|
||
function openProfileDialog() {
|
||
profileForm.nickname = userStore.nickname
|
||
profileDialog.value = true
|
||
}
|
||
|
||
function submitProfile() {
|
||
// 后端暂无个人资料修改接口
|
||
ElMessage.info('该功能后端接口开发中,敬请期待')
|
||
profileDialog.value = false
|
||
}
|
||
|
||
function submitPassword() {
|
||
if (pwdForm.newPassword !== pwdForm.confirmPassword) {
|
||
ElMessage.warning('两次输入的密码不一致')
|
||
return
|
||
}
|
||
// 后端暂无用户端修改密码接口
|
||
ElMessage.info('该功能后端接口开发中,敬请期待')
|
||
pwdDialog.value = false
|
||
pwdForm.oldPassword = ''
|
||
pwdForm.newPassword = ''
|
||
pwdForm.confirmPassword = ''
|
||
}
|
||
|
||
// ==================== 我的订单 ====================
|
||
const ORDER_TABS = [
|
||
{ label: '全部', value: undefined },
|
||
{ label: '待付款', value: 0 },
|
||
{ label: '待发货', value: 1 },
|
||
{ label: '待收货', value: 2 },
|
||
{ label: '已完成', value: 3 },
|
||
{ label: '已取消', value: 4 },
|
||
{ label: '已退款', value: 5 }
|
||
] as const
|
||
|
||
const ordersLoaded = ref(false)
|
||
const orderLoading = ref(false)
|
||
const orderTab = ref<number | undefined>(undefined)
|
||
const orderPage = ref<PageResult<OrderDetail>>({
|
||
records: [],
|
||
total: 0,
|
||
current: 1,
|
||
size: 5,
|
||
pages: 0
|
||
})
|
||
const orderActingId = ref<number | null>(null)
|
||
|
||
async function loadOrders() {
|
||
orderLoading.value = true
|
||
try {
|
||
orderPage.value = await getOrderPageApi(
|
||
orderPage.value.current,
|
||
orderPage.value.size,
|
||
orderTab.value
|
||
)
|
||
ordersLoaded.value = true
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
orderLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onOrderTabChange(name: any) {
|
||
const val = typeof name === 'number' ? name : undefined
|
||
orderTab.value = val
|
||
orderPage.value.current = 1
|
||
loadOrders()
|
||
}
|
||
|
||
function onOrderPageChange(page: number) {
|
||
orderPage.value.current = page
|
||
loadOrders()
|
||
}
|
||
|
||
async function handlePayOrder(order: OrderDetail) {
|
||
try {
|
||
await ElMessageBox.confirm('将跳转到支付宝沙箱完成支付,是否继续?', '提示', {
|
||
confirmButtonText: '去支付',
|
||
cancelButtonText: '取消',
|
||
type: 'info'
|
||
})
|
||
} catch {
|
||
return
|
||
}
|
||
orderActingId.value = order.id
|
||
try {
|
||
const data = await getOrderPayUrlApi(order.id)
|
||
if (data?.payUrl) {
|
||
window.location.href = data.payUrl
|
||
return
|
||
}
|
||
ElMessage.warning('支付宝支付链接生成失败')
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
orderActingId.value = null
|
||
}
|
||
}
|
||
|
||
async function handleCancelOrder(order: OrderDetail) {
|
||
try {
|
||
await ElMessageBox.confirm('确定取消该订单吗?', '提示', {
|
||
confirmButtonText: '取消订单',
|
||
cancelButtonText: '再想想',
|
||
type: 'warning'
|
||
})
|
||
} catch {
|
||
return
|
||
}
|
||
orderActingId.value = order.id
|
||
try {
|
||
await cancelOrderApi(order.id)
|
||
ElMessage.success('订单已取消')
|
||
loadOrders()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
orderActingId.value = null
|
||
}
|
||
}
|
||
|
||
async function handleReceiveOrder(order: OrderDetail) {
|
||
try {
|
||
await ElMessageBox.confirm('确认已收到货品吗?', '提示', {
|
||
confirmButtonText: '确认收货',
|
||
cancelButtonText: '取消',
|
||
type: 'info'
|
||
})
|
||
} catch {
|
||
return
|
||
}
|
||
orderActingId.value = order.id
|
||
try {
|
||
await receiveOrderApi(order.id)
|
||
ElMessage.success('已确认收货')
|
||
loadOrders()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
orderActingId.value = null
|
||
}
|
||
}
|
||
|
||
// ==================== 收货地址 ====================
|
||
const addressList = ref<Address[]>([])
|
||
const addressLoading = ref(false)
|
||
const addressDialog = ref(false)
|
||
const addressFormRef = ref()
|
||
const addressForm = reactive<AddressSaveReq>({
|
||
id: undefined,
|
||
receiver: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detail: '',
|
||
tag: '',
|
||
isDefault: 0
|
||
})
|
||
const addressSaving = ref(false)
|
||
|
||
const addressRules = {
|
||
receiver: [{ required: true, message: '请输入收货人姓名', trigger: 'blur' }],
|
||
phone: [
|
||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||
],
|
||
detail: [{ required: true, message: '请输入详细地址', trigger: 'blur' }]
|
||
}
|
||
|
||
async function loadAddresses() {
|
||
addressLoading.value = true
|
||
try {
|
||
addressList.value = await getAddressListApi()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
addressLoading.value = false
|
||
}
|
||
}
|
||
|
||
function openAddressDialog(address?: Address) {
|
||
if (address) {
|
||
Object.assign(addressForm, {
|
||
id: address.id,
|
||
receiver: address.receiver,
|
||
phone: address.phone,
|
||
province: address.province,
|
||
city: address.city,
|
||
district: address.district,
|
||
detail: address.detail,
|
||
tag: address.tag,
|
||
isDefault: address.isDefault
|
||
})
|
||
} else {
|
||
Object.assign(addressForm, {
|
||
id: undefined,
|
||
receiver: '',
|
||
phone: '',
|
||
province: '',
|
||
city: '',
|
||
district: '',
|
||
detail: '',
|
||
tag: '',
|
||
isDefault: 0
|
||
})
|
||
}
|
||
addressDialog.value = true
|
||
}
|
||
|
||
async function submitAddress() {
|
||
if (!addressFormRef.value) return
|
||
await addressFormRef.value.validate(async (valid: boolean) => {
|
||
if (!valid) return
|
||
addressSaving.value = true
|
||
try {
|
||
if (addressForm.id) {
|
||
await updateAddressApi(addressForm)
|
||
ElMessage.success('地址已更新')
|
||
} else {
|
||
await createAddressApi(addressForm)
|
||
ElMessage.success('地址已添加')
|
||
}
|
||
addressDialog.value = false
|
||
loadAddresses()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
addressSaving.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
async function handleDeleteAddress(address: Address) {
|
||
try {
|
||
await ElMessageBox.confirm('确定删除该地址吗?', '提示', {
|
||
confirmButtonText: '删除',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
await deleteAddressApi(address.id)
|
||
ElMessage.success('地址已删除')
|
||
loadAddresses()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
}
|
||
}
|
||
|
||
async function handleSetDefault(address: Address) {
|
||
try {
|
||
await setDefaultAddressApi(address.id)
|
||
ElMessage.success('已设为默认地址')
|
||
loadAddresses()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
}
|
||
}
|
||
|
||
// ==================== 我的收藏 ====================
|
||
const favoritesLoaded = ref(false)
|
||
const favoriteLoading = ref(false)
|
||
const favoritePage = ref<PageResult<FavoriteItem>>({
|
||
records: [],
|
||
total: 0,
|
||
current: 1,
|
||
size: 8,
|
||
pages: 0
|
||
})
|
||
|
||
async function loadFavorites() {
|
||
favoriteLoading.value = true
|
||
try {
|
||
favoritePage.value = await getFavoriteListApi(
|
||
favoritePage.value.current,
|
||
favoritePage.value.size
|
||
)
|
||
favoritesLoaded.value = true
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
favoriteLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onFavoritePageChange(page: number) {
|
||
favoritePage.value.current = page
|
||
loadFavorites()
|
||
}
|
||
|
||
async function handleCancelFavorite(item: FavoriteItem) {
|
||
try {
|
||
await cancelFavoriteApi(item.id)
|
||
ElMessage.success('已取消收藏')
|
||
loadFavorites()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
}
|
||
}
|
||
|
||
function goProduct(id: number) {
|
||
router.push(`/product/${id}`)
|
||
}
|
||
|
||
// ==================== 我的优惠券 ====================
|
||
const COUPON_TABS = [
|
||
{ label: '全部', value: undefined },
|
||
{ label: '未使用', value: 0 },
|
||
{ label: '已使用', value: 1 },
|
||
{ label: '已过期', value: 2 },
|
||
{ label: '已作废', value: 3 }
|
||
] as const
|
||
|
||
const couponList = ref<UserCoupon[]>([])
|
||
const couponLoading = ref(false)
|
||
const couponTab = ref<number | undefined>(undefined)
|
||
|
||
async function loadCoupons() {
|
||
couponLoading.value = true
|
||
try {
|
||
couponList.value = await getMyCouponsApi(couponTab.value)
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
couponLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onCouponTabChange(name: any) {
|
||
const val = typeof name === 'number' ? name : undefined
|
||
couponTab.value = val
|
||
loadCoupons()
|
||
}
|
||
|
||
// ==================== 领券中心 ====================
|
||
const claimList = ref<ClaimableCoupon[]>([])
|
||
const claimLoading = ref(false)
|
||
const claimingId = ref<number | null>(null)
|
||
|
||
async function loadClaimable() {
|
||
claimLoading.value = true
|
||
try {
|
||
claimList.value = await getReceivableCouponsApi()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
claimLoading.value = false
|
||
}
|
||
}
|
||
|
||
function claimMainText(c: ClaimableCoupon) {
|
||
if (c.type === 1) {
|
||
return `${Number((c.amount * 10).toFixed(1))}折`
|
||
}
|
||
return `¥${c.amount}`
|
||
}
|
||
|
||
function claimConditionText(c: ClaimableCoupon) {
|
||
if (c.type === 2 || c.minAmount <= 0) return '无门槛'
|
||
return `满 ¥${c.minAmount} 可用`
|
||
}
|
||
|
||
function claimStockText(c: ClaimableCoupon) {
|
||
if (c.total === -1) return '不限量'
|
||
return `剩余 ${c.remain} 张`
|
||
}
|
||
|
||
function claimValidText(c: ClaimableCoupon) {
|
||
return c.validDays
|
||
? `领取后 ${c.validDays} 天内有效`
|
||
: `有效期至 ${(c.endTime || '').slice(0, 10)}`
|
||
}
|
||
|
||
function claimDisabled(c: ClaimableCoupon) {
|
||
return c.claimed || (c.total !== -1 && c.remain <= 0)
|
||
}
|
||
|
||
async function handleClaim(c: ClaimableCoupon) {
|
||
if (claimDisabled(c) || claimingId.value !== null) return
|
||
claimingId.value = c.id
|
||
try {
|
||
await receiveCouponApi(c.id)
|
||
ElMessage.success('领取成功,可在「我的优惠券」查看')
|
||
loadClaimable()
|
||
loadCoupons()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
claimingId.value = null
|
||
}
|
||
}
|
||
|
||
// ==================== 挂载 ====================
|
||
onMounted(() => {
|
||
// 支持 /user?menu=orders 等直达指定板块(如抢购成功后跳转)
|
||
const menuQuery = route.query.menu as string | undefined
|
||
if (menuQuery && menuItems.some((i) => i.key === menuQuery)) {
|
||
switchMenu(menuQuery as MenuKey)
|
||
}
|
||
loadAddresses()
|
||
loadCoupons()
|
||
loadClaimable()
|
||
// 支付宝支付完成后跳回个人中心,直接展示订单列表
|
||
if (route.query.pay === 'success') {
|
||
activeMenu.value = 'orders'
|
||
loadOrders()
|
||
ElMessage.success('支付成功')
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="user-center">
|
||
<AppHeader />
|
||
|
||
<main class="uc-main">
|
||
<div class="uc-container">
|
||
<!-- 侧边栏 -->
|
||
<aside class="uc-sidebar">
|
||
<div class="uc-user-card">
|
||
<div class="avatar">{{ avatarText }}</div>
|
||
<div class="nickname">{{ userStore.displayName }}</div>
|
||
<div class="uid">用户ID:{{ userStore.userId || '-' }}</div>
|
||
</div>
|
||
|
||
<nav class="uc-nav">
|
||
<div
|
||
v-for="item in menuItems"
|
||
:key="item.key"
|
||
class="uc-nav-item"
|
||
:class="{ active: activeMenu === item.key }"
|
||
@click="switchMenu(item.key)"
|
||
>
|
||
<el-icon class="nav-icon"><component :is="item.icon" /></el-icon>
|
||
<span>{{ item.label }}</span>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="uc-logout" @click="handleLogout">
|
||
<el-icon class="nav-icon"><SwitchButton /></el-icon>
|
||
<span>退出登录</span>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 内容区 -->
|
||
<section class="uc-content">
|
||
<!-- ===== 个人资料 ===== -->
|
||
<div v-if="activeMenu === 'profile'" class="uc-panel">
|
||
<div class="panel-head">
|
||
<h3 class="panel-title">个人资料</h3>
|
||
<el-button :icon="Edit" round @click="openProfileDialog">
|
||
编辑资料
|
||
</el-button>
|
||
</div>
|
||
|
||
<div class="profile-card">
|
||
<div class="profile-avatar">{{ avatarText }}</div>
|
||
<div class="profile-info">
|
||
<div class="info-row">
|
||
<span class="label">昵称</span>
|
||
<span class="value">{{ userStore.nickname || '未设置' }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">用户ID</span>
|
||
<span class="value">{{ userStore.userId || '-' }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">手机号</span>
|
||
<span class="value">未绑定</span>
|
||
</div>
|
||
</div>
|
||
<el-button class="pwd-btn" text type="primary" @click="pwdDialog = true">
|
||
修改密码
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 我的订单 ===== -->
|
||
<div v-else-if="activeMenu === 'orders'" class="uc-panel">
|
||
<div class="panel-head">
|
||
<h3 class="panel-title">我的订单</h3>
|
||
</div>
|
||
|
||
<el-tabs v-model="orderTab" @tab-change="onOrderTabChange">
|
||
<el-tab-pane
|
||
v-for="tab in ORDER_TABS"
|
||
:key="tab.value"
|
||
:label="tab.label"
|
||
:name="tab.value as any"
|
||
/>
|
||
</el-tabs>
|
||
|
||
<div v-loading="orderLoading">
|
||
<template v-if="orderPage.records.length">
|
||
<div v-for="order in orderPage.records" :key="order.id" class="order-card">
|
||
<div class="order-head">
|
||
<span class="order-no">订单号:{{ order.orderNo }}</span>
|
||
<el-tag :type="(order.statusTagType as any) || 'info'" size="small">
|
||
{{ order.statusText }}
|
||
</el-tag>
|
||
</div>
|
||
|
||
<div class="order-body">
|
||
<div class="order-items">
|
||
<el-image
|
||
v-for="item in order.items.slice(0, 4)"
|
||
:key="item.id"
|
||
:src="item.productImage"
|
||
fit="cover"
|
||
class="order-thumb"
|
||
>
|
||
<template #error>
|
||
<div class="thumb-placeholder">🍪</div>
|
||
</template>
|
||
</el-image>
|
||
<span v-if="order.totalQuantity > 4" class="more-count">
|
||
+{{ order.totalQuantity - 4 }}
|
||
</span>
|
||
</div>
|
||
<div class="order-meta">
|
||
<span class="meta-time">{{ order.createTime }}</span>
|
||
<span class="meta-amount">
|
||
实付 <b>¥{{ Number(order.payAmount || 0).toFixed(2) }}</b>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="order-foot">
|
||
<el-button
|
||
v-if="order.status === 0"
|
||
size="small"
|
||
type="primary"
|
||
round
|
||
:loading="orderActingId === order.id"
|
||
@click="handlePayOrder(order)"
|
||
>
|
||
立即支付
|
||
</el-button>
|
||
<el-button
|
||
v-if="order.status === 2"
|
||
size="small"
|
||
type="success"
|
||
round
|
||
:loading="orderActingId === order.id"
|
||
@click="handleReceiveOrder(order)"
|
||
>
|
||
确认收货
|
||
</el-button>
|
||
<el-button
|
||
v-if="order.status === 0"
|
||
size="small"
|
||
plain
|
||
round
|
||
@click="handleCancelOrder(order)"
|
||
>
|
||
取消订单
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-pagination
|
||
v-if="orderPage.pages > 1"
|
||
class="uc-pagination"
|
||
background
|
||
layout="prev, pager, next, total"
|
||
:total="orderPage.total"
|
||
:current-page="orderPage.current"
|
||
:page-size="orderPage.size"
|
||
@current-change="onOrderPageChange"
|
||
/>
|
||
</template>
|
||
<EmptyState v-else description="暂无相关订单" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 收货地址 ===== -->
|
||
<div v-else-if="activeMenu === 'addresses'" class="uc-panel">
|
||
<div class="panel-head">
|
||
<h3 class="panel-title">收货地址</h3>
|
||
<el-button type="primary" :icon="Plus" round @click="openAddressDialog()">
|
||
新增地址
|
||
</el-button>
|
||
</div>
|
||
|
||
<div v-loading="addressLoading">
|
||
<template v-if="addressList.length">
|
||
<div class="address-grid">
|
||
<div
|
||
v-for="address in addressList"
|
||
:key="address.id"
|
||
class="address-card"
|
||
:class="{ 'is-default': address.isDefault === 1 }"
|
||
>
|
||
<div class="addr-head">
|
||
<span class="addr-receiver">{{ address.receiver }}</span>
|
||
<span class="addr-phone">{{ address.phone }}</span>
|
||
<el-tag
|
||
v-if="address.tag"
|
||
size="small"
|
||
effect="plain"
|
||
class="addr-tag"
|
||
>
|
||
{{ address.tag }}
|
||
</el-tag>
|
||
<el-tag v-if="address.isDefault === 1" size="small" type="danger">
|
||
默认
|
||
</el-tag>
|
||
</div>
|
||
<p class="addr-detail">{{ address.fullAddress || address.detail }}</p>
|
||
<div class="addr-foot">
|
||
<el-button
|
||
v-if="address.isDefault !== 1"
|
||
text
|
||
type="primary"
|
||
size="small"
|
||
:icon="Check"
|
||
@click="handleSetDefault(address)"
|
||
>
|
||
设为默认
|
||
</el-button>
|
||
<el-button
|
||
text
|
||
type="primary"
|
||
size="small"
|
||
:icon="Edit"
|
||
@click="openAddressDialog(address)"
|
||
>
|
||
编辑
|
||
</el-button>
|
||
<el-button
|
||
text
|
||
type="danger"
|
||
size="small"
|
||
:icon="Delete"
|
||
@click="handleDeleteAddress(address)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<EmptyState v-else description="还没有收货地址,点击右上角新增" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 我的收藏 ===== -->
|
||
<div v-else-if="activeMenu === 'favorites'" class="uc-panel">
|
||
<div class="panel-head">
|
||
<h3 class="panel-title">我的收藏</h3>
|
||
</div>
|
||
|
||
<div v-loading="favoriteLoading">
|
||
<template v-if="favoritePage.records.length">
|
||
<div class="favorite-grid">
|
||
<div
|
||
v-for="item in favoritePage.records"
|
||
:key="item.id"
|
||
class="favorite-card"
|
||
@click="goProduct(item.productId)"
|
||
>
|
||
<div class="fav-image">
|
||
<el-image :src="item.productMainImage" fit="cover" lazy>
|
||
<template #error>
|
||
<div class="fav-placeholder">🍪</div>
|
||
</template>
|
||
</el-image>
|
||
<div
|
||
class="fav-remove"
|
||
@click.stop="handleCancelFavorite(item)"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
</div>
|
||
</div>
|
||
<div class="fav-body">
|
||
<p class="fav-name">{{ item.productName }}</p>
|
||
<div class="fav-foot">
|
||
<span class="fav-price">
|
||
¥{{ Number(item.minPrice || 0).toFixed(2) }}
|
||
</span>
|
||
<span class="fav-time">{{ item.createTime }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-pagination
|
||
v-if="favoritePage.pages > 1"
|
||
class="uc-pagination"
|
||
background
|
||
layout="prev, pager, next, total"
|
||
:total="favoritePage.total"
|
||
:current-page="favoritePage.current"
|
||
:page-size="favoritePage.size"
|
||
@current-change="onFavoritePageChange"
|
||
/>
|
||
</template>
|
||
<EmptyState v-else description="暂无收藏的商品" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 领券中心 ===== -->
|
||
<div v-else-if="activeMenu === 'claim'" class="uc-panel">
|
||
<div class="panel-head">
|
||
<h3 class="panel-title">领券中心</h3>
|
||
</div>
|
||
|
||
<div v-loading="claimLoading">
|
||
<template v-if="claimList.length">
|
||
<div class="claim-grid">
|
||
<div
|
||
v-for="c in claimList"
|
||
:key="c.id"
|
||
class="claim-card"
|
||
>
|
||
<div class="claim-main">
|
||
<span class="claim-amount">{{ claimMainText(c) }}</span>
|
||
<span class="claim-type">{{ c.typeText }}</span>
|
||
</div>
|
||
<div class="claim-info">
|
||
<span class="claim-name">{{ c.name }}</span>
|
||
<span class="claim-condition">{{ claimConditionText(c) }}</span>
|
||
<span class="claim-meta">
|
||
{{ claimStockText(c) }} · {{ claimValidText(c) }}
|
||
</span>
|
||
<span v-if="c.description" class="claim-desc">{{ c.description }}</span>
|
||
</div>
|
||
<div class="claim-action">
|
||
<el-tag v-if="c.claimed" type="info">已领取</el-tag>
|
||
<el-tag v-else-if="c.total !== -1 && c.remain <= 0" type="danger">已抢完</el-tag>
|
||
<el-button
|
||
v-else
|
||
type="primary"
|
||
size="small"
|
||
round
|
||
:loading="claimingId === c.id"
|
||
@click="handleClaim(c)"
|
||
>立即领取</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<EmptyState v-else description="暂无可领取的优惠券" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 我的优惠券 ===== -->
|
||
<div v-else-if="activeMenu === 'coupons'" class="uc-panel">
|
||
<div class="panel-head">
|
||
<h3 class="panel-title">我的优惠券</h3>
|
||
</div>
|
||
|
||
<el-tabs v-model="couponTab" @tab-change="onCouponTabChange">
|
||
<el-tab-pane
|
||
v-for="tab in COUPON_TABS"
|
||
:key="tab.value"
|
||
:label="tab.label"
|
||
:name="tab.value as any"
|
||
/>
|
||
</el-tabs>
|
||
|
||
<div v-loading="couponLoading">
|
||
<template v-if="couponList.length">
|
||
<div class="coupon-grid">
|
||
<div
|
||
v-for="coupon in couponList"
|
||
:key="coupon.id"
|
||
class="coupon-card"
|
||
>
|
||
<div class="coupon-left">
|
||
<span class="coupon-name">{{ coupon.couponName }}</span>
|
||
<span class="coupon-type">
|
||
类型:{{ coupon.couponType === 0 ? '满减券' : '折扣券' }}
|
||
</span>
|
||
</div>
|
||
<div class="coupon-right">
|
||
<el-tag size="small" :type="coupon.status === 0 ? 'success' : 'info'">
|
||
{{ coupon.statusText }}
|
||
</el-tag>
|
||
<span class="coupon-expire">
|
||
有效期至 {{ (coupon.expireTime || '').slice(0, 10) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<EmptyState v-else description="暂无优惠券,去领券中心看看吧" />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<AppFooter />
|
||
|
||
<!-- 编辑资料弹窗(占位,后端接口开发中) -->
|
||
<el-dialog v-model="profileDialog" title="编辑资料" width="440px">
|
||
<el-form label-width="80px">
|
||
<el-form-item label="昵称">
|
||
<el-input v-model="profileForm.nickname" placeholder="请输入昵称" />
|
||
</el-form-item>
|
||
<el-form-item label="手机号">
|
||
<el-input v-model="profileForm.phone" placeholder="请输入手机号" />
|
||
</el-form-item>
|
||
<el-form-item label="性别">
|
||
<el-radio-group v-model="profileForm.gender">
|
||
<el-radio :value="0">保密</el-radio>
|
||
<el-radio :value="1">男</el-radio>
|
||
<el-radio :value="2">女</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="生日">
|
||
<el-date-picker
|
||
v-model="profileForm.birthday"
|
||
type="date"
|
||
placeholder="选择生日"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="profileDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="submitProfile">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 修改密码弹窗(占位,后端接口开发中) -->
|
||
<el-dialog v-model="pwdDialog" title="修改密码" width="440px">
|
||
<el-form label-width="90px">
|
||
<el-form-item label="原密码">
|
||
<el-input
|
||
v-model="pwdForm.oldPassword"
|
||
type="password"
|
||
show-password
|
||
placeholder="请输入原密码"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="新密码">
|
||
<el-input
|
||
v-model="pwdForm.newPassword"
|
||
type="password"
|
||
show-password
|
||
placeholder="请输入新密码"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="确认密码">
|
||
<el-input
|
||
v-model="pwdForm.confirmPassword"
|
||
type="password"
|
||
show-password
|
||
placeholder="请再次输入新密码"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="pwdDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="submitPassword">确认修改</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 地址编辑弹窗 -->
|
||
<el-dialog
|
||
v-model="addressDialog"
|
||
:title="addressForm.id ? '编辑地址' : '新增地址'"
|
||
width="520px"
|
||
>
|
||
<el-form
|
||
ref="addressFormRef"
|
||
:model="addressForm"
|
||
:rules="addressRules"
|
||
label-width="80px"
|
||
>
|
||
<el-form-item label="收货人" prop="receiver">
|
||
<el-input v-model="addressForm.receiver" placeholder="请输入收货人姓名" />
|
||
</el-form-item>
|
||
<el-form-item label="手机号" prop="phone">
|
||
<el-input v-model="addressForm.phone" placeholder="请输入手机号" />
|
||
</el-form-item>
|
||
<el-form-item label="省">
|
||
<el-input v-model="addressForm.province" placeholder="如:广东省" />
|
||
</el-form-item>
|
||
<el-form-item label="城市">
|
||
<el-input v-model="addressForm.city" placeholder="如:深圳市" />
|
||
</el-form-item>
|
||
<el-form-item label="区/县">
|
||
<el-input v-model="addressForm.district" placeholder="如:南山区" />
|
||
</el-form-item>
|
||
<el-form-item label="详细地址" prop="detail">
|
||
<el-input
|
||
v-model="addressForm.detail"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="街道、楼栋、门牌号等"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="标签">
|
||
<el-select v-model="addressForm.tag" placeholder="请选择标签" clearable>
|
||
<el-option label="家" value="家" />
|
||
<el-option label="公司" value="公司" />
|
||
<el-option label="学校" value="学校" />
|
||
</el-select>
|
||
</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>
|
||
<el-button @click="addressDialog = false">取消</el-button>
|
||
<el-button type="primary" :loading="addressSaving" @click="submitAddress">
|
||
保存
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.user-center {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: $bg-page;
|
||
}
|
||
|
||
.uc-main {
|
||
flex: 1;
|
||
width: 100%;
|
||
}
|
||
|
||
.uc-container {
|
||
max-width: $container-max;
|
||
margin: 0 auto;
|
||
padding: 24px 24px 0;
|
||
display: flex;
|
||
gap: 24px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
// ==================== 侧边栏 ====================
|
||
.uc-sidebar {
|
||
width: 220px;
|
||
flex-shrink: 0;
|
||
background: $bg-surface;
|
||
border: 2px solid $color-border;
|
||
border-radius: $radius-lg;
|
||
box-shadow: $clay-shadow-sm;
|
||
padding: 20px 12px;
|
||
position: sticky;
|
||
top: calc(#{$header-height} + 24px);
|
||
}
|
||
|
||
.uc-user-card {
|
||
text-align: center;
|
||
padding: 12px 8px 20px;
|
||
border-bottom: 2px solid $color-border;
|
||
margin-bottom: 12px;
|
||
|
||
.avatar {
|
||
width: 64px;
|
||
height: 64px;
|
||
margin: 0 auto 12px;
|
||
border-radius: $radius-full;
|
||
background: linear-gradient(135deg, $color-primary, $color-cta);
|
||
color: #fff;
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: $clay-shadow-sm;
|
||
}
|
||
|
||
.nickname {
|
||
font-size: $font-size-lg;
|
||
font-weight: 600;
|
||
color: $color-text-primary;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.uid {
|
||
font-size: $font-size-xs;
|
||
color: $color-text-placeholder;
|
||
}
|
||
}
|
||
|
||
.uc-nav {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.uc-nav-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 11px 14px;
|
||
border-radius: $radius-sm;
|
||
font-size: $font-size-md;
|
||
color: $color-text-regular;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, color 0.2s;
|
||
|
||
.nav-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
&:hover {
|
||
background: $bg-hover;
|
||
color: $color-primary;
|
||
}
|
||
|
||
&.active {
|
||
background: $color-primary-light;
|
||
color: $color-primary;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
.uc-logout {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 11px 14px;
|
||
margin-top: 12px;
|
||
border-top: 2px solid $color-border;
|
||
padding-top: 16px;
|
||
font-size: $font-size-md;
|
||
color: $color-danger;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
|
||
.nav-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
&:hover {
|
||
background: $bg-hover;
|
||
border-radius: $radius-sm;
|
||
}
|
||
}
|
||
|
||
// ==================== 内容区 ====================
|
||
.uc-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: $bg-surface;
|
||
border: 2px solid $color-border;
|
||
border-radius: $radius-lg;
|
||
box-shadow: $clay-shadow-sm;
|
||
padding: 24px;
|
||
min-height: 480px;
|
||
}
|
||
|
||
.uc-panel {
|
||
.panel-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20px;
|
||
|
||
.panel-title {
|
||
margin: 0;
|
||
font-size: $font-size-xl;
|
||
font-weight: 600;
|
||
color: $color-text-primary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.uc-pagination {
|
||
margin-top: 20px;
|
||
justify-content: center;
|
||
}
|
||
|
||
// ---- 个人资料 ----
|
||
.profile-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
background: $bg-page;
|
||
border: 2px solid $color-border;
|
||
border-radius: $radius-md;
|
||
padding: 28px;
|
||
|
||
.profile-avatar {
|
||
width: 80px;
|
||
height: 80px;
|
||
flex-shrink: 0;
|
||
border-radius: $radius-full;
|
||
background: linear-gradient(135deg, $color-primary, $color-cta);
|
||
color: #fff;
|
||
font-size: 34px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: $clay-shadow-sm;
|
||
}
|
||
|
||
.profile-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
|
||
.info-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
|
||
.label {
|
||
width: 64px;
|
||
font-size: $font-size-sm;
|
||
color: $color-text-placeholder;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.value {
|
||
font-size: $font-size-md;
|
||
color: $color-text-primary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.pwd-btn {
|
||
align-self: flex-end;
|
||
}
|
||
}
|
||
|
||
// ---- 订单 ----
|
||
.order-card {
|
||
border: 2px solid $color-border;
|
||
border-radius: $radius-md;
|
||
margin-bottom: 16px;
|
||
overflow: hidden;
|
||
|
||
.order-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
background: $bg-page;
|
||
border-bottom: 2px solid $color-border;
|
||
|
||
.order-no {
|
||
font-size: $font-size-sm;
|
||
color: $color-text-secondary;
|
||
}
|
||
}
|
||
|
||
.order-body {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 16px;
|
||
flex-wrap: wrap;
|
||
|
||
.order-items {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.order-thumb {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: $radius-sm;
|
||
border: 1px solid $color-border;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.thumb-placeholder {
|
||
width: 56px;
|
||
height: 56px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
background: $bg-page;
|
||
}
|
||
|
||
.more-count {
|
||
font-size: $font-size-sm;
|
||
color: $color-text-placeholder;
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
|
||
.order-meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 6px;
|
||
|
||
.meta-time {
|
||
font-size: $font-size-xs;
|
||
color: $color-text-placeholder;
|
||
}
|
||
|
||
.meta-amount {
|
||
font-size: $font-size-sm;
|
||
color: $color-text-secondary;
|
||
|
||
b {
|
||
font-size: $font-size-lg;
|
||
color: $color-primary;
|
||
font-family: 'Fredoka', sans-serif;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.order-foot {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
padding: 12px 16px;
|
||
border-top: 2px solid $color-border;
|
||
background: $bg-surface;
|
||
}
|
||
}
|
||
|
||
// ---- 收货地址 ----
|
||
.address-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.address-card {
|
||
border: 2px solid $color-border;
|
||
border-radius: $radius-md;
|
||
padding: 16px;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
|
||
&.is-default {
|
||
border-color: $color-primary;
|
||
box-shadow: $clay-shadow-sm;
|
||
}
|
||
|
||
.addr-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
flex-wrap: wrap;
|
||
|
||
.addr-receiver {
|
||
font-size: $font-size-lg;
|
||
font-weight: 600;
|
||
color: $color-text-primary;
|
||
}
|
||
|
||
.addr-phone {
|
||
font-size: $font-size-sm;
|
||
color: $color-text-secondary;
|
||
}
|
||
|
||
.addr-tag {
|
||
margin-left: auto;
|
||
}
|
||
}
|
||
|
||
.addr-detail {
|
||
margin: 0 0 12px;
|
||
font-size: $font-size-sm;
|
||
color: $color-text-regular;
|
||
line-height: 1.5;
|
||
min-height: 42px;
|
||
}
|
||
|
||
.addr-foot {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 4px;
|
||
border-top: 1px dashed $color-border;
|
||
padding-top: 10px;
|
||
}
|
||
}
|
||
|
||
// ---- 收藏 ----
|
||
.favorite-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.favorite-card {
|
||
border: 2px solid $color-border;
|
||
border-radius: $radius-md;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||
|
||
&:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: $clay-shadow-md;
|
||
}
|
||
|
||
.fav-image {
|
||
position: relative;
|
||
aspect-ratio: 1 / 1;
|
||
background: $bg-page;
|
||
|
||
.el-image,
|
||
.fav-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.fav-placeholder {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 40px;
|
||
background: $bg-page;
|
||
}
|
||
|
||
.fav-remove {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: $radius-full;
|
||
background: rgba(255, 255, 255, 0.92);
|
||
color: $color-danger;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 2px 8px rgba(80, 40, 20, 0.2);
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
|
||
&:hover {
|
||
background: $color-danger;
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
|
||
&:hover .fav-remove {
|
||
opacity: 1;
|
||
}
|
||
|
||
.fav-body {
|
||
padding: 10px 12px 12px;
|
||
|
||
.fav-name {
|
||
margin: 0 0 8px;
|
||
font-size: $font-size-sm;
|
||
color: $color-text-primary;
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.fav-foot {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.fav-price {
|
||
font-family: 'Fredoka', sans-serif;
|
||
font-size: $font-size-lg;
|
||
font-weight: 700;
|
||
color: $color-primary;
|
||
}
|
||
|
||
.fav-time {
|
||
font-size: $font-size-xs;
|
||
color: $color-text-placeholder;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 优惠券 ----
|
||
.coupon-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.coupon-card {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 16px 20px;
|
||
border: 2px solid $color-border;
|
||
border-radius: $radius-md;
|
||
background: $bg-page;
|
||
|
||
.coupon-left {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
|
||
.coupon-name {
|
||
font-size: $font-size-lg;
|
||
font-weight: 600;
|
||
color: $color-text-primary;
|
||
}
|
||
|
||
.coupon-type {
|
||
font-size: $font-size-xs;
|
||
color: $color-text-placeholder;
|
||
}
|
||
}
|
||
|
||
.coupon-right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 6px;
|
||
|
||
.coupon-expire {
|
||
font-size: $font-size-xs;
|
||
color: $color-text-placeholder;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 领券中心 ----
|
||
.claim-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.claim-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
padding: 18px 20px;
|
||
border: 2px solid $color-border;
|
||
border-radius: $radius-md;
|
||
background: $bg-surface;
|
||
box-shadow: $clay-shadow-sm;
|
||
|
||
.claim-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
width: 112px;
|
||
min-height: 78px;
|
||
border-radius: $radius-sm;
|
||
background: linear-gradient(135deg, $color-primary, $color-secondary);
|
||
color: #fff;
|
||
flex-shrink: 0;
|
||
|
||
.claim-amount {
|
||
font-size: $font-size-xl;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.claim-type {
|
||
font-size: $font-size-xs;
|
||
opacity: 0.92;
|
||
}
|
||
}
|
||
|
||
.claim-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.claim-name {
|
||
font-size: $font-size-lg;
|
||
font-weight: 600;
|
||
color: $color-text-primary;
|
||
}
|
||
|
||
.claim-condition {
|
||
font-size: $font-size-sm;
|
||
color: $color-danger;
|
||
}
|
||
|
||
.claim-meta {
|
||
font-size: $font-size-xs;
|
||
color: $color-text-placeholder;
|
||
}
|
||
|
||
.claim-desc {
|
||
font-size: $font-size-xs;
|
||
color: $color-text-secondary;
|
||
}
|
||
}
|
||
|
||
.claim-action {
|
||
width: 104px;
|
||
text-align: center;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
// ==================== 响应式 ====================
|
||
@media (max-width: 768px) {
|
||
.uc-container {
|
||
flex-direction: column;
|
||
padding: 16px 16px 0;
|
||
}
|
||
|
||
.uc-sidebar {
|
||
width: 100%;
|
||
position: static;
|
||
}
|
||
|
||
.uc-nav {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
|
||
.uc-nav-item {
|
||
flex: 1 1 40%;
|
||
}
|
||
}
|
||
|
||
.claim-card {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
text-align: center;
|
||
|
||
.claim-main {
|
||
width: 100%;
|
||
}
|
||
|
||
.claim-action {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.uc-logout {
|
||
justify-content: center;
|
||
}
|
||
}
|
||
</style>
|