初始版本
This commit is contained in:
770
app/pages/console/index.vue
Normal file
770
app/pages/console/index.vue
Normal file
@@ -0,0 +1,770 @@
|
||||
<template>
|
||||
<div class="console-home">
|
||||
<!-- 顶部欢迎区 -->
|
||||
<div class="welcome-section">
|
||||
<div class="welcome-content">
|
||||
<h1 class="welcome-title">欢迎回来 👋</h1>
|
||||
<p class="welcome-subtitle">管理您的应用、订单与账号,一站式控制台</p>
|
||||
</div>
|
||||
<div class="welcome-actions">
|
||||
<a-button type="primary" size="large" @click="navigateTo('/console/apps')">
|
||||
<template #icon><AppstoreOutlined /></template>
|
||||
进入应用中心
|
||||
</a-button>
|
||||
<a-button size="large" @click="navigateTo('/market')">
|
||||
<template #icon><ShopOutlined /></template>
|
||||
浏览应用商店
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 账户概览 -->
|
||||
<div class="account-overview">
|
||||
<div
|
||||
class="overview-card"
|
||||
@click="navigateTo('/console/recharge')"
|
||||
>
|
||||
<div class="overview-icon balance">
|
||||
<WalletOutlined />
|
||||
</div>
|
||||
<div class="overview-body">
|
||||
<div class="overview-label">账户余额</div>
|
||||
<div class="overview-value">
|
||||
<a-skeleton-input v-if="accountLoading" active size="small" style="width: 80px" />
|
||||
<span v-else>¥ {{ balanceText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-hint">充值 →</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overview-card"
|
||||
@click="navigateTo('/console/points')"
|
||||
>
|
||||
<div class="overview-icon points">
|
||||
<DollarOutlined />
|
||||
</div>
|
||||
<div class="overview-body">
|
||||
<div class="overview-label">积分</div>
|
||||
<div class="overview-value">
|
||||
<a-skeleton-input v-if="accountLoading" active size="small" style="width: 80px" />
|
||||
<span v-else>{{ userInfo?.points ?? 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-hint">明细 →</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overview-card"
|
||||
@click="navigateTo('/console/coupons')"
|
||||
>
|
||||
<div class="overview-icon coupon">
|
||||
<TagOutlined />
|
||||
</div>
|
||||
<div class="overview-body">
|
||||
<div class="overview-label">优惠券</div>
|
||||
<div class="overview-value">
|
||||
<a-skeleton-input v-if="accountLoading" active size="small" style="width: 80px" />
|
||||
<span v-else>{{ couponCount }} 张</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-hint">查看 →</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">快捷入口</h3>
|
||||
</div>
|
||||
<div class="quick-cards-grid">
|
||||
<div
|
||||
v-for="card in quickCards"
|
||||
:key="card.to"
|
||||
class="quick-card"
|
||||
@click="navigateTo(card.to)"
|
||||
>
|
||||
<div class="quick-card-icon" :style="{ background: card.bg }">
|
||||
<component :is="card.icon" :style="{ fontSize: '22px', color: card.color }" />
|
||||
</div>
|
||||
<div class="quick-card-info">
|
||||
<div class="quick-card-label">{{ card.label }}</div>
|
||||
<div class="quick-card-desc">{{ card.desc }}</div>
|
||||
</div>
|
||||
<RightOutlined class="quick-card-arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近使用的应用 -->
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">最近使用</h3>
|
||||
<NuxtLink to="/console/apps">
|
||||
<a-button type="link">
|
||||
查看全部 <RightOutlined />
|
||||
</a-button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="recentAppsLoading" class="recent-apps-loading">
|
||||
<a-skeleton active :paragraph="{ rows: 1 }" />
|
||||
</div>
|
||||
|
||||
<!-- 应用列表 -->
|
||||
<div v-else-if="recentApps.length > 0" class="recent-apps-grid">
|
||||
<div
|
||||
v-for="app in recentApps.slice(0, 6)"
|
||||
:key="app.productId"
|
||||
class="recent-app-card"
|
||||
@click="handleAppClick(app)"
|
||||
>
|
||||
<div class="recent-app-icon" :style="{ background: iconBgColor(app.productName) }">
|
||||
<img
|
||||
v-if="app.icon"
|
||||
:src="app.icon"
|
||||
:alt="app.productName"
|
||||
class="recent-app-icon-img"
|
||||
/>
|
||||
<span v-else class="recent-app-icon-text">{{ appTypeIcon(app.appType) }}</span>
|
||||
</div>
|
||||
<div class="recent-app-info">
|
||||
<div class="recent-app-name">{{ app.productName }}</div>
|
||||
<div class="recent-app-meta">
|
||||
<span class="recent-app-type">{{ appTypeName(app.type, app.appType) }}</span>
|
||||
<span class="recent-app-time">{{ formatTime(app.updateTime || app.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recent-app-entries">
|
||||
<template v-for="entry in getAppEntries(app)" :key="entry.type">
|
||||
<a-button
|
||||
v-if="entry.available"
|
||||
:type="entry.isPrimary ? 'primary' : 'default'"
|
||||
size="small"
|
||||
class="recent-app-enter-btn"
|
||||
@click.stop="handleEntryClick(entry, app)"
|
||||
>
|
||||
<component :is="entry.icon" />
|
||||
{{ entry.label }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="recent-apps-empty">
|
||||
<a-empty description="暂无最近使用的应用">
|
||||
<template #image>
|
||||
<div class="empty-icon">📦</div>
|
||||
</template>
|
||||
<a-button type="primary" @click="navigateTo('/console/apps')">去创建应用</a-button>
|
||||
</a-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用详情抽屉 -->
|
||||
<AppDetail v-model:open="detailOpen" :app="selectedApp" @deleted="handleDeletedFromDetail" @updated="handleUpdatedFromDetail" />
|
||||
|
||||
<!-- 小程序扫码弹窗 -->
|
||||
<QrCodeModal
|
||||
v-model:open="qrOpen"
|
||||
:qrcode-url="qrApp?.qrcode"
|
||||
:app-name="qrApp?.productName"
|
||||
:title="qrApp ? (APP_TYPE_NAME[qrApp.appType ?? 10] || '小程序') + '二维码' : ''"
|
||||
:tip="qrApp ? getScanTip(qrApp.appType ?? 20) : ''"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
GiftOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ShoppingCartOutlined,
|
||||
ShoppingOutlined,
|
||||
UserOutlined,
|
||||
ShopOutlined,
|
||||
RightOutlined,
|
||||
TeamOutlined,
|
||||
CustomerServiceOutlined,
|
||||
GlobalOutlined,
|
||||
QrcodeOutlined,
|
||||
DownloadOutlined,
|
||||
SettingOutlined,
|
||||
WalletOutlined,
|
||||
DollarOutlined,
|
||||
TagOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getJoinedApps, recordVisit } from '@/api/app/appProduct'
|
||||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||||
import { APP_TYPE, APP_TYPE_NAME } from '@/api/app/appProduct/model'
|
||||
import AppDetail from '@/components/developer/AppDetail.vue'
|
||||
import QrCodeModal from '@/components/QrCodeModal.vue'
|
||||
import { getAppEntries, executeEntry, getScanTip } from '@/utils/appEntry'
|
||||
import type { AppEntry } from '@/utils/appEntry'
|
||||
import { getUserInfo } from '@/api/layout'
|
||||
import { listUserCoupon } from '@/api/user/userCoupon'
|
||||
import type { User } from '@/api/system/user/model'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
definePageMeta({ layout: 'console' })
|
||||
|
||||
const userId = import.meta.client ? localStorage.getItem('UserId') : null
|
||||
|
||||
// ===== 账户概览 =====
|
||||
const accountLoading = ref(false)
|
||||
const userInfo = ref<User | null>(null)
|
||||
const couponCount = ref(0)
|
||||
|
||||
const balanceText = computed(() => {
|
||||
const b = userInfo.value?.balance ?? 0
|
||||
return (b / 100).toFixed(2)
|
||||
})
|
||||
|
||||
async function loadAccountOverview() {
|
||||
accountLoading.value = true
|
||||
try {
|
||||
const [uRes, cRes] = await Promise.allSettled([
|
||||
getUserInfo(),
|
||||
listUserCoupon({ status: 0 }),
|
||||
])
|
||||
if (uRes.status === 'fulfilled') userInfo.value = uRes.value
|
||||
couponCount.value = cRes.status === 'fulfilled' && Array.isArray(cRes.value) ? cRes.value.length : 0
|
||||
} catch {
|
||||
// 静默失败,不影响首页其他内容
|
||||
} finally {
|
||||
accountLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷入口配置
|
||||
const quickCards = [
|
||||
{
|
||||
label: '应用中心',
|
||||
desc: '管理我的应用',
|
||||
to: '/console/apps',
|
||||
icon: AppstoreOutlined,
|
||||
bg: '#eff6ff',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
label: '已购产品',
|
||||
desc: '查看授权与订阅',
|
||||
to: '/console/products',
|
||||
icon: ShoppingOutlined,
|
||||
bg: '#f0fdf4',
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
label: '订单记录',
|
||||
desc: '历史订单与账单',
|
||||
to: '/console/orders',
|
||||
icon: ShoppingCartOutlined,
|
||||
bg: '#fff7ed',
|
||||
color: '#f97316',
|
||||
},
|
||||
{
|
||||
label: '优惠券',
|
||||
desc: '查看可用优惠',
|
||||
to: '/console/coupons',
|
||||
icon: GiftOutlined,
|
||||
bg: '#fdf4ff',
|
||||
color: '#a855f7',
|
||||
},
|
||||
{
|
||||
label: '成员管理',
|
||||
desc: '团队与权限管理',
|
||||
to: '/console/account/members',
|
||||
icon: TeamOutlined,
|
||||
bg: '#fefce8',
|
||||
color: '#eab308',
|
||||
},
|
||||
{
|
||||
label: '工单管理',
|
||||
desc: '技术支持与反馈',
|
||||
to: '/console/tickets',
|
||||
icon: CustomerServiceOutlined,
|
||||
bg: '#f0f9ff',
|
||||
color: '#0ea5e9',
|
||||
},
|
||||
{
|
||||
label: '账号安全',
|
||||
desc: '密码与安全设置',
|
||||
to: '/console/account/security',
|
||||
icon: SafetyCertificateOutlined,
|
||||
bg: '#fff1f2',
|
||||
color: '#f43f5e',
|
||||
},
|
||||
]
|
||||
|
||||
// 最近使用的应用
|
||||
const recentApps = ref<AppProduct[]>([])
|
||||
const recentAppsLoading = ref(false)
|
||||
const detailOpen = ref(false)
|
||||
const selectedApp = ref<AppProduct | null>(null)
|
||||
|
||||
// 跳转方法(模板中不能直接调用 navigateTo)
|
||||
function navigateTo(path: string) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
// 入口处理
|
||||
function handleEntryClick(entry: AppEntry, app: AppProduct) {
|
||||
if (entry.type === 'scan-qr') {
|
||||
qrApp.value = app
|
||||
qrOpen.value = true
|
||||
return
|
||||
}
|
||||
executeEntry(entry)
|
||||
}
|
||||
|
||||
// 扫码弹窗
|
||||
const qrOpen = ref(false)
|
||||
const qrApp = ref<AppProduct | null>(null)
|
||||
|
||||
// 应用类型名称(使用统一枚举)
|
||||
function appTypeName(type?: number, appType?: number): string {
|
||||
return APP_TYPE_NAME[type ?? 10] ?? 'Web 应用'
|
||||
}
|
||||
|
||||
function appTypeIcon(appType?: number): string {
|
||||
const iconMap: Record<number, string> = {
|
||||
[APP_TYPE.WEBSITE]: '🌐',
|
||||
[APP_TYPE.WECHAT_MP]: '📱',
|
||||
[APP_TYPE.DOUYIN_MP]: '🎵',
|
||||
[APP_TYPE.BAIDU_MP]: '🔍',
|
||||
[APP_TYPE.ALIPAY_MP]: '💎',
|
||||
[APP_TYPE.ANDROID]: '🤖',
|
||||
[APP_TYPE.IOS]: '🍎',
|
||||
[APP_TYPE.MACOS]: '💻',
|
||||
[APP_TYPE.WINDOWS]: '🪟',
|
||||
[APP_TYPE.PLUGIN]: '🔌',
|
||||
}
|
||||
return iconMap[appType ?? 10] ?? '🌐'
|
||||
}
|
||||
|
||||
// 图标背景色
|
||||
const PALETTE = ['#f56a00', '#7265e6', '#ffbf00', '#00a2ae', '#87d068', '#108ee9']
|
||||
|
||||
function iconBgColor(name?: string) {
|
||||
if (!name) return PALETTE[0]
|
||||
let h = 0
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff
|
||||
return PALETTE[Math.abs(h) % PALETTE.length]
|
||||
}
|
||||
|
||||
function formatTime(timestamp?: string | number | Date) {
|
||||
if (!timestamp) return '-'
|
||||
const date = typeof timestamp === 'string' ? new Date(timestamp) : new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (days === 0) {
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
if (hours === 0) {
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
return minutes <= 1 ? '刚刚' : `${minutes}分钟前`
|
||||
}
|
||||
return `${hours}小时前`
|
||||
} else if (days === 1) {
|
||||
return '昨天'
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最近使用的应用(包括我创建的和被邀请参与的应用)
|
||||
async function loadRecentApps() {
|
||||
if (!userId) return
|
||||
|
||||
recentAppsLoading.value = true
|
||||
try {
|
||||
const uid = Number(userId)
|
||||
|
||||
// 获取我参与的所有应用(后端按 app_user.update_time 排序,实现"最近使用")
|
||||
const result = await getJoinedApps({ page: 1, limit: 10 })
|
||||
recentApps.value = result?.list || []
|
||||
} catch (e) {
|
||||
console.error('加载最近应用失败', e)
|
||||
} finally {
|
||||
recentAppsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppClick(app: AppProduct) {
|
||||
// 记录访问(异步,不阻塞)
|
||||
if (app.productId) {
|
||||
recordVisit(app.productId)
|
||||
}
|
||||
selectedApp.value = app
|
||||
detailOpen.value = true
|
||||
}
|
||||
|
||||
function handleDeletedFromDetail() {
|
||||
selectedApp.value = null
|
||||
detailOpen.value = false
|
||||
loadRecentApps()
|
||||
}
|
||||
|
||||
function handleUpdatedFromDetail(updatedApp: AppProduct) {
|
||||
const index = recentApps.value.findIndex((app) => app.productId === updatedApp.productId)
|
||||
if (index !== -1) {
|
||||
recentApps.value[index] = { ...recentApps.value[index], ...updatedApp }
|
||||
}
|
||||
if (selectedApp.value?.productId === updatedApp.productId) {
|
||||
selectedApp.value = { ...selectedApp.value, ...updatedApp }
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRecentApps()
|
||||
loadAccountOverview()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ===== 页面整体 ===== */
|
||||
.console-home {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
/* ===== 欢迎区 ===== */
|
||||
.welcome-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.welcome-section {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.welcome-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 区块样式 ===== */
|
||||
.section-block {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ===== 快捷入口 ===== */
|
||||
.quick-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.quick-card:hover {
|
||||
background: #fff;
|
||||
border-color: #d6e4ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.quick-card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quick-card-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.quick-card-desc {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.quick-card-arrow {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-card:hover .quick-card-arrow {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* ===== 最近使用 ===== */
|
||||
.recent-apps-loading {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.recent-apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.recent-app-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.recent-app-card:hover {
|
||||
background: #fff;
|
||||
border-color: #d6e4ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.recent-app-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.recent-app-icon-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.recent-app-icon-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.recent-app-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recent-app-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.recent-app-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.recent-app-type {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
background: #f0f0f0;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.recent-app-time {
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.recent-app-entries {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.recent-app-card:hover .recent-app-entries {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.recent-app-enter-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 移动端始终显示进入按钮 */
|
||||
@media (max-width: 768px) {
|
||||
.recent-app-entries {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-apps-empty {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* ===== 账户概览 ===== */
|
||||
.account-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.overview-card:hover {
|
||||
border-color: #d6e4ff;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.overview-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.overview-icon.balance {
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.overview-icon.points {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.overview-icon.coupon {
|
||||
background: linear-gradient(135deg, #fa8c16 0%, #ffc53d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.overview-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.overview-label {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.overview-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.overview-hint {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.overview-card:hover .overview-hint {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.account-overview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}</style>
|
||||
Reference in New Issue
Block a user