1
This commit is contained in:
@@ -5,6 +5,7 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import 'element-plus/dist/index.css'
|
||||
import {authApi} from './api/auth'
|
||||
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
||||
import {getOrCreateDeviceId} from './utils/deviceId'
|
||||
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
||||
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
||||
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
|
||||
@@ -46,6 +47,19 @@ const devices = ref<DeviceItem[]>([])
|
||||
const deviceQuota = ref<DeviceQuota>({limit: 0, used: 0})
|
||||
const userPermissions = ref<string>('')
|
||||
|
||||
// VIP状态
|
||||
const vipExpireTime = ref<Date | null>(null)
|
||||
const vipStatus = computed(() => {
|
||||
if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, status: 'expired' }
|
||||
const now = new Date()
|
||||
const expire = new Date(vipExpireTime.value)
|
||||
const daysLeft = Math.ceil((expire.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
if (daysLeft <= 0) return { isVip: false, daysLeft: 0, status: 'expired' }
|
||||
if (daysLeft <= 7) return { isVip: true, daysLeft, status: 'warning' }
|
||||
if (daysLeft <= 30) return { isVip: true, daysLeft, status: 'normal' }
|
||||
return { isVip: true, daysLeft, status: 'active' }
|
||||
})
|
||||
|
||||
// 更新对话框状态
|
||||
const showUpdateDialog = ref(false)
|
||||
|
||||
@@ -131,7 +145,7 @@ function handleMenuSelect(key: string) {
|
||||
addToHistory(key)
|
||||
}
|
||||
|
||||
async function handleLoginSuccess(data: { token: string; permissions?: string }) {
|
||||
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
|
||||
isAuthenticated.value = true
|
||||
showAuthDialog.value = false
|
||||
showRegDialog.value = false
|
||||
@@ -141,7 +155,15 @@ async function handleLoginSuccess(data: { token: string; permissions?: string })
|
||||
const username = getUsernameFromToken(data.token)
|
||||
currentUsername.value = username
|
||||
userPermissions.value = data?.permissions || ''
|
||||
await deviceApi.register({username})
|
||||
vipExpireTime.value = data?.expireTime ? new Date(data.expireTime) : null
|
||||
|
||||
// 获取设备ID并注册设备
|
||||
const deviceId = await getOrCreateDeviceId()
|
||||
await deviceApi.register({
|
||||
username,
|
||||
deviceId,
|
||||
os: navigator.platform
|
||||
})
|
||||
SSEManager.connect()
|
||||
} catch (e: any) {
|
||||
isAuthenticated.value = false
|
||||
@@ -169,6 +191,7 @@ async function logout() {
|
||||
isAuthenticated.value = false
|
||||
currentUsername.value = ''
|
||||
userPermissions.value = ''
|
||||
vipExpireTime.value = null
|
||||
showAuthDialog.value = true
|
||||
showDeviceDialog.value = false
|
||||
SSEManager.disconnect()
|
||||
@@ -207,9 +230,15 @@ async function checkAuth() {
|
||||
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
||||
|
||||
if (token) {
|
||||
await authApi.verifyToken(token)
|
||||
const verifyRes: any = await authApi.verifyToken(token)
|
||||
isAuthenticated.value = true
|
||||
currentUsername.value = getUsernameFromToken(token) || ''
|
||||
|
||||
userPermissions.value = verifyRes?.data?.permissions || verifyRes?.permissions || ''
|
||||
if (verifyRes?.data?.expireTime || verifyRes?.expireTime) {
|
||||
vipExpireTime.value = new Date(verifyRes?.data?.expireTime || verifyRes?.expireTime)
|
||||
}
|
||||
|
||||
SSEManager.connect()
|
||||
return
|
||||
}
|
||||
@@ -266,7 +295,7 @@ const SSEManager = {
|
||||
return
|
||||
}
|
||||
|
||||
let sseUrl = 'http://192.168.1.89:8080/monitor/account/events'
|
||||
let sseUrl = 'http://192.168.1.89:8085/monitor/account/events'
|
||||
try {
|
||||
const resp = await fetch('/api/config/server')
|
||||
if (resp.ok) sseUrl = (await resp.json()).sseUrl || sseUrl
|
||||
@@ -304,6 +333,14 @@ const SSEManager = {
|
||||
case 'PERMISSIONS_UPDATED':
|
||||
checkAuth()
|
||||
break
|
||||
case 'ACCOUNT_EXPIRED':
|
||||
vipExpireTime.value = null
|
||||
ElMessage.warning('您的VIP已过期,部分功能将受限')
|
||||
break
|
||||
case 'VIP_RENEWED':
|
||||
checkAuth()
|
||||
ElMessage.success('VIP已续费成功')
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('SSE消息处理失败:', err, '原始数据:', e.data)
|
||||
@@ -423,6 +460,25 @@ onUnmounted(() => {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- VIP状态卡片 -->
|
||||
<div v-if="isAuthenticated" class="vip-status-card" :class="'vip-' + vipStatus.status">
|
||||
<div class="vip-info">
|
||||
<div class="vip-status-text">
|
||||
<template v-if="vipStatus.isVip">
|
||||
<span v-if="vipStatus.status === 'warning'">即将到期</span>
|
||||
<span v-else>订阅中</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>已过期</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="vip-expire-date" v-if="vipExpireTime">
|
||||
有效期至:{{ vipExpireTime ? new Date(vipExpireTime).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
@@ -445,7 +501,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<keep-alive v-if="activeDashboard">
|
||||
<component :is="activeDashboard" :key="activeMenu"/>
|
||||
<component :is="activeDashboard" :key="activeMenu" :is-vip="vipStatus.isVip"/>
|
||||
</keep-alive>
|
||||
<div v-if="showPlaceholder" class="placeholder">
|
||||
<div class="placeholder-card">
|
||||
@@ -580,6 +636,8 @@ onUnmounted(() => {
|
||||
border-right: 1px solid #e8eaec;
|
||||
padding: 16px 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.platform-icons {
|
||||
@@ -789,4 +847,104 @@ onUnmounted(() => {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* VIP状态卡片样式 */
|
||||
.vip-status-card {
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(135deg, #FFFAF0 0%, #FFE4B5 50%, #FFD700 100%);
|
||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 正常状态和警告状态 - 统一温暖金色渐变 */
|
||||
.vip-status-card.vip-active,
|
||||
.vip-status-card.vip-normal,
|
||||
.vip-status-card.vip-warning {
|
||||
background: linear-gradient(135deg, #FFFAF0 0%, #FFE4B5 50%, #FFD700 100%);
|
||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 过期状态 - 灰色,垂直布局 */
|
||||
.vip-status-card.vip-expired {
|
||||
background: linear-gradient(135deg, #FAFAFA 0%, #E8E8E8 100%);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vip-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.vip-status-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #8B6914;
|
||||
text-align: left;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.vip-expire-date {
|
||||
font-size: 10px;
|
||||
color: #A67C00;
|
||||
line-height: 1.3;
|
||||
text-align: left;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 右侧徽章按钮 */
|
||||
.vip-badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 6px rgba(74, 144, 226, 0.3);
|
||||
}
|
||||
|
||||
/* 过期状态续费按钮 - 置底 */
|
||||
.vip-renew-btn {
|
||||
padding: 7px 0;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 6px rgba(74, 144, 226, 0.3);
|
||||
}
|
||||
|
||||
.vip-status-card.vip-expired .vip-info {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.vip-status-card.vip-expired .vip-status-text {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.vip-status-card.vip-expired .vip-expire-date {
|
||||
color: #B0B0B0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user