feat: add online device management and desktop settings integration
This commit is contained in:
107
frontend/app.js
107
frontend/app.js
@@ -264,6 +264,13 @@ createApp({
|
||||
has_password: false
|
||||
}
|
||||
},
|
||||
onlineDevices: {
|
||||
loading: false,
|
||||
kickingSessionId: '',
|
||||
items: [],
|
||||
error: '',
|
||||
lastLoadedAt: ''
|
||||
},
|
||||
|
||||
// 健康检测
|
||||
healthCheck: {
|
||||
@@ -945,11 +952,95 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
getOrCreateWebDeviceId() {
|
||||
const storageKey = 'wanwan_web_device_id_v1';
|
||||
const existing = localStorage.getItem(storageKey);
|
||||
if (existing && existing.trim()) {
|
||||
return existing.trim();
|
||||
}
|
||||
const generated = (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
|
||||
? crypto.randomUUID()
|
||||
: `web-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
localStorage.setItem(storageKey, generated);
|
||||
return generated;
|
||||
},
|
||||
|
||||
buildLoginClientMeta() {
|
||||
const platform = navigator.platform || '未知平台';
|
||||
const name = `${navigator.userAgent?.includes('Mobile') ? '移动端网页' : '网页端'} · ${platform || '未知平台'}`;
|
||||
return {
|
||||
client_type: navigator.userAgent?.includes('Mobile') ? 'mobile' : 'web',
|
||||
device_id: this.getOrCreateWebDeviceId(),
|
||||
device_name: name.slice(0, 120),
|
||||
platform: String(platform || '').slice(0, 80)
|
||||
};
|
||||
},
|
||||
|
||||
formatOnlineDeviceType(clientType) {
|
||||
if (clientType === 'desktop') return '桌面端';
|
||||
if (clientType === 'mobile') return '移动端';
|
||||
if (clientType === 'api') return 'API';
|
||||
return '网页端';
|
||||
},
|
||||
|
||||
async loadOnlineDevices(silent = false) {
|
||||
if (!silent) {
|
||||
this.onlineDevices.loading = true;
|
||||
}
|
||||
this.onlineDevices.error = '';
|
||||
try {
|
||||
const response = await axios.get(`${this.apiBase}/api/user/online-devices`);
|
||||
if (response.data?.success) {
|
||||
this.onlineDevices.items = Array.isArray(response.data.devices) ? response.data.devices : [];
|
||||
this.onlineDevices.lastLoadedAt = new Date().toISOString();
|
||||
return;
|
||||
}
|
||||
this.onlineDevices.error = response.data?.message || '加载在线设备失败';
|
||||
} catch (error) {
|
||||
this.onlineDevices.error = error.response?.data?.message || '加载在线设备失败';
|
||||
} finally {
|
||||
this.onlineDevices.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async kickOnlineDevice(device) {
|
||||
const sessionId = String(device?.session_id || '').trim();
|
||||
if (!sessionId || this.onlineDevices.kickingSessionId) return;
|
||||
|
||||
const isCurrent = !!device?.is_current;
|
||||
const tip = isCurrent
|
||||
? '确定要下线当前设备吗?下线后需要重新登录。'
|
||||
: '确定要强制该设备下线吗?';
|
||||
if (!confirm(tip)) return;
|
||||
|
||||
this.onlineDevices.kickingSessionId = sessionId;
|
||||
try {
|
||||
const response = await axios.post(`${this.apiBase}/api/user/online-devices/${encodeURIComponent(sessionId)}/kick`);
|
||||
if (response.data?.success) {
|
||||
this.showToast('success', '成功', response.data?.message || '设备已下线');
|
||||
if (response.data?.kicked_current) {
|
||||
this.handleTokenExpired();
|
||||
return;
|
||||
}
|
||||
await this.loadOnlineDevices(true);
|
||||
return;
|
||||
}
|
||||
this.showToast('error', '失败', response.data?.message || '踢下线失败');
|
||||
} catch (error) {
|
||||
this.showToast('error', '失败', error.response?.data?.message || '踢下线失败');
|
||||
} finally {
|
||||
this.onlineDevices.kickingSessionId = '';
|
||||
}
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
this.errorMessage = '';
|
||||
this.loginLoading = true;
|
||||
try {
|
||||
const response = await axios.post(`${this.apiBase}/api/login`, this.loginForm);
|
||||
const response = await axios.post(`${this.apiBase}/api/login`, {
|
||||
...this.loginForm,
|
||||
...this.buildLoginClientMeta()
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
// token 和 refreshToken 都通过 HttpOnly Cookie 自动管理
|
||||
@@ -1444,6 +1535,11 @@ handleDragLeave(e) {
|
||||
localStorage.removeItem('adminTab');
|
||||
this.showResendVerify = false;
|
||||
this.resendVerifyEmail = '';
|
||||
this.onlineDevices.items = [];
|
||||
this.onlineDevices.kickingSessionId = '';
|
||||
this.onlineDevices.loading = false;
|
||||
this.onlineDevices.error = '';
|
||||
this.onlineDevices.lastLoadedAt = '';
|
||||
|
||||
// 停止定期检查
|
||||
this.stopProfileSync();
|
||||
@@ -1545,6 +1641,11 @@ handleDragLeave(e) {
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('lastView');
|
||||
this.stopProfileSync();
|
||||
this.onlineDevices.items = [];
|
||||
this.onlineDevices.kickingSessionId = '';
|
||||
this.onlineDevices.loading = false;
|
||||
this.onlineDevices.error = '';
|
||||
this.onlineDevices.lastLoadedAt = '';
|
||||
},
|
||||
|
||||
// 启动token自动刷新定时器
|
||||
@@ -3918,6 +4019,7 @@ handleDragLeave(e) {
|
||||
}
|
||||
break;
|
||||
case 'settings':
|
||||
this.loadOnlineDevices();
|
||||
if (this.user && !this.user.is_admin) {
|
||||
this.loadDownloadTrafficReport();
|
||||
}
|
||||
@@ -4891,6 +4993,9 @@ handleDragLeave(e) {
|
||||
// 普通用户进入设置页面时加载OSS配置
|
||||
this.loadOssConfig();
|
||||
this.loadDownloadTrafficReport();
|
||||
this.loadOnlineDevices();
|
||||
} else if (newView === 'settings') {
|
||||
this.loadOnlineDevices();
|
||||
}
|
||||
|
||||
// 记住最后停留的视图(需合法且已登录)
|
||||
|
||||
Reference in New Issue
Block a user