feat(controlled, controller): 新增配对码生成与兑换功能
被控端:新增生成一次性配对码并弹窗展示,支持倒计时与复制;控制端:新增输入配对码兑换绑定,刷新设备列表,并在设备列表中显示在线状态和点击连接。同时添加统一 401 拦截器,令牌失效时触发重新激活。
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user