feat(client): 实现账号设备试用期管理功能
- 新增设备试用期过期时间字段及管理接口 - 实现试用期状态检查与过期提醒逻辑 - 支持账号类型区分试用与付费用户 - 添加设备注册时自动设置3天试用期- 实现VIP状态刷新与过期类型判断 -优化账号列表查询支持按客户端用户名过滤 - 更新客户端设备管理支持试用期控制- 完善登录流程支持试用期状态提示 -修复设备离线通知缺少用户名参数问题 - 调整账号默认设置清除逻辑关联客户端用户名
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user