1
This commit is contained in:
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 Deluze
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "generic",
|
"provider": "generic",
|
||||||
"url": "http://192.168.1.89:8080/static/updates/"
|
"url": "http://192.168.1.89:8085/static/updates/"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
|
|||||||
2131
electron-vue-template/package-lock.json
generated
2131
electron-vue-template/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -311,9 +311,9 @@ app.whenReady().then(() => {
|
|||||||
splashWindow.loadFile(splashPath);
|
splashWindow.loadFile(splashPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
openAppIfNotOpened();
|
// openAppIfNotOpened();
|
||||||
}, 2000);
|
// }, 2000);
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import {authApi} from './api/auth'
|
import {authApi} from './api/auth'
|
||||||
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
||||||
|
import {getOrCreateDeviceId} from './utils/deviceId'
|
||||||
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
||||||
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
||||||
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.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 deviceQuota = ref<DeviceQuota>({limit: 0, used: 0})
|
||||||
const userPermissions = ref<string>('')
|
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)
|
const showUpdateDialog = ref(false)
|
||||||
|
|
||||||
@@ -131,7 +145,7 @@ function handleMenuSelect(key: string) {
|
|||||||
addToHistory(key)
|
addToHistory(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoginSuccess(data: { token: string; permissions?: string }) {
|
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
|
||||||
isAuthenticated.value = true
|
isAuthenticated.value = true
|
||||||
showAuthDialog.value = false
|
showAuthDialog.value = false
|
||||||
showRegDialog.value = false
|
showRegDialog.value = false
|
||||||
@@ -141,7 +155,15 @@ async function handleLoginSuccess(data: { token: string; permissions?: string })
|
|||||||
const username = getUsernameFromToken(data.token)
|
const username = getUsernameFromToken(data.token)
|
||||||
currentUsername.value = username
|
currentUsername.value = username
|
||||||
userPermissions.value = data?.permissions || ''
|
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()
|
SSEManager.connect()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
@@ -169,6 +191,7 @@ async function logout() {
|
|||||||
isAuthenticated.value = false
|
isAuthenticated.value = false
|
||||||
currentUsername.value = ''
|
currentUsername.value = ''
|
||||||
userPermissions.value = ''
|
userPermissions.value = ''
|
||||||
|
vipExpireTime.value = null
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
showDeviceDialog.value = false
|
showDeviceDialog.value = false
|
||||||
SSEManager.disconnect()
|
SSEManager.disconnect()
|
||||||
@@ -207,9 +230,15 @@ async function checkAuth() {
|
|||||||
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
await authApi.verifyToken(token)
|
const verifyRes: any = await authApi.verifyToken(token)
|
||||||
isAuthenticated.value = true
|
isAuthenticated.value = true
|
||||||
currentUsername.value = getUsernameFromToken(token) || ''
|
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()
|
SSEManager.connect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -266,7 +295,7 @@ const SSEManager = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let sseUrl = 'http://192.168.1.89:8080/monitor/account/events'
|
let sseUrl = 'http://192.168.1.89:8085/monitor/account/events'
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/config/server')
|
const resp = await fetch('/api/config/server')
|
||||||
if (resp.ok) sseUrl = (await resp.json()).sseUrl || sseUrl
|
if (resp.ok) sseUrl = (await resp.json()).sseUrl || sseUrl
|
||||||
@@ -304,6 +333,14 @@ const SSEManager = {
|
|||||||
case 'PERMISSIONS_UPDATED':
|
case 'PERMISSIONS_UPDATED':
|
||||||
checkAuth()
|
checkAuth()
|
||||||
break
|
break
|
||||||
|
case 'ACCOUNT_EXPIRED':
|
||||||
|
vipExpireTime.value = null
|
||||||
|
ElMessage.warning('您的VIP已过期,部分功能将受限')
|
||||||
|
break
|
||||||
|
case 'VIP_RENEWED':
|
||||||
|
checkAuth()
|
||||||
|
ElMessage.success('VIP已续费成功')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('SSE消息处理失败:', err, '原始数据:', e.data)
|
console.error('SSE消息处理失败:', err, '原始数据:', e.data)
|
||||||
@@ -423,6 +460,25 @@ onUnmounted(() => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
@@ -445,7 +501,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<keep-alive v-if="activeDashboard">
|
<keep-alive v-if="activeDashboard">
|
||||||
<component :is="activeDashboard" :key="activeMenu"/>
|
<component :is="activeDashboard" :key="activeMenu" :is-vip="vipStatus.isVip"/>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
<div v-if="showPlaceholder" class="placeholder">
|
<div v-if="showPlaceholder" class="placeholder">
|
||||||
<div class="placeholder-card">
|
<div class="placeholder-card">
|
||||||
@@ -580,6 +636,8 @@ onUnmounted(() => {
|
|||||||
border-right: 1px solid #e8eaec;
|
border-right: 1px solid #e8eaec;
|
||||||
padding: 16px 12px;
|
padding: 16px 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-icons {
|
.platform-icons {
|
||||||
@@ -789,4 +847,104 @@ onUnmounted(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
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>
|
</style>
|
||||||
@@ -2,25 +2,31 @@ import { http } from './http'
|
|||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
login(params: { username: string; password: string }) {
|
login(params: { username: string; password: string }) {
|
||||||
return http.post('/api/login', params)
|
// 直接调用 RuoYi 后端的登录接口
|
||||||
|
return http.post('/monitor/account/login', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
register(params: { username: string; password: string }) {
|
register(params: { username: string; password: string }) {
|
||||||
return http.post('/api/register', params)
|
// 直接调用 RuoYi 后端的注册接口
|
||||||
|
return http.post('/monitor/account/register', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
checkUsername(username: string) {
|
checkUsername(username: string) {
|
||||||
return http.get('/api/check-username', { username })
|
// 直接调用 RuoYi 后端的用户名检查接口
|
||||||
|
return http.get('/monitor/account/check-username', { username })
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyToken(token: string) {
|
verifyToken(token: string) {
|
||||||
return http.post('/api/verify', { token })
|
// 直接调用 RuoYi 后端的验证接口
|
||||||
|
return http.post('/monitor/account/verify', { token })
|
||||||
},
|
},
|
||||||
|
|
||||||
logout(token: string) {
|
logout(token: string) {
|
||||||
|
// 保留客户端的 logout(用于清理本地状态)
|
||||||
return http.postVoid('/api/logout', { token })
|
return http.postVoid('/api/logout', { token })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 以下缓存相关接口仍使用客户端服务(用于本地 SQLite 存储)
|
||||||
deleteTokenCache() {
|
deleteTokenCache() {
|
||||||
return http.postVoid('/api/cache/delete?key=token')
|
return http.postVoid('/api/cache/delete?key=token')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,26 +2,32 @@ import { http } from './http'
|
|||||||
|
|
||||||
export const deviceApi = {
|
export const deviceApi = {
|
||||||
getQuota(username: string) {
|
getQuota(username: string) {
|
||||||
return http.get('/api/device/quota', { username })
|
// 直接调用 RuoYi 后端的设备配额接口
|
||||||
|
return http.get('/monitor/device/quota', { username })
|
||||||
},
|
},
|
||||||
|
|
||||||
list(username: string) {
|
list(username: string) {
|
||||||
return http.get('/api/device/list', { username })
|
// 直接调用 RuoYi 后端的设备列表接口
|
||||||
|
return http.get('/monitor/device/list', { username })
|
||||||
},
|
},
|
||||||
|
|
||||||
register(payload: { username: string }) {
|
register(payload: { username: string }) {
|
||||||
return http.post('/api/device/register', payload)
|
// 直接调用 RuoYi 后端的设备注册接口
|
||||||
|
return http.post('/monitor/device/register', payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(payload: { deviceId: string }) {
|
remove(payload: { deviceId: string }) {
|
||||||
return http.post('/api/device/remove', payload)
|
// 直接调用 RuoYi 后端的设备移除接口
|
||||||
|
return http.post('/monitor/device/remove', payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
|
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
|
||||||
return http.post('/api/device/heartbeat', payload)
|
// 直接调用 RuoYi 后端的心跳接口
|
||||||
|
return http.post('/monitor/device/heartbeat', payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
offline(payload: { deviceId: string }) {
|
offline(payload: { deviceId: string }) {
|
||||||
return http.post('/api/device/offline', payload)
|
// 直接调用 RuoYi 后端的离线接口
|
||||||
|
return http.post('/monitor/device/offline', payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
export type HttpMethod = 'GET' | 'POST';
|
export type HttpMethod = 'GET' | 'POST';
|
||||||
|
|
||||||
const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb
|
const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb
|
||||||
const BASE_RUOYI = 'http://192.168.1.89:8080';
|
const BASE_RUOYI = 'http://192.168.1.89:8085';
|
||||||
|
|
||||||
function resolveBase(path: string): string {
|
function resolveBase(path: string): string {
|
||||||
// 走 ruoyi-admin 的路径:鉴权与版本、平台工具路由
|
// 走 ruoyi-admin 的路径:鉴权、设备管理、版本、平台工具路由
|
||||||
|
if (path.startsWith('/monitor/account')) return BASE_RUOYI; // 账号认证相关
|
||||||
|
if (path.startsWith('/monitor/device')) return BASE_RUOYI; // 设备管理
|
||||||
if (path.startsWith('/system/')) return BASE_RUOYI; // 版本控制器 VersionController
|
if (path.startsWith('/system/')) return BASE_RUOYI; // 版本控制器 VersionController
|
||||||
if (path.startsWith('/tool/banma')) return BASE_RUOYI; // 既有规则保留
|
if (path.startsWith('/tool/banma')) return BASE_RUOYI; // 既有规则保留
|
||||||
// 其他默认走客户端服务
|
// 其他默认走客户端服务
|
||||||
@@ -41,7 +43,12 @@ async function request<T>(path: string, options: RequestInit): Promise<T> {
|
|||||||
}
|
}
|
||||||
const contentType = res.headers.get('content-type') || '';
|
const contentType = res.headers.get('content-type') || '';
|
||||||
if (contentType.includes('application/json')) {
|
if (contentType.includes('application/json')) {
|
||||||
return (await res.json()) as T;
|
const json: any = await res.json();
|
||||||
|
// 检查业务状态码
|
||||||
|
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
|
||||||
|
throw new Error(json.msg || json.message || '请求失败');
|
||||||
|
}
|
||||||
|
return json as T;
|
||||||
}
|
}
|
||||||
return (await res.text()) as unknown as T;
|
return (await res.text()) as unknown as T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { amazonApi } from '../../api/amazon'
|
import { amazonApi } from '../../api/amazon'
|
||||||
import { handlePlatformFileExport } from '../../utils/settings'
|
import { handlePlatformFileExport } from '../../utils/settings'
|
||||||
|
|
||||||
|
// 接收VIP状态
|
||||||
|
const props = defineProps<{
|
||||||
|
isVip: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
const loading = ref(false) // 主加载状态
|
const loading = ref(false) // 主加载状态
|
||||||
const tableLoading = ref(false) // 表格加载状态
|
const tableLoading = ref(false) // 表格加载状态
|
||||||
@@ -85,6 +90,22 @@ async function onDrop(e: DragEvent) {
|
|||||||
|
|
||||||
// 批量获取产品信息 - 核心数据处理逻辑
|
// 批量获取产品信息 - 核心数据处理逻辑
|
||||||
async function batchGetProductInfo(asinList: string[]) {
|
async function batchGetProductInfo(asinList: string[]) {
|
||||||
|
// VIP检查
|
||||||
|
if (!props.isVip) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'VIP已过期,数据采集功能受限。请联系管理员续费后继续使用。',
|
||||||
|
'VIP功能限制',
|
||||||
|
{
|
||||||
|
confirmButtonText: '我知道了',
|
||||||
|
showCancelButton: false,
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentAsin.value = '正在处理...'
|
currentAsin.value = '正在处理...'
|
||||||
progressPercentage.value = 0
|
progressPercentage.value = 0
|
||||||
@@ -165,8 +186,6 @@ async function startQueuedFetch() {
|
|||||||
|
|
||||||
// 导出Excel数据
|
// 导出Excel数据
|
||||||
const exportLoading = ref(false)
|
const exportLoading = ref(false)
|
||||||
const exportProgress = ref(0)
|
|
||||||
const showExportProgress = ref(false)
|
|
||||||
|
|
||||||
async function exportToExcel() {
|
async function exportToExcel() {
|
||||||
if (!localProductData.value.length) {
|
if (!localProductData.value.length) {
|
||||||
@@ -175,12 +194,6 @@ async function exportToExcel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exportLoading.value = true
|
exportLoading.value = true
|
||||||
showExportProgress.value = true
|
|
||||||
exportProgress.value = 0
|
|
||||||
|
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
if (exportProgress.value < 90) exportProgress.value += Math.random() * 20
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
// 生成Excel HTML格式
|
// 生成Excel HTML格式
|
||||||
let html = `<table>
|
let html = `<table>
|
||||||
@@ -198,17 +211,12 @@ async function exportToExcel() {
|
|||||||
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
|
||||||
const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls`
|
const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls`
|
||||||
|
|
||||||
await handlePlatformFileExport('amazon', blob, fileName)
|
const success = await handlePlatformFileExport('amazon', blob, fileName)
|
||||||
|
|
||||||
clearInterval(progressInterval)
|
if (success) {
|
||||||
exportProgress.value = 100
|
showMessage('Excel文件导出成功!', 'success')
|
||||||
showMessage('Excel文件导出成功!', 'success')
|
}
|
||||||
|
exportLoading.value = false
|
||||||
setTimeout(() => {
|
|
||||||
showExportProgress.value = false
|
|
||||||
exportLoading.value = false
|
|
||||||
exportProgress.value = 0
|
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取卖家/配送方信息 - 数据处理辅助函数
|
// 获取卖家/配送方信息 - 数据处理辅助函数
|
||||||
@@ -360,13 +368,6 @@ onMounted(async () => {
|
|||||||
<div class="step-header"><div class="title">导出数据</div></div>
|
<div class="step-header"><div class="title">导出数据</div></div>
|
||||||
<div class="action-buttons column">
|
<div class="action-buttons column">
|
||||||
<el-button size="small" class="w100 btn-blue" :disabled="!localProductData.length || loading || exportLoading" :loading="exportLoading" @click="exportToExcel">{{ exportLoading ? '导出中...' : '导出Excel' }}</el-button>
|
<el-button size="small" class="w100 btn-blue" :disabled="!localProductData.length || loading || exportLoading" :loading="exportLoading" @click="exportToExcel">{{ exportLoading ? '导出中...' : '导出Excel' }}</el-button>
|
||||||
<!-- 导出进度条 -->
|
|
||||||
<div v-if="showExportProgress" class="export-progress">
|
|
||||||
<div class="export-progress-bar">
|
|
||||||
<div class="export-progress-fill" :style="{ width: exportProgress + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
<div class="export-progress-text">{{ Math.round(exportProgress) }}%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -529,10 +530,6 @@ onMounted(async () => {
|
|||||||
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
|
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
|
||||||
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
|
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
|
||||||
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
||||||
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
|
|
||||||
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
|
|
||||||
.export-progress-fill { height: 100%; background: #1677FF; border-radius: 2px; transition: width 0.3s ease; }
|
|
||||||
.export-progress-text { font-size: 11px; color: #1677FF; font-weight: 500; min-width: 32px; text-align: right; }
|
|
||||||
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
|
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
|
||||||
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
|
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
|
||||||
.table-wrapper { flex: 1; overflow: auto; }
|
.table-wrapper { flex: 1; overflow: auto; }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { User } from '@element-plus/icons-vue'
|
import { User } from '@element-plus/icons-vue'
|
||||||
import { authApi } from '../../api/auth'
|
import { authApi } from '../../api/auth'
|
||||||
import { deviceApi } from '../../api/device'
|
import { deviceApi } from '../../api/device'
|
||||||
|
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -11,7 +12,7 @@ interface Props {
|
|||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(e: 'update:modelValue', value: boolean): void
|
||||||
(e: 'loginSuccess', data: { token: string; user: any }): void
|
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string }): void
|
||||||
(e: 'showRegister'): void
|
(e: 'showRegister'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,16 +32,26 @@ async function handleAuth() {
|
|||||||
|
|
||||||
authLoading.value = true
|
authLoading.value = true
|
||||||
try {
|
try {
|
||||||
await deviceApi.register({ username: authForm.value.username })
|
// 获取或生成设备ID
|
||||||
const loginRes: any = await authApi.login(authForm.value)
|
const deviceId = await getOrCreateDeviceId()
|
||||||
const data = loginRes?.data || loginRes
|
|
||||||
|
// 注册设备
|
||||||
|
await deviceApi.register({
|
||||||
|
username: authForm.value.username,
|
||||||
|
deviceId: deviceId,
|
||||||
|
os: navigator.platform
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
const loginRes: any = await authApi.login({
|
||||||
|
...authForm.value,
|
||||||
|
clientId: deviceId
|
||||||
|
})
|
||||||
|
|
||||||
emit('loginSuccess', {
|
emit('loginSuccess', {
|
||||||
token: data.token,
|
token: loginRes.data.accessToken || loginRes.data.token,
|
||||||
user: {
|
permissions: loginRes.data.permissions,
|
||||||
username: data.username,
|
expireTime: loginRes.data.expireTime
|
||||||
permissions: data.permissions
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
ElMessage.success('登录成功')
|
ElMessage.success('登录成功')
|
||||||
resetForm()
|
resetForm()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { User } from '@element-plus/icons-vue'
|
import { User } from '@element-plus/icons-vue'
|
||||||
import { authApi } from '../../api/auth'
|
import { authApi } from '../../api/auth'
|
||||||
|
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -10,7 +11,7 @@ interface Props {
|
|||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(e: 'update:modelValue', value: boolean): void
|
||||||
(e: 'loginSuccess', data: { token: string; user: any }): void
|
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string }): void
|
||||||
(e: 'backToLogin'): void
|
(e: 'backToLogin'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +43,8 @@ async function checkUsernameAvailability() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await authApi.checkUsername(registerForm.value.username)
|
const res: any = await authApi.checkUsername(registerForm.value.username)
|
||||||
const data = res?.data || res
|
// 后端返回 {code: 200, data: true/false},data 直接是布尔值
|
||||||
usernameCheckResult.value = data?.available || false
|
usernameCheckResult.value = res.data
|
||||||
} catch {
|
} catch {
|
||||||
usernameCheckResult.value = null
|
usernameCheckResult.value = null
|
||||||
}
|
}
|
||||||
@@ -54,23 +55,34 @@ async function handleRegister() {
|
|||||||
|
|
||||||
registerLoading.value = true
|
registerLoading.value = true
|
||||||
try {
|
try {
|
||||||
await authApi.register({
|
// 获取设备ID
|
||||||
|
const deviceId = await getOrCreateDeviceId()
|
||||||
|
|
||||||
|
// 注册账号(传递设备ID用于判断是否赠送VIP)
|
||||||
|
const registerRes: any = await authApi.register({
|
||||||
username: registerForm.value.username,
|
username: registerForm.value.username,
|
||||||
password: registerForm.value.password
|
password: registerForm.value.password,
|
||||||
|
deviceId: deviceId
|
||||||
})
|
})
|
||||||
|
|
||||||
const loginRes: any = await authApi.login({
|
// 显示注册成功和VIP信息
|
||||||
username: registerForm.value.username,
|
if (registerRes.data.expireTime) {
|
||||||
password: registerForm.value.password
|
const expireDate = new Date(registerRes.data.expireTime)
|
||||||
})
|
const now = new Date()
|
||||||
const loginData = loginRes?.data || loginRes
|
const daysLeft = Math.ceil((expireDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
emit('loginSuccess', {
|
if (daysLeft > 0) {
|
||||||
token: loginData.token,
|
ElMessage.success(`注册成功!您获得了 ${daysLeft} 天VIP体验`)
|
||||||
user: {
|
} else {
|
||||||
username: loginData.username,
|
ElMessage.warning('注册成功!该设备已使用过新人福利,请联系管理员续费')
|
||||||
permissions: loginData.permissions
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用注册返回的token直接登录
|
||||||
|
emit('loginSuccess', {
|
||||||
|
token: registerRes.data.accessToken || registerRes.data.token,
|
||||||
|
permissions: registerRes.data.permissions,
|
||||||
|
expireTime: registerRes.data.expireTime
|
||||||
})
|
})
|
||||||
resetForm()
|
resetForm()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -117,14 +117,16 @@ const info = ref({
|
|||||||
const SKIP_VERSION_KEY = 'skipped_version'
|
const SKIP_VERSION_KEY = 'skipped_version'
|
||||||
const REMIND_LATER_KEY = 'remind_later_time'
|
const REMIND_LATER_KEY = 'remind_later_time'
|
||||||
|
|
||||||
async function autoCheck() {
|
async function autoCheck(silent = false) {
|
||||||
try {
|
try {
|
||||||
version.value = await (window as any).electronAPI.getJarVersion()
|
version.value = await (window as any).electronAPI.getJarVersion()
|
||||||
const checkRes: any = await updateApi.checkUpdate(version.value)
|
const checkRes: any = await updateApi.checkUpdate(version.value)
|
||||||
const result = checkRes?.data || checkRes
|
const result = checkRes?.data || checkRes
|
||||||
|
|
||||||
if (!result.needUpdate) {
|
if (!result.needUpdate) {
|
||||||
ElMessage.info('当前已是最新版本')
|
if (!silent) {
|
||||||
|
ElMessage.info('当前已是最新版本')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,10 +151,14 @@ async function autoCheck() {
|
|||||||
}
|
}
|
||||||
show.value = true
|
show.value = true
|
||||||
stage.value = 'check'
|
stage.value = 'check'
|
||||||
ElMessage.success('发现新版本')
|
if (!silent) {
|
||||||
|
ElMessage.success('发现新版本')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查更新失败:', error)
|
console.error('检查更新失败:', error)
|
||||||
ElMessage.error('检查更新失败')
|
if (!silent) {
|
||||||
|
ElMessage.error('检查更新失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +245,7 @@ async function installUpdate() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
version.value = await (window as any).electronAPI.getJarVersion()
|
version.value = await (window as any).electronAPI.getJarVersion()
|
||||||
|
await autoCheck(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, onMounted} from 'vue'
|
import {ref, computed, onMounted} from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {rakutenApi} from '../../api/rakuten'
|
import {rakutenApi} from '../../api/rakuten'
|
||||||
import { batchConvertImages } from '../../utils/imageProxy'
|
import { batchConvertImages } from '../../utils/imageProxy'
|
||||||
import { handlePlatformFileExport } from '../../utils/settings'
|
import { handlePlatformFileExport } from '../../utils/settings'
|
||||||
|
|
||||||
|
// 接收VIP状态
|
||||||
|
const props = defineProps<{
|
||||||
|
isVip: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
// UI 与加载状态
|
// UI 与加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableLoading = ref(false)
|
const tableLoading = ref(false)
|
||||||
@@ -114,17 +119,22 @@ function needsSearch(product: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadLatest() {
|
async function loadLatest() {
|
||||||
const resp = await rakutenApi.getLatestProducts()
|
const resp: any = await rakutenApi.getLatestProducts()
|
||||||
allProducts.value = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
|
const products = resp.data.products || []
|
||||||
|
allProducts.value = products.map((p: any) => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchProductInternal(product: any) {
|
async function searchProductInternal(product: any) {
|
||||||
if (!product || !product.imgUrl) return
|
if (!product || !product.imgUrl) return
|
||||||
if (!needsSearch(product)) return
|
if (!needsSearch(product)) return
|
||||||
const res = await rakutenApi.search1688(product.imgUrl, currentBatchId.value)
|
if (!props.isVip) {
|
||||||
const data = res
|
ElMessage.warning('VIP已过期,1688识图功能受限')
|
||||||
const skuJson = (data as any)?.skuPriceJson ?? (data as any)?.skuPrice
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await rakutenApi.search1688(product.imgUrl, currentBatchId.value)
|
||||||
|
const data = res.data
|
||||||
|
const skuJson = data.skuPriceJson || data.skuPrice
|
||||||
Object.assign(product, {
|
Object.assign(product, {
|
||||||
mapRecognitionLink: data.mapRecognitionLink,
|
mapRecognitionLink: data.mapRecognitionLink,
|
||||||
freight: data.freight,
|
freight: data.freight,
|
||||||
@@ -186,8 +196,24 @@ async function onDrop(e: DragEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 点击“获取数据
|
// 点击"获取数据
|
||||||
async function handleStartSearch() {
|
async function handleStartSearch() {
|
||||||
|
// VIP检查
|
||||||
|
if (!props.isVip) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'VIP已过期,数据采集功能受限。请联系管理员续费后继续使用。',
|
||||||
|
'VIP功能限制',
|
||||||
|
{
|
||||||
|
confirmButtonText: '我知道了',
|
||||||
|
showCancelButton: false,
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (pendingFile.value) {
|
if (pendingFile.value) {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -199,8 +225,8 @@ async function handleStartSearch() {
|
|||||||
progressPercentage.value = 0
|
progressPercentage.value = 0
|
||||||
totalProducts.value = 0
|
totalProducts.value = 0
|
||||||
processedProducts.value = 0
|
processedProducts.value = 0
|
||||||
const resp = await rakutenApi.getProducts({file: pendingFile.value, batchId: currentBatchId.value})
|
const resp: any = await rakutenApi.getProducts({file: pendingFile.value, batchId: currentBatchId.value})
|
||||||
const products = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
|
const products = (resp.data.products || []).map((p: any) => ({...p, skuPrices: parseSkuPrices(p)}))
|
||||||
|
|
||||||
if (products.length === 0) {
|
if (products.length === 0) {
|
||||||
showMessage('未采集到数据,请检查代理或店铺是否存在', 'warning')
|
showMessage('未采集到数据,请检查代理或店铺是否存在', 'warning')
|
||||||
@@ -373,9 +399,11 @@ async function exportToExcel() {
|
|||||||
})
|
})
|
||||||
const fileName = `乐天商品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
const fileName = `乐天商品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||||
|
|
||||||
await handlePlatformFileExport('rakuten', blob, fileName)
|
const success = await handlePlatformFileExport('rakuten', blob, fileName)
|
||||||
|
|
||||||
showMessage('Excel文件导出成功!', 'success')
|
if (success) {
|
||||||
|
showMessage('Excel文件导出成功!', 'success')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage('导出失败', 'error')
|
showMessage('导出失败', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import AccountManager from '../common/AccountManager.vue'
|
|||||||
import { batchConvertImages } from '../../utils/imageProxy'
|
import { batchConvertImages } from '../../utils/imageProxy'
|
||||||
import { handlePlatformFileExport } from '../../utils/settings'
|
import { handlePlatformFileExport } from '../../utils/settings'
|
||||||
|
|
||||||
|
// 接收VIP状态
|
||||||
|
const props = defineProps<{
|
||||||
|
isVip: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
type Shop = { id: string; shopName: string }
|
type Shop = { id: string; shopName: string }
|
||||||
|
|
||||||
const accounts = ref<BanmaAccount[]>([])
|
const accounts = ref<BanmaAccount[]>([])
|
||||||
@@ -86,6 +91,22 @@ function handleCurrentChange(page: number) {
|
|||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
if (isFetching.value) return
|
if (isFetching.value) return
|
||||||
|
|
||||||
|
// VIP检查
|
||||||
|
if (!props.isVip) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'VIP已过期,数据采集功能受限。请联系管理员续费后继续使用。',
|
||||||
|
'VIP功能限制',
|
||||||
|
{
|
||||||
|
confirmButtonText: '我知道了',
|
||||||
|
showCancelButton: false,
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
isFetching.value = true
|
isFetching.value = true
|
||||||
showProgress.value = true
|
showProgress.value = true
|
||||||
@@ -237,9 +258,11 @@ async function exportToExcel() {
|
|||||||
})
|
})
|
||||||
const fileName = `斑马订单数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
const fileName = `斑马订单数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||||
|
|
||||||
await handlePlatformFileExport('zebra', blob, fileName)
|
const success = await handlePlatformFileExport('zebra', blob, fileName)
|
||||||
|
|
||||||
showMessage('Excel文件导出成功!', 'success')
|
if (success) {
|
||||||
|
showMessage('Excel文件导出成功!', 'success')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage('导出失败', 'error')
|
showMessage('导出失败', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
70
electron-vue-template/src/renderer/utils/deviceId.ts
Normal file
70
electron-vue-template/src/renderer/utils/deviceId.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 设备ID管理工具
|
||||||
|
* 从客户端服务获取硬件UUID(通过 wmic 命令)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASE_CLIENT = 'http://localhost:8081'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从客户端服务获取硬件设备ID
|
||||||
|
* 客户端会使用 wmic 命令获取硬件UUID(仅Windows)
|
||||||
|
*/
|
||||||
|
async function fetchDeviceIdFromClient(): Promise<string> {
|
||||||
|
const response = await fetch(`${BASE_CLIENT}/api/device-id`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'omit',
|
||||||
|
cache: 'no-store'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取设备ID失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
const deviceId = result?.data
|
||||||
|
|
||||||
|
if (!deviceId) {
|
||||||
|
throw new Error('设备ID为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建设备ID
|
||||||
|
* 1. 优先从本地缓存读取
|
||||||
|
* 2. 如果没有缓存,从客户端服务获取(使用硬件UUID)
|
||||||
|
* 3. 保存到本地缓存
|
||||||
|
*/
|
||||||
|
export async function getOrCreateDeviceId(): Promise<string> {
|
||||||
|
try {
|
||||||
|
// 尝试从本地缓存获取
|
||||||
|
const response = await fetch(`${BASE_CLIENT}/api/cache/get?key=deviceId`)
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json()
|
||||||
|
const cachedDeviceId = result?.data
|
||||||
|
if (cachedDeviceId) {
|
||||||
|
return cachedDeviceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('从缓存读取设备ID失败:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从客户端服务获取新的设备ID(硬件UUID)
|
||||||
|
const newDeviceId = await fetchDeviceIdFromClient()
|
||||||
|
|
||||||
|
// 保存到本地缓存
|
||||||
|
try {
|
||||||
|
await fetch(`${BASE_CLIENT}/api/cache/save`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ key: 'deviceId', value: newDeviceId })
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('保存设备ID到缓存失败:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDeviceId
|
||||||
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export async function handlePlatformFileExport(
|
|||||||
platform: Platform,
|
platform: Platform,
|
||||||
blob: Blob,
|
blob: Blob,
|
||||||
defaultFileName: string
|
defaultFileName: string
|
||||||
): Promise<void> {
|
): Promise<boolean> {
|
||||||
const config = getPlatformExportConfig(platform)
|
const config = getPlatformExportConfig(platform)
|
||||||
|
|
||||||
if (!config.exportPath) {
|
if (!config.exportPath) {
|
||||||
@@ -105,10 +105,13 @@ export async function handlePlatformFileExport(
|
|||||||
|
|
||||||
if (!result.canceled && result.filePath) {
|
if (!result.canceled && result.filePath) {
|
||||||
await writeFileToPath(blob, result.filePath)
|
await writeFileToPath(blob, result.filePath)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
} else {
|
} else {
|
||||||
const filePath = `${config.exportPath}/${defaultFileName}`
|
const filePath = `${config.exportPath}/${defaultFileName}`
|
||||||
await writeFileToPath(blob, filePath)
|
await writeFileToPath(blob, filePath)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</parent>
|
</parent>
|
||||||
<groupId>com.tashow.erp</groupId>
|
<groupId>com.tashow.erp</groupId>
|
||||||
<artifactId>erp_client_sb</artifactId>
|
<artifactId>erp_client_sb</artifactId>
|
||||||
<version>2.4.8</version>
|
<version>2.4.7</version>
|
||||||
<name>erp_client_sb</name>
|
<name>erp_client_sb</name>
|
||||||
<description>erp客户端</description>
|
<description>erp客户端</description>
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -12,77 +12,30 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端本地服务控制器
|
||||||
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
@Autowired
|
|
||||||
private IAuthService authService;
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private AuthTokenRepository authTokenRepository;
|
private AuthTokenRepository authTokenRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private CacheDataRepository cacheDataRepository;
|
private CacheDataRepository cacheDataRepository;
|
||||||
@PostMapping("/login")
|
|
||||||
public ResponseEntity<?> login(@RequestBody Map<String, Object> loginData) {
|
|
||||||
String username = (String) loginData.get("username");
|
|
||||||
String password = (String) loginData.get("password");
|
|
||||||
Map<String, Object> result = authService.login(username, password);
|
|
||||||
Object success = result.get("success");
|
|
||||||
Object tokenObj = result.get("token");
|
|
||||||
if (Boolean.TRUE.equals(success) && tokenObj instanceof String token && token != null && !token.isEmpty()) {
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
|
|
||||||
return ResponseEntity.ok().headers(headers).body(result);
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/verify")
|
|
||||||
public ResponseEntity<?> verifyToken(@RequestBody Map<String, Object> data) {
|
|
||||||
String token = (String) data.get("token");
|
|
||||||
if (token == null) {
|
|
||||||
return ResponseEntity.ok(Map.of("code", 400, "message", "token不能为空"));
|
|
||||||
}
|
|
||||||
Map<String, Object> result = authService.verifyToken(token);
|
|
||||||
return ResponseEntity.ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/register")
|
|
||||||
public ResponseEntity<?> register(@RequestBody Map<String, Object> registerData) {
|
|
||||||
String username = (String) registerData.get("username");
|
|
||||||
String password = (String) registerData.get("password");
|
|
||||||
if (username == null || password == null) {
|
|
||||||
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名和密码不能为空"));
|
|
||||||
}
|
|
||||||
Map<String, Object> result = authService.register(username, password);
|
|
||||||
Object success2 = result.get("success");
|
|
||||||
Object tokenObj2 = result.get("token");
|
|
||||||
if (Boolean.TRUE.equals(success2) && tokenObj2 instanceof String token && token != null && !token.isEmpty()) {
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.add(HttpHeaders.SET_COOKIE, buildHttpOnlyCookie("FX_TOKEN", token, 2 * 24 * 60 * 60));
|
|
||||||
return ResponseEntity.ok().headers(headers).body(result);
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录(清理本地状态)
|
||||||
|
*/
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
public ResponseEntity<?> logout(@RequestBody Map<String, Object> data) {
|
public ResponseEntity<?> logout(@RequestBody Map<String, Object> data) {
|
||||||
authService.logout();
|
// 清理本地缓存
|
||||||
|
try {
|
||||||
|
cacheDataRepository.deleteByCacheKey("token");
|
||||||
|
cacheDataRepository.deleteByCacheKey("deviceId");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
return ResponseEntity.ok(Map.of("code", 0, "message", "退出成功"));
|
return ResponseEntity.ok(Map.of("code", 0, "message", "退出成功"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/check-username")
|
|
||||||
public ResponseEntity<?> checkUsername(@RequestParam String username) {
|
|
||||||
if (username == null || username.trim().isEmpty()) {
|
|
||||||
return ResponseEntity.ok(Map.of("code", 400, "message", "用户名不能为空"));
|
|
||||||
}
|
|
||||||
boolean available = authService.checkUsername(username);
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"code", 200,
|
|
||||||
"message", "检查成功",
|
|
||||||
"data", available
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存认证密钥
|
* 保存认证密钥
|
||||||
*/
|
*/
|
||||||
@@ -179,15 +132,12 @@ public class AuthController {
|
|||||||
return JsonData.buildSuccess("会话已恢复");
|
return JsonData.buildSuccess("会话已恢复");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildHttpOnlyCookie(String name, String value, int maxAgeSeconds) {
|
/**
|
||||||
StringBuilder sb = new StringBuilder();
|
* 获取设备ID(硬件UUID)
|
||||||
sb.append(name).append("=").append(value).append(";");
|
*/
|
||||||
sb.append(" Path=/;");
|
@GetMapping("/device-id")
|
||||||
sb.append(" HttpOnly;");
|
public JsonData getDeviceId() {
|
||||||
sb.append(" SameSite=Strict;");
|
String deviceId = com.tashow.erp.utils.DeviceUtils.generateDeviceId();
|
||||||
if (maxAgeSeconds > 0) {
|
return JsonData.buildSuccess(deviceId);
|
||||||
sb.append(" Max-Age=").append(maxAgeSeconds).append(";");
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package com.tashow.erp.controller;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.*;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import com.tashow.erp.utils.ApiForwarder;
|
|
||||||
import com.tashow.erp.utils.DeviceUtils;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备管理代理控制器
|
|
||||||
* 简化职责:透传请求到后端服务
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
public class DeviceProxyController {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册设备
|
|
||||||
*/
|
|
||||||
@Autowired
|
|
||||||
private ApiForwarder apiForwarder;
|
|
||||||
|
|
||||||
@PostMapping("/api/device/register")
|
|
||||||
public ResponseEntity<?> deviceRegister(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
|
|
||||||
Map<String, Object> deviceData = new HashMap<>(body);
|
|
||||||
deviceData.put("deviceId", DeviceUtils.generateDeviceId());
|
|
||||||
return apiForwarder.post("/monitor/device/register", deviceData, auth);
|
|
||||||
}
|
|
||||||
@PostMapping("/api/device/remove")
|
|
||||||
public ResponseEntity<?> deviceRemove(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
|
|
||||||
return apiForwarder.post("/monitor/device/remove", body, auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/api/device/offline")
|
|
||||||
public ResponseEntity<?> deviceOffline(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
|
|
||||||
return apiForwarder.post("/monitor/device/offline", body, auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设备心跳
|
|
||||||
*/
|
|
||||||
@PostMapping("/api/device/heartbeat")
|
|
||||||
public ResponseEntity<?> deviceHeartbeat(@RequestBody Map<String, Object> body, @RequestHeader(value = "Authorization", required = false) String auth) {
|
|
||||||
return apiForwarder.post("/monitor/device/heartbeat", body, auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/api/device/quota")
|
|
||||||
public ResponseEntity<?> deviceQuota(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) {
|
|
||||||
return apiForwarder.get("/monitor/device/quota?username=" + username, auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/api/device/list")
|
|
||||||
public ResponseEntity<?> deviceList(@RequestParam("username") String username, @RequestHeader(value = "Authorization", required = false) String auth) {
|
|
||||||
return apiForwarder.get("/monitor/device/list?username=" + username, auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@ import com.tashow.erp.service.IAuthService;
|
|||||||
import com.tashow.erp.utils.ApiForwarder;
|
import com.tashow.erp.utils.ApiForwarder;
|
||||||
import com.tashow.erp.utils.DeviceUtils;
|
import com.tashow.erp.utils.DeviceUtils;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -20,12 +21,11 @@ public class AuthServiceImpl implements IAuthService {
|
|||||||
|
|
||||||
@Value("${project.version:2.1.0}")
|
@Value("${project.version:2.1.0}")
|
||||||
private String appVersion;
|
private String appVersion;
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@org.springframework.beans.factory.annotation.Autowired
|
@Autowired
|
||||||
private ApiForwarder apiForwarder;
|
private ApiForwarder apiForwarder;
|
||||||
|
|
||||||
@org.springframework.beans.factory.annotation.Autowired
|
@Autowired
|
||||||
private com.tashow.erp.repository.CacheDataRepository cacheDataRepository;
|
private com.tashow.erp.repository.CacheDataRepository cacheDataRepository;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -164,7 +164,6 @@ public class AuthServiceImpl implements IAuthService {
|
|||||||
Map<String, Object> verifyData = new HashMap<>();
|
Map<String, Object> verifyData = new HashMap<>();
|
||||||
verifyData.put("token", token);
|
verifyData.put("token", token);
|
||||||
JsonNode response = sendPostRequest("/monitor/account/verify", verifyData);
|
JsonNode response = sendPostRequest("/monitor/account/verify", verifyData);
|
||||||
|
|
||||||
if (response.has("code") && response.get("code").asInt() == 200) {
|
if (response.has("code") && response.get("code").asInt() == 200) {
|
||||||
JsonNode dataNode = response.has("data") ? response.get("data") : response;
|
JsonNode dataNode = response.has("data") ? response.get("data") : response;
|
||||||
result.put("success", true);
|
result.put("success", true);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
|
|||||||
public class BanmaOrderServiceImpl implements IBanmaOrderService {
|
public class BanmaOrderServiceImpl implements IBanmaOrderService {
|
||||||
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderServiceImpl.class);
|
private static final Logger logger = LoggerUtil.getLogger(BanmaOrderServiceImpl.class);
|
||||||
private static final String SERVICE_NAME = "banma";
|
private static final String SERVICE_NAME = "banma";
|
||||||
private static final String RUOYI_ADMIN_BASE = "http://127.0.0.1:8080";
|
private static final String RUOYI_ADMIN_BASE = "http://192.168.1.89:8085";
|
||||||
private static final String API_URL = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d";
|
private static final String API_URL = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&_t=%d";
|
||||||
private static final String API_URL_WITH_TIME = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d";
|
private static final String API_URL_WITH_TIME = "https://banma365.cn/api/order/list?%srecipientName=&page=%d&size=%d&markFlag=0&state=4&orderedAtStart=%s&orderedAtEnd=%s&_t=%d";
|
||||||
private static final String TRACKING_URL = "https://banma365.cn/zebraExpressHub/web/tracking/getByExpressNumber/%s";
|
private static final String TRACKING_URL = "https://banma365.cn/zebraExpressHub/web/tracking/getByExpressNumber/%s";
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ public class DeviceUtils {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 静默处理异常
|
// 静默处理异常
|
||||||
}
|
}
|
||||||
return UUID.randomUUID().toString();
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ import com.tashow.erp.service.IAuthService;
|
|||||||
@Component
|
@Component
|
||||||
public class ErrorReporter {
|
public class ErrorReporter {
|
||||||
|
|
||||||
@Value("${server.monitor.url:http://localhost:8080}")
|
@Value("${api.server.base-url}")
|
||||||
private String serverUrl;
|
private String serverUrl;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ api:
|
|||||||
server:
|
server:
|
||||||
# 主服务器API配置
|
# 主服务器API配置
|
||||||
# base-url: "http://8.138.23.49:8080"
|
# base-url: "http://8.138.23.49:8080"
|
||||||
base-url: "http://192.168.1.89:8080"
|
base-url: "http://192.168.1.89:8085"
|
||||||
paths:
|
paths:
|
||||||
monitor: "/monitor/client/api"
|
monitor: "/monitor/client/api"
|
||||||
login: "/monitor/account/login"
|
login: "/monitor/account/login"
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ public class ClientAccountController extends BaseController {
|
|||||||
private JwtRsaKeyService jwtRsaKeyService;
|
private JwtRsaKeyService jwtRsaKeyService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SseHubService sseHubService;
|
private SseHubService sseHubService;
|
||||||
|
@Autowired
|
||||||
|
private ClientDeviceMapper clientDeviceMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询账号列表
|
* 查询账号列表
|
||||||
@@ -137,9 +139,6 @@ public class ClientAccountController extends BaseController {
|
|||||||
if (!"0".equals(account.getStatus())) {
|
if (!"0".equals(account.getStatus())) {
|
||||||
return AjaxResult.error("账号已被停用");
|
return AjaxResult.error("账号已被停用");
|
||||||
}
|
}
|
||||||
if (account.getExpireTime() != null && account.getExpireTime().before(new Date())) {
|
|
||||||
return AjaxResult.error("账号已过期");
|
|
||||||
}
|
|
||||||
String clientId = loginData.get("clientId");
|
String clientId = loginData.get("clientId");
|
||||||
String accessToken = Jwts.builder()
|
String accessToken = Jwts.builder()
|
||||||
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
@@ -151,7 +150,6 @@ public class ClientAccountController extends BaseController {
|
|||||||
.claim("clientId", clientId)
|
.claim("clientId", clientId)
|
||||||
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
||||||
.compact();
|
.compact();
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("accessToken", accessToken);
|
result.put("accessToken", accessToken);
|
||||||
result.put("permissions", account.getPermissions());
|
result.put("permissions", account.getPermissions());
|
||||||
@@ -181,6 +179,15 @@ public class ClientAccountController extends BaseController {
|
|||||||
result.put("username", username);
|
result.put("username", username);
|
||||||
result.put("permissions", account.getPermissions());
|
result.put("permissions", account.getPermissions());
|
||||||
result.put("accountName", account.getAccountName());
|
result.put("accountName", account.getAccountName());
|
||||||
|
result.put("expireTime", account.getExpireTime());
|
||||||
|
// 计算VIP状态
|
||||||
|
if (account.getExpireTime() != null) {
|
||||||
|
boolean isExpired = account.getExpireTime().before(new Date());
|
||||||
|
result.put("isVip", !isExpired);
|
||||||
|
} else {
|
||||||
|
result.put("isVip", false);
|
||||||
|
}
|
||||||
|
|
||||||
return AjaxResult.success("验证成功", result);
|
return AjaxResult.success("验证成功", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,36 +210,53 @@ public class ClientAccountController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 客户端账号注册
|
* 客户端账号注册
|
||||||
|
* 新设备注册送3天VIP,同一设备ID重复注册不赠送
|
||||||
*/
|
*/
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public AjaxResult register(@RequestBody ClientAccount clientAccount) {
|
public AjaxResult register(@RequestBody Map<String, String> registerData) {
|
||||||
if (StringUtils.isEmpty(clientAccount.getUsername()) || StringUtils.isEmpty(clientAccount.getPassword())) {
|
String username = registerData.get("username");
|
||||||
return AjaxResult.error("用户名和密码不能为空");
|
String password = registerData.get("password");
|
||||||
}
|
String deviceId = registerData.get("deviceId");
|
||||||
if (clientAccount.getPassword().length() < 6) {
|
ClientAccount clientAccount = new ClientAccount();
|
||||||
return AjaxResult.error("密码长度不能少于6位");
|
clientAccount.setUsername(username);
|
||||||
}
|
clientAccount.setAccountName(username);
|
||||||
if (clientAccountService.selectClientAccountByUsername(clientAccount.getUsername()) != null) {
|
|
||||||
return AjaxResult.error("用户名已存在");
|
|
||||||
}
|
|
||||||
clientAccount.setCreateBy("system");
|
clientAccount.setCreateBy("system");
|
||||||
clientAccount.setStatus("0");
|
clientAccount.setStatus("0");
|
||||||
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
|
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
|
||||||
clientAccount.setPassword(passwordEncoder.encode(clientAccount.getPassword()));
|
clientAccount.setPassword(passwordEncoder.encode(password));
|
||||||
if (clientAccount.getExpireTime() == null) {
|
|
||||||
Date expireDate = new Date(System.currentTimeMillis() + 90L * 24 * 60 * 60 * 1000);
|
// 检查设备ID是否已注册过(赠送VIP逻辑)
|
||||||
clientAccount.setExpireTime(expireDate);
|
boolean isNewDevice = true;
|
||||||
|
if (!StringUtils.isEmpty(deviceId)) {
|
||||||
|
ClientDevice existingDevice = clientDeviceMapper.selectByDeviceId(deviceId);
|
||||||
|
isNewDevice = (existingDevice == null);
|
||||||
}
|
}
|
||||||
|
int vipDays;
|
||||||
|
if (isNewDevice) {
|
||||||
|
vipDays = 3;
|
||||||
|
} else {
|
||||||
|
vipDays = 0; // 立即过期,需要续费
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vipDays > 0) {
|
||||||
|
Date expireDate = new Date(System.currentTimeMillis() + vipDays * 24L * 60 * 60 * 1000);
|
||||||
|
clientAccount.setExpireTime(expireDate);
|
||||||
|
} else {
|
||||||
|
clientAccount.setExpireTime(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
int result = clientAccountService.insertClientAccount(clientAccount);
|
int result = clientAccountService.insertClientAccount(clientAccount);
|
||||||
if (result <= 0) {
|
if (result <= 0) {
|
||||||
return AjaxResult.error("注册失败");
|
return AjaxResult.error("注册失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
String accessToken = Jwts.builder()
|
String accessToken = Jwts.builder()
|
||||||
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
.setSubject(clientAccount.getUsername())
|
.setSubject(clientAccount.getUsername())
|
||||||
.setIssuedAt(new Date())
|
.setIssuedAt(new Date())
|
||||||
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
|
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
|
||||||
.claim("accountId", clientAccount.getId())
|
.claim("accountId", clientAccount.getId())
|
||||||
|
.claim("clientId", deviceId)
|
||||||
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
|
||||||
.compact();
|
.compact();
|
||||||
|
|
||||||
@@ -256,5 +280,43 @@ public class ClientAccountController extends BaseController {
|
|||||||
return AjaxResult.success(account == null);
|
return AjaxResult.success(account == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 续费账号
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:account:edit')")
|
||||||
|
@Log(title = "账号续费", businessType = BusinessType.UPDATE)
|
||||||
|
@PostMapping("/renew")
|
||||||
|
public AjaxResult renew(@RequestBody Map<String, Object> data) {
|
||||||
|
Long accountId = Long.valueOf(data.get("accountId").toString());
|
||||||
|
Integer days = Integer.valueOf(data.get("days").toString());
|
||||||
|
|
||||||
|
ClientAccount account = clientAccountService.selectClientAccountById(accountId);
|
||||||
|
if (account == null) {
|
||||||
|
return AjaxResult.error("账号不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||||
|
if (account.getExpireTime() != null && account.getExpireTime().after(new Date())) {
|
||||||
|
cal.setTime(account.getExpireTime());
|
||||||
|
} else {
|
||||||
|
cal.setTime(new Date());
|
||||||
|
}
|
||||||
|
cal.add(java.util.Calendar.DAY_OF_MONTH, days);
|
||||||
|
Date newExpireTime = cal.getTime();
|
||||||
|
|
||||||
|
account.setExpireTime(newExpireTime);
|
||||||
|
account.setUpdateBy(getUsername());
|
||||||
|
clientAccountService.updateClientAccount(account);
|
||||||
|
|
||||||
|
// 通过SSE推送续费通知给该账号的所有在线设备
|
||||||
|
try {
|
||||||
|
sseHubService.sendEventToAllDevices(account.getUsername(), "VIP_RENEWED",
|
||||||
|
"{\"expireTime\":\"" + newExpireTime + "\"}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
// SSE推送失败不影响续费操作
|
||||||
|
}
|
||||||
|
|
||||||
|
return AjaxResult.success("续费成功,新的过期时间:" + newExpireTime);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
|||||||
import com.ruoyi.common.utils.ip.IpUtils;
|
import com.ruoyi.common.utils.ip.IpUtils;
|
||||||
import com.ruoyi.system.domain.ClientDevice;
|
import com.ruoyi.system.domain.ClientDevice;
|
||||||
import com.ruoyi.system.mapper.ClientDeviceMapper;
|
import com.ruoyi.system.mapper.ClientDeviceMapper;
|
||||||
|
import com.ruoyi.system.mapper.ClientAccountMapper;
|
||||||
|
import com.ruoyi.system.domain.ClientAccount;
|
||||||
import com.ruoyi.web.sse.SseHubService;
|
import com.ruoyi.web.sse.SseHubService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -19,9 +21,28 @@ public class ClientDeviceController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ClientDeviceMapper clientDeviceMapper;
|
private ClientDeviceMapper clientDeviceMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private ClientAccountMapper clientAccountMapper;
|
||||||
|
@Autowired
|
||||||
private SseHubService sseHubService;
|
private SseHubService sseHubService;
|
||||||
private static final int DEFAULT_LIMIT = 3;
|
private static final int DEFAULT_LIMIT = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取账号的设备数量限制
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @return 设备数量限制,如果账号不存在或未配置则返回默认值
|
||||||
|
*/
|
||||||
|
private int getDeviceLimit(String username) {
|
||||||
|
if (username == null || username.isEmpty()) {
|
||||||
|
return DEFAULT_LIMIT;
|
||||||
|
}
|
||||||
|
ClientAccount account = clientAccountMapper.selectClientAccountByUsername(username);
|
||||||
|
if (account == null || account.getDeviceLimit() == null) {
|
||||||
|
return DEFAULT_LIMIT;
|
||||||
|
}
|
||||||
|
return account.getDeviceLimit();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询设备配额与已使用数量
|
* 查询设备配额与已使用数量
|
||||||
*
|
*
|
||||||
@@ -35,8 +56,9 @@ public class ClientDeviceController {
|
|||||||
for (ClientDevice d : all) {
|
for (ClientDevice d : all) {
|
||||||
if (!"removed".equals(d.getStatus())) used++;
|
if (!"removed".equals(d.getStatus())) used++;
|
||||||
}
|
}
|
||||||
|
int limit = getDeviceLimit(username);
|
||||||
Map<String, Object> map = new HashMap<>();
|
Map<String, Object> map = new HashMap<>();
|
||||||
map.put("limit", DEFAULT_LIMIT);
|
map.put("limit", limit);
|
||||||
map.put("used", used);
|
map.put("used", used);
|
||||||
return AjaxResult.success(map);
|
return AjaxResult.success(map);
|
||||||
}
|
}
|
||||||
@@ -55,26 +77,26 @@ public class ClientDeviceController {
|
|||||||
return AjaxResult.success(active);
|
return AjaxResult.success(active);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 设备注册(幂等)
|
* 设备注册
|
||||||
*
|
|
||||||
* 根据 deviceId 判断:
|
|
||||||
* - 不存在:插入新记录(检查设备数量限制)
|
|
||||||
* - 已存在:更新设备信息
|
|
||||||
*/
|
*/
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) {
|
public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) {
|
||||||
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
||||||
String ip = IpUtils.getIpAddr(request);
|
String ip = IpUtils.getIpAddr(request);
|
||||||
String deviceName = request.getHeader("X-Client-User") + "@" + ip + " (" + request.getHeader("X-Client-OS") + ")";
|
// 从请求体读取用户名和操作系统,构建设备名称
|
||||||
|
String username = device.getUsername();
|
||||||
|
String os = device.getOs();
|
||||||
|
String deviceName = username + "@" + ip + " (" + os + ")";
|
||||||
if (exists == null) {
|
if (exists == null) {
|
||||||
// 检查设备数量限制
|
// 检查设备数量限制
|
||||||
|
int deviceLimit = getDeviceLimit(device.getUsername());
|
||||||
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(device.getUsername());
|
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(device.getUsername());
|
||||||
int activeDeviceCount = 0;
|
int activeDeviceCount = 0;
|
||||||
for (ClientDevice d : userDevices) {
|
for (ClientDevice d : userDevices) {
|
||||||
if (!"removed".equals(d.getStatus())) activeDeviceCount++;
|
if (!"removed".equals(d.getStatus())) activeDeviceCount++;
|
||||||
}
|
}
|
||||||
if (activeDeviceCount >= DEFAULT_LIMIT) {
|
if (activeDeviceCount >= deviceLimit) {
|
||||||
return AjaxResult.error("设备数量已达上限(" + DEFAULT_LIMIT + "个),请先移除其他设备");
|
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
|
||||||
}
|
}
|
||||||
device.setIp(ip);
|
device.setIp(ip);
|
||||||
device.setStatus("online");
|
device.setStatus("online");
|
||||||
@@ -139,7 +161,6 @@ public class ClientDeviceController {
|
|||||||
public AjaxResult offline(@RequestBody Map<String, String> body) {
|
public AjaxResult offline(@RequestBody Map<String, String> body) {
|
||||||
String deviceId = body.get("deviceId");
|
String deviceId = body.get("deviceId");
|
||||||
if (deviceId == null) return AjaxResult.error("deviceId不能为空");
|
if (deviceId == null) return AjaxResult.error("deviceId不能为空");
|
||||||
|
|
||||||
ClientDevice device = clientDeviceMapper.selectByDeviceId(deviceId);
|
ClientDevice device = clientDeviceMapper.selectByDeviceId(deviceId);
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
device.setStatus("offline");
|
device.setStatus("offline");
|
||||||
@@ -157,16 +178,20 @@ public class ClientDeviceController {
|
|||||||
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
|
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
|
||||||
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
|
||||||
String ip = IpUtils.getIpAddr(request);
|
String ip = IpUtils.getIpAddr(request);
|
||||||
String deviceName = request.getHeader("X-Client-User") + "@" + ip + " (" + request.getHeader("X-Client-OS") + ")";
|
// 从请求体读取用户名和操作系统,构建设备名称
|
||||||
|
String username = device.getUsername() ;
|
||||||
|
String os = device.getOs();
|
||||||
|
String deviceName = username + "@" + ip + " (" + os + ")";
|
||||||
if (exists == null) {
|
if (exists == null) {
|
||||||
// 检查设备数量限制
|
// 检查设备数量限制
|
||||||
|
int deviceLimit = getDeviceLimit(device.getUsername());
|
||||||
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(device.getUsername());
|
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(device.getUsername());
|
||||||
int activeDeviceCount = 0;
|
int activeDeviceCount = 0;
|
||||||
for (ClientDevice d : userDevices) {
|
for (ClientDevice d : userDevices) {
|
||||||
if (!"removed".equals(d.getStatus())) activeDeviceCount++;
|
if (!"removed".equals(d.getStatus())) activeDeviceCount++;
|
||||||
}
|
}
|
||||||
if (activeDeviceCount >= DEFAULT_LIMIT) {
|
if (activeDeviceCount >= deviceLimit) {
|
||||||
return AjaxResult.error("设备数量已达上限(" + DEFAULT_LIMIT + "个),请先移除其他设备");
|
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
|
||||||
}
|
}
|
||||||
device.setIp(ip);
|
device.setIp(ip);
|
||||||
device.setStatus("online");
|
device.setStatus("online");
|
||||||
|
|||||||
@@ -92,6 +92,28 @@ public class SseHubService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向指定账号的所有设备推送消息
|
||||||
|
*/
|
||||||
|
public void sendEventToAllDevices(String username, String type, String message) {
|
||||||
|
if (username == null || username.isEmpty()) return;
|
||||||
|
|
||||||
|
// 遍历所有会话,找到匹配的username
|
||||||
|
String prefix = username + ":";
|
||||||
|
sessionEmitters.forEach((key, emitter) -> {
|
||||||
|
if (key.startsWith(prefix)) {
|
||||||
|
try {
|
||||||
|
String data = message != null ? message : "{}";
|
||||||
|
String eventData = "{\"type\":\"" + type + "\",\"message\":" + escapeJson(data) + "}";
|
||||||
|
emitter.send(SseEmitter.event().data(eventData));
|
||||||
|
} catch (IOException e) {
|
||||||
|
sessionEmitters.remove(key);
|
||||||
|
try { emitter.complete(); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新设备状态
|
* 更新设备状态
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ruoyi.web.task;
|
||||||
|
|
||||||
|
import com.ruoyi.system.service.IBanmaAccountService;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class BanmaTokenRefreshTask {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IBanmaAccountService banmaAccountService;
|
||||||
|
|
||||||
|
// 每两天凌晨3点
|
||||||
|
@Scheduled(cron = "0 0 3 */2 * ?")
|
||||||
|
public void refreshAllTokens() {
|
||||||
|
banmaAccountService.refreshAllTokens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ruoyi.web.task;
|
||||||
|
|
||||||
|
import com.ruoyi.system.domain.ClientDevice;
|
||||||
|
import com.ruoyi.system.mapper.ClientDeviceMapper;
|
||||||
|
import com.ruoyi.web.sse.SseHubService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备心跳定时任务
|
||||||
|
*
|
||||||
|
* @author ruoyi
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DeviceHeartbeatTask {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClientDeviceMapper clientDeviceMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SseHubService sseHubService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每30秒发送一次ping,保持SSE连接活跃
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedRate = 30000)
|
||||||
|
public void sendHeartbeatPing() {
|
||||||
|
List<ClientDevice> onlineDevices = clientDeviceMapper.selectOnlineDevices();
|
||||||
|
for (ClientDevice device : onlineDevices) {
|
||||||
|
sseHubService.sendPing(device.getUsername(), device.getDeviceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每2分钟清理一次过期设备
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedRate = 120000)
|
||||||
|
public void cleanExpiredDevices() {
|
||||||
|
clientDeviceMapper.updateExpiredDevicesOffline();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ qiniu:
|
|||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
server:
|
server:
|
||||||
# 服务器的HTTP端口,默认为8080
|
# 服务器的HTTP端口,默认为8080
|
||||||
port: 8080
|
port: 8085
|
||||||
servlet:
|
servlet:
|
||||||
# 应用的访问路径
|
# 应用的访问路径
|
||||||
context-path: /
|
context-path: /
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ public class ClientAccount extends BaseEntity
|
|||||||
/** 功能权限配置(JSON格式) */
|
/** 功能权限配置(JSON格式) */
|
||||||
private String permissions;
|
private String permissions;
|
||||||
|
|
||||||
|
/** 设备数量限制 */
|
||||||
|
@Excel(name = "设备数量限制")
|
||||||
|
private Integer deviceLimit;
|
||||||
|
|
||||||
public void setId(Long id)
|
public void setId(Long id)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -133,4 +137,14 @@ public class ClientAccount extends BaseEntity
|
|||||||
{
|
{
|
||||||
return permissions;
|
return permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDeviceLimit(Integer deviceLimit)
|
||||||
|
{
|
||||||
|
this.deviceLimit = deviceLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDeviceLimit()
|
||||||
|
{
|
||||||
|
return deviceLimit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="allowedIpRange" column="allowed_ip_range" />
|
<result property="allowedIpRange" column="allowed_ip_range" />
|
||||||
<result property="remark" column="remark" />
|
<result property="remark" column="remark" />
|
||||||
<result property="permissions" column="permissions" />
|
<result property="permissions" column="permissions" />
|
||||||
|
<result property="deviceLimit" column="device_limit" />
|
||||||
<result property="createBy" column="create_by" />
|
<result property="createBy" column="create_by" />
|
||||||
<result property="createTime" column="create_time" />
|
<result property="createTime" column="create_time" />
|
||||||
<result property="updateBy" column="update_by" />
|
<result property="updateBy" column="update_by" />
|
||||||
@@ -22,7 +23,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<sql id="selectClientAccountVo">
|
<sql id="selectClientAccountVo">
|
||||||
select id, account_name, username, password, status, expire_time,
|
select id, account_name, username, password, status, expire_time,
|
||||||
allowed_ip_range, remark, permissions, create_by, create_time, update_by, update_time
|
allowed_ip_range, remark, permissions, device_limit, create_by, create_time, update_by, update_time
|
||||||
from client_account
|
from client_account
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="allowedIpRange != null">allowed_ip_range,</if>
|
<if test="allowedIpRange != null">allowed_ip_range,</if>
|
||||||
<if test="remark != null">remark,</if>
|
<if test="remark != null">remark,</if>
|
||||||
<if test="permissions != null">permissions,</if>
|
<if test="permissions != null">permissions,</if>
|
||||||
|
<if test="deviceLimit != null">device_limit,</if>
|
||||||
<if test="createBy != null">create_by,</if>
|
<if test="createBy != null">create_by,</if>
|
||||||
create_time
|
create_time
|
||||||
</trim>
|
</trim>
|
||||||
@@ -69,6 +71,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="allowedIpRange != null">#{allowedIpRange},</if>
|
<if test="allowedIpRange != null">#{allowedIpRange},</if>
|
||||||
<if test="remark != null">#{remark},</if>
|
<if test="remark != null">#{remark},</if>
|
||||||
<if test="permissions != null">#{permissions},</if>
|
<if test="permissions != null">#{permissions},</if>
|
||||||
|
<if test="deviceLimit != null">#{deviceLimit},</if>
|
||||||
<if test="createBy != null">#{createBy},</if>
|
<if test="createBy != null">#{createBy},</if>
|
||||||
sysdate()
|
sysdate()
|
||||||
</trim>
|
</trim>
|
||||||
@@ -85,6 +88,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="allowedIpRange != null">allowed_ip_range = #{allowedIpRange},</if>
|
<if test="allowedIpRange != null">allowed_ip_range = #{allowedIpRange},</if>
|
||||||
<if test="remark != null">remark = #{remark},</if>
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
<if test="permissions != null">permissions = #{permissions},</if>
|
<if test="permissions != null">permissions = #{permissions},</if>
|
||||||
|
<if test="deviceLimit != null">device_limit = #{deviceLimit},</if>
|
||||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
update_time = sysdate()
|
update_time = sysdate()
|
||||||
</trim>
|
</trim>
|
||||||
|
|||||||
@@ -166,17 +166,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
select * from client_info where client_id = #{clientId}
|
select * from client_info where client_id = #{clientId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 查询客户端信息列表 -->
|
|
||||||
<select id="selectClientInfoList" parameterType="ClientInfo" resultMap="ClientInfoResult">
|
<select id="selectClientInfoList" parameterType="ClientInfo" resultMap="ClientInfoResult">
|
||||||
select * from client_info
|
select
|
||||||
|
d.device_id as client_id,
|
||||||
|
d.username,
|
||||||
|
d.os as os_name,
|
||||||
|
d.ip as ip_address,
|
||||||
|
d.last_active_at as last_active_time,
|
||||||
|
d.create_time as auth_time,
|
||||||
|
CASE WHEN d.status = 'online' THEN '1' ELSE '0' END as online,
|
||||||
|
a.account_name as hostname,
|
||||||
|
'' as app_version,
|
||||||
|
'' as os_version,
|
||||||
|
'' as java_version
|
||||||
|
from client_device d
|
||||||
|
left join client_account a on d.username COLLATE utf8mb4_unicode_ci = a.username
|
||||||
<where>
|
<where>
|
||||||
<if test="clientId != null and clientId != ''">AND client_id like concat('%', #{clientId}, '%')</if>
|
<if test="clientId != null and clientId != ''">AND d.device_id like concat('%', #{clientId}, '%')</if>
|
||||||
<if test="username != null and username != ''">AND username like concat('%', #{username}, '%')</if>
|
<if test="username != null and username != ''">AND d.username like concat('%', #{username}, '%')</if>
|
||||||
<if test="osName != null and osName != ''">AND os_name like concat('%', #{osName}, '%')</if>
|
<if test="osName != null and osName != ''">AND d.os like concat('%', #{osName}, '%')</if>
|
||||||
<if test="status != null and status != ''">AND status = #{status}</if>
|
<if test="online != null and online != ''">
|
||||||
<if test="online != null and online != ''">AND online = #{online}</if>
|
AND d.status =
|
||||||
|
<choose>
|
||||||
|
<when test="online == '1'">
|
||||||
|
'online'
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
'offline'
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
</where>
|
</where>
|
||||||
order by last_active_time desc
|
order by d.last_active_at desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 查询客户端错误报告列表 -->
|
<!-- 查询客户端错误报告列表 -->
|
||||||
@@ -223,14 +244,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
order by collect_time desc
|
order by collect_time desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 查询在线客户端数量 -->
|
<!-- 查询在线客户端数量 - 基于 client_device 表 -->
|
||||||
<select id="selectOnlineClientCount" resultType="int">
|
<select id="selectOnlineClientCount" resultType="int">
|
||||||
select count(*) from client_info where online = '1'
|
select count(*) from client_device where status = 'online'
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 查询客户端总数 -->
|
<!-- 查询客户端总数 - 基于 client_device 表 -->
|
||||||
<select id="selectTotalClientCount" resultType="int">
|
<select id="selectTotalClientCount" resultType="int">
|
||||||
SELECT COUNT(*) FROM client_info
|
SELECT COUNT(*) FROM client_device WHERE status != 'removed'
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 查询今日错误数量 -->
|
<!-- 查询今日错误数量 -->
|
||||||
@@ -342,11 +363,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
from client_data_report
|
from client_data_report
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<!-- 查询最近7天的客户端活跃趋势 -->
|
<!-- 查询最近7天的客户端活跃趋势 - 基于 client_device 表 -->
|
||||||
<select id="selectClientActiveTrend" resultType="map">
|
<select id="selectClientActiveTrend" resultType="map">
|
||||||
SELECT
|
SELECT
|
||||||
days.date_str as date,
|
days.date_str as date,
|
||||||
IFNULL(counts.client_count, 0) as count
|
IFNULL(counts.device_count, 0) as count
|
||||||
FROM
|
FROM
|
||||||
(
|
(
|
||||||
SELECT DATE_FORMAT(CURDATE() - INTERVAL 6 DAY, '%Y-%m-%d') as date_str
|
SELECT DATE_FORMAT(CURDATE() - INTERVAL 6 DAY, '%Y-%m-%d') as date_str
|
||||||
@@ -360,15 +381,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
DATE_FORMAT(auth_time, '%Y-%m-%d') as auth_date,
|
DATE_FORMAT(create_time, '%Y-%m-%d') as create_date,
|
||||||
COUNT(DISTINCT client_id) as client_count
|
COUNT(*) as device_count
|
||||||
FROM
|
FROM
|
||||||
client_info
|
client_device
|
||||||
WHERE
|
WHERE
|
||||||
auth_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
|
create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
|
||||||
|
AND status != 'removed'
|
||||||
GROUP BY
|
GROUP BY
|
||||||
DATE_FORMAT(auth_time, '%Y-%m-%d')
|
DATE_FORMAT(create_time, '%Y-%m-%d')
|
||||||
) counts ON days.date_str = counts.auth_date
|
) counts ON days.date_str = counts.create_date
|
||||||
ORDER BY
|
ORDER BY
|
||||||
days.date_str ASC
|
days.date_str ASC
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ VUE_APP_TITLE = ERP管理系统
|
|||||||
ENV = 'production'
|
ENV = 'production'
|
||||||
|
|
||||||
# ERP管理系统/生产环境
|
# ERP管理系统/生产环境
|
||||||
VUE_APP_BASE_API = 'http://8.138.23.49:8080'
|
VUE_APP_BASE_API = 'http://8.138.23.49:8085'
|
||||||
|
|||||||
@@ -60,3 +60,12 @@ export function checkUsername(username) {
|
|||||||
params: { username }
|
params: { username }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 续费客户端账号
|
||||||
|
export function renewAccount(data) {
|
||||||
|
return request({
|
||||||
|
url: '/monitor/account/renew',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -12,13 +12,13 @@
|
|||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="el-icon-user"></i>
|
<i class="el-icon-user"></i>
|
||||||
<span>当前在线客户端</span>
|
<span>当前在线设备</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<count-to :start-val="0" :end-val="statisticsData.onlineCount" :duration="2500" class="card-value" />
|
<count-to :start-val="0" :end-val="statisticsData.onlineCount" :duration="2500" class="card-value" />
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<span>总注册客户端: {{ statisticsData.totalCount }}</span>
|
<span>总设备数: {{ statisticsData.totalCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -60,13 +60,13 @@
|
|||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="el-icon-s-data"></i>
|
<i class="el-icon-s-data"></i>
|
||||||
<span>离线客户端</span>
|
<span>离线设备</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<count-to :start-val="0" :end-val="statisticsData.totalCount - statisticsData.onlineCount" :duration="2500" class="card-value" />
|
<count-to :start-val="0" :end-val="statisticsData.totalCount - statisticsData.onlineCount" :duration="2500" class="card-value" />
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<span>离线时长: {{ statisticsData.avgOfflineTime || '未知' }}</span>
|
<span>离线设备数</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="el-icon-data-analysis"></i>
|
<i class="el-icon-data-analysis"></i>
|
||||||
<span>近7天新增客户端趋势</span>
|
<span>近7天新增设备趋势</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
@@ -109,25 +109,25 @@
|
|||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="el-icon-s-grid"></i>
|
<i class="el-icon-s-grid"></i>
|
||||||
<span>活跃客户端列表</span>
|
<span>活跃设备列表</span>
|
||||||
<el-button style="float: right; padding: 3px 0;" type="text" @click="refreshClients">
|
<el-button style="float: right; padding: 3px 0;" type="text" @click="refreshClients">
|
||||||
<i class="el-icon-refresh"></i> 刷新
|
<i class="el-icon-refresh"></i> 刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-table :data="clientList" style="width: 100%" v-loading="loading">
|
<el-table :data="clientList" style="width: 100%" v-loading="loading">
|
||||||
<el-table-column prop="clientId" label="客户端ID" width="240"></el-table-column>
|
<el-table-column prop="clientId" label="设备ID" width="240" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column prop="username" label="用户名" width="120"></el-table-column>
|
<el-table-column prop="username" label="账号" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="hostname" label="设备名称" width="150" show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column prop="osName" label="操作系统" width="120"></el-table-column>
|
<el-table-column prop="osName" label="操作系统" width="120"></el-table-column>
|
||||||
<el-table-column prop="appVersion" label="应用版本" width="100"></el-table-column>
|
|
||||||
<el-table-column prop="ipAddress" label="IP地址" width="150"></el-table-column>
|
<el-table-column prop="ipAddress" label="IP地址" width="150"></el-table-column>
|
||||||
<el-table-column prop="lastActiveTime" label="最后活跃时间" width="180"></el-table-column>
|
<el-table-column prop="lastActiveTime" label="最后活跃时间" width="180"></el-table-column>
|
||||||
<el-table-column prop="online" label="状态">
|
<el-table-column prop="online" label="状态" width="100">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
<el-tag :type="scope.row.online === '1' ? 'success' : 'info'">{{ scope.row.online === '1' ? '在线' : '离线' }}</el-tag>
|
<el-tag :type="scope.row.online === '1' ? 'success' : 'info'">{{ scope.row.online === '1' ? '在线' : '离线' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180">
|
<el-table-column label="操作" width="120">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
<el-button size="mini" type="text" @click="viewClientData(scope.row)">详情</el-button>
|
<el-button size="mini" type="text" @click="viewClientData(scope.row)">详情</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -150,19 +150,17 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 客户端详情弹窗 -->
|
<!-- 客户端详情弹窗 -->
|
||||||
<el-dialog :title="'客户端详情: ' + currentClient.clientId" v-model="detailDialogVisible" width="50%">
|
<el-dialog :title="'设备详情: ' + currentClient.hostname" v-model="detailDialogVisible" width="50%">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="用户名">{{ currentClient.username }}</el-descriptions-item>
|
<el-descriptions-item label="设备ID">{{ currentClient.clientId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="账号名">{{ currentClient.username }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="设备名称">{{ currentClient.hostname }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="currentClient.online === '1' ? 'success' : 'info'">{{ currentClient.online === '1' ? '在线' : '离线' }}</el-tag>
|
<el-tag :type="currentClient.online === '1' ? 'success' : 'info'">{{ currentClient.online === '1' ? '在线' : '离线' }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="操作系统">{{ currentClient.osName }}</el-descriptions-item>
|
<el-descriptions-item label="操作系统">{{ currentClient.osName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="系统版本">{{ currentClient.osVersion }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="Java版本">{{ currentClient.javaVersion }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="应用版本">{{ currentClient.appVersion }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="IP地址">{{ currentClient.ipAddress }}</el-descriptions-item>
|
<el-descriptions-item label="IP地址">{{ currentClient.ipAddress }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="主机名">{{ currentClient.hostname }}</el-descriptions-item>
|
<el-descriptions-item label="注册时间">{{ currentClient.authTime }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="认证时间">{{ currentClient.authTime }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="最后活跃">{{ currentClient.lastActiveTime }}</el-descriptions-item>
|
<el-descriptions-item label="最后活跃">{{ currentClient.lastActiveTime }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -273,12 +271,12 @@ export default {
|
|||||||
errorRate: 0,
|
errorRate: 0,
|
||||||
avgOfflineTime: '未知'
|
avgOfflineTime: '未知'
|
||||||
},
|
},
|
||||||
// 近7天新增客户端趋势数据
|
// 近7天新增设备趋势数据
|
||||||
onlineClientData: {
|
onlineClientData: {
|
||||||
labels: [],
|
labels: [],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: '新增客户端数',
|
label: '新增设备数',
|
||||||
data: [],
|
data: [],
|
||||||
backgroundColor: 'rgba(103, 194, 58, 0.2)',
|
backgroundColor: 'rgba(103, 194, 58, 0.2)',
|
||||||
borderColor: 'rgb(103, 194, 58)',
|
borderColor: 'rgb(103, 194, 58)',
|
||||||
|
|||||||
@@ -89,13 +89,22 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="过期时间" align="center" prop="expireTime" width="180">
|
<el-table-column label="过期时间" align="center" prop="expireTime" width="180">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ scope.row.expireTime }}</span>
|
<el-tag
|
||||||
<el-tag v-if="isExpired(scope.row)" type="danger" size="mini">已过期</el-tag>
|
:type="getRemainingDays(scope.row).type"
|
||||||
|
size="small"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
>
|
||||||
|
{{ getRemainingDays(scope.row).text }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="设备限制" align="center" prop="deviceLimit" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.deviceLimit || 3 }}台</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
|
||||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
|
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
@@ -153,16 +162,37 @@
|
|||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
></el-date-picker>
|
></el-date-picker>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="快速续费" v-if="form.id">
|
||||||
|
<el-select v-model="form.renewDays" placeholder="选择续费套餐(可选)" style="width: 100%;" clearable>
|
||||||
|
<el-option label="月付(30天)" :value="30"></el-option>
|
||||||
|
<el-option label="季付(90天)" :value="90"></el-option>
|
||||||
|
<el-option label="半年付(180天)" :value="180"></el-option>
|
||||||
|
<el-option label="年付(365天)" :value="365"></el-option>
|
||||||
|
</el-select>
|
||||||
|
<div v-if="form.renewDays" style="margin-top: 8px; color: #409EFF; font-size: 12px;">
|
||||||
|
<i class="el-icon-info"></i> 续费后到期时间:{{ calculateNewExpireTime() }}
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备数量限制" prop="deviceLimit">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.deviceLimit"
|
||||||
|
:min="1"
|
||||||
|
:max="20"
|
||||||
|
placeholder="请输入设备数量限制"
|
||||||
|
style="width: 100%;"
|
||||||
|
></el-input-number>
|
||||||
|
<span style="color: #909399; font-size: 12px;">允许同时登录的设备数量,默认3台</span>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="功能权限" prop="permissions">
|
<el-form-item label="功能权限" prop="permissions">
|
||||||
<div class="permission-config">
|
<div class="permission-config">
|
||||||
<el-checkbox-group v-model="selectedPermissions" @change="onPermissionChange">
|
<el-checkbox-group v-model="selectedPermissions" @change="onPermissionChange">
|
||||||
<el-checkbox label="rakuten">日本乐天平台</el-checkbox>
|
<el-checkbox label="rakuten"><i class="el-icon-goods"></i> 日本乐天平台</el-checkbox>
|
||||||
<el-checkbox label="amazon">亚马逊平台</el-checkbox>
|
<el-checkbox label="amazon"><i class="el-icon-shopping-cart-2"></i> 亚马逊平台</el-checkbox>
|
||||||
<el-checkbox label="zebra">斑马平台</el-checkbox>
|
<el-checkbox label="zebra"><i class="el-icon-postcard"></i> 斑马平台</el-checkbox>
|
||||||
<el-checkbox label="shopee">虾皮购物平台</el-checkbox>
|
<el-checkbox label="shopee"><i class="el-icon-shopping-bag-2"></i> 虾皮购物平台</el-checkbox>
|
||||||
<el-checkbox label="toolbox">工具箱功能</el-checkbox>
|
<el-checkbox label="toolbox"><i class="el-icon-box"></i> 工具箱功能</el-checkbox>
|
||||||
<el-checkbox label="dataCollection">数据采集功能</el-checkbox>
|
<el-checkbox label="dataCollection"><i class="el-icon-document-copy"></i> 数据采集功能</el-checkbox>
|
||||||
<el-checkbox label="priceCompare">1688比价功能</el-checkbox>
|
<el-checkbox label="priceCompare"><i class="el-icon-price-tag"></i> 1688比价功能</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -201,7 +231,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listAccount, getAccount, delAccount, addAccount, updateAccount, registerAccount, checkUsername } from "@/api/monitor/account";
|
import { listAccount, getAccount, delAccount, addAccount, updateAccount, registerAccount, checkUsername, renewAccount } from "@/api/monitor/account";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Account",
|
name: "Account",
|
||||||
@@ -309,6 +339,8 @@ export default {
|
|||||||
password: null,
|
password: null,
|
||||||
status: "0",
|
status: "0",
|
||||||
expireTime: null,
|
expireTime: null,
|
||||||
|
renewDays: null,
|
||||||
|
deviceLimit: 3,
|
||||||
remark: null,
|
remark: null,
|
||||||
permissions: null
|
permissions: null
|
||||||
};
|
};
|
||||||
@@ -353,10 +385,24 @@ export default {
|
|||||||
this.$refs["form"].validate(valid => {
|
this.$refs["form"].validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (this.form.id != null) {
|
if (this.form.id != null) {
|
||||||
updateAccount(this.form).then(response => {
|
// 如果选择了续费,先执行续费
|
||||||
this.$modal.msgSuccess("修改成功");
|
const promises = [];
|
||||||
|
if (this.form.renewDays) {
|
||||||
|
promises.push(renewAccount({
|
||||||
|
accountId: this.form.id,
|
||||||
|
days: this.form.renewDays
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// 执行更新
|
||||||
|
promises.push(updateAccount(this.form));
|
||||||
|
|
||||||
|
Promise.all(promises).then(() => {
|
||||||
|
const msg = this.form.renewDays ? "修改并续费成功" : "修改成功";
|
||||||
|
this.$modal.msgSuccess(msg);
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.getList();
|
this.getList();
|
||||||
|
}).catch(error => {
|
||||||
|
this.$modal.msgError('操作失败: ' + (error.message || '未知错误'));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addAccount(this.form).then(response => {
|
addAccount(this.form).then(response => {
|
||||||
@@ -471,6 +517,48 @@ export default {
|
|||||||
cancelRegister() {
|
cancelRegister() {
|
||||||
this.registerOpen = false;
|
this.registerOpen = false;
|
||||||
this.resetRegisterForm();
|
this.resetRegisterForm();
|
||||||
|
},
|
||||||
|
/** 计算续费后的新到期时间 */
|
||||||
|
calculateNewExpireTime() {
|
||||||
|
if (!this.form.renewDays) return '';
|
||||||
|
|
||||||
|
let baseDate;
|
||||||
|
if (this.form.expireTime && new Date(this.form.expireTime) > new Date()) {
|
||||||
|
// 未过期,从到期时间延长
|
||||||
|
baseDate = new Date(this.form.expireTime);
|
||||||
|
} else {
|
||||||
|
// 已过期或无到期时间,从当前时间开始
|
||||||
|
baseDate = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDate.setDate(baseDate.getDate() + this.form.renewDays);
|
||||||
|
return baseDate.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 获取剩余天数 */
|
||||||
|
getRemainingDays(row) {
|
||||||
|
if (!row.expireTime) {
|
||||||
|
return { text: '已过期', type: 'danger' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const expireDate = new Date(row.expireTime);
|
||||||
|
const diffTime = expireDate - now;
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays < 0) {
|
||||||
|
return { text: '已过期', type: 'danger' };
|
||||||
|
} else if (diffDays <= 3) {
|
||||||
|
return { text: `剩余 ${diffDays} 天`, type: 'warning' };
|
||||||
|
} else {
|
||||||
|
return { text: `剩余 ${diffDays} 天`, type: 'success' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -478,25 +566,34 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.permission-config {
|
.permission-config {
|
||||||
padding: 10px;
|
padding: 15px;
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #DCDFE6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #fafafa;
|
background: linear-gradient(135deg, #f5f7fa 0%, #fafbfc 100%);
|
||||||
}
|
|
||||||
|
|
||||||
.permission-config .el-checkbox {
|
|
||||||
display: block;
|
|
||||||
margin: 8px 0;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.permission-config .el-checkbox-group {
|
.permission-config .el-checkbox-group {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.permission-config .el-checkbox + .el-checkbox {
|
.permission-config .el-checkbox {
|
||||||
margin-left: 0;
|
margin: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-config .el-checkbox:hover {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-config .el-checkbox i {
|
||||||
|
margin-right: 6px;
|
||||||
|
color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checking {
|
.checking {
|
||||||
|
|||||||
Reference in New Issue
Block a user