From cb9868192796ed0d22141086f0af9bb081874290 Mon Sep 17 00:00:00 2001 From: Leo <98382335+gaoziman@users.noreply.github.com> Date: Wed, 8 Oct 2025 02:26:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B8=E5=BF=83=E6=9E=B6=E6=9E=84=EF=BC=9A?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9C=8D=E5=8A=A1=E5=B1=82=E3=80=81=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E5=92=8C=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加API服务层(src/service/) - HTTP客户端配置 - 登录认证API - 系统管理API(用户、角色、菜单、部门、字典、文件等) - 监控API(在线用户、服务器、缓存、Redis等) - 添加路由系统(src/router/) - 路由实例配置 - 路由守卫逻辑 - 静态路由和内置路由 - 添加状态管理(src/store/) - 认证状态(auth) - 路由状态(router) - 应用状态(app) - 标签页状态(tab) - 字典状态(dict) --- src/router/guard.ts | 150 ++++++++++++ src/router/index.ts | 29 +++ src/router/routes.inner.ts | 77 ++++++ src/router/routes.static.ts | 25 ++ src/service/api/auth/index.ts | 84 +++++++ src/service/api/dashboard/index.ts | 84 +++++++ src/service/api/dashboard/types.ts | 91 +++++++ src/service/api/monitor/cache/index.ts | 55 +++++ src/service/api/monitor/cache/types.ts | 33 +++ src/service/api/monitor/job/index.ts | 77 ++++++ src/service/api/monitor/job/types.ts | 102 ++++++++ src/service/api/monitor/online/index.ts | 33 +++ src/service/api/monitor/online/types.ts | 97 ++++++++ src/service/api/monitor/redis/index.ts | 15 ++ src/service/api/monitor/redis/types.ts | 21 ++ src/service/api/monitor/server/index.ts | 19 ++ src/service/api/monitor/server/types.ts | 123 ++++++++++ src/service/api/personal/index.ts | 66 ++++++ src/service/api/system/dict/index.ts | 183 ++++++++++++++ src/service/api/system/dict/types.ts | 145 ++++++++++++ src/service/api/system/file/index.ts | 94 ++++++++ src/service/api/system/file/types.ts | 109 +++++++++ src/service/api/system/loginlog/index.ts | 54 +++++ src/service/api/system/loginlog/types.ts | 42 ++++ src/service/api/system/menu/index.ts | 138 +++++++++++ src/service/api/system/menu/types.ts | 103 ++++++++ src/service/api/system/operlog/index.ts | 61 +++++ src/service/api/system/operlog/types.ts | 71 ++++++ src/service/api/system/picture/index.ts | 94 ++++++++ src/service/api/system/picture/types.ts | 95 ++++++++ src/service/api/system/role/index.ts | 86 +++++++ src/service/api/system/role/types.ts | 54 +++++ src/service/api/system/user/index.ts | 121 ++++++++++ src/service/api/system/user/types.ts | 73 ++++++ src/service/http/alova.ts | 116 +++++++++ src/service/http/config.ts | 36 +++ src/service/http/handle.ts | 145 ++++++++++++ src/service/http/index.ts | 15 ++ src/store/app/index.ts | 137 +++++++++++ src/store/app/theme.json | 24 ++ src/store/auth.ts | 141 +++++++++++ src/store/dict.ts | 102 ++++++++ src/store/index.ts | 15 ++ src/store/router/helper.ts | 288 +++++++++++++++++++++++ src/store/router/index.ts | 147 ++++++++++++ src/store/tab.ts | 116 +++++++++ 46 files changed, 3986 insertions(+) create mode 100644 src/router/guard.ts create mode 100644 src/router/index.ts create mode 100644 src/router/routes.inner.ts create mode 100644 src/router/routes.static.ts create mode 100644 src/service/api/auth/index.ts create mode 100644 src/service/api/dashboard/index.ts create mode 100644 src/service/api/dashboard/types.ts create mode 100644 src/service/api/monitor/cache/index.ts create mode 100644 src/service/api/monitor/cache/types.ts create mode 100644 src/service/api/monitor/job/index.ts create mode 100644 src/service/api/monitor/job/types.ts create mode 100644 src/service/api/monitor/online/index.ts create mode 100644 src/service/api/monitor/online/types.ts create mode 100644 src/service/api/monitor/redis/index.ts create mode 100644 src/service/api/monitor/redis/types.ts create mode 100644 src/service/api/monitor/server/index.ts create mode 100644 src/service/api/monitor/server/types.ts create mode 100644 src/service/api/personal/index.ts create mode 100644 src/service/api/system/dict/index.ts create mode 100644 src/service/api/system/dict/types.ts create mode 100644 src/service/api/system/file/index.ts create mode 100644 src/service/api/system/file/types.ts create mode 100644 src/service/api/system/loginlog/index.ts create mode 100644 src/service/api/system/loginlog/types.ts create mode 100644 src/service/api/system/menu/index.ts create mode 100644 src/service/api/system/menu/types.ts create mode 100644 src/service/api/system/operlog/index.ts create mode 100644 src/service/api/system/operlog/types.ts create mode 100644 src/service/api/system/picture/index.ts create mode 100644 src/service/api/system/picture/types.ts create mode 100644 src/service/api/system/role/index.ts create mode 100644 src/service/api/system/role/types.ts create mode 100644 src/service/api/system/user/index.ts create mode 100644 src/service/api/system/user/types.ts create mode 100644 src/service/http/alova.ts create mode 100644 src/service/http/config.ts create mode 100644 src/service/http/handle.ts create mode 100644 src/service/http/index.ts create mode 100644 src/store/app/index.ts create mode 100644 src/store/app/theme.json create mode 100644 src/store/auth.ts create mode 100644 src/store/dict.ts create mode 100644 src/store/index.ts create mode 100644 src/store/router/helper.ts create mode 100644 src/store/router/index.ts create mode 100644 src/store/tab.ts diff --git a/src/router/guard.ts b/src/router/guard.ts new file mode 100644 index 0000000..d0e4a75 --- /dev/null +++ b/src/router/guard.ts @@ -0,0 +1,150 @@ +import type { Router } from 'vue-router' +import { useAppStore, useRouteStore, useTabStore } from '@/store' +import { local } from '@/utils' +import { handleRouterError } from '@/utils/router-safety' + +const title = import.meta.env.VITE_APP_NAME + +export function setupRouterGuard(router: Router) { + const appStore = useAppStore() + const routeStore = useRouteStore() + const tabStore = useTabStore() + + router.beforeEach(async (to, from, next) => { + try { + // 判断是否是外链,如果是直接打开网页并拦截跳转 + if (to.meta.href) { + window.open(to.meta.href) + next(false) // 取消当前导航 + return + } + // 开始 loadingBar + appStore.showProgress && window.$loadingBar?.start() + + // 判断有无TOKEN,登录鉴权 + const isLogin = Boolean(local.get('accessToken')) + + // 如果是login路由,直接放行 + if (to.name === 'login') { + // login页面不需要任何认证检查,直接放行 + // 继续执行后面的逻辑 + } + // 如果路由明确设置了requiresAuth为false,直接放行 + else if (to.meta.requiresAuth === false) { + // 明确设置为false的路由直接放行 + // 继续执行后面的逻辑 + } + // 如果路由设置了requiresAuth为true,且用户未登录,重定向到登录页 + else if (to.meta.requiresAuth === true && !isLogin) { + const redirect = to.name === '404' ? undefined : to.fullPath + next({ path: '/login', query: { redirect } }) + return + } + + // 判断路由有无进行初始化 + if (!routeStore.isInitAuthRoute) { + // 只有在已登录状态下才初始化路由,避免退出登录时的无效请求 + if (!isLogin) { + // 未登录状态下,如果访问的不是登录页,重定向到登录页 + if (to.name !== 'login') { + const redirect = to.name === '404' ? undefined : to.fullPath + next({ path: '/login', query: { redirect } }) + return + } + } + else { + // 已登录状态下才初始化路由 + try { + await routeStore.initAuthRoute() + } + catch (error) { + console.error('路由初始化失败:', error) + + // 检查是否是网络错误 + const isNetworkError = !navigator.onLine + || (error instanceof Error && ( + error.message.includes('网络') + || error.message.includes('Network') + || error.message.includes('fetch') + || error.message.includes('timeout') + )) + + if (isNetworkError) { + // 网络错误,清除认证信息并重定向到登录页 + local.remove('accessToken') + local.remove('refreshToken') + } + + // 路由初始化失败,重定向到登录页 + next({ path: '/login' }) + return + } + } + + // 动态路由加载完回到根路由 + if (to.name === 'notFound' || to.name === '404') { + // 等待权限路由加载好了,回到之前的路由,否则404 + next({ + path: to.fullPath, + replace: true, + query: to.query, + hash: to.hash, + }) + return + } + } + + // 如果用户已登录且访问login页面,重定向到首页 + if (to.name === 'login' && isLogin) { + next({ path: import.meta.env.VITE_HOME_PATH || '/dashboard' }) + return + } + + next() + } + catch (error) { + console.error('路由守卫执行错误:', error) + // 发生错误时确保loadingBar结束 + appStore.showProgress && window.$loadingBar?.error?.() + next(false) + } + }) + + router.beforeResolve((to) => { + try { + // 设置菜单高亮 + routeStore.setActiveMenu(to.meta.activeMenu ?? to.fullPath) + // 添加tabs + tabStore.addTab(to) + // 设置高亮标签; + tabStore.setCurrentTab(to.fullPath as string) + } + catch (error) { + console.error('路由beforeResolve错误:', error) + } + }) + + router.afterEach((to, from, failure) => { + try { + // 修改网页标题 + document.title = `${to.meta.title} - ${title}` + // 结束 loadingBar + if (failure) { + appStore.showProgress && window.$loadingBar?.error?.() + } + else { + appStore.showProgress && window.$loadingBar?.finish() + } + } + catch (error) { + console.error('路由afterEach错误:', error) + appStore.showProgress && window.$loadingBar?.error?.() + } + }) + + // 添加路由错误处理 + router.onError((error) => { + handleRouterError(error, '全局路由') + appStore.showProgress && window.$loadingBar?.error?.() + }) +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..947c30c --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,29 @@ +import type { App } from 'vue' +import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' +import { setupRouterGuard } from './guard' +import { routes } from './routes.inner' +import { createNavigationGuard } from '@/utils/navigation-guard' + +const { VITE_ROUTE_MODE = 'hash', VITE_BASE_URL } = import.meta.env +export const router = createRouter({ + history: VITE_ROUTE_MODE === 'hash' ? createWebHashHistory(VITE_BASE_URL) : createWebHistory(VITE_BASE_URL), + routes, + // 添加路由配置优化 + scrollBehavior(to, from, savedPosition) { + if (savedPosition) { + return savedPosition + } + return { top: 0 } + }, +}) + +// 创建导航防护实例 +export const navigationGuard = createNavigationGuard(router) + +// 安装vue路由 +export async function installRouter(app: App) { + // 添加路由守卫 + setupRouterGuard(router) + app.use(router) + await router.isReady() // https://router.vuejs.org/zh/api/index.html#isready +} diff --git a/src/router/routes.inner.ts b/src/router/routes.inner.ts new file mode 100644 index 0000000..a18cf4c --- /dev/null +++ b/src/router/routes.inner.ts @@ -0,0 +1,77 @@ +import type { RouteRecordRaw } from 'vue-router' +import { safeAsyncComponent } from '@/utils/component-guard' + +/* 页面中的一些固定路由,错误页等 */ +export const routes: RouteRecordRaw[] = [ + { + path: '/', + name: 'root', + redirect: '/appRoot', + children: [ + ], + }, + { + path: '/login', + name: 'login', + component: safeAsyncComponent( + () => import('@/views/login/index.vue'), + { delay: 0, timeout: 8000 }, + ), + meta: { + title: '登录', + withoutTab: true, + }, + }, + { + path: '/403', + name: '403', + component: safeAsyncComponent( + () => import('@/views/error/403/index.vue'), + { delay: 0, timeout: 5000 }, + ), + meta: { + title: '用户无权限', + withoutTab: true, + }, + }, + { + path: '/404', + name: '404', + component: safeAsyncComponent( + () => import('@/views/error/404/index.vue'), + { delay: 0, timeout: 5000 }, + ), + meta: { + title: '找不到页面', + icon: 'icon-park-outline:ghost', + withoutTab: true, + }, + }, + { + path: '/500', + name: '500', + component: safeAsyncComponent( + () => import('@/views/error/500/index.vue'), + { delay: 0, timeout: 5000 }, + ), + meta: { + title: '服务器错误', + icon: 'icon-park-outline:close-wifi', + withoutTab: true, + }, + }, + { + path: '/:pathMatch(.*)*', + component: safeAsyncComponent( + () => import('@/views/error/404/index.vue'), + { delay: 0, timeout: 5000 }, + ), + name: 'notFound', + meta: { + title: '找不到页面', + icon: 'icon-park-outline:ghost', + withoutTab: true, + }, + }, + +] diff --git a/src/router/routes.static.ts b/src/router/routes.static.ts new file mode 100644 index 0000000..bd8099b --- /dev/null +++ b/src/router/routes.static.ts @@ -0,0 +1,25 @@ +export const staticRoutes: AppRoute.RowRoute[] = [ + { + name: 'dict-data', + path: '/system/dict/data', + title: '字典数据详情', + requiresAuth: true, + icon: 'icon-park-outline:table-file', + menuType: '2', + componentPath: 'system/dict/data', + id: 1001, + pid: null, + hide: true, // 隐藏在菜单中,只用于路由跳转 + }, + // { + // name: 'personal-center', + // path: '/personal-center', + // title: '个人中心', + // requiresAuth: true, + // icon: 'icon-park-outline:id-card-h', + // menuType: '2', + // componentPath: '/personal-center/index', + // id: 4, + // pid: null, + // }, +] diff --git a/src/service/api/auth/index.ts b/src/service/api/auth/index.ts new file mode 100644 index 0000000..0ebfbb4 --- /dev/null +++ b/src/service/api/auth/index.ts @@ -0,0 +1,84 @@ +import { request } from '../../http' + +interface LoginRequest { + loginName: string + password: string + codeKey: string + securityCode: string + rememberMe?: boolean +} + +interface RegisterRequest { + loginName: string + password: string + userName: string + codeKey: string + securityCode: string +} + +interface LoginResponse { + tokenName: string + tokenValue: string +} + +interface CaptchaResponse { + codeKey: string + captchaPicture: string + captchaText?: string +} + +export function fetchLogin(data: LoginRequest) { + const methodInstance = request.Post>('/auth/login', data) + methodInstance.meta = { + authRole: null, + } + return methodInstance +} + +export function fetchLogout() { + return request.Get>('/auth/logout') +} + +export function fetchRegister(data: RegisterRequest) { + const methodInstance = request.Post>('/auth/register', data) + methodInstance.meta = { + authRole: null, + } + return methodInstance +} + +export function fetchUpdateToken(data: any) { + const method = request.Post>('/updateToken', data) + method.meta = { + authRole: 'refreshToken', + } + return method +} + +export function fetchUserRoutesOld(params: { id: number }) { + return request.Get>('/getUserRoutes', { params }) +} + +export function fetchLoginUserInfo() { + return request.Get>('/coder/sysLoginUser/getLoginUserInformation') +} + +// 验证码相关接口 + +// 获取PNG格式验证码 +export function fetchCaptchaPng() { + const methodInstance = request.Get>('/captcha/png') + methodInstance.meta = { + authRole: null, + } + return methodInstance +} + +// 获取GIF格式验证码 +export function fetchCaptchaGif() { + const methodInstance = request.Get>('/captcha/gif') + methodInstance.meta = { + authRole: null, + } + return methodInstance +} diff --git a/src/service/api/dashboard/index.ts b/src/service/api/dashboard/index.ts new file mode 100644 index 0000000..56861b4 --- /dev/null +++ b/src/service/api/dashboard/index.ts @@ -0,0 +1,84 @@ +import { request } from '../../http' +import type { + DailyActivityStatsVo, + DashboardQueryBo, + DashboardStatisticsVo, + LoginStatsVo, + LoginTrendVo, + StorageStatsVo, + UserStatsVo, +} from './types' + +// 重新导出类型供外部使用 +export type { + DailyActivityStatsVo, + DashboardQueryBo, + DashboardStatisticsVo, + LoginStatsVo, + LoginTrendItemVo, + LoginTrendVo, + StorageStatsVo, + UserStatsVo, +} from './types' + +// 仪表盘相关API + +/** + * 获取仪表盘统计数据 + */ +export function getDashboardStatistics() { + return request.Get>('/coder/dashboard/getStatistics') +} + +/** + * 获取登录趋势数据 + * @param days 查询天数,默认7天 + */ +export function getLoginTrend(days: number = 7) { + return request.Get>('/coder/dashboard/getLoginTrend', { + params: { days }, + }) +} + +/** + * 获取完整仪表盘数据 + * @param params 查询参数 + */ +export function getAllDashboardData(params: DashboardQueryBo = {}) { + const { includeTrend = true, trendDays = 7 } = params + return request.Get>('/coder/dashboard/getAllData', { + params: { includeTrend, trendDays }, + }) +} + +/** + * 获取用户统计数据 + */ +export function getUserStats() { + return request.Get>('/coder/dashboard/getUserStats') +} + +/** + * 获取登录统计数据 + * @param params 查询参数 + */ +export function getLoginStats(params: DashboardQueryBo = {}) { + const { includeTrend = false, trendDays = 7 } = params + return request.Get>('/coder/dashboard/getLoginStats', { + params: { includeTrend, trendDays }, + }) +} + +/** + * 获取存储统计数据 + */ +export function getStorageStats() { + return request.Get>('/coder/dashboard/getStorageStats') +} + +/** + * 获取今日活跃统计数据 + */ +export function getDailyActivityStats() { + return request.Get>('/coder/dashboard/getDailyActivityStats') +} diff --git a/src/service/api/dashboard/types.ts b/src/service/api/dashboard/types.ts new file mode 100644 index 0000000..7737a4e --- /dev/null +++ b/src/service/api/dashboard/types.ts @@ -0,0 +1,91 @@ +// 仪表盘API类型定义 + +// 用户统计数据 +export interface UserStatsVo { + /** 总用户数 */ + totalUsers: number + /** 今日新增用户数 */ + todayNewUsers: number + /** 活跃用户数 */ + activeUsers: number + /** 当前在线用户数 */ + onlineUsers: number +} + +// 登录趋势项 +export interface LoginTrendItemVo { + /** 日期(YYYY-MM-DD格式) */ + date: string + /** 当日登录次数 */ + count: number + /** 显示标签(用于图表展示) */ + label: string +} + +// 登录统计数据 +export interface LoginStatsVo { + /** 今日登录次数 */ + todayLogins: number + /** 累计登录次数 */ + totalLogins: number + /** 登录趋势数据 */ + loginTrend?: LoginTrendItemVo[] +} + +// 存储统计数据 +export interface StorageStatsVo { + /** 总文件数 */ + totalFiles: number + /** 总图片数 */ + totalImages: number + /** 总存储大小(格式化) */ + totalSize: string + /** 今日上传文件数 */ + todayUploads: number + /** 存储使用率(百分比) */ + storageUsage: number + /** 可用空间(格式化) */ + availableSpace: string +} + +// 今日活跃统计数据 +export interface DailyActivityStatsVo { + /** 今日访问量 */ + todayVisits: number + /** 今日操作数 */ + todayOperations: number + /** 活跃用户数 */ + activeUsers: number + /** 新增内容数 */ + newContent: number + /** API调用次数 */ + apiCalls: number + /** 平均响应时间(毫秒) */ + avgResponseTime: number +} + +// 仪表盘统计数据响应对象 +export interface DashboardStatisticsVo { + /** 用户统计数据 */ + userStats?: UserStatsVo + /** 登录统计数据 */ + loginStats?: LoginStatsVo + /** 存储统计数据 */ + storageStats?: StorageStatsVo + /** 今日活跃统计数据 */ + dailyActivityStats?: DailyActivityStatsVo +} + +// 登录趋势数据响应对象 +export interface LoginTrendVo { + /** 登录趋势数据列表 */ + loginTrend: LoginTrendItemVo[] +} + +// 查询参数类型 +export interface DashboardQueryBo { + /** 是否包含趋势数据 */ + includeTrend?: boolean + /** 趋势数据天数 */ + trendDays?: number +} diff --git a/src/service/api/monitor/cache/index.ts b/src/service/api/monitor/cache/index.ts new file mode 100644 index 0000000..ed505b8 --- /dev/null +++ b/src/service/api/monitor/cache/index.ts @@ -0,0 +1,55 @@ +import { request } from '@/service/http' +import type { DeleteCacheKeyBo, GetCacheValueBo, SysCacheVo } from './types' + +/** + * 查询Redis缓存所有Key + */ +export function getRedisCache() { + return request.Get>('/coder/monitor/cache/getRedisCache') +} + +/** + * 查询Redis缓存键名列表 + */ +export function getCacheKeys(cacheName: string) { + // 对缓存名称进行URL编码,处理冒号等特殊字符 + const encodedCacheName = encodeURIComponent(cacheName) + return request.Get>(`/coder/monitor/cache/getCacheKeys/${encodedCacheName}`) +} + +/** + * 获取Redis缓存内容 + */ +export function getCacheValue(data: GetCacheValueBo) { + return request.Post>('/coder/monitor/cache/getValue', data) +} + +/** + * 删除Redis指定名称缓存 + */ +export function deleteCacheName(cacheName: string) { + // 对缓存名称进行URL编码,处理冒号等特殊字符 + const encodedCacheName = encodeURIComponent(cacheName) + return request.Post>(`/coder/monitor/cache/deleteCacheName/${encodedCacheName}`) +} + +/** + * 删除Redis指定键名缓存 + */ +export function deleteCacheKey(data: DeleteCacheKeyBo) { + return request.Post>('/coder/monitor/cache/deleteCacheKey', data) +} + +/** + * 删除Redis所有信息 + */ +export function deleteCacheAll() { + return request.Post>('/coder/monitor/cache/deleteCacheAll') +} + +// 重新导出类型供外部使用 +export type { + DeleteCacheKeyBo, + GetCacheValueBo, + SysCacheVo, +} from './types' diff --git a/src/service/api/monitor/cache/types.ts b/src/service/api/monitor/cache/types.ts new file mode 100644 index 0000000..8b2ad61 --- /dev/null +++ b/src/service/api/monitor/cache/types.ts @@ -0,0 +1,33 @@ +/** + * 缓存管理相关类型定义 + */ + +// 缓存信息 +export interface SysCacheVo { + /** 缓存名称 */ + cacheName: string + /** 缓存键名 */ + cacheKey?: string + /** 缓存内容 */ + cacheValue?: string + /** 缓存过期时间 */ + expireTime?: string + /** 备注信息 */ + remark?: string +} + +// 获取缓存内容请求参数 +export interface GetCacheValueBo { + /** 缓存名称 */ + cacheName: string + /** 缓存键名 */ + cacheKey: string +} + +// 删除缓存键请求参数 +export interface DeleteCacheKeyBo { + /** 缓存名称 */ + cacheName?: string + /** 缓存键名 */ + cacheKey: string +} diff --git a/src/service/api/monitor/job/index.ts b/src/service/api/monitor/job/index.ts new file mode 100644 index 0000000..2f60fba --- /dev/null +++ b/src/service/api/monitor/job/index.ts @@ -0,0 +1,77 @@ +import { request } from '@/service/http' +import type { + PageSysJobVo, + SysJobForm, + SysJobQueryBo, + SysJobSearchForm, + SysJobVo, +} from './types' + +// 重新导出类型定义 +export type { PageSysJobVo, SysJobForm, SysJobQueryBo, SysJobSearchForm, SysJobVo } + +/** + * 分页查询定时任务列表 + */ +export function getSysJobListPage(params: SysJobQueryBo) { + return request.Get>('/coder/sysJob/listPage', { params }) +} + +/** + * 查询所有定时任务 + */ +export function getSysJobList(params?: SysJobQueryBo) { + return request.Get>('/coder/sysJob/list', { params }) +} + +/** + * 根据ID查询定时任务详情 + */ +export function getSysJobById(id: string) { + return request.Get>(`/coder/sysJob/getById/${id}`) +} + +/** + * 新增定时任务 + */ +export function addSysJob(data: SysJobForm) { + return request.Post>('/coder/sysJob/add', data) +} + +/** + * 修改定时任务 + */ +export function updateSysJob(data: SysJobForm) { + return request.Post>('/coder/sysJob/update', data) +} + +/** + * 删除定时任务 + */ +export function deleteSysJobById(id: string) { + return request.Post>(`/coder/sysJob/deleteById/${id}`) +} + +/** + * 批量删除定时任务 + */ +export function batchDeleteSysJob(jobIds: string[]) { + return request.Post>('/coder/sysJob/batchDelete', jobIds) +} + +/** + * 修改任务状态 + * @param id 任务ID + * @param jobStatus 任务状态[0正常 1暂停] + * @param policyStatus 计划策略[1-立即执行 2-执行一次 3-放弃执行] + */ +export function updateSysJobStatus(id: string, jobStatus: string, policyStatus: string) { + return request.Post>(`/coder/sysJob/updateStatus/${id}/${jobStatus}/${policyStatus}`) +} + +/** + * 立即执行任务 + */ +export function runSysJobNow(id: string) { + return request.Get>(`/coder/sysJob/runNow/${id}`) +} diff --git a/src/service/api/monitor/job/types.ts b/src/service/api/monitor/job/types.ts new file mode 100644 index 0000000..4e7a396 --- /dev/null +++ b/src/service/api/monitor/job/types.ts @@ -0,0 +1,102 @@ +/** + * 定时任务管理 - 类型定义 + */ + +/** + * 定时任务实体 + */ +export interface SysJobVo { + /** 任务ID */ + jobId: string + /** 任务名称 */ + jobName: string + /** 任务类型[1-管理平台 2-小程序 3-App] */ + jobType: string + /** 类路径 */ + classPath: string + /** 方法名称 */ + methodName: string + /** cron执行表达式 */ + cronExpression: string + /** cron计划策略[1-立即执行 2-执行一次 3-放弃执行] */ + policyStatus: string + /** 任务状态[0正常 1暂停] */ + jobStatus: string + /** 任务参数 */ + jobParams?: string + /** 任务备注 */ + remark?: string + /** 创建者 */ + createBy?: string + /** 创建时间 */ + createTime?: string + /** 更新者 */ + updateBy?: string + /** 更新时间 */ + updateTime?: string +} + +/** + * 定时任务查询参数 + */ +export interface SysJobQueryBo { + /** 页码 */ + pageNo?: number + /** 页大小 */ + pageSize?: number + /** 任务名称 */ + jobName?: string + /** 任务类型 */ + jobType?: string + /** 任务状态 */ + jobStatus?: string +} + +/** + * 定时任务搜索表单 + */ +export interface SysJobSearchForm { + /** 任务名称 */ + jobName?: string + /** 任务类型 */ + jobType?: string + /** 任务状态 */ + jobStatus?: string +} + +/** + * 定时任务表单数据 + */ +export interface SysJobForm { + /** 任务ID */ + jobId?: string + /** 任务名称 */ + jobName: string + /** 任务类型[1-管理平台 2-小程序 3-App] */ + jobType: string + /** 类路径 */ + classPath: string + /** 方法名称 */ + methodName: string + /** cron执行表达式 */ + cronExpression: string + /** cron计划策略[1-立即执行 2-执行一次 3-放弃执行] */ + policyStatus: string + /** 任务状态[0正常 1暂停] */ + jobStatus: string + /** 任务参数 */ + jobParams?: string + /** 任务备注 */ + remark?: string +} + +/** + * 分页结果 + */ +export interface PageSysJobVo { + records: SysJobVo[] + total: number + size: number + current: number + pages: number +} diff --git a/src/service/api/monitor/online/index.ts b/src/service/api/monitor/online/index.ts new file mode 100644 index 0000000..5b43ea2 --- /dev/null +++ b/src/service/api/monitor/online/index.ts @@ -0,0 +1,33 @@ +import { request } from '@/service/http' +import type { + OnlineUserCountVo, + OnlineUserSearchForm, + OnlineUserVo, + PageOnlineUserVo, + SysUserOnlineQueryBo, +} from './types' + +// 重新导出类型定义 +export type { OnlineUserCountVo, OnlineUserSearchForm, OnlineUserVo, PageOnlineUserVo, SysUserOnlineQueryBo } + +/** + * 分页查询在线用户列表 + */ +export function getOnlineUserListPage(params: SysUserOnlineQueryBo) { + return request.Get>('/coder/sysUserOnline/listPage', { params }) +} + +/** + * 强制注销 + * @param userId 用户ID + */ +export function logoutUser(userId: string) { + return request.Get>(`/coder/sysUserOnline/logout/${userId}`) +} + +/** + * 获取在线用户统计信息 + */ +export function getOnlineUserCount() { + return request.Get>('/coder/sysUserOnline/count') +} diff --git a/src/service/api/monitor/online/types.ts b/src/service/api/monitor/online/types.ts new file mode 100644 index 0000000..cf6ae08 --- /dev/null +++ b/src/service/api/monitor/online/types.ts @@ -0,0 +1,97 @@ +/** + * 在线用户监控 - 类型定义 + */ + +/** + * 在线用户实体 + */ +export interface OnlineUserVo { + /** 用户ID */ + userId: string + /** 登录名称 */ + loginName: string + /** 用户名 */ + userName: string + /** 用户头像 */ + avatar?: string + /** 性别[1-男 2-女 3-未知] */ + sex?: string + /** 手机号 */ + phone?: string + /** 邮箱 */ + email?: string + /** 用户类型[1-系统用户 2-注册用户 3-微信用户] */ + userType?: string + /** 城市ID[可新增城市表-暂时无用] */ + cityId?: string + /** 登录时间 */ + loginTime?: string + /** 创建时间 */ + createTime?: string + /** 登录IP */ + loginIp?: string + /** 登录地址 */ + loginAddress?: string + /** 浏览器类型 */ + browser?: string + /** 操作系统 */ + os?: string + /** 设备名字 */ + deviceName?: string + /** 是否超级管理员 */ + isCoderAdmin?: boolean +} + +/** + * 在线用户查询参数 + */ +export interface SysUserOnlineQueryBo { + /** 页码 */ + pageNo?: number + /** 页大小 */ + pageSize?: number + /** 登录名称 */ + loginName?: string + /** 用户名字 */ + userName?: string + /** IP地址 */ + loginIp?: string +} + +/** + * 在线用户搜索表单 + */ +export interface OnlineUserSearchForm { + /** 登录名称 */ + loginName?: string + /** 用户名字 */ + userName?: string + /** IP地址 */ + loginIp?: string +} + +/** + * 分页在线用户响应 + */ +export interface PageOnlineUserVo { + /** 总记录数 */ + total: number + /** 当前页 */ + current: number + /** 每页大小 */ + size: number + /** 总页数 */ + pages: number + /** 数据列表 */ + records: OnlineUserVo[] +} + +/** + * 在线用户统计响应 + */ +export interface OnlineUserCountVo { + /** 在线用户总数 */ + onlineCount: number + /** 统计时间戳 */ + timestamp: number +} diff --git a/src/service/api/monitor/redis/index.ts b/src/service/api/monitor/redis/index.ts new file mode 100644 index 0000000..da69ed9 --- /dev/null +++ b/src/service/api/monitor/redis/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/service/http' +import type { RedisInfoVo } from './types' + +/** + * 获取Redis监控信息 + */ +export function getRedisInformation() { + return request.Get>('/coder/monitor/redis/getRedisInformation') +} + +// 重新导出类型供外部使用 +export type { + RedisCommandStatVo, + RedisInfoVo, +} from './types' diff --git a/src/service/api/monitor/redis/types.ts b/src/service/api/monitor/redis/types.ts new file mode 100644 index 0000000..59a1156 --- /dev/null +++ b/src/service/api/monitor/redis/types.ts @@ -0,0 +1,21 @@ +/** + * Redis监控相关类型定义 + */ + +// Redis命令统计项 +export interface RedisCommandStatVo { + /** 命令名称 */ + name: string + /** 调用次数 */ + value: string +} + +// Redis监控信息 +export interface RedisInfoVo { + /** Redis基本信息 */ + info: Record + /** 数据库大小 */ + dbSize: number + /** 命令统计 */ + commandStats: RedisCommandStatVo[] +} diff --git a/src/service/api/monitor/server/index.ts b/src/service/api/monitor/server/index.ts new file mode 100644 index 0000000..40629c8 --- /dev/null +++ b/src/service/api/monitor/server/index.ts @@ -0,0 +1,19 @@ +import { request } from '@/service/http' +import type { ServerVo } from './types' + +/** + * 获取服务器监控信息 + */ +export function getServerInformation() { + return request.Get>('/coder/monitor/server/getServerInformation') +} + +// 重新导出类型供外部使用 +export type { + CpuVo, + JvmVo, + MemVo, + ServerVo, + SysFileVo, + SysVo, +} from './types' diff --git a/src/service/api/monitor/server/types.ts b/src/service/api/monitor/server/types.ts new file mode 100644 index 0000000..93cf904 --- /dev/null +++ b/src/service/api/monitor/server/types.ts @@ -0,0 +1,123 @@ +/** + * 服务器监控相关类型定义 + */ + +// CPU信息 +export interface CpuVo { + /** 核心数 */ + cpuNum: number + /** CPU总的使用率 */ + total: number + /** CPU系统使用率 */ + sys: number + /** CPU用户使用率 */ + used: number + /** CPU当前等待率 */ + wait: number + /** CPU当前空闲率 */ + free: number + /** CPU使用率百分比 */ + cpuUsage: number + /** CPU系统使用率百分比 */ + sysUsage: number + /** CPU用户使用率百分比 */ + userUsage: number + /** CPU等待率百分比 */ + waitUsage: number + /** CPU空闲率百分比 */ + freeUsage: number +} + +// 内存信息 +export interface MemVo { + /** 内存总量 */ + total: number + /** 已用内存 */ + used: number + /** 剩余内存 */ + free: number + /** 内存使用率 */ + usage: number + /** 总内存(格式化) */ + totalStr: string + /** 已用内存(格式化) */ + usedStr: string + /** 剩余内存(格式化) */ + freeStr: string +} + +// JVM信息 +export interface JvmVo { + /** 当前JVM占用的内存总数(M) */ + total: number + /** JVM最大可用内存总数(M) */ + max: number + /** JVM空闲内存(M) */ + free: number + /** JDK版本 */ + version: string + /** JDK路径 */ + home: string + /** JVM已用内存 */ + used: number + /** JVM内存使用率 */ + usage: number + /** 总内存(格式化) */ + totalStr: string + /** 已用内存(格式化) */ + usedStr: string + /** 剩余内存(格式化) */ + freeStr: string + /** 最大内存(格式化) */ + maxStr: string + /** JVM启动时间 */ + startTime: string + /** JVM运行时间 */ + runTime: string +} + +// 系统信息 +export interface SysVo { + /** 服务器名称 */ + computerName: string + /** 服务器IP */ + computerIp: string + /** 项目路径 */ + userDir: string + /** 操作系统 */ + osName: string + /** 系统架构 */ + osArch: string +} + +// 磁盘文件信息 +export interface SysFileVo { + /** 盘符路径 */ + dirName: string + /** 盘符类型 */ + sysTypeName: string + /** 文件类型 */ + typeName: string + /** 总大小 */ + total: string + /** 剩余大小 */ + free: string + /** 已经使用量 */ + used: string + /** 资源的使用率 */ + usage: number +} + +// 服务器信息 +export interface ServerVo { + /** CPU相关信息 */ + cpu: CpuVo + /** 内存相关信息 */ + mem: MemVo + /** JVM相关信息 */ + jvm: JvmVo + /** 服务器相关信息 */ + sys: SysVo + /** 磁盘相关信息 */ + sysFiles: SysFileVo[] +} diff --git a/src/service/api/personal/index.ts b/src/service/api/personal/index.ts new file mode 100644 index 0000000..67d9ccb --- /dev/null +++ b/src/service/api/personal/index.ts @@ -0,0 +1,66 @@ +import { request } from '../../http' + +// 个人资料数据类型 +export interface PersonalDataVo { + userId: number + userName: string + loginName: string + email?: string + phone?: string + sex?: string + avatar?: string + userStatus?: string + createTime?: string + roleNames?: string[] // 角色名称数组 + roleName?: string // 主要角色名称 +} + +// 修改个人资料请求类型 +export interface UpdatePersonalBo { + userName?: string + email?: string + phone?: string + sex?: string + avatar?: string +} + +// 修改密码请求类型 +export interface UpdatePasswordBo { + password: string + newPassword: string + confirmPassword: string +} + +/** + * 获取个人资料 + */ +export function getPersonalData() { + return request.Get>('/coder/sysLoginUser/getPersonalData') +} + +/** + * 修改个人基本资料 + */ +export function updateBasicData(data: UpdatePersonalBo) { + return request.Post>('/coder/sysLoginUser/updateBasicData', data) +} + +/** + * 修改登录密码 + */ +export function updatePassword(data: UpdatePasswordBo) { + return request.Post>('/coder/sysLoginUser/updateUserPwd', data) +} + +/** + * 上传头像文件 + * @param file 文件 + * @param fileSize 文件大小限制(MB) + */ +export function uploadAvatar(file: File, fileSize: number = 5) { + const formData = new FormData() + formData.append('file', file) + // 注意:不要手动设置 Content-Type,让浏览器自动设置以包含正确的 boundary + // 使用数字标识符 "1" 代表头像类型,避免数据库字段长度限制 + return request.Post>(`/coder/file/uploadFile/${fileSize}/pictures/1`, formData) +} diff --git a/src/service/api/system/dict/index.ts b/src/service/api/system/dict/index.ts new file mode 100644 index 0000000..bfa4c00 --- /dev/null +++ b/src/service/api/system/dict/index.ts @@ -0,0 +1,183 @@ +/** + * 字典管理 API 接口 + */ + +import { request } from '@/service/http' +import type { + DictDataForm, + DictDataOption, + DictDataQueryBo, + DictDataVo, + DictTypeForm, + DictTypeOption, + DictTypeQueryBo, + DictTypeVo, + PageDictDataVo, + PageDictTypeVo, +} from './types' + +// 重新导出类型供外部使用 +export type { + DictDataForm, + DictDataOption, + DictDataQueryBo, + DictDataSearchForm, + DictDataVo, + DictTypeForm, + DictTypeOption, + DictTypeQueryBo, + DictTypeSearchForm, + DictTypeVo, + PageDictDataVo, + PageDictTypeVo, +} from './types' + +export { DictStatus, DictTag } from './types' + +/** ======================= 字典类型管理 API ======================= */ + +/** + * 分页查询字典类型列表 + */ +export function getDictTypeList(params: DictTypeQueryBo) { + return request.Get>('/coder/sysDictType/listPage', { + params, + }) +} + +/** + * 查询所有字典类型 + */ +export function getAllDictTypes() { + return request.Get>('/coder/sysDictType/list') +} + +/** + * 根据ID查询字典类型详情 + */ +export function getDictTypeById(id: string) { + return request.Get>(`/coder/sysDictType/getById/${id}`) +} + +/** + * 新增字典类型 + */ +export function addDictType(data: DictTypeForm) { + return request.Post>('/coder/sysDictType/add', data) +} + +/** + * 修改字典类型 + */ +export function updateDictType(data: DictTypeForm) { + return request.Post>('/coder/sysDictType/update', data) +} + +/** + * 删除字典类型 + */ +export function deleteDictType(id: string) { + return request.Post>(`/coder/sysDictType/deleteById/${id}`) +} + +/** + * 批量删除字典类型 + */ +export function batchDeleteDictType(ids: string[]) { + return request.Post>('/coder/sysDictType/batchDelete', ids) +} + +/** + * 修改字典类型状态 + */ +export function updateDictTypeStatus(dictId: string, dictStatus: string) { + return request.Post>(`/coder/sysDictType/updateStatus/${dictId}/${dictStatus}`) +} + +/** + * 查询字典类型下拉框选项 + */ +export function getDictTypeOptions() { + return request.Get>('/coder/sysDictType/listDictType') +} + +/** ======================= 字典数据管理 API ======================= */ + +/** + * 分页查询字典数据列表 + */ +export function getDictDataList(params: DictDataQueryBo) { + return request.Get>('/coder/sysDictData/listPage', { + params, + }) +} + +/** + * 查询所有字典数据 + */ +export function getAllDictData() { + return request.Get>('/coder/sysDictData/list') +} + +/** + * 根据ID查询字典数据详情 + */ +export function getDictDataById(id: string) { + return request.Get>(`/coder/sysDictData/getById/${id}`) +} + +/** + * 新增字典数据 + */ +export function addDictData(data: DictDataForm) { + return request.Post>('/coder/sysDictData/add', data) +} + +/** + * 获取最新排序号 + */ +export function getDictDataSorted(dictType: string) { + return request.Get>(`/coder/sysDictData/getSorted/${dictType}`) +} + +/** + * 修改字典数据 + */ +export function updateDictData(data: DictDataForm) { + return request.Post>('/coder/sysDictData/update', data) +} + +/** + * 删除字典数据 + */ +export function deleteDictData(id: string) { + return request.Post>(`/coder/sysDictData/deleteById/${id}`) +} + +/** + * 批量删除字典数据 + */ +export function batchDeleteDictData(ids: string[]) { + return request.Post>('/coder/sysDictData/batchDelete', ids) +} + +/** + * 修改字典数据状态 + */ +export function updateDictDataStatus(dictId: string, dictStatus: string) { + return request.Post>(`/coder/sysDictData/updateStatus/${dictId}/${dictStatus}`) +} + +/** + * 根据类型查询字典数据 + */ +export function getDictDataByType(dictType: string) { + return request.Get>(`/coder/sysDictData/listDataByType/${dictType}`) +} + +/** + * 同步字典缓存到Redis + */ +export function syncDictCache() { + return request.Get>('/coder/sysDictData/listDictCacheRedis') +} diff --git a/src/service/api/system/dict/types.ts b/src/service/api/system/dict/types.ts new file mode 100644 index 0000000..74d3477 --- /dev/null +++ b/src/service/api/system/dict/types.ts @@ -0,0 +1,145 @@ +/** + * 字典管理相关的类型定义 + */ + +/** 字典类型相关类型定义 */ + +// 字典类型实体 +export interface DictTypeVo { + dictId: string + dictType: string + dictName: string + dictStatus: string + createBy?: string + createTime?: string + updateBy?: string + updateTime?: string + remark?: string +} + +// 字典类型查询参数 +export interface DictTypeQueryBo { + pageNo?: number + pageSize?: number + dictName?: string + dictType?: string + dictStatus?: string +} + +// 字典类型搜索表单 +export interface DictTypeSearchForm { + dictName?: string + dictType?: string + dictStatus?: string +} + +// 字典类型表单 +export interface DictTypeForm { + dictId?: string + dictType: string + dictName: string + dictStatus: string + remark?: string +} + +/** 字典数据相关类型定义 */ + +// 字典数据实体 +export interface DictDataVo { + dictId: string + dictLabel: string + dictValue: string + dictType: string + dictStatus: string + dictTag: string + dictColor?: string + sorted: number + createBy?: string + createTime?: string + updateBy?: string + updateTime?: string + remark?: string +} + +// 字典数据查询参数 +export interface DictDataQueryBo { + pageNo?: number + pageSize?: number + dictType?: string + dictLabel?: string + dictStatus?: string +} + +// 字典数据搜索表单 +export interface DictDataSearchForm { + dictType?: string + dictLabel?: string + dictStatus?: string +} + +// 字典数据表单 +export interface DictDataForm { + dictId?: string + dictLabel: string + dictValue: string + dictType: string + dictStatus: string + dictTag: string + dictColor?: string + sorted: number + remark?: string +} + +/** 分页结果类型 */ + +// 字典类型分页结果 +export interface PageDictTypeVo { + records: DictTypeVo[] + total: number + size: number + current: number + pages: number +} + +// 字典数据分页结果 +export interface PageDictDataVo { + records: DictDataVo[] + total: number + size: number + current: number + pages: number +} + +/** 选项类型 */ + +// 字典类型下拉选项 +export interface DictTypeOption { + dictType: string + dictName: string +} + +// 字典数据选项 +export interface DictDataOption { + dictLabel: string + dictValue: string + dictType: string + dictTag: string + dictColor?: string +} + +/** 状态常量 */ + +// 字典状态枚举 +export const DictStatus = { + NORMAL: '0', // 正常 + DISABLED: '1', // 停用 +} as const + +// 字典标签类型枚举 +export const DictTag = { + PRIMARY: 'primary', + SUCCESS: 'success', + INFO: 'info', + WARNING: 'warning', + ERROR: 'error', +} as const diff --git a/src/service/api/system/file/index.ts b/src/service/api/system/file/index.ts new file mode 100644 index 0000000..e8b0298 --- /dev/null +++ b/src/service/api/system/file/index.ts @@ -0,0 +1,94 @@ +import { request } from '../../../http' +import type { + FileUploadResult, + PageSysFileVo, + SysFileQueryBo, + SysFileVo, +} from './types' + +// 重新导出类型供外部使用 +export type { + FileServiceType, + FileTypeEnum, + FileUploadResult, + PageSysFileVo, + SysFileQueryBo, + SysFileSearchForm, + SysFileVo, +} from './types' + +// 重新导出常量值供外部使用 +export { + FILE_SERVICE_OPTIONS, + FILE_TYPE_OPTIONS, +} from './types' + +// 文件管理相关API + +// 分页查询文件列表 +export function getSysFileList(params: SysFileQueryBo) { + return request.Get>('/coder/sysFile/listPage', { params }) +} + +// 查询所有文件(不分页) +export function getAllSysFiles(params?: Omit) { + return request.Get>('/coder/sysFile/list', { params }) +} + +// 根据ID查询文件 +export function getSysFileById(id: number) { + return request.Get>(`/coder/sysFile/getById/${id}`) +} + +// 新增文件记录 +export function addSysFile(data: SysFileVo) { + return request.Post>('/coder/sysFile/add', data) +} + +// 修改文件信息 +export function updateSysFile(data: SysFileVo) { + return request.Post>('/coder/sysFile/update', data) +} + +// 删除文件 +export function deleteSysFile(id: number) { + return request.Post>(`/coder/sysFile/deleteById/${id}`) +} + +// 批量删除文件 +export function batchDeleteSysFiles(ids: number[]) { + return request.Post>('/coder/sysFile/batchDelete', ids) +} + +// 文件上传相关API + +// 上传文件 +export function uploadFile(file: File, folderName: string, fileSize = 2, fileParam = '-1', storageType?: string) { + const formData = new FormData() + formData.append('file', file) + + // 如果指定了存储类型,添加到表单数据中 + if (storageType) { + formData.append('storageType', storageType) + } + + return request.Post(`/coder/file/uploadFile/${fileSize}/${folderName}/${fileParam}`, formData) +} + +// 匿名上传文件(无需登录) +export function uploadAnyFile(file: File, folderName: string, fileSize = 2, fileParam = '-1', storageType?: string) { + const formData = new FormData() + formData.append('file', file) + + // 如果指定了存储类型,添加到表单数据中 + if (storageType) { + formData.append('storageType', storageType) + } + + return request.Post(`/coder/file/uploadAnyFile/${fileSize}/${folderName}/${fileParam}`, formData) +} + +// 兼容性导出 - 保持原有函数名以确保向后兼容 +export const fetchSysFilePage = getSysFileList +export const fetchAllSysFiles = getAllSysFiles +export const fetchSysFileById = getSysFileById diff --git a/src/service/api/system/file/types.ts b/src/service/api/system/file/types.ts new file mode 100644 index 0000000..0fee1b7 --- /dev/null +++ b/src/service/api/system/file/types.ts @@ -0,0 +1,109 @@ +/** + * 文件管理相关类型定义 + */ + +// 文件查询条件类型 +export interface SysFileQueryBo { + pageNo?: number + pageSize?: number + fileName?: string + fileSuffix?: string + fileService?: string + fileType?: string +} + +// 文件信息类型 +export interface SysFileVo { + fileId?: number + fileName: string + newName?: string + fileType: string + fileSize?: string + fileSuffix: string + fileUpload?: string + filePath?: string + fileService: string + createTime?: string + createBy?: string + updateTime?: string + updateBy?: string +} + +// 文件搜索表单类型 +export interface SysFileSearchForm { + fileName?: string + fileSuffix?: string + fileService?: string + fileType?: string +} + +// 分页结果类型 +export interface PageSysFileVo { + records: SysFileVo[] + total: number + size: number + current: number + pages: number +} + +// 文件上传结果类型 +export interface FileUploadResult { + fileName: string + newName: string + fileSize: string + suffixName: string + filePath: string + fileUploadPath: string +} + +// 文件服务类型枚举 +export enum FileServiceType { + LOCAL = 'LOCAL', // 本地存储 + MINIO = 'MINIO', // MinIO对象存储 + OSS = 'OSS', // 阿里云对象存储 +} + +// 文件服务类型映射(用于数据库存储) +export const FileServiceTypeMapping = { + LOCAL: '1', + MINIO: '2', + OSS: '3', +} as const + +// 文件类型枚举 +export enum FileTypeEnum { + ALL = '0', // 全部 + IMAGE = '1', // 图片 + DOCUMENT = '2', // 文档 + AUDIO = '3', // 音频 + VIDEO = '4', // 视频 + ARCHIVE = '5', // 压缩包 + APPLICATION = '6', // 应用程序 + OTHER = '9', // 其他 +} + +// 文件类型选项 +export const FILE_TYPE_OPTIONS = [ + { label: '全部', value: FileTypeEnum.ALL }, + { label: '图片', value: FileTypeEnum.IMAGE }, + { label: '文档', value: FileTypeEnum.DOCUMENT }, + { label: '音频', value: FileTypeEnum.AUDIO }, + { label: '视频', value: FileTypeEnum.VIDEO }, + { label: '压缩包', value: FileTypeEnum.ARCHIVE }, + { label: '应用程序', value: FileTypeEnum.APPLICATION }, + { label: '其他', value: FileTypeEnum.OTHER }, +] + +// 文件服务类型选项 +export const FILE_SERVICE_OPTIONS = [ + { label: '本地存储', value: FileServiceType.LOCAL }, + { label: 'MinIO存储', value: FileServiceType.MINIO }, + { label: '阿里云OSS', value: FileServiceType.OSS }, +] + +// 文件服务类型数据库值选项(用于搜索) +export const FILE_SERVICE_DB_OPTIONS = [ + { label: '本地存储', value: '1' }, + { label: 'MinIO存储', value: '2' }, + { label: '阿里云OSS', value: '3' }, +] diff --git a/src/service/api/system/loginlog/index.ts b/src/service/api/system/loginlog/index.ts new file mode 100644 index 0000000..337034a --- /dev/null +++ b/src/service/api/system/loginlog/index.ts @@ -0,0 +1,54 @@ +import { request } from '../../../http' +import type { LoginLogForm, LoginLogQueryBo, LoginLogVo } from './types' + +// 重新导出类型供外部使用 +export type { LoginLogForm, LoginLogQueryBo, LoginLogVo } from './types' + +/** + * 分页查询登录日志列表 + */ +export function getLoginLogListPage(params: LoginLogQueryBo) { + return request.Get>>('/coder/sysLoginLog/listPage', { params }) +} + +/** + * 查询登录日志列表 + */ +export function getLoginLogList() { + return request.Get>('/coder/sysLoginLog/list') +} + +/** + * 根据ID查询登录日志详情 + */ +export function getLoginLogById(id: number) { + return request.Get>(`/coder/sysLoginLog/getById/${id}`) +} + +/** + * 新增登录日志 + */ +export function addLoginLog(data: LoginLogForm) { + return request.Post>('/coder/sysLoginLog/add', data) +} + +/** + * 修改登录日志 + */ +export function updateLoginLog(data: LoginLogForm) { + return request.Post>('/coder/sysLoginLog/update', data) +} + +/** + * 删除登录日志 + */ +export function deleteLoginLog(id: number) { + return request.Post>(`/coder/sysLoginLog/deleteById/${id}`) +} + +/** + * 批量删除登录日志 + */ +export function batchDeleteLoginLog(ids: number[]) { + return request.Post>('/coder/sysLoginLog/batchDelete', ids) +} diff --git a/src/service/api/system/loginlog/types.ts b/src/service/api/system/loginlog/types.ts new file mode 100644 index 0000000..bbece85 --- /dev/null +++ b/src/service/api/system/loginlog/types.ts @@ -0,0 +1,42 @@ +/** + * 登录日志模块类型定义 + */ + +// 登录日志查询参数 +export interface LoginLogQueryBo { + pageNo?: number + pageSize?: number + loginName?: string + loginIp?: string + loginStatus?: string + deviceName?: string + beginTime?: string + endTime?: string +} + +// 登录日志响应数据 +export interface LoginLogVo { + loginLogId: number + loginName: string + deviceName?: string + loginIp: string + loginAddress?: string + browser?: string + os?: string + loginStatus: string + message?: string + loginTime: string +} + +// 登录日志表单数据 +export interface LoginLogForm { + loginLogId?: number + loginName: string + deviceName?: string + loginIp: string + loginLocation?: string + loginBrowser?: string + loginOs?: string + loginStatus: string + loginMsg?: string +} diff --git a/src/service/api/system/menu/index.ts b/src/service/api/system/menu/index.ts new file mode 100644 index 0000000..a55974e --- /dev/null +++ b/src/service/api/system/menu/index.ts @@ -0,0 +1,138 @@ +import { request } from '../../../http' +import type { + MenuCascaderBo, + MenuForm, + MenuNormalResponse, + MenuQueryBo, + MenuRouterBo, + MenuVo, +} from './types' + +// 重新导出类型供外部使用 +export type { + MenuCascaderBo, + MenuForm, + MenuNormalResponse, + MenuQueryBo, + MenuRouterBo, + MenuVo, + RoleMenuPermissionBo, +} from './types' + +// 兼容性类型别名 +export interface MenuPermissionData extends MenuNormalResponse {} + +// 菜单管理基础API + +/** + * 分页查询菜单列表 + */ +export function getMenuListPage(params: MenuQueryBo) { + return request.Get>>('/coder/sysMenu/listPage', { params }) +} + +/** + * 查询菜单列表 + */ +export function getMenuList(params?: MenuQueryBo) { + return request.Get>('/coder/sysMenu/list', { params }) +} + +/** + * 根据ID查询菜单详情 + */ +export function getMenuById(id: string) { + return request.Get>(`/coder/sysMenu/getById/${id}`) +} + +/** + * 新增菜单 + */ +export function addMenu(data: MenuForm) { + return request.Post>('/coder/sysMenu/add', data) +} + +/** + * 修改菜单 + */ +export function updateMenu(data: MenuForm) { + return request.Post>('/coder/sysMenu/update', data) +} + +/** + * 删除菜单 + */ +export function deleteMenu(id: string) { + return request.Post>(`/coder/sysMenu/deleteById/${id}`) +} + +/** + * 批量删除菜单 + */ +export function batchDeleteMenu(ids: string[]) { + return request.Post>('/coder/sysMenu/batchDelete', ids) +} + +/** + * 修改菜单状态 + */ +export function updateMenuStatus(id: string, menuStatus: string) { + return request.Post>(`/coder/sysMenu/updateStatus/${id}/${menuStatus}`) +} + +/** + * 修改菜单展开状态 + */ +export function updateMenuSpread(id: string, isSpread: string) { + return request.Post>(`/coder/sysMenu/updateSpread/${id}/${isSpread}`) +} + +/** + * 获取菜单级联下拉框数据 + */ +export function getMenuCascaderList() { + return request.Get>('/coder/sysMenu/cascaderList') +} + +// 菜单路由管理相关API + +/** + * 获取用户动态路由信息 + */ +export function getUserRoutes() { + return request.Get>('/coder/sysMenu/listRouters') +} + +/** + * 查询所有正常的菜单和展开节点(角色分配菜单权限使用) + */ +export function getMenuPermissionData() { + return request.Get>('/coder/sysMenu/listMenuNormal') +} + +/** + * 根据角色ID查询菜单权限ID列表 + */ +export function getMenuIdsByRoleId(roleId: string) { + return request.Get>(`/coder/sysMenu/listMenuIdsByRoleId/${roleId}`) +} + +/** + * 保存角色和菜单权限之间的关系 + */ +export function saveRoleMenuPermission(roleId: string, menuIds: string[]) { + // 空数组时传递 ["-1"] 表示取消所有权限 + // 保持字符串格式避免精度丢失 + const menuIdsStr = menuIds.length > 0 ? menuIds : ['-1'] + + return request.Post>('/coder/sysMenu/saveRoleMenu', { + roleId, + menuIds: menuIdsStr, + }) +} + +// 兼容性导出 - 保持原有函数名以确保向后兼容 +export const fetchUserRoutes = getUserRoutes +export const fetchMenuPermissionData = getMenuPermissionData +export const fetchMenuIdsByRoleId = getMenuIdsByRoleId +export const saveRoleMenu = saveRoleMenuPermission diff --git a/src/service/api/system/menu/types.ts b/src/service/api/system/menu/types.ts new file mode 100644 index 0000000..0aa5b68 --- /dev/null +++ b/src/service/api/system/menu/types.ts @@ -0,0 +1,103 @@ +/** + * 菜单管理模块类型定义 + */ + +// 菜单查询参数 +export interface MenuQueryBo { + pageNo?: number + pageSize?: number + menuName?: string + menuStatus?: string + auth?: string +} + +// 菜单响应数据 +export interface MenuVo { + menuId: string // 改为字符串避免大整数精度丢失 + menuName: string + enName?: string + parentId: string // 改为字符串避免大整数精度丢失 + menuType: string + path?: string + name?: string + component?: string + icon?: string + auth?: string + menuStatus: string + activeMenu?: string + isHide: string + isLink?: string + isKeepAlive: string + isFull?: string + isAffix: string + isSpread: string + sorted: number + createBy?: string + createTime?: string + updateBy?: string + updateTime?: string + remark?: string + children?: MenuVo[] +} + +// 菜单表单数据 +export interface MenuForm { + menuId?: string // 改为字符串避免大整数精度丢失 + menuName: string + enName?: string + parentId: string // 改为字符串避免大整数精度丢失 + menuType: string + path?: string + name?: string + component?: string + icon?: string + auth?: string + menuStatus: string + activeMenu?: string + isHide: string + isLink?: string + isKeepAlive: string + isFull?: string + isAffix: string + isSpread: string + sorted: number + remark?: string +} + +// 菜单路由数据 +export interface MenuRouterBo { + menuId: string // 改为字符串避免大整数精度丢失 + menuName: string + path: string + component?: string + redirect?: string + meta?: { + title: string + icon?: string + auth?: string + roles?: string[] + keepAlive?: boolean + affix?: boolean + hide?: boolean + } + children?: MenuRouterBo[] +} + +// 级联选择器数据 +export interface MenuCascaderBo { + value: string // 改为字符串避免大整数精度丢失 + label: string + children?: MenuCascaderBo[] +} + +// 菜单正常数据返回结构 +export interface MenuNormalResponse { + menuList: MenuVo[] + spreadList: string[] // 改为字符串数组避免大整数精度丢失 +} + +// 角色菜单权限分配请求参数 +export interface RoleMenuPermissionBo { + roleId: string // 字符串格式避免精度丢失 + menuIds: string[] // 字符串格式避免精度丢失 +} diff --git a/src/service/api/system/operlog/index.ts b/src/service/api/system/operlog/index.ts new file mode 100644 index 0000000..394d287 --- /dev/null +++ b/src/service/api/system/operlog/index.ts @@ -0,0 +1,61 @@ +import { request } from '../../../http' +import type { OperLogQueryBo, OperLogVo } from './types' + +// 重新导出类型供外部使用 +export type { OperLogForm, OperLogQueryBo, OperLogSearchForm, OperLogVo } from './types' + +/** + * 分页查询操作日志列表 + */ +export function getOperLogListPage(params: OperLogQueryBo) { + return request.Get>>('/coder/sysOperLog/listPage', { params }) +} + +/** + * 根据ID查询操作日志详情 + */ +export function getOperLogById(operId: number) { + return request.Get>(`/coder/sysOperLog/getById/${operId}`) +} + +/** + * 查询操作日志详情 + */ +export function getOperLogDetailById(operId: number) { + return request.Get>(`/coder/sysOperLog/getDetailById/${operId}`) +} + +/** + * 删除操作日志 + */ +export function deleteOperLog(operId: number) { + return request.Post>(`/coder/sysOperLog/deleteById/${operId}`) +} + +/** + * 批量删除操作日志 + */ +export function batchDeleteOperLog(operIds: number[]) { + return request.Post>('/coder/sysOperLog/batchDelete', operIds) +} + +/** + * 清空操作日志 + */ +export function clearOperLog() { + return request.Post>('/coder/sysOperLog/clear') +} + +/** + * 获取操作统计 + */ +export function getOperLogStatistics() { + return request.Get>>('/coder/sysOperLog/statistics') +} + +/** + * 获取仪表盘统计 + */ +export function getOperLogDashboard() { + return request.Get>>('/coder/sysOperLog/dashboard') +} diff --git a/src/service/api/system/operlog/types.ts b/src/service/api/system/operlog/types.ts new file mode 100644 index 0000000..4840eb9 --- /dev/null +++ b/src/service/api/system/operlog/types.ts @@ -0,0 +1,71 @@ +/** + * 操作日志模块类型定义 + */ + +// 操作日志查询参数 +export interface OperLogQueryBo { + pageNo?: number + pageSize?: number + operName?: string // 操作名称 + operMan?: string // 操作人员 + operType?: string // 操作类型 + operStatus?: string // 操作状态 (0成功 1失败) + operUrl?: string // 请求URL + requestMethod?: string // 请求方式 + operIp?: string // 操作IP + beginTime?: string // 开始时间 + endTime?: string // 结束时间 +} + +// 操作日志响应数据 +export interface OperLogVo { + operId: number // 操作主键 + operName: string // 操作名称 + operType: string // 操作类型 + methodName: string // 方法名称 + requestMethod?: string // 请求方式 + systemType?: string // 系统类型 + operMan: string // 操作人员 + operUrl: string // 请求URL + operIp: string // 主机地址 + operLocation?: string // 操作地点 + operParam?: string // 请求参数 + jsonResult?: string // 返回参数 + operStatus: string // 操作状态 (0成功 1失败) + errorMsg?: string // 错误消息 + operTime: string // 操作时间 + costTime?: string // 消耗时间 +} + +// 操作日志表单数据 +export interface OperLogForm { + operId?: number + operName: string + operType: string + methodName: string + requestMethod?: string + systemType?: string + operMan: string + operUrl: string + operIp: string + operLocation?: string + operParam?: string + jsonResult?: string + operStatus: string + errorMsg?: string + costTime?: string +} + +// 操作日志搜索表单 +export interface OperLogSearchForm { + operName?: string + operMan?: string + operType?: string + operStatus?: string | null + operUrl?: string + requestMethod?: string + operIp?: string + timeRange?: [number, number] | null + beginTime?: string + endTime?: string +} diff --git a/src/service/api/system/picture/index.ts b/src/service/api/system/picture/index.ts new file mode 100644 index 0000000..1cd65fa --- /dev/null +++ b/src/service/api/system/picture/index.ts @@ -0,0 +1,94 @@ +import { request } from '../../../http' +import type { + PageSysPictureVo, + PictureUploadResult, + SysPictureQueryBo, + SysPictureVo, +} from './types' + +// 重新导出类型供外部使用 +export type { + PageSysPictureVo, + PictureServiceType, + PictureTypeEnum, + PictureUploadResult, + SysPictureQueryBo, + SysPictureSearchForm, + SysPictureVo, +} from './types' + +// 重新导出常量值供外部使用 +export { + PICTURE_SERVICE_OPTIONS, + PICTURE_TYPE_OPTIONS, +} from './types' + +// 图库管理相关API + +// 分页查询图片列表 +export function getSysPictureList(params: SysPictureQueryBo) { + return request.Get>('/coder/sysPicture/listPage', { params }) +} + +// 查询所有图片(不分页) +export function getAllSysPictures(params?: Omit) { + return request.Get>('/coder/sysPicture/list', { params }) +} + +// 根据ID查询图片 +export function getSysPictureById(id: number) { + return request.Get>(`/coder/sysPicture/getById/${id}`) +} + +// 新增图片记录 +export function addSysPicture(data: SysPictureVo) { + return request.Post>('/coder/sysPicture/add', data) +} + +// 修改图片信息 +export function updateSysPicture(data: SysPictureVo) { + return request.Post>('/coder/sysPicture/update', data) +} + +// 删除图片 +export function deleteSysPicture(id: number) { + return request.Post>(`/coder/sysPicture/deleteById/${id}`) +} + +// 批量删除图片 +export function batchDeleteSysPictures(ids: number[]) { + return request.Post>('/coder/sysPicture/batchDelete', ids) +} + +// 图片上传相关API + +// 上传图片 +export function uploadPicture(file: File, pictureType = '9', fileSize = 2, storageType?: string) { + const formData = new FormData() + formData.append('file', file) + + // 如果指定了存储类型,添加到表单数据中 + if (storageType) { + formData.append('storageType', storageType) + } + + return request.Post(`/coder/file/uploadFile/${fileSize}/pictures/${pictureType}`, formData) +} + +// 匿名上传图片(无需登录) +export function uploadAnyPicture(file: File, pictureType = '9', fileSize = 2, storageType?: string) { + const formData = new FormData() + formData.append('file', file) + + // 如果指定了存储类型,添加到表单数据中 + if (storageType) { + formData.append('storageType', storageType) + } + + return request.Post(`/coder/file/uploadAnyFile/${fileSize}/pictures/${pictureType}`, formData) +} + +// 兼容性导出 - 保持原有函数名以确保向后兼容 +export const fetchSysPicturePage = getSysPictureList +export const fetchAllSysPictures = getAllSysPictures +export const fetchSysPictureById = getSysPictureById diff --git a/src/service/api/system/picture/types.ts b/src/service/api/system/picture/types.ts new file mode 100644 index 0000000..09329a9 --- /dev/null +++ b/src/service/api/system/picture/types.ts @@ -0,0 +1,95 @@ +/** + * 图库管理相关类型定义 + */ + +// 图片查询条件类型 +export interface SysPictureQueryBo { + pageNo?: number + pageSize?: number + pictureName?: string + pictureSuffix?: string + pictureService?: string + pictureType?: string +} + +// 图片信息类型 +export interface SysPictureVo { + pictureId?: number + pictureName: string + newName?: string + pictureSize?: string + pictureSuffix: string + pictureUpload?: string + picturePath?: string + pictureService: string + pictureType: string + createTime?: string + createBy?: string + updateTime?: string + updateBy?: string +} + +// 图片搜索表单类型 +export interface SysPictureSearchForm { + pictureName?: string + pictureSuffix?: string + pictureService?: string + pictureType?: string +} + +// 分页结果类型 +export interface PageSysPictureVo { + records: SysPictureVo[] + total: number + size: number + current: number + pages: number +} + +// 图片上传结果类型 +export interface PictureUploadResult { + fileName: string + newName: string + fileSize: string + suffixName: string + filePath: string + fileUploadPath: string +} + +// 图片服务类型枚举 +export enum PictureServiceType { + LOCAL = '1', // 本地存储 + MINIO = '2', // MinIO对象存储 + OSS = '3', // 阿里云对象存储 +} + +// 图片类型枚举 +export enum PictureTypeEnum { + ALL = '0', // 全部数据 + USER_AVATAR = '1', // 用户头像 + ANIMATION = '2', // 动漫分类 + BEAUTY = '3', // 美女分类 + SCENERY = '4', // 风景分类 + STAR = '5', // 明星分类 + ANIMAL = '6', // 动物分类 + OTHER = '9', // 其他分类 +} + +// 图片类型选项 +export const PICTURE_TYPE_OPTIONS = [ + { label: '全部数据', value: PictureTypeEnum.ALL }, + { label: '用户头像', value: PictureTypeEnum.USER_AVATAR }, + { label: '动漫分类', value: PictureTypeEnum.ANIMATION }, + { label: '美女分类', value: PictureTypeEnum.BEAUTY }, + { label: '风景分类', value: PictureTypeEnum.SCENERY }, + { label: '明星分类', value: PictureTypeEnum.STAR }, + { label: '动物分类', value: PictureTypeEnum.ANIMAL }, + { label: '其他分类', value: PictureTypeEnum.OTHER }, +] + +// 图片服务类型选项 +export const PICTURE_SERVICE_OPTIONS = [ + { label: '本地存储', value: PictureServiceType.LOCAL }, + { label: 'MinIO存储', value: PictureServiceType.MINIO }, + { label: '阿里云OSS', value: PictureServiceType.OSS }, +] diff --git a/src/service/api/system/role/index.ts b/src/service/api/system/role/index.ts new file mode 100644 index 0000000..c376db7 --- /dev/null +++ b/src/service/api/system/role/index.ts @@ -0,0 +1,86 @@ +import { request } from '../../../http' +import type { + RoleForm, + RoleQueryBo, + RoleSelectVo, + RoleTransferVo, + RoleVo, +} from './types' + +// 重新导出类型供外部使用 +export type { + RoleForm, + RoleQueryBo, + RoleSearchForm, + RoleSelectVo, + RoleTransferVo, + RoleVo, +} from './types' + +// 角色管理相关API + +// 分页查询角色列表 +export function getRolePage(params: RoleQueryBo) { + return request.Get>>('/coder/sysRole/listPage', { params }) +} + +// 获取所有角色列表 +export function getRoleList() { + return request.Get>('/coder/sysRole/list') +} + +// 根据ID查询角色详情 +export function getRoleById(id: number) { + return request.Get>(`/coder/sysRole/getById/${id}`) +} + +// 新增角色 +export function addRole(data: RoleForm) { + return request.Post>('/coder/sysRole/add', data) +} + +// 修改角色 +export function updateRole(data: RoleForm) { + return request.Post>('/coder/sysRole/update', data) +} + +// 删除角色 +export function deleteRole(id: number) { + return request.Post>(`/coder/sysRole/deleteById/${id}`) +} + +// 批量删除角色 +export function batchDeleteRoles(ids: number[]) { + return request.Post>('/coder/sysRole/batchDelete', ids) +} + +// 修改角色状态 +export function updateRoleStatus(roleId: number, roleStatus: string) { + return request.Post>(`/coder/sysRole/updateStatus/${roleId}/${roleStatus}`) +} + +// 获取最新排序号 +export function getRoleSorted() { + return request.Get>('/coder/sysRole/getSorted') +} + +// 获取角色下拉框数据 +export function getRoleElSelect() { + return request.Get>('/coder/sysRole/listRoleElSelect') +} + +// 查询正常角色穿梭框 +export function getNormalRoleForUser(userId: number) { + return request.Get>(`/coder/sysRole/listNormalRole/${userId}`) +} + +// 分配用户角色 +export function assignUserRole(userId: number, roleIds: string) { + return request.Get>(`/coder/sysRole/assignUserRole/${userId}/${roleIds}`) +} + +// 兼容性导出 - 保持原有函数名以确保向后兼容 +export const fetchRoleList = getRoleList +export const fetchRoleElSelect = getRoleElSelect +export const fetchNormalRoleForUser = getNormalRoleForUser +export const fetchRolePage = getRolePage diff --git a/src/service/api/system/role/types.ts b/src/service/api/system/role/types.ts new file mode 100644 index 0000000..0d6004d --- /dev/null +++ b/src/service/api/system/role/types.ts @@ -0,0 +1,54 @@ +// 角色信息类型 +export interface RoleVo { + roleId: number + roleName: string + roleCode: string + roleStatus: string + remark?: string + sorted?: number + createTime?: string + updateTime?: string + createBy?: string + updateBy?: string +} + +// 角色查询参数类型 +export interface RoleQueryBo { + pageNo?: number + pageSize?: number + roleName?: string + roleCode?: string + roleStatus?: string + beginTime?: string + endTime?: string +} + +// 角色搜索表单类型 +export interface RoleSearchForm { + roleName?: string + roleCode?: string + roleStatus?: string | null + timeRange?: [number, number] | null +} + +// 角色表单类型 +export interface RoleForm { + roleId?: number + roleName: string + roleCode: string + roleStatus: string + remark: string + sorted?: number +} + +// 角色选择器选项类型 +export interface RoleSelectVo { + label: string + value: number +} + +// 角色穿梭框选项类型 +export interface RoleTransferVo { + data1: RoleSelectVo[] + data2: number[] +} diff --git a/src/service/api/system/user/index.ts b/src/service/api/system/user/index.ts new file mode 100644 index 0000000..4ee5565 --- /dev/null +++ b/src/service/api/system/user/index.ts @@ -0,0 +1,121 @@ +import { request } from '../../../http' +import type { + PageUserVo, + PasswordUpdateBo, + PersonalDataVo, + UserQueryBo, + UserVo, +} from './types' + +// 重新导出类型供外部使用 +export type { + PageUserVo, + PasswordUpdateBo, + PersonalDataVo, + UserQueryBo, + UserSearchForm, + UserVo, +} from './types' + +// 用户管理相关API + +// 分页查询用户列表 +export function getUserList(params: UserQueryBo) { + return request.Get>('/coder/sysLoginUser/listPage', { params }) +} + +// 查询所有用户(不分页) +export function getAllUsers(params?: Omit) { + return request.Get>('/coder/sysLoginUser/list', { params }) +} + +// 根据ID查询用户 +export function getUserById(id: number) { + return request.Get>(`/coder/sysLoginUser/getById/${id}`) +} + +// 新增用户 +export function addUser(data: UserVo) { + return request.Post>('/coder/sysLoginUser/add', data) +} + +// 修改用户信息 +export function updateUser(data: UserVo) { + return request.Post>('/coder/sysLoginUser/update', data) +} + +// 删除用户 +export function deleteUser(id: number) { + return request.Post>(`/coder/sysLoginUser/deleteById/${id}`) +} + +// 批量删除用户 +export function batchDeleteUsers(ids: number[]) { + return request.Post>('/coder/sysLoginUser/batchDelete', ids) +} + +// 修改用户状态 +export function updateUserStatus(userId: number, userStatus: string) { + return request.Post>(`/coder/sysLoginUser/updateStatus/${userId}/${userStatus}`) +} + +// 重置用户密码 +export function resetUserPassword(id: number, password: string) { + return request.Post>(`/coder/sysLoginUser/resetPwd/${id}/${password}`) +} + +// 个人资料相关API + +// 获取个人资料 +export function getPersonalData() { + return request.Get>('/coder/sysLoginUser/getPersonalData') +} + +// 修改个人资料 +export function updatePersonalData(data: PersonalDataVo) { + return request.Post>('/coder/sysLoginUser/updateBasicData', data) +} + +// 修改登录密码 +export function updateUserPassword(data: PasswordUpdateBo) { + return request.Post>('/coder/sysLoginUser/updateUserPwd', data) +} + +// 用户数据导入导出相关API + +// 下载用户导入模板 +export function downloadExcelTemplate() { + const method = request.Get('/coder/sysLoginUser/downloadExcelTemplate') + method.meta = { + isBlob: true, + } + return method +} + +// 导出用户数据 +export function exportExcelData(params?: UserQueryBo) { + const method = request.Get('/coder/sysLoginUser/exportExcelData', { params }) + method.meta = { + isBlob: true, + } + return method +} + +// 导入用户数据 +export function importUserData(file: File, updateSupport = false) { + const formData = new FormData() + formData.append('file', file) + formData.append('updateSupport', String(updateSupport)) + + return request.Post>('/coder/sysLoginUser/importExcelData', formData) +} + +// 兼容性导出 - 保持原有函数名以确保向后兼容 +export const fetchUserPage = getUserList +export const fetchAllUsers = getAllUsers +export const fetchUserById = getUserById diff --git a/src/service/api/system/user/types.ts b/src/service/api/system/user/types.ts new file mode 100644 index 0000000..37e5689 --- /dev/null +++ b/src/service/api/system/user/types.ts @@ -0,0 +1,73 @@ +// 用户查询参数类型 +export interface UserQueryBo { + pageNo?: number + pageSize?: number + loginName?: string + userName?: string + userType?: string + phone?: string + sex?: string + userStatus?: string + beginTime?: string + endTime?: string +} + +// 用户信息类型 (完整的用户实体) +export interface UserVo { + userId: number + loginName: string + userName: string + password?: string + userType: string + email?: string + phone?: string + sex?: string + avatar?: string + userStatus: string // 0-启用 1-停用 + loginIp?: string + loginTime?: string + pwdUpdateTime?: string + remark?: string + createBy?: string + createTime?: string + updateBy?: string + updateTime?: string + roleIds?: number[] +} + +// 分页结果类型 +export interface PageUserVo { + records: UserVo[] + total: number + size: number + current: number + pages: number +} + +// 个人资料类型 +export interface PersonalDataVo { + userName: string + email?: string + phone?: string + sex?: string + avatar?: string + remark?: string +} + +// 密码修改类型 +export interface PasswordUpdateBo { + oldPassword: string + newPassword: string + confirmPassword: string +} + +// 用户搜索表单类型 +export interface UserSearchForm { + loginName?: string + userName?: string + phone?: string + userStatus?: string + beginTime?: string + endTime?: string + timeRange?: [number, number] | null +} diff --git a/src/service/http/alova.ts b/src/service/http/alova.ts new file mode 100644 index 0000000..a7d38d5 --- /dev/null +++ b/src/service/http/alova.ts @@ -0,0 +1,116 @@ +import { local } from '@/utils' +import { coiMsgError } from '@/utils/coi' +import { createAlova } from 'alova' +import { createServerTokenAuthentication } from 'alova/client' +import adapterFetch from 'alova/fetch' +import VueHook from 'alova/vue' +import type { VueHookType } from 'alova/vue' +import { + DEFAULT_ALOVA_OPTIONS, + DEFAULT_BACKEND_OPTIONS, +} from './config' +import { + handleBusinessError, + handleRefreshToken, + handleResponseError, + handleServiceResult, +} from './handle' + +const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({ + // 服务端判定token过期 + refreshTokenOnSuccess: { + // 当服务端返回401时,表示token过期 + isExpired: (response, method) => { + const isExpired = method.meta && method.meta.isExpired + return response.status === 401 && !isExpired + }, + + // 当token过期时触发,在此函数中触发刷新token + handler: async (_response, method) => { + // 此处采取限制,防止过期请求无限循环重发 + if (!method.meta) + method.meta = { isExpired: true } + else + method.meta.isExpired = true + + await handleRefreshToken() + }, + }, + // 添加token到请求头 + assignToken: (method) => { + method.config.headers.Authorization = `Bearer ${local.get('accessToken')}` + }, +}) + +// docs path of alova.js https://alova.js.org/ +export function createAlovaInstance( + alovaConfig: Service.AlovaConfig, + backendConfig?: Service.BackendConfig, +) { + const _backendConfig = { ...DEFAULT_BACKEND_OPTIONS, ...backendConfig } + const _alovaConfig = { ...DEFAULT_ALOVA_OPTIONS, ...alovaConfig } + + return createAlova({ + statesHook: VueHook, + requestAdapter: adapterFetch(), + cacheFor: null, + baseURL: _alovaConfig.baseURL, + timeout: _alovaConfig.timeout, + + beforeRequest: onAuthRequired((method) => { + if (method.meta?.isFormPost) { + method.config.headers['Content-Type'] = 'application/x-www-form-urlencoded' + method.data = new URLSearchParams(method.data as URLSearchParams).toString() + } + alovaConfig.beforeRequest?.(method) + }), + responded: onResponseRefreshToken({ + // 请求成功的拦截器 + onSuccess: async (response, method) => { + const { status } = response + + if (status === 200) { + // 返回blob数据 + if (method.meta?.isBlob) + return response.blob() + + // 返回json数据 + const apiData = await response.json() + // 请求成功 + if (apiData[_backendConfig.codeKey] === _backendConfig.successCode) + return handleServiceResult(apiData) + + // 业务请求失败 + const errorResult = handleBusinessError(apiData, _backendConfig) + return handleServiceResult(errorResult, false) + } + // 接口请求失败 + const errorResult = await handleResponseError(response) + return handleServiceResult(errorResult, false) + }, + onError: (error, _method) => { + // 根据错误类型提供更友好的提示 + let userMessage = '网络请求失败,请稍后重试' + + if (error.name === 'AbortError') { + userMessage = '请求已取消' + } + else if (error.name === 'TimeoutError') { + userMessage = '请求超时,请检查网络连接' + } + else if (error.message.includes('fetch')) { + userMessage = '网络连接失败,请检查网络状态' + } + else if (error.message.includes('413')) { + userMessage = '文件大小超出限制,请选择较小的文件' + } + + coiMsgError(userMessage) + }, + + onComplete: async (_method) => { + // 处理请求完成逻辑 + }, + }), + }) +} diff --git a/src/service/http/config.ts b/src/service/http/config.ts new file mode 100644 index 0000000..23d7484 --- /dev/null +++ b/src/service/http/config.ts @@ -0,0 +1,36 @@ +import { $t } from '@/utils' + +/** 默认实例的Aixos配置 */ +export const DEFAULT_ALOVA_OPTIONS = { + // 请求超时时间,默认15秒 + timeout: 15 * 1000, +} + +/** 默认实例的后端字段配置 */ +export const DEFAULT_BACKEND_OPTIONS = { + codeKey: 'code', + dataKey: 'data', + msgKey: 'msg', + successCode: 1, +} + +/** 请求不成功各种状态的错误 */ +export const ERROR_STATUS = { + default: $t('http.defaultTip'), + 400: $t('http.400'), + 401: $t('http.401'), + 403: $t('http.403'), + 404: $t('http.404'), + 405: $t('http.405'), + 408: $t('http.408'), + 413: '文件大小超出限制,请选择较小的文件', + 500: $t('http.500'), + 501: $t('http.501'), + 502: $t('http.502'), + 503: $t('http.503'), + 504: $t('http.504'), + 505: $t('http.505'), +} + +/** 没有错误提示的code */ +export const ERROR_NO_TIP_STATUS = [10000] diff --git a/src/service/http/handle.ts b/src/service/http/handle.ts new file mode 100644 index 0000000..eb8e450 --- /dev/null +++ b/src/service/http/handle.ts @@ -0,0 +1,145 @@ +import { fetchUpdateToken } from '@/service/api/auth' +import { useAuthStore } from '@/store' +import { local } from '@/utils' +import { coiMsgError } from '@/utils/coi' +import { + ERROR_NO_TIP_STATUS, + ERROR_STATUS, +} from './config' + +type ErrorStatus = keyof typeof ERROR_STATUS + +/** + * @description: 处理请求成功,但返回后端服务器报错 + * @param {Response} response + * @return {*} + */ +export async function handleResponseError(response: Response) { + const error: Service.RequestError = { + errorType: 'Response Error', + code: 0, + message: ERROR_STATUS.default, + data: null, + } + const errorCode: ErrorStatus = response.status as ErrorStatus + let message = ERROR_STATUS[errorCode] || ERROR_STATUS.default + + // 尝试解析响应体中的具体错误信息 + try { + const responseText = await response.text() + if (responseText) { + try { + const responseData = JSON.parse(responseText) + // 如果后端返回了具体的错误信息,优先使用 + if (responseData && responseData.msg) { + message = responseData.msg + } + else if (responseData && responseData.message) { + message = responseData.message + } + } + catch { + // 如果不是JSON格式,可能是HTML错误页面,使用预定义消息 + } + } + } + catch { + // 读取错误响应失败,使用默认错误消息 + } + + Object.assign(error, { code: errorCode, message }) + + // 检查是否是401未授权错误,直接执行登出 + if (response.status === 401) { + const authStore = useAuthStore() + authStore.logout() + return error + } + + showError(error) + + return error +} + +/** + * @description: + * @param {Record} data 接口返回的后台数据 + * @param {Service} config 后台字段配置 + * @return {*} + */ +export function handleBusinessError(data: Record, config: Required) { + const { codeKey, msgKey } = config + const error: Service.RequestError = { + errorType: 'Business Error', + code: data[codeKey], + message: data[msgKey], + data: data.data, + } + + // 检查是否是token失效的业务错误 + const errorMessage = data[msgKey] || '' + const isTokenExpired = errorMessage.includes('token 无效') + || errorMessage.includes('Token无效') + || errorMessage.includes('未提供有效的Token') + || errorMessage.includes('登录已过期') + || errorMessage.includes('未登录') + + if (isTokenExpired) { + // Token失效,执行登出逻辑 + const authStore = useAuthStore() + authStore.logout() + return error + } + + showError(error) + + return error +} + +/** + * @description: 统一成功和失败返回类型 + * @param {any} data + * @param {boolean} isSuccess + * @return {*} result + */ +export function handleServiceResult(data: any, isSuccess: boolean = true) { + const result = { + isSuccess, + errorType: null, + ...data, + } + return result +} + +/** + * @description: 处理接口token刷新 + * @return {*} + */ +export async function handleRefreshToken() { + const authStore = useAuthStore() + const isAutoRefresh = import.meta.env.VITE_AUTO_REFRESH_TOKEN === 'Y' + if (!isAutoRefresh) { + await authStore.logout() + return + } + + // 刷新token + const { data } = await fetchUpdateToken({ refreshToken: local.get('refreshToken') }) + if (data) { + local.set('accessToken', data.accessToken) + local.set('refreshToken', data.refreshToken) + } + else { + // 刷新失败,退出 + await authStore.logout() + } +} + +export function showError(error: Service.RequestError) { + // 如果error不需要提示,则跳过 + const code = Number(error.code) + if (ERROR_NO_TIP_STATUS.includes(code)) + return + + coiMsgError(error.message) +} diff --git a/src/service/http/index.ts b/src/service/http/index.ts new file mode 100644 index 0000000..e4f28c5 --- /dev/null +++ b/src/service/http/index.ts @@ -0,0 +1,15 @@ +import { generateProxyPattern } from '@/../build/proxy' +import { serviceConfig } from '@/../service.config' +import { createAlovaInstance } from './alova' + +const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y' + +const { url } = generateProxyPattern(serviceConfig[import.meta.env.MODE]) + +export const request = createAlovaInstance({ + baseURL: isHttpProxy ? url.proxy : url.value, +}) + +export const blankInstance = createAlovaInstance({ + baseURL: '', +}) diff --git a/src/store/app/index.ts b/src/store/app/index.ts new file mode 100644 index 0000000..d22854c --- /dev/null +++ b/src/store/app/index.ts @@ -0,0 +1,137 @@ +import type { GlobalThemeOverrides } from 'naive-ui' +import { local, setLocale } from '@/utils' +import { colord } from 'colord' +import { set } from 'radash' +import themeConfig from './theme.json' + +export type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out' +export type LayoutMode = 'leftMenu' | 'topMenu' | 'mixMenu' + +const { VITE_DEFAULT_LANG, VITE_COPYRIGHT_INFO } = import.meta.env + +const docEle = ref(document.documentElement) + +const { isFullscreen, toggle } = useFullscreen(docEle) + +const { system, store } = useColorMode({ + emitAuto: true, +}) + +export const useAppStore = defineStore('app-store', { + state: () => { + return { + footerText: VITE_COPYRIGHT_INFO, + lang: VITE_DEFAULT_LANG, + theme: themeConfig as GlobalThemeOverrides, + primaryColor: themeConfig.common.primaryColor, + collapsed: false, + grayMode: false, + colorWeak: false, + loadFlag: true, + showLogo: true, + showTabs: true, + showFooter: true, + showProgress: true, + showBreadcrumb: true, + showBreadcrumbIcon: true, + showSetting: false, + transitionAnimation: 'zoom-out' as TransitionAnimation, + layoutMode: 'leftMenu' as LayoutMode, + contentFullScreen: false, + menuAccordion: true, + } + }, + getters: { + storeColorMode() { + return store.value + }, + colorMode() { + return store.value === 'auto' ? system.value : store.value + }, + fullScreen() { + return isFullscreen.value + }, + }, + actions: { + // 重置所有设置 + resetAlltheme() { + this.theme = themeConfig + this.primaryColor = '#4834D4' + this.collapsed = false + this.grayMode = false + this.colorWeak = false + this.loadFlag = true + this.showLogo = true + this.showTabs = true + this.showFooter = true + this.showBreadcrumb = true + this.showBreadcrumbIcon = true + this.transitionAnimation = 'zoom-out' + this.layoutMode = 'leftMenu' + this.contentFullScreen = false + this.menuAccordion = true + + // 重置所有配色 + this.setPrimaryColor(this.primaryColor) + }, + setAppLang(lang: App.lang) { + setLocale(lang) + local.set('lang', lang) + this.lang = lang + }, + /* 设置主题色 */ + setPrimaryColor(color: string) { + const brightenColor = colord(color).lighten(0.05).toHex() + const darkenColor = colord(color).darken(0.05).toHex() + set(this.theme, 'common.primaryColor', color) + set(this.theme, 'common.primaryColorHover', brightenColor) + set(this.theme, 'common.primaryColorPressed', darkenColor) + set(this.theme, 'common.primaryColorSuppl', brightenColor) + }, + setColorMode(mode: 'light' | 'dark' | 'auto') { + store.value = mode + }, + /* 切换侧边栏收缩 */ + toggleCollapse() { + this.collapsed = !this.collapsed + }, + /* 切换全屏 */ + toggleFullScreen() { + toggle() + }, + /** + * @description: 页面内容重载 + * @param {number} delay - 延迟毫秒数 + * @return {*} + */ + async reloadPage(delay = 600) { + this.loadFlag = false + await nextTick() + if (delay) { + setTimeout(() => { + this.loadFlag = true + }, delay) + } + else { + this.loadFlag = true + } + }, + /* 切换色弱模式 */ + toggleColorWeak() { + docEle.value.classList.toggle('color-weak') + this.colorWeak = docEle.value.classList.contains('color-weak') + }, + /* 切换灰色模式 */ + toggleGrayMode() { + docEle.value.classList.toggle('gray-mode') + this.grayMode = docEle.value.classList.contains('gray-mode') + }, + /* 切换菜单手风琴模式 */ + toggleMenuAccordion() { + this.menuAccordion = !this.menuAccordion + }, + }, + persist: { + storage: localStorage, + }, +}) diff --git a/src/store/app/theme.json b/src/store/app/theme.json new file mode 100644 index 0000000..bce48db --- /dev/null +++ b/src/store/app/theme.json @@ -0,0 +1,24 @@ +{ + "common": { + "primaryColor": "#4834D4", + "primaryColorHover": "#5b49d8", + "primaryColorPressed": "#3d2ac5", + "primaryColorSuppl": "#5b49d8", + "infoColor": "#2080f0", + "infoColorHover": "#4098fc", + "infoColorPressed": "#1060c9", + "infoColorSuppl": "#4098fc", + "successColor": "#18a058", + "successColorHover": "#36ad6a", + "successColorPressed": "#0c7a43", + "successColorSuppl": "#36ad6a", + "warningColor": "#f0a020", + "warningColorHover": "#fcb040", + "warningColorPressed": "#c97c10", + "warningColorSuppl": "#fcb040", + "errorColor": "#d03050", + "errorColorHover": "#de576d", + "errorColorPressed": "#ab1f3f", + "errorColorSuppl": "#de576d" + } +} diff --git a/src/store/auth.ts b/src/store/auth.ts new file mode 100644 index 0000000..a68156d --- /dev/null +++ b/src/store/auth.ts @@ -0,0 +1,141 @@ +import { router } from '@/router' +import { fetchLogin, fetchLoginUserInfo, fetchLogout } from '@/service/api/auth' +import { local } from '@/utils' +import { coiMsgSuccess } from '@/utils/coi' +import { useRouteStore } from './router' +import { useTabStore } from './tab' + +interface AuthStatus { + userInfo: Api.Login.Info | null + token: string +} +export const useAuthStore = defineStore('auth-store', { + state: (): AuthStatus => { + return { + userInfo: local.get('userInfo'), + token: local.get('accessToken') || '', + } + }, + getters: { + /** 是否登录 */ + isLogin(state) { + return Boolean(state.token) + }, + }, + actions: { + /* 登录退出,重置用户信息等 */ + async logout() { + const route = unref(router.currentRoute) + + // 先清除本地缓存,立即使token失效 + this.clearAuthStorage() + // 重置当前存储库 + this.$reset() + // 清空路由、菜单等数据 + const routeStore = useRouteStore() + routeStore.resetRouteStore() + // 清空标签栏数据 + const tabStore = useTabStore() + tabStore.clearAllTabs() + + // 最后调用后端退出登录接口 + try { + await fetchLogout() + } + catch (error) { + // 后端接口调用失败不影响前端清理 + console.warn('后端退出登录接口调用失败:', error) + } + + // 重定向到登录页 + if (route.meta.requiresAuth) { + router.push({ + name: 'login', + query: { + redirect: route.fullPath, + }, + }) + } + }, + clearAuthStorage() { + local.remove('accessToken') + local.remove('refreshToken') + local.remove('userInfo') + }, + + /* 用户登录 */ + async login(loginName: string, password: string, codeKey: string, securityCode: string, rememberMe = false) { + try { + const { isSuccess, data } = await fetchLogin({ loginName, password, codeKey, securityCode, rememberMe }) + if (!isSuccess) { + // 登录失败时抛出错误,让上层组件处理验证码刷新 + throw new Error('登录失败') + } + + // 保存Token + local.set('accessToken', data.tokenValue) + this.token = data.tokenValue + + // 获取用户信息 + const userInfoResult = await fetchLoginUserInfo() + if (!userInfoResult.isSuccess) { + // 获取用户信息失败时也抛出错误 + throw new Error('获取用户信息失败') + } + + // 处理登录信息 - 转换后端返回的数据结构 + const userInfo = { + id: userInfoResult.data.loginUser.userId, + userId: userInfoResult.data.loginUser.userId, + userName: userInfoResult.data.loginUser.userName, + avatar: userInfoResult.data.loginUser.avatar, + role: userInfoResult.data.roles, + buttons: userInfoResult.data.buttons, // 用户权限按钮列表 + accessToken: data.tokenValue, + refreshToken: data.tokenValue, // 没有单独的refreshToken,暂时使用相同值 + } + await this.handleLoginInfo(userInfo as Api.Login.Info) + } + catch (e) { + console.warn('[Login Error]:', e) + // 重新抛出错误,确保上层组件能够捕获 + throw e + } + }, + + /* 处理登录返回的数据 */ + async handleLoginInfo(data: Api.Login.Info) { + // 将token和userInfo保存下来 + local.set('userInfo', data) + local.set('accessToken', data.accessToken) + local.set('refreshToken', data.refreshToken) + this.token = data.accessToken + this.userInfo = data + + // 添加路由和菜单 + const routeStore = useRouteStore() + await routeStore.initAuthRoute() + + // 进行重定向跳转 + const route = unref(router.currentRoute) + const query = route.query as { redirect: string } + + // 登录成功提示 + coiMsgSuccess('登录成功!') + + router.push({ + path: query.redirect || import.meta.env.VITE_HOME_PATH || '/dashboard', + }) + }, + + /* 更新用户信息 */ + updateUserInfo(updates: Partial) { + if (this.userInfo) { + // 更新内存中的用户信息 + this.userInfo = { ...this.userInfo, ...updates } + // 更新本地存储 + local.set('userInfo', this.userInfo) + } + }, + }, +}) diff --git a/src/store/dict.ts b/src/store/dict.ts new file mode 100644 index 0000000..a42ce0c --- /dev/null +++ b/src/store/dict.ts @@ -0,0 +1,102 @@ +import { getDictDataByType } from '@/service/api/system/dict' +import type { DictDataOption } from '@/service/api/system/dict' + +type DictValue = string | number | boolean | null | undefined + +export const useDictStore = defineStore('dict-store', () => { + const dictMap = ref>({}) + const loadingMap = ref>({}) + const pendingMap: Record> = {} + + function getDictOptions(dictType: string) { + return dictMap.value[dictType] ?? [] + } + + function getDictOption(dictType: string, value: DictValue) { + if (value === undefined || value === null) + return undefined + + const target = String(value) + return getDictOptions(dictType).find(option => option.dictValue === target) + } + + function getDictLabel(dictType: string, value: DictValue, fallback?: string) { + const option = getDictOption(dictType, value) + if (option) + return option.dictLabel + + if (fallback !== undefined) + return fallback + + if (value === undefined || value === null || value === '') + return '' + + return String(value) + } + + async function fetchDict(dictType: string, force = false) { + if (!dictType) + return [] as DictDataOption[] + + if (!force && dictMap.value[dictType]) + return dictMap.value[dictType] + + if (!force && pendingMap[dictType]) + return pendingMap[dictType] + + const promise = (async () => { + loadingMap.value[dictType] = true + try { + const { isSuccess, data } = await getDictDataByType(dictType) + if (isSuccess && Array.isArray(data)) + dictMap.value[dictType] = data + else if (!dictMap.value[dictType]) + dictMap.value[dictType] = [] + + return dictMap.value[dictType] + } + finally { + loadingMap.value[dictType] = false + delete pendingMap[dictType] + } + })() + + if (!force) + pendingMap[dictType] = promise + + return promise + } + + async function fetchDicts(dictTypes: string[], force = false) { + const uniqueTypes = Array.from(new Set(dictTypes.filter(Boolean))) + await Promise.all(uniqueTypes.map(type => fetchDict(type, force))) + } + + function isLoading(dictType: string) { + return Boolean(loadingMap.value[dictType]) + } + + function invalidate(dictType?: string) { + if (dictType) { + delete dictMap.value[dictType] + delete loadingMap.value[dictType] + delete pendingMap[dictType] + } + else { + dictMap.value = {} + loadingMap.value = {} + Object.keys(pendingMap).forEach(key => delete pendingMap[key]) + } + } + + return { + dictMap, + fetchDict, + fetchDicts, + getDictOptions, + getDictOption, + getDictLabel, + isLoading, + invalidate, + } +}) diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..1df836f --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,15 @@ +import type { App } from 'vue' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' + +export * from './app/index' +export * from './auth' +export * from './router' +export * from './tab' +export * from './dict' + +// 安装pinia全局状态库 +export function installPinia(app: App) { + const pinia = createPinia() + pinia.use(piniaPluginPersistedstate) + app.use(pinia) +} diff --git a/src/store/router/helper.ts b/src/store/router/helper.ts new file mode 100644 index 0000000..08b59e2 --- /dev/null +++ b/src/store/router/helper.ts @@ -0,0 +1,288 @@ +import type { MenuOption } from 'naive-ui' +import type { RouteRecordRaw } from 'vue-router' +import { h } from 'vue' +import { usePermission } from '@/hooks' +import Layout from '@/layouts/index.vue' +import { arrayToTree, renderIcon } from '@/utils' +import { safeAsyncComponent } from '@/utils/component-guard' +import { clone, min, omit, pick } from 'radash' +import { RouterLink } from 'vue-router' + +const metaFields: AppRoute.MetaKeys[] + = ['title', 'icon', 'requiresAuth', 'roles', 'auth', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType'] + +// 将后端菜单数据转换为前端路由数据 +function transformBackendToRoute(backendRoute: AppRoute.BackendRoute): AppRoute.RowRoute { + return { + id: backendRoute.menuId, + pid: backendRoute.parentId === 0 ? null : backendRoute.parentId, + name: backendRoute.name, + path: backendRoute.path, + componentPath: backendRoute.component || null, + redirect: backendRoute.redirect || undefined, + title: backendRoute.menuName, + icon: backendRoute.icon, + auth: backendRoute.auth, // 权限标识 + requiresAuth: true, // 动态路由都需要认证 + hide: backendRoute.isHide === '0', // 0-隐藏 1-显示 + keepAlive: backendRoute.isKeepAlive === '0', // 0-是 1-否 + pinTab: backendRoute.isAffix === '0', // 0-是 1-否 + activeMenu: backendRoute.activeMenu || undefined, + menuType: backendRoute.menuType as AppRoute.MenuType, + href: backendRoute.isLink === '0' ? backendRoute.path : undefined, // 如果是外链 + } +} + +function standardizedRoutes(route: AppRoute.RowRoute[]) { + return clone(route).map((i) => { + const route = omit(i, metaFields) + + Reflect.set(route, 'meta', pick(i, metaFields)) + return route + }) as AppRoute.Route[] +} + +// 处理路由数据的主函数 - 支持动态和静态路由以及混合模式 +export function createRoutes(routeData: (AppRoute.BackendRoute | AppRoute.RowRoute)[]) { + const { hasPermission } = usePermission() + + // 处理混合数据:分别处理后端数据和前端数据 + const backendRoutes = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[] + const frontendRoutes = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[] + + // 转换后端路由数据 + const transformedBackendRoutes = backendRoutes.map(transformBackendToRoute) + + // 合并所有路由 + const routes = [...frontendRoutes, ...transformedBackendRoutes] + + // Structure the meta field + let resultRouter = standardizedRoutes(routes) + + // Route permission filtering + resultRouter = resultRouter.filter(i => hasPermission(i.meta.roles)) + + // Generate routes, no need to import files for those with redirect + const modules = import.meta.glob('@/views/**/*.vue') + resultRouter = resultRouter.map((item: AppRoute.Route) => { + if (item.componentPath && !item.redirect) { + // 对于动态路由,只有菜单类型才需要组件;对于静态路由,都需要组件 + const needComponent = item.meta.menuType === '2' || !item.meta.menuType + if (needComponent) { + // 处理组件路径,确保正确的路径格式 + let componentPath = item.componentPath + // 确保路径以 / 开头 + if (!componentPath.startsWith('/')) { + componentPath = `/${componentPath}` + } + // 确保路径以 .vue 结尾 + if (!componentPath.endsWith('.vue')) { + componentPath = `${componentPath}.vue` + } + const fullPath = `/src/views${componentPath}` + const originalComponent = modules[fullPath] + + // 如果组件未找到,输出调试信息并提供默认组件 + if (!originalComponent) { + // console.warn(`组件未找到: ${fullPath}`) + // console.warn('可用组件路径:', Object.keys(modules).slice(0, 10)) // 只显示前10个避免日志过长 + + // 为找不到组件的页面提供一个默认的空页面组件 + item.component = safeAsyncComponent( + () => Promise.resolve({ + template: ` +
+
+

页面开发中

+

组件路径: ${fullPath}

+

请联系开发人员创建对应的页面组件

+
+
+ `, + }), + { + delay: 0, + timeout: 5000, + }, + ) + } + else { + // 使用安全的异步组件加载器包装原有组件 + item.component = safeAsyncComponent( + originalComponent as any, + { + delay: 100, + timeout: 10000, + onError: (error, retry, fail, attempts) => { + console.error(`组件加载失败: ${fullPath}`, error) + if (attempts <= 2) { + retry() + } + else { + fail() + } + }, + }, + ) + } + } + else if (item.meta.menuType === '1') { + // 目录类型不需要组件,但需要确保有children + item.component = undefined + } + } + return item + }) + + // Generate route tree + resultRouter = arrayToTree(resultRouter) as AppRoute.Route[] + + const appRootRoute: RouteRecordRaw = { + path: '/appRoot', + name: 'appRoot', + redirect: import.meta.env.VITE_HOME_PATH || '/dashboard', + component: Layout, + meta: { + title: '', + icon: 'icon-park-outline:home', + }, + children: [], + } + + // Set the correct redirect path for the route + setRedirect(resultRouter) + + // Insert the processed route into the root route + appRootRoute.children = resultRouter as unknown as RouteRecordRaw[] + return appRootRoute +} + +// Generate an array of route names that need to be kept alive +export function generateCacheRoutes(routes: AppRoute.RowRoute[]) { + return routes + .filter(i => i.keepAlive) + .map(i => i.name) +} + +function setRedirect(routes: AppRoute.Route[]) { + routes.forEach((route) => { + if (route.children) { + if (!route.redirect) { + // Filter out a collection of child elements that are not hidden + const visibleChilds = route.children.filter(child => !child.meta.hide) + + // Redirect page to the path of the first child element by default + let target = visibleChilds[0] + + // Filter out pages with the order attribute + const orderChilds = visibleChilds.filter(child => child.meta.order) + + if (orderChilds.length > 0) + target = min(orderChilds, i => i.meta.order!) as AppRoute.Route + + if (target) + route.redirect = target.path + } + + setRedirect(route.children) + } + }) +} + +/* 生成侧边菜单的数据 */ +export function createMenus(routeData: (AppRoute.BackendRoute | AppRoute.RowRoute)[]) { + // 处理混合数据:分别处理后端数据和前端数据 + const backendRoutes = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[] + const frontendRoutes = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[] + + // 转换后端路由数据 + const transformedBackendRoutes = backendRoutes.map(transformBackendToRoute) + + // 合并所有路由 + const userRoutes = [...frontendRoutes, ...transformedBackendRoutes] + + const resultMenus = standardizedRoutes(userRoutes) + + // filter menus that do not need to be displayed + const visibleMenus = resultMenus.filter(route => !route.meta.hide && route.meta.menuType !== '3') // 过滤按钮类型 + + // 处理权限过滤和父子关系 + const menusWithPermission = processMenuPermissions(visibleMenus) + + // generate side menu + return arrayToTree(transformAuthRoutesToMenus(menusWithPermission)) +} + +// 处理菜单权限,确保有子菜单权限时父菜单也可见 +function processMenuPermissions(routes: AppRoute.Route[]): AppRoute.Route[] { + const { hasPermission } = usePermission() + + // 创建路由映射表 + const routeMap = new Map() + routes.forEach(route => routeMap.set(route.id, route)) + + // 找出有权限的路由 + const authorizedRoutes = new Set() + + routes.forEach((route) => { + if (hasPermission(route.meta.roles)) { + authorizedRoutes.add(route.id) + + // 如果是页面类型(menuType='2')或没有menuType的路由,确保其父菜单也被包含 + if (!route.meta.menuType || route.meta.menuType === '2') { + let parentId = route.pid + while (parentId !== null && parentId !== undefined) { + const parentRoute = routeMap.get(parentId) + if (parentRoute) { + authorizedRoutes.add(parentId) + parentId = parentRoute.pid + } + else { + break + } + } + } + } + }) + + // 返回有权限的路由 + return routes.filter(route => authorizedRoutes.has(route.id)) +} + +// render the returned routing table as a sidebar +function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) { + return userRoutes + // Sort the menu according to the order size + .sort((a, b) => { + if (a.meta && a.meta.order && b.meta && b.meta.order) + return a.meta.order - b.meta.order + else if (a.meta && a.meta.order) + return -1 + else if (b.meta && b.meta.order) + return 1 + else return 0 + }) + // Convert to side menu data structure + .map((item) => { + const target: MenuOption = { + id: item.id, + pid: item.pid, + label: + (!item.meta.menuType || item.meta.menuType === '2') + ? () => + h( + RouterLink, + { + to: { + path: item.path, + }, + }, + { default: () => item.meta.title }, + ) + : () => item.meta.title, + key: item.path, + icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined, + } + return target + }) +} diff --git a/src/store/router/index.ts b/src/store/router/index.ts new file mode 100644 index 0000000..51ea39d --- /dev/null +++ b/src/store/router/index.ts @@ -0,0 +1,147 @@ +import type { MenuOption } from 'naive-ui' +import { router } from '@/router' +import { staticRoutes } from '@/router/routes.static' +import { fetchUserRoutes } from '@/service/api/system/menu' +import { $t } from '@/utils' +import { coiMsgError } from '@/utils/coi' +import { createMenus, createRoutes, generateCacheRoutes } from './helper' + +interface RoutesStatus { + isInitAuthRoute: boolean + menus: MenuOption[] + rowRoutes: AppRoute.RowRoute[] + backendRoutes: AppRoute.BackendRoute[] + activeMenu: string | null + cacheRoutes: string[] +} +export const useRouteStore = defineStore('route-store', { + state: (): RoutesStatus => { + return { + isInitAuthRoute: false, + activeMenu: null, + menus: [], + rowRoutes: [], + backendRoutes: [], + cacheRoutes: [], + } + }, + actions: { + resetRouteStore() { + this.resetRoutes() + this.$reset() + }, + resetRoutes() { + // 获取所有路由名称 + const allRouteNames = router.getRoutes().map(route => route.name).filter(Boolean) + + // 保护固定路由,不删除这些基础路由 + const protectedRoutes = ['root', 'login', '403', '404', '500', 'notFound'] + + // 删除除了保护路由之外的所有路由 + allRouteNames.forEach((name) => { + if (name && !protectedRoutes.includes(name as string)) { + if (router.hasRoute(name)) { + router.removeRoute(name) + } + } + }) + }, + // set the currently highlighted menu key + setActiveMenu(key: string) { + this.activeMenu = key + }, + + async initRouteInfo() { + // 始终加载静态路由(仪表盘等基础路由) + const allRoutes = [...staticRoutes] + + if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') { + try { + // 获取动态路由并合并 + const { isSuccess, data } = await fetchUserRoutes() + + if (isSuccess && data) { + const dynamicRoutes = Array.isArray(data) ? data : [] + return [...allRoutes, ...dynamicRoutes] + } + } + catch (error) { + // 检查是否是网络错误 + const isNetworkError = !navigator.onLine + || (error instanceof Error && ( + error.message.includes('网络') + || error.message.includes('Network') + || error.message.includes('fetch') + || error.message.includes('timeout') + || error.message.includes('ERR_NETWORK') + )) + + if (isNetworkError) { + throw new Error('网络连接失败,请检查网络状态') + } + } + } + + return allRoutes + }, + async initAuthRoute() { + this.isInitAuthRoute = false + + // Initialize route information + const routeData = await this.initRouteInfo() + if (!routeData) { + coiMsgError($t(`app.getRouteError`)) + return + } + + // 检查是否包含动态路由数据(通过是否有menuId字段判断) + const hasDynamicRoutes = routeData.some(item => 'menuId' in item) + + if (hasDynamicRoutes) { + // 混合模式:分离静态路由和动态路由 + const staticRouteData = routeData.filter(item => !('menuId' in item)) as AppRoute.RowRoute[] + const dynamicRouteData = routeData.filter(item => 'menuId' in item) as AppRoute.BackendRoute[] + + // 保存动态路由原始数据 + this.backendRoutes = dynamicRouteData + + // 转换动态路由为前端格式 + const transformedDynamicRoutes = dynamicRouteData.map(item => ({ + id: item.menuId, + pid: item.parentId === 0 ? null : item.parentId, + name: item.name, + path: item.path, + componentPath: item.component || null, + redirect: item.redirect || undefined, + title: item.menuName, + icon: item.icon, + requiresAuth: true, + hide: item.isHide === '0', + keepAlive: item.isKeepAlive === '0', + pinTab: item.isAffix === '0', + activeMenu: item.activeMenu || undefined, + menuType: item.menuType as AppRoute.MenuType, + href: item.isLink === '0' ? item.path : undefined, + } as AppRoute.RowRoute)) + + // 合并静态路由和转换后的动态路由 + this.rowRoutes = [...staticRouteData, ...transformedDynamicRoutes] + this.cacheRoutes = generateCacheRoutes(this.rowRoutes) + } + else { + // 纯静态路由模式 + this.rowRoutes = routeData as AppRoute.RowRoute[] + this.cacheRoutes = generateCacheRoutes(this.rowRoutes) + } + + // Generate actual route and insert + const routes = createRoutes(routeData) + router.addRoute(routes) + + // Generate side menu + this.menus = createMenus(routeData) + + this.isInitAuthRoute = true + }, + }, +}) diff --git a/src/store/tab.ts b/src/store/tab.ts new file mode 100644 index 0000000..c553f6e --- /dev/null +++ b/src/store/tab.ts @@ -0,0 +1,116 @@ +import type { RouteLocationNormalized } from 'vue-router' +import { navigationGuard } from '@/router' + +interface TabState { + pinTabs: RouteLocationNormalized[] + tabs: RouteLocationNormalized[] + currentTabPath: string +} +export const useTabStore = defineStore('tab-store', { + state: (): TabState => { + return { + pinTabs: [], + tabs: [], + currentTabPath: '', + } + }, + getters: { + allTabs: state => [...state.pinTabs, ...state.tabs], + }, + actions: { + addTab(route: RouteLocationNormalized) { + // 根据meta确定是否不添加,可用于错误页,登录页等 + if (route.meta.withoutTab) + return + + // 如果标签名称已存在则不添加 + if (this.hasExistTab(route.fullPath as string)) + return + + // 根据meta.pinTab传递到不同的分组中 + if (route.meta.pinTab) + this.pinTabs.push(route) + else + this.tabs.push(route) + }, + async closeTab(fullPath: string) { + try { + const tabsLength = this.tabs.length + // 如果动态标签大于一个,才会标签跳转 + if (this.tabs.length > 1) { + // 获取关闭的标签索引 + const index = this.getTabIndex(fullPath) + const isLast = index + 1 === tabsLength + // 如果是关闭的当前页面,路由跳转到原先标签的后一个标签 + if (this.currentTabPath === fullPath && !isLast) { + // 跳转到后一个标签 + await navigationGuard.safePush(this.tabs[index + 1].fullPath) + } + else if (this.currentTabPath === fullPath && isLast) { + // 已经是最后一个了,就跳转前一个 + await navigationGuard.safePush(this.tabs[index - 1].fullPath) + } + } + // 删除标签 + this.tabs = this.tabs.filter((item) => { + return item.fullPath !== fullPath + }) + // 删除后如果清空了,就跳转到默认首页 + if (tabsLength - 1 === 0) + await navigationGuard.safePush('/') + } + catch (error) { + console.error('关闭标签页时发生错误:', error) + } + }, + + closeOtherTabs(fullPath: string) { + const index = this.getTabIndex(fullPath) + this.tabs = this.tabs.filter((item, i) => i === index) + }, + closeLeftTabs(fullPath: string) { + const index = this.getTabIndex(fullPath) + this.tabs = this.tabs.filter((item, i) => i >= index) + }, + closeRightTabs(fullPath: string) { + const index = this.getTabIndex(fullPath) + this.tabs = this.tabs.filter((item, i) => i <= index) + }, + clearAllTabs() { + this.tabs.length = 0 + this.pinTabs.length = 0 + }, + async closeAllTabs() { + try { + this.tabs.length = 0 + await navigationGuard.safePush('/') + } + catch (error) { + console.error('关闭所有标签页时发生错误:', error) + } + }, + + hasExistTab(fullPath: string) { + const _tabs = [...this.tabs, ...this.pinTabs] + return _tabs.some((item) => { + return item.fullPath === fullPath + }) + }, + /* 设置当前激活的标签 */ + setCurrentTab(fullPath: string) { + this.currentTabPath = fullPath + }, + getTabIndex(fullPath: string) { + return this.tabs.findIndex((item) => { + return item.fullPath === fullPath + }) + }, + modifyTab(fullPath: string, modifyFn: (route: RouteLocationNormalized) => void) { + const index = this.getTabIndex(fullPath) + modifyFn(this.tabs[index]) + }, + }, + persist: { + storage: sessionStorage, + }, +})