From ce45d93ce5526835efb1410caa7fa29fae43edac Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 21:35:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(session):=20=E5=9C=A8=E7=BA=BF=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E9=A1=B5=E9=9D=A2=20+=20=E7=94=A8=E6=88=B7=E9=94=81?= =?UTF-8?q?=E5=AE=9A/=E8=A7=A3=E9=94=81=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 monitor/session 在线会话页面(列表 + 强退 + 踢全部) - 用户页新增锁定/解锁操作,锁定状态独立标签展示 - api/session.ts + api/user.ts 扩展 Co-Authored-By: Claude Opus 4.8 --- src/api/session.ts | 39 +++++++++ src/api/user.ts | 19 ++++- src/pages/monitor/session/index.vue | 128 ++++++++++++++++++++++++++++ src/pages/system/user/index.vue | 46 +++++++++- typed-router.d.ts | 13 +++ 5 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 src/api/session.ts create mode 100644 src/pages/monitor/session/index.vue diff --git a/src/api/session.ts b/src/api/session.ts new file mode 100644 index 0000000..38ccb44 --- /dev/null +++ b/src/api/session.ts @@ -0,0 +1,39 @@ +import { http } from '@/api/http' + +export interface Session { + sid: string | null + userId: string + username: string + nickname: string | null + ip: string | null + userAgent: string | null + loginAt: number + expireAt: number +} + +export interface SessionListQuery { + page: number + pageSize: number + username?: string + ip?: string +} + +export interface SessionListResult { + list: Session[] + total: number + page: number + pageSize: number +} + +/** 活跃会话分页列表。 */ +export function listSessions(query: SessionListQuery, signal?: AbortSignal): Promise { + const params: Record = { page: query.page, pageSize: query.pageSize } + if (query.username) params.username = query.username + if (query.ip) params.ip = query.ip + return http.get('/sessions', { params, signal }) +} + +/** 单会话强退。 */ +export function kickSession(sid: string): Promise { + return http.post(`/sessions/${sid}/kick`) +} diff --git a/src/api/user.ts b/src/api/user.ts index 84a3091..063fefc 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,6 +1,6 @@ import { http } from '@/api/http' -export type UserStatus = 'enabled' | 'disabled' | 'locked' +export type UserStatus = 'enabled' | 'disabled' export interface User { id: string @@ -11,6 +11,8 @@ export interface User { phone: string | null avatar: string | null status: UserStatus + lockedAt: number | null + lockReason: string | null orgCode: string | null createdAt: number updatedAt: number @@ -86,3 +88,18 @@ export function listUserRoles(userId: string, signal?: AbortSignal): Promise { return http.put(`/users/${userId}/roles`, { roleCodes }) } + +/** 踢出用户全部会话。 */ +export function kickSessions(userId: string): Promise { + return http.post(`/users/${userId}/kick-sessions`) +} + +/** 锁定用户(可附原因)。 */ +export function lockUser(userId: string, reason?: string): Promise { + return http.post(`/users/${userId}/lock`, { reason }) +} + +/** 解锁用户。 */ +export function unlockUser(userId: string): Promise { + return http.post(`/users/${userId}/unlock`) +} diff --git a/src/pages/monitor/session/index.vue b/src/pages/monitor/session/index.vue new file mode 100644 index 0000000..cd8e22a --- /dev/null +++ b/src/pages/monitor/session/index.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/pages/system/user/index.vue b/src/pages/system/user/index.vue index c67b3ad..b6a22c9 100644 --- a/src/pages/system/user/index.vue +++ b/src/pages/system/user/index.vue @@ -43,7 +43,9 @@ import { createUser, deleteUser, listUsers, + lockUser, resetPassword, + unlockUser, updateUser, type UserListQuery, type UserStatus, @@ -63,7 +65,7 @@ const columns = computed(() => { { prop: 'nickname', label: '昵称', minWidth: 100 }, { prop: 'email', label: '邮箱', minWidth: 160 }, { prop: 'phone', label: '手机', minWidth: 120 }, - { prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'user_status' }, + { prop: 'displayStatus', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'user_status' }, { prop: 'orgCode', label: '所属组织', minWidth: 110, formatter: (v) => orgName(v) }, { prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' }, ] @@ -78,7 +80,7 @@ const searchSchema: JsonSchema = { status: { type: 'string', title: '状态', - dictCode: 'user_status', + dictCode: 'common_status', placeholder: '全部', }, }, @@ -102,7 +104,7 @@ const formSchema = computed(() => ({ status: { type: 'string', title: '状态', - dictCode: 'user_status', + dictCode: 'common_status', default: 'enabled', }, orgCode: { @@ -121,6 +123,8 @@ const actions: CrudAction[] = [ 'delete', { label: '分配角色', type: 'primary', permission: 'user:assign-role', onClick: (row) => assignRolesAction(row) }, { label: '重置密码', type: 'warning', permission: 'user:reset-pwd', onClick: (row) => resetPwd(row) }, + { label: '锁定', type: 'danger', permission: 'user:lock', show: (row) => !row.lockedAt, onClick: (row) => lockAction(row) }, + { label: '解锁', type: 'success', permission: 'user:unlock', show: (row) => !!row.lockedAt, onClick: (row) => unlockAction(row) }, ] function toListQuery(query: PageQuery): UserListQuery { @@ -135,7 +139,13 @@ function toListQuery(query: PageQuery): UserListQuery { } } -const list = (query: PageQuery, signal?: AbortSignal) => listUsers(toListQuery(query), signal) +const list = async (query: PageQuery, signal?: AbortSignal) => { + const result = await listUsers(toListQuery(query), signal) + return { + ...result, + list: result.list.map((u) => ({ ...u, displayStatus: u.lockedAt ? 'locked' : u.status })), + } +} const create = (data: Record) => createUser(data) const update = (id: string, data: Record) => updateUser(id, data) const remove = (id: string) => deleteUser(id) @@ -166,6 +176,34 @@ async function assignRolesAction(row: Record): Promise { const crudRef = ref() +async function lockAction(row: Record): Promise { + let reason: string | undefined + try { + const res = await ElMessageBox.prompt(`锁定用户「${row.username}」`, '锁定用户', { + inputPlaceholder: '锁定原因(可选)', + confirmButtonText: '锁定', + cancelButtonText: '取消', + }) + reason = res.value || undefined + } catch { + return // 用户取消 + } + await lockUser(String(row.id), reason) + ElMessage.success('已锁定') + crudRef.value?.reload() +} + +async function unlockAction(row: Record): Promise { + try { + await ElMessageBox.confirm(`确认解锁用户「${row.username}」?`, '提示', { type: 'warning' }) + } catch { + return + } + await unlockUser(String(row.id)) + ElMessage.success('已解锁') + crudRef.value?.reload() +} + async function batchDelete(rows: Record[]): Promise { await ElMessageBox.confirm(`确认删除选中的 ${rows.length} 个用户?`, '提示', { type: 'warning' }) const result = await batchDeleteUsers(rows.map((r) => String(r.id))) diff --git a/typed-router.d.ts b/typed-router.d.ts index ec91c61..f110b09 100644 --- a/typed-router.d.ts +++ b/typed-router.d.ts @@ -79,6 +79,13 @@ declare module 'vue-router/auto-routes' { Record, | never >, + '/monitor/session/': RouteRecordInfo< + '/monitor/session/', + '/monitor/session', + Record, + Record, + | never + >, '/register': RouteRecordInfo< '/register', '/register', @@ -224,6 +231,12 @@ declare module 'vue-router/auto-routes' { views: | never } + 'src/pages/monitor/session/index.vue': { + routes: + | '/monitor/session/' + views: + | never + } 'src/pages/register.vue': { routes: | '/register'