refactor: clean up Member module and update Console
- Remove redundant PointsRule repositories (use single PointsRuleRepository) - Clean up Member migrations and consolidate to single Init migration - Update Console frontend API and components for Tenant - Add H5LinkService for member H5 integration
This commit is contained in:
parent
cd9be23693
commit
5cbfb2866c
@ -71,4 +71,13 @@ export namespace TenantApi {
|
|||||||
export async function updateTenantSettings(id: number, data: FenglingApi.Tenant.TenantSettings) {
|
export async function updateTenantSettings(id: number, data: FenglingApi.Tenant.TenantSettings) {
|
||||||
return requestClient.put<void, FenglingApi.Tenant.TenantSettings>(`${apiPrefix}/tenants/${id}/settings`, data);
|
return requestClient.put<void, FenglingApi.Tenant.TenantSettings>(`${apiPrefix}/tenants/${id}/settings`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface H5LinkResult {
|
||||||
|
link: string;
|
||||||
|
qrCodeBase64: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getH5Link(id: number) {
|
||||||
|
return requestClient.get<H5LinkResult>(`${apiPrefix}/tenants/${id}/h5-link`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,10 @@ export namespace FenglingApi {
|
|||||||
expiresAt?: string
|
expiresAt?: string
|
||||||
description?: string
|
description?: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
customDomain?: string
|
||||||
|
basePath?: string
|
||||||
|
logo?: string
|
||||||
|
h5BaseUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateTenantDto {
|
export interface CreateTenantDto {
|
||||||
@ -25,6 +29,10 @@ export namespace FenglingApi {
|
|||||||
expiresAt?: string
|
expiresAt?: string
|
||||||
status?: string
|
status?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
customDomain?: string
|
||||||
|
basePath?: string
|
||||||
|
logo?: string
|
||||||
|
h5BaseUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTenantDto {
|
export interface UpdateTenantDto {
|
||||||
@ -36,6 +44,10 @@ export namespace FenglingApi {
|
|||||||
expiresAt?: string
|
expiresAt?: string
|
||||||
status?: string
|
status?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
customDomain?: string
|
||||||
|
basePath?: string
|
||||||
|
logo?: string
|
||||||
|
h5BaseUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantSettings {
|
export interface TenantSettings {
|
||||||
|
|||||||
@ -114,6 +114,31 @@ const handleSave = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const qrCodeVisible = ref(false);
|
||||||
|
const qrCodeUrl = ref('');
|
||||||
|
const currentTenantName = ref('');
|
||||||
|
|
||||||
|
const handleCopyLink = async (row: FenglingApi.Tenant.Tenant) => {
|
||||||
|
try {
|
||||||
|
const { link } = await TenantApi.getH5Link(row.id);
|
||||||
|
await navigator.clipboard.writeText(link);
|
||||||
|
ElMessage.success('Link copied to clipboard');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('Failed to copy link');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowQrCode = async (row: FenglingApi.Tenant.Tenant) => {
|
||||||
|
try {
|
||||||
|
const { link, qrCodeBase64 } = await TenantApi.getH5Link(row.id);
|
||||||
|
qrCodeUrl.value = `data:image/png;base64,${qrCodeBase64}`;
|
||||||
|
currentTenantName.value = row.name;
|
||||||
|
qrCodeVisible.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('Failed to generate QR code');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTenants();
|
loadTenants();
|
||||||
});
|
});
|
||||||
@ -157,9 +182,11 @@ onMounted(() => {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="expiresAt" label="Expires At" width="180" />
|
<el-table-column prop="expiresAt" label="Expires At" width="180" />
|
||||||
<el-table-column prop="createdAt" label="Created At" width="180" />
|
<el-table-column prop="createdAt" label="Created At" width="180" />
|
||||||
<el-table-column label="Actions" width="200" fixed="right">
|
<el-table-column label="Actions" width="280" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button size="small" @click="handleEdit(row)">Edit</el-button>
|
<el-button size="small" @click="handleEdit(row)">Edit</el-button>
|
||||||
|
<el-button size="small" type="success" @click="handleCopyLink(row)">Copy Link</el-button>
|
||||||
|
<el-button size="small" type="warning" @click="handleShowQrCode(row)">QR Code</el-button>
|
||||||
<el-popconfirm title="Are you sure to delete this tenant?" @confirm="handleDelete(row.id)">
|
<el-popconfirm title="Are you sure to delete this tenant?" @confirm="handleDelete(row.id)">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button size="small" type="danger">Delete</el-button>
|
<el-button size="small" type="danger">Delete</el-button>
|
||||||
@ -218,5 +245,14 @@ onMounted(() => {
|
|||||||
<el-button type="primary" @click="handleSave">Save</el-button>
|
<el-button type="primary" @click="handleSave">Save</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="qrCodeVisible" :title="currentTenantName + ' - QR Code'" width="300px">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<img :src="qrCodeUrl" alt="QR Code" style="max-width: 250px;" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="qrCodeVisible = false">Close</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
221
apps/web-ele/src/views/member-h5/auth/login.vue
Normal file
221
apps/web-ele/src/views/member-h5/auth/login.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const phone = ref('')
|
||||||
|
const code = ref('')
|
||||||
|
const countdown = ref(0)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const mode = ref<'login' | 'register'>('login')
|
||||||
|
|
||||||
|
const redirect = computed(() => route.query.redirect as string || '/')
|
||||||
|
|
||||||
|
const phoneRules = [
|
||||||
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const codeRules = [
|
||||||
|
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||||
|
{ pattern: /^\d{4,6}$/, message: '请输入4-6位验证码', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const sendCode = async () => {
|
||||||
|
if (countdown.value > 0) return
|
||||||
|
if (!phone.value || !/^1[3-9]\d{9}$/.test(phone.value)) {
|
||||||
|
alert('请输入正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
countdown.value = 60
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// TODO: 调用发送验证码 API
|
||||||
|
console.log('发送验证码到:', phone.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录/注册
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!phone.value || !code.value) {
|
||||||
|
alert('请填写完整信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
// TODO: 调用登录 API
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
router.push(redirect.value)
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信登录
|
||||||
|
const handleWechatLogin = () => {
|
||||||
|
// TODO: 微信登录逻辑
|
||||||
|
console.log('微信登录')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付宝登录
|
||||||
|
const handleAlipayLogin = () => {
|
||||||
|
// TODO: 支付宝登录逻辑
|
||||||
|
console.log('支付宝登录')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex flex-col">
|
||||||
|
<!-- 背景装饰 -->
|
||||||
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
|
<div class="absolute top-1/4 -left-32 w-64 h-64 bg-purple-500/20 rounded-full blur-3xl"></div>
|
||||||
|
<div class="absolute bottom-1/4 -right-32 w-64 h-64 bg-pink-500/20 rounded-full blur-3xl"></div>
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 顶部 Logo 区域 -->
|
||||||
|
<div class="relative z-10 pt-16 px-8 text-center">
|
||||||
|
<div class="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-3xl shadow-2xl shadow-purple-500/30 mb-6">
|
||||||
|
<span class="text-4xl">🔔</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-white mb-2">风铃</h1>
|
||||||
|
<p class="text-slate-400">享受品质生活</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表单区域 -->
|
||||||
|
<div class="relative z-10 flex-1 mt-12 px-6">
|
||||||
|
<div class="max-w-sm mx-auto">
|
||||||
|
<!-- Tab 切换 -->
|
||||||
|
<div class="flex bg-slate-800/50 rounded-2xl p-1 mb-8">
|
||||||
|
<button
|
||||||
|
@click="mode = 'login'"
|
||||||
|
class="flex-1 py-3 rounded-xl text-sm font-medium transition-all"
|
||||||
|
:class="mode === 'login' ? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg' : 'text-slate-400 hover:text-white'"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="mode = 'register'"
|
||||||
|
class="flex-1 py-3 rounded-xl text-sm font-medium transition-all"
|
||||||
|
:class="mode === 'register' ? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg' : 'text-slate-400 hover:text-white'"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手机号输入 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm text-slate-400 mb-2">手机号</label>
|
||||||
|
<input
|
||||||
|
v-model="phone"
|
||||||
|
type="tel"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
maxlength="11"
|
||||||
|
class="w-full bg-slate-800/80 border border-slate-700 rounded-2xl px-5 py-4 text-white placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 验证码输入 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block text-sm text-slate-400 mb-2">验证码</label>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<input
|
||||||
|
v-model="code"
|
||||||
|
type="tel"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
maxlength="6"
|
||||||
|
class="flex-1 bg-slate-800/80 border border-slate-700 rounded-2xl px-5 py-4 text-white placeholder-slate-500 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 transition-all"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="sendCode"
|
||||||
|
:disabled="countdown > 0"
|
||||||
|
class="px-4 py-4 bg-slate-800/80 border border-slate-700 rounded-2xl text-sm font-medium transition-all"
|
||||||
|
:class="countdown > 0 ? 'text-slate-500 cursor-not-allowed' : 'text-purple-400 hover:text-purple-300 hover:border-purple-500'"
|
||||||
|
>
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<button
|
||||||
|
@click="handleSubmit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="w-full py-4 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-2xl text-white font-medium shadow-lg shadow-purple-500/30 hover:shadow-purple-500/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<span v-if="isLoading" class="inline-flex items-center gap-2">
|
||||||
|
<svg class="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
处理中...
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ mode === 'login' ? '登 录' : '注 册' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 第三方登录 -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<div class="flex-1 h-px bg-slate-700"></div>
|
||||||
|
<span class="text-slate-500 text-sm">其他登录方式</span>
|
||||||
|
<div class="flex-1 h-px bg-slate-700"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-6">
|
||||||
|
<button
|
||||||
|
@click="handleWechatLogin"
|
||||||
|
class="w-14 h-14 bg-slate-800/80 rounded-2xl flex items-center justify-center text-2xl hover:bg-slate-700 transition-colors"
|
||||||
|
>
|
||||||
|
💬
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="handleAlipayLogin"
|
||||||
|
class="w-14 h-14 bg-slate-800/80 rounded-2xl flex items-center justify-center text-2xl hover:bg-slate-700 transition-colors"
|
||||||
|
>
|
||||||
|
💳
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部协议 -->
|
||||||
|
<div class="relative z-10 p-6 text-center">
|
||||||
|
<p class="text-slate-500 text-xs">
|
||||||
|
登录即表示同意
|
||||||
|
<a href="#" class="text-purple-400">《用户协议》</a>
|
||||||
|
和
|
||||||
|
<a href="#" class="text-purple-400">《隐私政策》</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 页面进入动画 */
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative.z-10 {
|
||||||
|
animation: slideUp 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative.z-10:nth-child(2) { animation-delay: 0.1s; }
|
||||||
|
.relative.z-10:nth-child(3) { animation-delay: 0.2s; }
|
||||||
|
</style>
|
||||||
201
apps/web-ele/src/views/member-h5/home/index.vue
Normal file
201
apps/web-ele/src/views/member-h5/home/index.vue
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 模拟数据 - 游客模式
|
||||||
|
const isLoggedIn = ref(false)
|
||||||
|
|
||||||
|
const userInfo = ref({
|
||||||
|
nickname: '会员用户',
|
||||||
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Fengling',
|
||||||
|
level: 5,
|
||||||
|
levelName: '黄金会员',
|
||||||
|
points: 1280,
|
||||||
|
coupons: 3,
|
||||||
|
orders: 12
|
||||||
|
})
|
||||||
|
|
||||||
|
// 功能菜单
|
||||||
|
const menuGroups = ref([
|
||||||
|
{
|
||||||
|
title: '我的资产',
|
||||||
|
items: [
|
||||||
|
{ icon: '🪙', label: '积分中心', path: '/points', requiresAuth: false },
|
||||||
|
{ icon: '👑', label: '会员等级', path: '/level', requiresAuth: false },
|
||||||
|
{ icon: '🎫', label: '我的卡券', path: '/coupons', requiresAuth: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订单管理',
|
||||||
|
items: [
|
||||||
|
{ icon: '📦', label: '我的订单', path: '/orders', requiresAuth: true },
|
||||||
|
{ icon: '📍', label: '收货地址', path: '/addresses', requiresAuth: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '账户设置',
|
||||||
|
items: [
|
||||||
|
{ icon: '👤', label: '个人资料', path: '/profile', requiresAuth: true },
|
||||||
|
{ icon: '🔒', label: '账户安全', path: '/security', requiresAuth: true },
|
||||||
|
{ icon: '⚙️', label: '设置', path: '/settings', requiresAuth: false },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleMenuClick = (item: any) => {
|
||||||
|
if (item.requiresAuth && !isLoggedIn.value) {
|
||||||
|
router.push('/login?redirect=' + item.path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push(item.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToLogin = () => {
|
||||||
|
router.push('/login?redirect=/')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100">
|
||||||
|
<!-- 顶部用户区域 -->
|
||||||
|
<div class="relative bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 pb-24 pt-12">
|
||||||
|
<!-- 装饰元素 -->
|
||||||
|
<div class="absolute top-4 right-4 w-24 h-24 bg-white/10 rounded-full blur-xl"></div>
|
||||||
|
<div class="absolute -bottom-8 -left-8 w-32 h-32 bg-white/10 rounded-full blur-2xl"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 px-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- 头像 -->
|
||||||
|
<div class="relative">
|
||||||
|
<img
|
||||||
|
:src="userInfo.avatar"
|
||||||
|
class="w-20 h-20 rounded-full border-4 border-white/30 shadow-lg"
|
||||||
|
alt="avatar"
|
||||||
|
/>
|
||||||
|
<div v-if="isLoggedIn" class="absolute -bottom-1 -right-1 bg-yellow-400 text-yellow-900 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||||
|
Lv.{{ userInfo.level }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<div class="flex-1 text-white">
|
||||||
|
<template v-if="isLoggedIn">
|
||||||
|
<h2 class="text-xl font-bold">{{ userInfo.nickname }}</h2>
|
||||||
|
<p class="text-white/80 text-sm">{{ userInfo.levelName }}</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h2 class="text-xl font-bold">欢迎来到风铃</h2>
|
||||||
|
<button
|
||||||
|
@click="goToLogin"
|
||||||
|
class="mt-2 text-sm bg-white/20 hover:bg-white/30 px-4 py-1.5 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
登录 / 注册
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快捷数据卡片 -->
|
||||||
|
<div class="relative px-6 -mt-16 z-20">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<!-- 积分卡片 -->
|
||||||
|
<div
|
||||||
|
@click="handleMenuClick(menuGroups[0].items[0])"
|
||||||
|
class="flex-1 bg-white rounded-2xl p-4 shadow-md hover:shadow-lg transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
|
||||||
|
🪙
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-2xl font-bold text-slate-800">{{ isLoggedIn ? userInfo.points : '---' }}</p>
|
||||||
|
<p class="text-xs text-slate-500">积分</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡券卡片 -->
|
||||||
|
<div
|
||||||
|
@click="handleMenuClick(menuGroups[0].items[2])"
|
||||||
|
class="flex-1 bg-white rounded-2xl p-4 shadow-md hover:shadow-lg transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-12 h-12 bg-rose-100 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
|
||||||
|
🎫
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-2xl font-bold text-slate-800">{{ isLoggedIn ? userInfo.coupons : '---' }}</p>
|
||||||
|
<p class="text-xs text-slate-500">卡券</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订单卡片 -->
|
||||||
|
<div
|
||||||
|
@click="handleMenuClick(menuGroups[1].items[0])"
|
||||||
|
class="flex-1 bg-white rounded-2xl p-4 shadow-md hover:shadow-lg transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
|
||||||
|
📦
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-2xl font-bold text-slate-800">{{ isLoggedIn ? userInfo.orders : '---' }}</p>
|
||||||
|
<p class="text-xs text-slate-500">订单</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能菜单 -->
|
||||||
|
<div class="px-6 mt-6 pb-8">
|
||||||
|
<template v-for="group in menuGroups" :key="group.title">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-sm font-semibold text-slate-500 mb-3 px-2">{{ group.title }}</h3>
|
||||||
|
<div class="bg-white rounded-2xl overflow-hidden shadow-sm">
|
||||||
|
<template v-for="(item, index) in group.items" :key="item.path">
|
||||||
|
<div
|
||||||
|
@click="handleMenuClick(item)"
|
||||||
|
class="flex items-center gap-4 p-4 hover:bg-slate-50 cursor-pointer transition-colors"
|
||||||
|
:class="{ 'border-t border-slate-100': index > 0 }"
|
||||||
|
>
|
||||||
|
<span class="text-2xl">{{ item.icon }}</span>
|
||||||
|
<span class="flex-1 text-slate-700 font-medium">{{ item.label }}</span>
|
||||||
|
<span v-if="item.requiresAuth && !isLoggedIn" class="text-xs text-slate-400">
|
||||||
|
需登录
|
||||||
|
</span>
|
||||||
|
<span class="text-slate-300">›</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 可选:添加一些微动画 */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white {
|
||||||
|
animation: fadeInUp 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white:nth-child(2) { animation-delay: 0.1s; }
|
||||||
|
.bg-white:nth-child(3) { animation-delay: 0.2s; }
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user