feat(client): 实现账号设备试用期管理功能

- 新增设备试用期过期时间字段及管理接口
- 实现试用期状态检查与过期提醒逻辑
- 支持账号类型区分试用与付费用户
- 添加设备注册时自动设置3天试用期- 实现VIP状态刷新与过期类型判断
-优化账号列表查询支持按客户端用户名过滤
- 更新客户端设备管理支持试用期控制- 完善登录流程支持试用期状态提示
-修复设备离线通知缺少用户名参数问题
- 调整账号默认设置清除逻辑关联客户端用户名
This commit is contained in:
2025-10-17 14:17:02 +08:00
parent 132299c4b7
commit 6e1b4d00de
18 changed files with 348 additions and 129 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmounted} from 'vue'
import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmounted, provide} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
@@ -8,6 +8,7 @@ import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
import {getOrCreateDeviceId} from './utils/deviceId'
import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token'
import {CONFIG} from './api/http'
import {getSettings} from './utils/settings'
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
@@ -16,6 +17,7 @@ const AmazonDashboard = defineAsyncComponent(() => import('./components/amazon/A
const ZebraDashboard = defineAsyncComponent(() => import('./components/zebra/ZebraDashboard.vue'))
const UpdateDialog = defineAsyncComponent(() => import('./components/common/UpdateDialog.vue'))
const SettingsDialog = defineAsyncComponent(() => import('./components/common/SettingsDialog.vue'))
const TrialExpiredDialog = defineAsyncComponent(() => import('./components/common/TrialExpiredDialog.vue'))
const dashboardsMap: Record<string, Component> = {
rakuten: RakutenDashboard,
@@ -51,23 +53,39 @@ const userPermissions = ref<string>('')
// VIP状态
const vipExpireTime = ref<Date | null>(null)
const deviceTrialExpired = ref(false)
const accountType = ref<string>('trial')
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' }
})
// 功能可用性账号VIP + 设备试用期)
const canUseFunctions = computed(() => {
// 付费账号不受设备限制
if (accountType.value === 'paid') return vipStatus.value.isVip
// 试用账号需要账号VIP有效 且 设备未过期
return vipStatus.value.isVip && !deviceTrialExpired.value
})
// 更新对话框状态
const showUpdateDialog = ref(false)
const updateDialogRef = ref()
// 设置对话框状态
const showSettingsDialog = ref(false)
// 试用期过期对话框
const showTrialExpiredDialog = ref(false)
const trialExpiredType = ref<'device' | 'account' | 'both'>('device')
// 菜单配置 - 复刻ERP客户端格式
const menuConfig = [
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R'},
@@ -147,7 +165,7 @@ function handleMenuSelect(key: string) {
addToHistory(key)
}
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }) {
try {
setToken(data.token)
isAuthenticated.value = true
@@ -157,6 +175,8 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
currentUsername.value = getUsernameFromToken(data.token)
userPermissions.value = data.permissions || ''
vipExpireTime.value = data.expireTime ? new Date(data.expireTime) : null
accountType.value = data.accountType || 'trial'
deviceTrialExpired.value = data.deviceTrialExpired || false
const deviceId = await getOrCreateDeviceId()
await deviceApi.register({
@@ -165,6 +185,31 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
os: navigator.platform
})
SSEManager.connect()
// 根据不同场景显示提示
const accountExpired = vipExpireTime.value && new Date() > vipExpireTime.value
const deviceExpired = deviceTrialExpired.value
const isPaid = accountType.value === 'paid'
if (isPaid) {
// 场景5: 付费用户
ElMessage.success('登录成功')
} else if (deviceExpired && accountExpired) {
// 场景4: 试用已到期,请订阅
trialExpiredType.value = 'both'
showTrialExpiredDialog.value = true
} else if (accountExpired) {
// 场景3: 账号试用已到期,请订阅
trialExpiredType.value = 'account'
showTrialExpiredDialog.value = true
} else if (deviceExpired) {
// 场景2: 设备试用已到期,请更换设备或订阅
trialExpiredType.value = 'device'
showTrialExpiredDialog.value = true
} else {
// 场景1: 允许使用
ElMessage.success('登录成功')
}
} catch (e: any) {
isAuthenticated.value = false
showAuthDialog.value = true
@@ -179,6 +224,8 @@ function clearLocalAuth() {
currentUsername.value = ''
userPermissions.value = ''
vipExpireTime.value = null
deviceTrialExpired.value = false
accountType.value = 'trial'
showAuthDialog.value = true
showDeviceDialog.value = false
SSEManager.disconnect()
@@ -187,7 +234,7 @@ function clearLocalAuth() {
async function logout() {
try {
const deviceId = getClientIdFromToken()
if (deviceId) await deviceApi.offline({ deviceId })
if (deviceId) await deviceApi.offline({ deviceId, username: currentUsername.value })
} catch (error) {
console.warn('离线通知失败:', error)
}
@@ -235,6 +282,8 @@ async function checkAuth() {
isAuthenticated.value = true
currentUsername.value = getUsernameFromToken(token)
userPermissions.value = res.data.permissions || ''
deviceTrialExpired.value = res.data.deviceTrialExpired || false
accountType.value = res.data.accountType || 'trial'
if (res.data.expireTime) {
vipExpireTime.value = new Date(res.data.expireTime)
@@ -249,6 +298,38 @@ async function checkAuth() {
}
}
// 刷新VIP状态采集前调用
async function refreshVipStatus() {
try {
const token = getToken()
if (!token) return false
const res = await authApi.verifyToken(token)
deviceTrialExpired.value = res.data.deviceTrialExpired || false
accountType.value = res.data.accountType || 'trial'
if (res.data.expireTime) {
vipExpireTime.value = new Date(res.data.expireTime)
}
return true
} catch {
return false
}
}
// 判断过期类型
function checkExpiredType(): 'device' | 'account' | 'both' {
const accountExpired = vipExpireTime.value && new Date() > vipExpireTime.value
const deviceExpired = deviceTrialExpired.value
if (deviceExpired && accountExpired) return 'both'
if (accountExpired) return 'account'
if (deviceExpired) return 'device'
return 'account' // 默认
}
// 提供给子组件使用
provide('refreshVipStatus', refreshVipStatus)
provide('checkExpiredType', checkExpiredType)
const SSEManager = {
connection: null as EventSource | null,
@@ -364,7 +445,7 @@ async function confirmRemoveDevice(row: DeviceItem) {
type: 'warning'
})
await deviceApi.remove({deviceId: row.deviceId})
await deviceApi.remove({deviceId: row.deviceId, username: currentUsername.value})
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1)
@@ -382,8 +463,30 @@ async function confirmRemoveDevice(row: DeviceItem) {
onMounted(async () => {
showContent()
await checkAuth()
// 检查是否有待安装的更新
await checkPendingUpdate()
})
async function checkPendingUpdate() {
try {
const result = await (window as any).electronAPI.checkPendingUpdate()
if (result && result.hasPendingUpdate) {
// 有待安装的更新,直接弹出安装对话框
showUpdateDialog.value = true
}
} catch (error) {
console.error('检查待安装更新失败:', error)
}
}
// 处理自动更新配置变化
function handleAutoUpdateChanged(enabled: boolean) {
if (enabled && updateDialogRef.value) {
updateDialogRef.value.checkForUpdatesNow()
}
}
onUnmounted(() => {
SSEManager.disconnect()
})
@@ -461,7 +564,7 @@ onUnmounted(() => {
</div>
</div>
<keep-alive v-if="activeDashboard">
<component :is="activeDashboard" :key="activeMenu" :is-vip="vipStatus.isVip"/>
<component :is="activeDashboard" :key="activeMenu" :is-vip="canUseFunctions"/>
</keep-alive>
<div v-if="showPlaceholder" class="placeholder">
<div class="placeholder-card">
@@ -483,10 +586,13 @@ onUnmounted(() => {
@back-to-login="backToLogin"/>
<!-- 更新对话框 -->
<UpdateDialog v-model="showUpdateDialog" />
<UpdateDialog ref="updateDialogRef" v-model="showUpdateDialog" />
<!-- 设置对话框 -->
<SettingsDialog v-model="showSettingsDialog" />
<SettingsDialog v-model="showSettingsDialog" @auto-update-changed="handleAutoUpdateChanged" />
<!-- 试用期过期弹框 -->
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
<!-- 设备管理弹框 -->
<el-dialog