feat(controlled, controller): 新增配对码生成与兑换功能

被控端:新增生成一次性配对码并弹窗展示,支持倒计时与复制;控制端:新增输入配对码兑换绑定,刷新设备列表,并在设备列表中显示在线状态和点击连接。同时添加统一 401 拦截器,令牌失效时触发重新激活。
This commit is contained in:
TongTongStudio
2026-08-03 01:57:42 +08:00
parent ded6fe5ab7
commit e689b999e6
29 changed files with 1096 additions and 86 deletions

View File

@@ -30,10 +30,13 @@
<el-table v-loading="loading" :data="users" stripe>
<el-table-column prop="username" label="用户名" min-width="150">
<template #default="{ row }">
<span>{{ row.username }}</span>
<span class="link-user" @click="openBindings(row)">{{ row.username }}</span>
<el-tag v-if="row.admin" type="danger" size="small" style="margin-left: 6px">
管理员
</el-tag>
<el-tag v-if="row.deviceCount > 0" type="info" size="small" style="margin-left: 6px">
{{ row.deviceCount }} 台设备
</el-tag>
</template>
</el-table-column>
<el-table-column prop="userId" label="用户 ID" min-width="200" show-overflow-tooltip />
@@ -90,6 +93,7 @@
<el-button link type="info">更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="bindings">查看绑定设备</el-dropdown-item>
<el-dropdown-item command="sessions">查看会话</el-dropdown-item>
<el-dropdown-item command="reset">重置密码</el-dropdown-item>
<el-dropdown-item command="admin">
@@ -160,6 +164,40 @@
</el-table-column>
</el-table>
</el-dialog>
<!-- 绑定关系点击用户名查看该用户绑定的终端设备 -->
<el-dialog v-model="bindingsVisible" :title="`「${bindingUser?.username}」绑定的终端设备`" width="760px">
<el-table v-loading="bindingLoading" :data="userBindings" stripe size="small" empty-text="该用户暂未绑定任何终端设备">
<el-table-column prop="deviceUid" label="设备 ID" min-width="200" show-overflow-tooltip />
<el-table-column label="别名" min-width="140">
<template #default="{ row }">{{ row.alias || '—' }}</template>
</el-table-column>
<el-table-column label="角色" width="100">
<template #default="{ row }">
<el-tag :type="row.role === 'OWNER' ? 'primary' : 'info'" size="small">
{{ row.role === 'OWNER' ? '所有者' : '成员' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'danger'" size="small">
{{ row.status === 'ACTIVE' ? '有效' : '已撤销' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="在线" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.online ? 'success' : 'info'" size="small" effect="dark">
{{ row.online ? '在线' : '离线' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="绑定时间" min-width="170">
<template #default="{ row }">{{ fmtTime(row.boundAt) }}</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
@@ -178,9 +216,10 @@ import {
unbanUser,
kickUser,
getUserSessions,
getUserBindings,
kickSession,
} from '@/api/admin';
import type { AccountStatus, UserAccountView, UserSessionView } from '@/api/admin';
import type { AccountStatus, UserAccountView, UserSessionView, UserBindingView } from '@/api/admin';
const users = ref<UserAccountView[]>([]);
const loading = ref(false);
@@ -348,6 +387,26 @@ async function openSessions(row: UserAccountView) {
}
}
// ---------- 绑定关系(点击用户名查看该用户绑定的终端设备) ----------
const bindingsVisible = ref(false);
const bindingUser = ref<UserAccountView | null>(null);
const userBindings = ref<UserBindingView[]>([]);
const bindingLoading = ref(false);
async function openBindings(row: UserAccountView) {
bindingUser.value = row;
bindingsVisible.value = true;
bindingLoading.value = true;
try {
userBindings.value = await getUserBindings(row.userId);
} catch {
userBindings.value = [];
ElMessage.error('加载绑定关系失败');
} finally {
bindingLoading.value = false;
}
}
async function handleKickSession(row: UserSessionView) {
await kickSession(row.sessionId);
ElMessage.success('会话已踢出');
@@ -356,7 +415,8 @@ async function handleKickSession(row: UserSessionView) {
}
function handleCommand(cmd: string, row: UserAccountView) {
if (cmd === 'sessions') openSessions(row);
if (cmd === 'bindings') openBindings(row);
else if (cmd === 'sessions') openSessions(row);
else if (cmd === 'reset') openReset(row);
else if (cmd === 'admin') handleToggleAdmin(row);
else if (cmd === 'delete') handleDelete(row);
@@ -383,6 +443,14 @@ onMounted(load);
align-items: center;
gap: 12px;
}
.link-user {
color: var(--el-color-primary);
cursor: pointer;
font-weight: 500;
}
.link-user:hover {
text-decoration: underline;
}
.page-title {
margin: 0 0 4px;
font-size: 20px;