diff --git a/electron-vue-template/img.png b/electron-vue-template/img.png new file mode 100644 index 0000000..4ed51e3 Binary files /dev/null and b/electron-vue-template/img.png differ diff --git a/electron-vue-template/public/icon/vipExclusive.png b/electron-vue-template/public/icon/vipExclusive.png new file mode 100644 index 0000000..927befa Binary files /dev/null and b/electron-vue-template/public/icon/vipExclusive.png differ diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index 3675586..fb548f8 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -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 {splashApi} from './api/splash' import {getOrCreateDeviceId} from './utils/deviceId' import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token' import {CONFIG} from './api/http' @@ -61,12 +62,12 @@ const vipExpireTime = ref(null) const deviceTrialExpired = ref(false) const accountType = ref('trial') const vipStatus = computed(() => { - if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType: 'account' } - + if (!vipExpireTime.value) return {isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType: 'account'} + const now = new Date() const expire = new Date(vipExpireTime.value) const msLeft = expire.getTime() - now.getTime() - + // 精确判断:当前时间 >= 过期时间,则已过期(与后端逻辑一致) if (msLeft <= 0) { const accountExpired = true @@ -75,22 +76,22 @@ const vipStatus = computed(() => { if (deviceExpired && accountExpired) expiredType = 'both' else if (accountExpired) expiredType = 'account' else if (deviceExpired) expiredType = 'device' - - return { isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType } + + return {isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType} } - + const hoursLeft = Math.floor(msLeft / (1000 * 60 * 60)) const daysLeft = Math.floor(msLeft / (1000 * 60 * 60 * 24)) - + let expiredType: 'device' | 'account' | 'both' | 'subscribe' = 'subscribe' if (accountType.value === 'trial' && deviceTrialExpired.value) { expiredType = 'device' // 试用账号且设备过期 } - - if (daysLeft === 0) return { isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType } - if (daysLeft <= 7) return { isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType } - if (daysLeft <= 30) return { isVip: true, daysLeft, hoursLeft, status: 'normal', expiredType } - return { isVip: true, daysLeft, hoursLeft, status: 'active', expiredType } + + if (daysLeft === 0) return {isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType} + if (daysLeft <= 7) return {isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType} + if (daysLeft <= 30) return {isVip: true, daysLeft, hoursLeft, status: 'normal', expiredType} + return {isVip: true, daysLeft, hoursLeft, status: 'active', expiredType} }) // 功能可用性(账号VIP + 设备试用期) @@ -118,6 +119,9 @@ const showAccountManager = ref(false) // 当前版本 const currentVersion = ref('') +// 品牌logo +const brandLogoUrl = ref('') + // 菜单配置 - 复刻ERP客户端格式 const menuConfig = [ {key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R', iconImage: rakutenIcon}, @@ -198,19 +202,25 @@ function handleMenuSelect(key: string) { addToHistory(key) } -async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string; accountType?: string; deviceTrialExpired?: boolean }) { +async function handleLoginSuccess(data: { + token: string; + permissions?: string; + expireTime?: string; + accountType?: string; + deviceTrialExpired?: boolean +}) { try { setToken(data.token) isAuthenticated.value = true showAuthDialog.value = false showRegDialog.value = false - + 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({ username: currentUsername.value, @@ -218,7 +228,7 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e os: navigator.platform }) SSEManager.connect() - + // 同步当前账号的设置到 Electron 主进程 syncSettingsToElectron() @@ -231,7 +241,7 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e isAuthenticated.value = false showAuthDialog.value = true removeToken() - + } } @@ -251,7 +261,7 @@ function clearLocalAuth() { async function logout() { try { const deviceId = getClientIdFromToken() - if (deviceId) await deviceApi.offline({ deviceId, username: currentUsername.value }) + if (deviceId) await deviceApi.offline({deviceId, username: currentUsername.value}) } catch (error) { console.warn('离线通知失败:', error) } @@ -270,7 +280,8 @@ async function handleUserClick() { cancelButtonText: '取消' }) await logout() - } catch {} + } catch { + } } function showRegisterDialog() { @@ -280,7 +291,7 @@ function showRegisterDialog() { function backToLogin() { showRegDialog.value = false - + showAuthDialog.value = true } @@ -300,13 +311,13 @@ async function checkAuth() { 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) } - + SSEManager.connect() - + // 同步当前账号的设置到 Electron 主进程 syncSettingsToElectron() } catch { @@ -322,7 +333,7 @@ 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' @@ -346,10 +357,10 @@ async function syncSettingsToElectron() { try { const username = getUsernameFromToken() const settings = getSettings(username) - + // 同步关闭行为 await (window as any).electronAPI.setCloseAction(settings.closeAction || 'quit') - + // 同步启动配置 await (window as any).electronAPI.setLaunchConfig({ autoLaunch: settings.autoLaunch || false, @@ -360,6 +371,19 @@ async function syncSettingsToElectron() { } } +// 加载品牌logo +async function loadBrandLogo() { + try { + const username = getUsernameFromToken() + if (!username) return + + const res = await splashApi.getBrandLogo(username) + brandLogoUrl.value = res.data.url + } catch (error) { + brandLogoUrl.value = '' + } +} + // 提供给子组件使用 provide('refreshVipStatus', refreshVipStatus) provide('vipStatus', vipStatus) @@ -518,24 +542,32 @@ async function confirmRemoveDevice(row: DeviceItem) { onMounted(async () => { showContent() await checkAuth() - + // 检查是否有待安装的更新 await checkPendingUpdate() - + // 加载当前版本 try { currentVersion.value = await (window as any).electronAPI.getJarVersion() } catch (error) { console.warn('获取当前版本失败:', error) } - + + // 加载品牌logo + loadBrandLogo() + + // 监听品牌logo变化 + window.addEventListener('brandLogoChanged', (e: any) => { + brandLogoUrl.value = e.detail + }) + // 全局阻止文件拖拽到窗口(避免意外打开文件) // 只在指定的 dropzone 区域处理拖拽上传 document.addEventListener('dragover', (e) => { e.preventDefault() e.stopPropagation() }, false) - + document.addEventListener('drop', (e) => { e.preventDefault() e.stopPropagation() @@ -584,6 +616,12 @@ onUnmounted(() => {
logo
+ + +
+ +
+ -
+
- 有效期至:{{ new Date(vipExpireTime).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) }} + 有效期至:{{ + new Date(vipExpireTime).toLocaleDateString('zh-CN', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) + }}
@@ -672,16 +717,20 @@ onUnmounted(() => { @back-to-login="backToLogin"/> - + - + - + - + { @@ -829,6 +880,24 @@ onUnmounted(() => { text-align: left; } +/* 品牌logo区域 */ +.brand-logo-section { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 48px; + margin-bottom: 12px; + padding: 0 4px; + box-sizing: border-box; +} + +.brand-logo { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + .menu { list-style: none; padding: 0; @@ -952,6 +1021,7 @@ onUnmounted(() => { font-size: 13px; color: #606266; } + .device-dialog-header { display: flex; flex-direction: column; @@ -959,31 +1029,45 @@ onUnmounted(() => { padding: 12px 0 4px 0; margin-left: 40px; } + .device-dialog :deep(.el-dialog__header) { text-align: center; } -.device-dialog :deep(.el-dialog__body) { padding-top: 0; } + +.device-dialog :deep(.el-dialog__body) { + padding-top: 0; +} + .device-illustration { width: 180px; height: auto; object-fit: contain; margin-bottom: 8px; } + .device-title { font-size: 18px; font-weight: 600; color: #303133; margin-bottom: 6px; } -.device-count { color: #909399; font-weight: 500; } -.device-subtitle { font-size: 12px; color: #909399; } + +.device-count { + color: #909399; + font-weight: 500; +} + +.device-subtitle { + font-size: 12px; + color: #909399; +} /* 浮动版本信息 */ .version-info { position: fixed; right: 10px; bottom: 10px; - background: rgba(255,255,255,0.9); + background: rgba(255, 255, 255, 0.9); padding: 5px 10px; border-radius: 4px; font-size: 12px; diff --git a/electron-vue-template/src/renderer/api/splash.ts b/electron-vue-template/src/renderer/api/splash.ts index 13ff308..6acfec2 100644 --- a/electron-vue-template/src/renderer/api/splash.ts +++ b/electron-vue-template/src/renderer/api/splash.ts @@ -27,6 +27,24 @@ export const splashApi = { // 删除自定义开屏图片(恢复默认) async deleteSplashImage(username: string) { return http.post<{ data: string }>(`/monitor/account/splash-image/delete?username=${username}`) + }, + + // 上传品牌logo + async uploadBrandLogo(file: File, username: string) { + const formData = new FormData() + formData.append('file', file) + formData.append('username', username) + return http.upload<{ data: { url: string; fileName: string } }>('/monitor/account/brand-logo/upload', formData) + }, + + // 获取当前用户的品牌logo + async getBrandLogo(username: string) { + return http.get<{ data: { url: string } }>('/monitor/account/brand-logo', { username }) + }, + + // 删除品牌logo + async deleteBrandLogo(username: string) { + return http.post<{ data: string }>(`/monitor/account/brand-logo/delete?username=${username}`) } } diff --git a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue index 02b9a33..7ec5f07 100644 --- a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue +++ b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue @@ -35,16 +35,17 @@ async function handleAuth() { try { // 获取或生成设备ID const deviceId = await getOrCreateDeviceId() - + // 登录 const loginRes: any = await authApi.login({ ...authForm.value, clientId: deviceId }) - // 保存开屏图片配置(不阻塞登录) + // 保存开屏图片配置和品牌logo(不阻塞登录) saveSplashConfigInBackground(authForm.value.username) - + saveBrandLogoInBackground(authForm.value.username) + emit('loginSuccess', { token: loginRes.data.accessToken || loginRes.data.token, permissions: loginRes.data.permissions, @@ -90,6 +91,18 @@ async function saveSplashConfigInBackground(username: string) { console.error('[开屏图片] 保存配置失败:', error) } } + +// 保存品牌logo配置 +async function saveBrandLogoInBackground(username: string) { + try { + const res = await splashApi.getBrandLogo(username) + const url = res?.data?.url || '' + // 触发App.vue加载品牌logo + window.dispatchEvent(new CustomEvent('brandLogoChanged', { detail: url })) + } catch (error) { + console.error('[品牌logo] 加载配置失败:', error) + } +} @@ -940,9 +1066,21 @@ onMounted(() => { font-weight: 600; color: #1F2937; text-align: left; +} + +.section-title-row { + display: flex; + align-items: center; + gap: 8px; margin-bottom: 8px; } +.vip-exclusive-logo { + width: 60px; + height: auto; + vertical-align: middle; +} + .section-header .section-title { margin-bottom: 0; } @@ -1100,6 +1238,53 @@ onMounted(() => { margin-top: 8px; } +/* 图片预览容器 */ +.image-preview-wrapper { + position: relative; + width: 75%; +} + +.preview-image { + width: 100%; + height: 100%; + object-fit: contain; +} + +.image-buttons { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + gap: 8px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.image-preview-wrapper:hover .image-buttons { + opacity: 1; +} + +.img-btn { + padding: 6px 14px; + background: rgba(0, 0, 0, 0.6); + border: none; + border-radius: 4px; + color: #fff; + font-size: 12px; + cursor: pointer; + transition: background 0.2s ease; +} + +.img-btn:hover:not(:disabled) { + background: rgba(0, 0, 0, 0.8); +} + +.img-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .feedback-form :deep(.el-textarea__inner) { border-color: #E5E6EB; border-radius: 6px; @@ -1312,22 +1497,7 @@ onMounted(() => { color: #1F2937; } -/* 开屏图片样式 */ -.splash-preview-container { - width: 100%; - height: 180px; - border-radius: 6px; - overflow: hidden; - border: 1px solid #E5E6EB; - background: #F8F9FA; -} - -.splash-preview-image { - width: 100%; - height: 100%; - object-fit: contain; -} - +/* 占位符 */ .splash-placeholder { width: 100%; height: 180px; @@ -1350,12 +1520,6 @@ onMounted(() => { font-size: 13px; color: #86909C; } - -.splash-actions { - display: flex; - align-items: center; - gap: 8px; -}