1
This commit is contained in:
@@ -118,11 +118,34 @@ async function handleLoginSuccess(data: { token: string; permissions?: string })
|
|||||||
currentUsername.value = username
|
currentUsername.value = username
|
||||||
userPermissions.value = data?.permissions || ''
|
userPermissions.value = data?.permissions || ''
|
||||||
await deviceApi.register({ username })
|
await deviceApi.register({ username })
|
||||||
|
|
||||||
|
// 建立SSE连接
|
||||||
|
SSEManager.connect()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 设备注册失败不影响登录流程,静默处理
|
// 设备注册失败不影响登录流程,静默处理
|
||||||
console.warn('设备注册失败:', e)
|
console.warn('设备注册失败:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
// 删除后端token缓存 - 复刻HTML版本逻辑
|
||||||
|
await fetch('/api/cache/delete?key=token', { method: 'POST' })
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('删除后端token缓存失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理前端状态
|
||||||
|
try { localStorage.removeItem('token') } catch {}
|
||||||
|
isAuthenticated.value = false
|
||||||
|
currentUsername.value = ''
|
||||||
|
userPermissions.value = ''
|
||||||
|
showAuthDialog.value = true
|
||||||
|
showDeviceDialog.value = false
|
||||||
|
|
||||||
|
// 关闭SSE连接
|
||||||
|
SSEManager.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
async function handleUserClick() {
|
async function handleUserClick() {
|
||||||
if (!isAuthenticated.value) {
|
if (!isAuthenticated.value) {
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
@@ -130,14 +153,7 @@ async function handleUserClick() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确认退出登录?', '提示', { type: 'warning', confirmButtonText: '退出', cancelButtonText: '取消' })
|
await ElMessageBox.confirm('确认退出登录?', '提示', { type: 'warning', confirmButtonText: '退出', cancelButtonText: '取消' })
|
||||||
const token = localStorage.getItem('token') || ''
|
await logout()
|
||||||
try { await authApi.logout(token) } catch {}
|
|
||||||
try { localStorage.removeItem('token') } catch {}
|
|
||||||
isAuthenticated.value = false
|
|
||||||
currentUsername.value = ''
|
|
||||||
userPermissions.value = ''
|
|
||||||
showAuthDialog.value = true
|
|
||||||
showDeviceDialog.value = false
|
|
||||||
ElMessage.success('已退出登录')
|
ElMessage.success('已退出登录')
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -177,6 +193,9 @@ async function checkAuth() {
|
|||||||
if (u) currentUsername.value = u
|
if (u) currentUsername.value = u
|
||||||
}
|
}
|
||||||
userPermissions.value = response.permissions || ''
|
userPermissions.value = response.permissions || ''
|
||||||
|
|
||||||
|
// 认证成功后建立SSE连接
|
||||||
|
SSEManager.connect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -204,6 +223,67 @@ function getUsernameFromToken(token?: string) {
|
|||||||
return payload.username || ''
|
return payload.username || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSE管理器 - 简化封装
|
||||||
|
const SSEManager = {
|
||||||
|
connection: null as EventSource | null,
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if (this.connection) return
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const clientId = getClientIdFromToken(token)
|
||||||
|
if (!token || !clientId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 简化配置获取,失败时使用默认配置
|
||||||
|
let sseUrl = 'http://192.168.1.89:8080/monitor/account/events'
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/config/server')
|
||||||
|
if (resp.ok) {
|
||||||
|
const config = await resp.json()
|
||||||
|
sseUrl = config.sseUrl || sseUrl
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const src = new EventSource(`${sseUrl}?clientId=${clientId}&token=${token}`)
|
||||||
|
this.connection = src
|
||||||
|
|
||||||
|
src.onmessage = (e) => this.handleMessage(e)
|
||||||
|
src.onerror = () => this.handleError()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('SSE连接失败:', e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMessage(e: MessageEvent) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(e.data)
|
||||||
|
switch (payload.type) {
|
||||||
|
case 'DEVICE_REMOVED':
|
||||||
|
case 'FORCE_LOGOUT':
|
||||||
|
logout()
|
||||||
|
ElMessage.warning('会话已失效,请重新登录')
|
||||||
|
break
|
||||||
|
case 'PERMISSIONS_UPDATED':
|
||||||
|
checkAuth()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleError() {
|
||||||
|
this.disconnect()
|
||||||
|
setTimeout(() => this.connect(), 3000)
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.connection) {
|
||||||
|
try { this.connection.close() } catch {}
|
||||||
|
this.connection = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openDeviceManager() {
|
async function openDeviceManager() {
|
||||||
if (!isAuthenticated.value) {
|
if (!isAuthenticated.value) {
|
||||||
showAuthDialog.value = true
|
showAuthDialog.value = true
|
||||||
@@ -241,12 +321,13 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
|||||||
await deviceApi.remove({ deviceId: row.deviceId })
|
await deviceApi.remove({ deviceId: row.deviceId })
|
||||||
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
||||||
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
|
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
|
||||||
if (row.isCurrent) {
|
|
||||||
// 当前设备被移除,清理登录状态
|
// 如果是本机设备被移除,执行logout - 复刻HTML版本逻辑
|
||||||
isAuthenticated.value = false
|
const clientId = getClientIdFromToken()
|
||||||
showAuthDialog.value = true
|
if (row.deviceId === clientId) {
|
||||||
try { localStorage.removeItem('token') } catch {}
|
await logout()
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success('已移除设备')
|
ElMessage.success('已移除设备')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* 用户取消或失败 */
|
/* 用户取消或失败 */
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格加载遮罩 -->
|
<!-- 表格加载遮罩 -->
|
||||||
<div v-if="tableLoading" class="table-loading">
|
<div v-if="tableLoading && paginatedData.length === 0" class="table-loading">
|
||||||
<div class="spinner">⟳</div>
|
<div class="spinner">⟳</div>
|
||||||
<div>加载中...</div>
|
<div>加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -366,8 +366,8 @@ onMounted(async () => {
|
|||||||
.progress-section { margin: 15px 0 10px 0; }
|
.progress-section { margin: 15px 0 10px 0; }
|
||||||
.progress-box { padding: 8px 0; }
|
.progress-box { padding: 8px 0; }
|
||||||
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
||||||
.progress-bar { flex: 1; height: 6px; background: #ebeef5; border-radius: 3px; overflow: hidden; }
|
.progress-bar { flex: 1; height: 3px; background: #ebeef5; border-radius: 2px; overflow: hidden; }
|
||||||
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 3px; transition: width 0.3s ease; }
|
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 2px; transition: width 0.3s ease; }
|
||||||
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
||||||
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
||||||
.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; }
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ onMounted(loadLatest)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格加载遮罩 -->
|
<!-- 表格加载遮罩 -->
|
||||||
<div v-if="tableLoading" class="table-loading">
|
<div v-if="tableLoading && paginatedData.length === 0" class="table-loading">
|
||||||
<div class="spinner">⟳</div>
|
<div class="spinner">⟳</div>
|
||||||
<div>加载中...</div>
|
<div>加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -454,20 +454,8 @@ onMounted(loadLatest)
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar { flex: 1; height: 3px; background: #ebeef5; border-radius: 2px; overflow: hidden; }
|
||||||
flex: 1;
|
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 2px; transition: width 0.3s ease; }
|
||||||
height: 6px;
|
|
||||||
background: #ebeef5;
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #409EFF, #66b1ff);
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
.progress-text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -174,20 +174,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 进度条显示 -->
|
<!-- 进度条显示:移动到底部,以免挤压表头 -->
|
||||||
<div class="progress-section" v-if="showProgress">
|
|
||||||
<div class="progress-box">
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-text">{{ progressPercentage }}%</div>
|
|
||||||
</div>
|
|
||||||
<div class="current-status" v-if="fetchTotalItems > 0">
|
|
||||||
{{ progressPercentage >= 100 ? '完成' : `获取中... (${allOrderData.length}/${fetchTotalItems})` }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 数据显示区域 -->
|
<!-- 数据显示区域 -->
|
||||||
@@ -256,15 +243,19 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格加载遮罩 -->
|
<!-- 表格加载遮罩:仅在无数据时显示 -->
|
||||||
<div v-if="loading && !allOrderData.length" class="table-loading">
|
<div v-if="loading && !allOrderData.length" class="table-loading">
|
||||||
<div class="spinner">⟳</div>
|
<div class="spinner">⟳</div>
|
||||||
<div>加载中...</div>
|
<div>加载中...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页器 -->
|
<!-- 底部区域:进度条 + 分页器 -->
|
||||||
<div class="pagination-fixed">
|
<div class="pagination-fixed">
|
||||||
|
<div v-if="showProgress" class="progress-bottom">
|
||||||
|
<div class="progress-bar"><div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div></div>
|
||||||
|
<div class="progress-text">{{ progressPercentage }}%</div>
|
||||||
|
</div>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
background
|
background
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
@@ -296,8 +287,8 @@ export default {
|
|||||||
.progress-section { margin: 15px 0 10px 0; }
|
.progress-section { margin: 15px 0 10px 0; }
|
||||||
.progress-box { padding: 8px 0; }
|
.progress-box { padding: 8px 0; }
|
||||||
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
||||||
.progress-bar { flex: 1; height: 6px; background: #ebeef5; border-radius: 3px; overflow: hidden; }
|
.progress-bar { flex: 1; height: 3px; background: #ebeef5; border-radius: 2px; overflow: hidden; }
|
||||||
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 3px; transition: width 0.3s ease; }
|
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 2px; transition: width 0.3s ease; }
|
||||||
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
||||||
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
||||||
.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; }
|
||||||
@@ -326,6 +317,7 @@ export default {
|
|||||||
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.6; }
|
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.6; }
|
||||||
.empty-text { font-size: 14px; color: #909399; }
|
.empty-text { font-size: 14px; color: #909399; }
|
||||||
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; }
|
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.progress-bottom { display: flex; align-items: center; gap: 8px; margin-right: auto; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user