From f1cb5723b260e63d7319a2284b613a30c132fb60 Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 12:45:53 +0800 Subject: [PATCH] feat: wire all management pages to backend (config/dict/org/resource/role/audit/login-log) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 401 自动登出:http 会话过期回调 + main.ts 注册(清 menu/tabs + 跳登录) - 参数配置/字典/组织/资源/角色/操作日志/登录日志对接真实接口 - 角色管理新增权限分配树(el-tree 勾选资源,/roles/:code/resources) - 操作日志/登录日志只读页(ty-search-form + ty-table + useRequest) - listOrgs 迁到 api/org.ts Co-Authored-By: Claude Opus 4.8 --- components.d.ts | 2 + src/api/audit-log.ts | 34 +++ src/api/config.ts | 54 ++++ src/api/dict.ts | 74 ++++++ src/api/http.ts | 8 + src/api/login-log.ts | 27 ++ src/api/org.ts | 39 +++ src/api/resource.ts | 51 ++++ src/api/role.ts | 62 +++++ src/api/user.ts | 11 - src/main.ts | 17 +- src/pages/login/index.vue | 25 +- src/pages/monitor/audit/index.vue | 115 +++++++++ src/pages/monitor/login-log/index.vue | 109 ++++++++ src/pages/system/config/index.vue | 82 ++---- src/pages/system/dict/index.vue | 240 +++++------------- src/pages/system/org/index.vue | 118 +++------ src/pages/system/resource/index.vue | 149 ++++------- .../system/role/assign-resources-form.vue | 53 ++++ src/pages/system/role/index.vue | 122 +++++++++ src/pages/system/user/index.vue | 2 +- stats.html | 2 +- typed-router.d.ts | 52 ++++ 23 files changed, 995 insertions(+), 453 deletions(-) create mode 100644 src/api/audit-log.ts create mode 100644 src/api/config.ts create mode 100644 src/api/dict.ts create mode 100644 src/api/login-log.ts create mode 100644 src/api/org.ts create mode 100644 src/api/resource.ts create mode 100644 src/api/role.ts create mode 100644 src/pages/monitor/audit/index.vue create mode 100644 src/pages/monitor/login-log/index.vue create mode 100644 src/pages/system/role/assign-resources-form.vue create mode 100644 src/pages/system/role/index.vue diff --git a/components.d.ts b/components.d.ts index 8723fde..4e3b52d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -44,6 +44,7 @@ declare module 'vue' { ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTag: typeof import('element-plus/es')['ElTag'] + ElTree: typeof import('element-plus/es')['ElTree'] ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] Icon: typeof import('./src/components/icon/icon.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] @@ -101,6 +102,7 @@ declare global { const ElTable: typeof import('element-plus/es')['ElTable'] const ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] const ElTag: typeof import('element-plus/es')['ElTag'] + const ElTree: typeof import('element-plus/es')['ElTree'] const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] const Icon: typeof import('./src/components/icon/icon.vue')['default'] const RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/src/api/audit-log.ts b/src/api/audit-log.ts new file mode 100644 index 0000000..a955862 --- /dev/null +++ b/src/api/audit-log.ts @@ -0,0 +1,34 @@ +import { http } from '@/api/http' +import type { PageResult } from '@/types/crud' + +export interface AuditLog { + id: string + tenantCode: string + userId: string | null + username: string | null + module: string | null + action: string | null + method: string | null + path: string | null + status: string | null + ip: string | null + userAgent: string | null + durationMs: number | null + createdAt: string +} + +export interface AuditLogQuery { + page: number + pageSize: number + username?: string + module?: string + status?: string +} + +export function listAuditLogs(query: AuditLogQuery, signal?: AbortSignal): Promise> { + const params: Record = { page: query.page, pageSize: query.pageSize } + if (query.username) params.username = query.username + if (query.module) params.module = query.module + if (query.status) params.status = query.status + return http.get>('/audit-logs', { params, signal }) +} diff --git a/src/api/config.ts b/src/api/config.ts new file mode 100644 index 0000000..ddca32c --- /dev/null +++ b/src/api/config.ts @@ -0,0 +1,54 @@ +import { http } from '@/api/http' + +export interface Config { + id: string + tenantCode: string + key: string + value: string | null + remark: string | null + createdAt: string + updatedAt: string +} + +export interface ConfigListQuery { + page: number + pageSize: number + key?: string + value?: string +} + +export interface ConfigListResult { + list: Config[] + total: number + page: number + pageSize: number +} + +export interface ConfigUpsertData { + key?: string + value?: string + remark?: string +} + +/** 配置分页列表。 */ +export function listConfigs(query: ConfigListQuery, signal?: AbortSignal): Promise { + const params: Record = { page: query.page, pageSize: query.pageSize } + if (query.key) params.key = query.key + if (query.value) params.value = query.value + return http.get('/configs', { params, signal }) +} + +/** 新增配置。 */ +export function createConfig(data: ConfigUpsertData): Promise { + return http.post('/configs', data) +} + +/** 更新配置(key 不可改)。 */ +export function updateConfig(id: string, data: ConfigUpsertData): Promise { + return http.patch(`/configs/${id}`, data) +} + +/** 删除配置(软删)。 */ +export function deleteConfig(id: string): Promise { + return http.delete(`/configs/${id}`) +} diff --git a/src/api/dict.ts b/src/api/dict.ts new file mode 100644 index 0000000..6d392cf --- /dev/null +++ b/src/api/dict.ts @@ -0,0 +1,74 @@ +import { http } from '@/api/http' + +export interface DictType { + id: string + tenantCode: string + code: string + name: string + status: 'enabled' | 'disabled' + remark: string | null + createdAt: string +} + +export interface DictItem { + id: string + tenantCode: string + dictTypeCode: string + label: string + value: string + sort: number + status: 'enabled' | 'disabled' + createdAt: string +} + +export interface PageResult { + list: T[] + total: number + page: number + pageSize: number +} + +// ---- 字典类型 ---- + +export function listDictTypes(page = 1, pageSize = 1000, signal?: AbortSignal): Promise> { + return http.get>('/dict-types', { params: { page, pageSize }, signal }) +} + +export function createDictType(data: { code: string; name: string; remark?: string }): Promise { + return http.post('/dict-types', data) +} + +export function updateDictType(id: string, data: { name?: string; status?: string; remark?: string }): Promise { + return http.patch(`/dict-types/${id}`, data) +} + +export function deleteDictType(id: string): Promise { + return http.delete(`/dict-types/${id}`) +} + +// ---- 字典项 ---- + +/** 某字典类型下的全部字典项(后端不分页,按 sort 升序返回)。 */ +export function listDictItems(dictTypeCode: string, signal?: AbortSignal): Promise { + return http.get('/dict-items', { params: { dictTypeCode }, signal }) +} + +export function createDictItem(data: { + dictTypeCode: string + label: string + value: string + sort?: number +}): Promise { + return http.post('/dict-items', data) +} + +export function updateDictItem( + id: string, + data: { label?: string; value?: string; sort?: number; status?: string }, +): Promise { + return http.patch(`/dict-items/${id}`, data) +} + +export function deleteDictItem(id: string): Promise { + return http.delete(`/dict-items/${id}`) +} diff --git a/src/api/http.ts b/src/api/http.ts index 6d5f3de..b50a4fd 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -19,6 +19,13 @@ export class ApiError extends Error { } } +/** 会话过期回调(token 刷新失败时触发),由应用启动时注册。 */ +let onAuthExpired: (() => void) | null = null + +export function setAuthExpiredHandler(handler: () => void): void { + onAuthExpired = handler +} + interface HttpClientOptions { baseURL?: string timeout?: number @@ -68,6 +75,7 @@ export class HttpClient { return this.instance.request(config) } catch { clearTokens() + onAuthExpired?.() } } const body = error.response?.data as ApiEnvelope | undefined diff --git a/src/api/login-log.ts b/src/api/login-log.ts new file mode 100644 index 0000000..fefb040 --- /dev/null +++ b/src/api/login-log.ts @@ -0,0 +1,27 @@ +import { http } from '@/api/http' +import type { PageResult } from '@/types/crud' + +export interface LoginLog { + id: string + tenantCode: string + username: string | null + ip: string | null + userAgent: string | null + status: string + message: string | null + createdAt: string +} + +export interface LoginLogQuery { + page: number + pageSize: number + username?: string + status?: string +} + +export function listLoginLogs(query: LoginLogQuery, signal?: AbortSignal): Promise> { + const params: Record = { page: query.page, pageSize: query.pageSize } + if (query.username) params.username = query.username + if (query.status) params.status = query.status + return http.get>('/login-logs', { params, signal }) +} diff --git a/src/api/org.ts b/src/api/org.ts new file mode 100644 index 0000000..2aec3e4 --- /dev/null +++ b/src/api/org.ts @@ -0,0 +1,39 @@ +import { http } from '@/api/http' + +export interface Org { + id: string + tenantCode: string + parentCode: string | null + code: string + name: string + leader: string | null + sort: number + status: 'enabled' | 'disabled' + createdAt: string +} + +/** 组织列表(后端按 sort 升序返回普通数组,前端自行组树)。 */ +export function listOrgs(signal?: AbortSignal): Promise { + return http.get('/orgs', { signal }) +} + +export function createOrg(data: { + code: string + name: string + parentCode?: string | null + leader?: string + sort?: number +}): Promise { + return http.post('/orgs', data) +} + +export function updateOrg( + id: string, + data: { name?: string; parentCode?: string | null; leader?: string; sort?: number; status?: string }, +): Promise { + return http.patch(`/orgs/${id}`, data) +} + +export function deleteOrg(id: string): Promise { + return http.delete(`/orgs/${id}`) +} diff --git a/src/api/resource.ts b/src/api/resource.ts new file mode 100644 index 0000000..89b1e74 --- /dev/null +++ b/src/api/resource.ts @@ -0,0 +1,51 @@ +import { http } from '@/api/http' + +export type ResourceType = 'menu' | 'button' | 'api' + +export interface Resource { + id: string + tenantCode: string + parentCode: string | null + type: ResourceType + code: string + name: string + path: string | null + component: string | null + method: string | null + apiPath: string | null + icon: string | null + sort: number + status: 'enabled' | 'disabled' + createdAt: string +} + +export interface ResourceUpsertData { + type?: ResourceType + code?: string + name?: string + parentCode?: string | null + path?: string + component?: string + method?: string + apiPath?: string + icon?: string + sort?: number + status?: string +} + +/** 资源列表(后端按 sort 升序返回普通数组,前端自行组树)。 */ +export function listResources(signal?: AbortSignal): Promise { + return http.get('/resources', { signal }) +} + +export function createResource(data: ResourceUpsertData): Promise { + return http.post('/resources', data) +} + +export function updateResource(id: string, data: ResourceUpsertData): Promise { + return http.patch(`/resources/${id}`, data) +} + +export function deleteResource(id: string): Promise { + return http.delete(`/resources/${id}`) +} diff --git a/src/api/role.ts b/src/api/role.ts new file mode 100644 index 0000000..59f18da --- /dev/null +++ b/src/api/role.ts @@ -0,0 +1,62 @@ +import { http } from '@/api/http' + +export type DataScope = 'all' | 'dept' | 'dept_and_child' | 'self' | 'custom' + +export interface Role { + id: string + tenantCode: string + code: string + name: string + dataScope: DataScope + status: 'enabled' | 'disabled' + remark: string | null + createdAt: string +} + +export interface RoleListQuery { + page: number + pageSize: number + name?: string + code?: string + status?: string +} + +export interface RoleListResult { + list: Role[] + total: number + page: number + pageSize: number +} + +export function listRoles(query: RoleListQuery, signal?: AbortSignal): Promise { + const params: Record = { page: query.page, pageSize: query.pageSize } + if (query.name) params.name = query.name + if (query.code) params.code = query.code + if (query.status) params.status = query.status + return http.get('/roles', { params, signal }) +} + +export function createRole(data: { code: string; name: string; dataScope?: DataScope; remark?: string }): Promise { + return http.post('/roles', data) +} + +export function updateRole( + id: string, + data: { name?: string; dataScope?: DataScope; status?: string; remark?: string }, +): Promise { + return http.patch(`/roles/${id}`, data) +} + +export function deleteRole(id: string): Promise { + return http.delete(`/roles/${id}`) +} + +/** 角色已分配的资源 code 列表(注意:URL 的 :id 是 roleCode 而非 UUID)。 */ +export function listAssignedResources(roleCode: string, signal?: AbortSignal): Promise { + return http.get(`/roles/${roleCode}/resources`, { signal }) +} + +/** 分配资源(注意:URL 的 :id 是 roleCode 而非 UUID)。 */ +export function assignResources(roleCode: string, resourceCodes: string[]): Promise { + return http.put(`/roles/${roleCode}/resources`, { resourceCodes }) +} diff --git a/src/api/user.ts b/src/api/user.ts index 2a1ab68..af2e65a 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -42,12 +42,6 @@ export interface UserUpsertData { status?: UserStatus } -export interface Org { - id: string - code: string - name: string -} - /** 用户分页列表。 */ export function listUsers(query: UserListQuery, signal?: AbortSignal): Promise { const params: Record = { page: query.page, pageSize: query.pageSize } @@ -77,8 +71,3 @@ export function deleteUser(id: string): Promise { export function resetPassword(id: string, password: string): Promise { return http.patch(`/users/${id}/password`, { password }) } - -/** 组织列表(用于组织下拉)。 */ -export function listOrgs(signal?: AbortSignal): Promise { - return http.get('/orgs', { signal }) -} diff --git a/src/main.ts b/src/main.ts index 313bfeb..9471103 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,16 +2,29 @@ import { createApp } from 'vue' import 'element-plus/theme-chalk/dark/css-vars.css' import App from './App.vue' -import { installRouter } from './router' -import { installStore } from '@/stores' +import router, { installRouter } from './router' +import pinia, { installStore } from '@/stores' import { installAssets } from '@/plugins/assets' import { Env } from '@/utils/env.ts' import { installI18n } from '@/i18n' +import { setAuthExpiredHandler } from '@/api/http' +import { useMenuStore } from '@/stores/modules/menu' +import { useTabsStore } from '@/stores/modules/tabs' const app = createApp(App) installRouter(app) installStore(app) + +// 会话过期(401 刷新失败)时:清状态 + 跳登录 +setAuthExpiredHandler(() => { + useMenuStore(pinia).reset() + useTabsStore(pinia).removeAll() + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } +}) + installI18n(app) installAssets() diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue index d2d8f88..dc46ca1 100644 --- a/src/pages/login/index.vue +++ b/src/pages/login/index.vue @@ -112,6 +112,7 @@ import { useAuthStore } from '@/stores/modules/auth' import { useAppStore } from '@/stores/modules/app' import { defaultLoginConfig } from '@/config/login' import { applyTheme } from '@/utils/theme' +import { useRequest } from '@/composables/useRequest' const router = useRouter() const route = useRoute() @@ -134,7 +135,6 @@ if (qUsername) model.username = String(qUsername) const captcha = ref('') const captchaCode = ref('') const remember = ref(true) -const loading = ref(false) const error = ref('') const rootStyle = computed(() => (config.background ? { background: config.background } : {})) @@ -172,6 +172,19 @@ function refreshCaptcha(): void { } refreshCaptcha() +const { loading, run: doLogin } = useRequest( + () => auth.login(config.tenantCode, model.username, model.password), + { + manual: true, + onSuccess: () => { + router.push(config.homePath) + }, + onError: (e) => { + error.value = (e as Error).message + }, + }, +) + async function onSubmit(): Promise { if (loading.value) return error.value = '' @@ -186,15 +199,7 @@ async function onSubmit(): Promise { captcha.value = '' return } - loading.value = true - try { - await auth.login(config.tenantCode, model.username, model.password) - router.push(config.homePath) - } catch (e) { - error.value = (e as Error).message - } finally { - loading.value = false - } + await doLogin() } function onForget(): void { diff --git a/src/pages/monitor/audit/index.vue b/src/pages/monitor/audit/index.vue new file mode 100644 index 0000000..944e8ca --- /dev/null +++ b/src/pages/monitor/audit/index.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/pages/monitor/login-log/index.vue b/src/pages/monitor/login-log/index.vue new file mode 100644 index 0000000..d1761ef --- /dev/null +++ b/src/pages/monitor/login-log/index.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/src/pages/system/config/index.vue b/src/pages/system/config/index.vue index a3a943d..8ede9fe 100644 --- a/src/pages/system/config/index.vue +++ b/src/pages/system/config/index.vue @@ -10,11 +10,13 @@ :remove="remove" id-key="id" name-key="key" + :edit-readonly-keys="['key']" /> diff --git a/src/pages/system/dict/index.vue b/src/pages/system/dict/index.vue index 2b2c3a7..618be9c 100644 --- a/src/pages/system/dict/index.vue +++ b/src/pages/system/dict/index.vue @@ -57,124 +57,49 @@ diff --git a/src/pages/system/role/index.vue b/src/pages/system/role/index.vue new file mode 100644 index 0000000..a144cad --- /dev/null +++ b/src/pages/system/role/index.vue @@ -0,0 +1,122 @@ + + + diff --git a/src/pages/system/user/index.vue b/src/pages/system/user/index.vue index 5fd22a4..9775bc2 100644 --- a/src/pages/system/user/index.vue +++ b/src/pages/system/user/index.vue @@ -36,13 +36,13 @@ import { useRequest } from '@/composables/useRequest' import { createUser, deleteUser, - listOrgs, listUsers, resetPassword, updateUser, type UserListQuery, type UserStatus, } from '@/api/user' +import { listOrgs } from '@/api/org' import ResetPasswordForm from './reset-password-form.vue' const STATUS_MAP = { diff --git a/stats.html b/stats.html index 3771372..d1e3a10 100644 --- a/stats.html +++ b/stats.html @@ -4930,7 +4930,7 @@ var drawChart = (function (exports) {