核心架构:添加服务层、路由和状态管理

- 添加API服务层(src/service/)
  - HTTP客户端配置
  - 登录认证API
  - 系统管理API(用户、角色、菜单、部门、字典、文件等)
  - 监控API(在线用户、服务器、缓存、Redis等)
- 添加路由系统(src/router/)
  - 路由实例配置
  - 路由守卫逻辑
  - 静态路由和内置路由
- 添加状态管理(src/store/)
  - 认证状态(auth)
  - 路由状态(router)
  - 应用状态(app)
  - 标签页状态(tab)
  - 字典状态(dict)
This commit is contained in:
Leo 2025-10-08 02:26:46 +08:00
parent 715270aa49
commit cb98681927
46 changed files with 3986 additions and 0 deletions

150
src/router/guard.ts Normal file
View File

@ -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?.()
})
}

29
src/router/index.ts Normal file
View File

@ -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
}

View File

@ -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,
},
},
]

View File

@ -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,
// },
]

View File

@ -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<Service.ResponseResult<LoginResponse>>('/auth/login', data)
methodInstance.meta = {
authRole: null,
}
return methodInstance
}
export function fetchLogout() {
return request.Get<Service.ResponseResult<string>>('/auth/logout')
}
export function fetchRegister(data: RegisterRequest) {
const methodInstance = request.Post<Service.ResponseResult<string>>('/auth/register', data)
methodInstance.meta = {
authRole: null,
}
return methodInstance
}
export function fetchUpdateToken(data: any) {
const method = request.Post<Service.ResponseResult<LoginResponse>>('/updateToken', data)
method.meta = {
authRole: 'refreshToken',
}
return method
}
export function fetchUserRoutesOld(params: { id: number }) {
return request.Get<Service.ResponseResult<AppRoute.RowRoute[]>>('/getUserRoutes', { params })
}
export function fetchLoginUserInfo() {
return request.Get<Service.ResponseResult<Api.Login.UserInfoResponse>>('/coder/sysLoginUser/getLoginUserInformation')
}
// 验证码相关接口
// 获取PNG格式验证码
export function fetchCaptchaPng() {
const methodInstance = request.Get<Service.ResponseResult<CaptchaResponse>>('/captcha/png')
methodInstance.meta = {
authRole: null,
}
return methodInstance
}
// 获取GIF格式验证码
export function fetchCaptchaGif() {
const methodInstance = request.Get<Service.ResponseResult<CaptchaResponse>>('/captcha/gif')
methodInstance.meta = {
authRole: null,
}
return methodInstance
}

View File

@ -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<Service.ResponseResult<DashboardStatisticsVo>>('/coder/dashboard/getStatistics')
}
/**
*
* @param days 7
*/
export function getLoginTrend(days: number = 7) {
return request.Get<Service.ResponseResult<LoginTrendVo>>('/coder/dashboard/getLoginTrend', {
params: { days },
})
}
/**
*
* @param params
*/
export function getAllDashboardData(params: DashboardQueryBo = {}) {
const { includeTrend = true, trendDays = 7 } = params
return request.Get<Service.ResponseResult<DashboardStatisticsVo>>('/coder/dashboard/getAllData', {
params: { includeTrend, trendDays },
})
}
/**
*
*/
export function getUserStats() {
return request.Get<Service.ResponseResult<UserStatsVo>>('/coder/dashboard/getUserStats')
}
/**
*
* @param params
*/
export function getLoginStats(params: DashboardQueryBo = {}) {
const { includeTrend = false, trendDays = 7 } = params
return request.Get<Service.ResponseResult<LoginStatsVo>>('/coder/dashboard/getLoginStats', {
params: { includeTrend, trendDays },
})
}
/**
*
*/
export function getStorageStats() {
return request.Get<Service.ResponseResult<StorageStatsVo>>('/coder/dashboard/getStorageStats')
}
/**
*
*/
export function getDailyActivityStats() {
return request.Get<Service.ResponseResult<DailyActivityStatsVo>>('/coder/dashboard/getDailyActivityStats')
}

View File

@ -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
}

55
src/service/api/monitor/cache/index.ts vendored Normal file
View File

@ -0,0 +1,55 @@
import { request } from '@/service/http'
import type { DeleteCacheKeyBo, GetCacheValueBo, SysCacheVo } from './types'
/**
* Redis缓存所有Key
*/
export function getRedisCache() {
return request.Get<Service.ResponseResult<SysCacheVo[]>>('/coder/monitor/cache/getRedisCache')
}
/**
* Redis缓存键名列表
*/
export function getCacheKeys(cacheName: string) {
// 对缓存名称进行URL编码处理冒号等特殊字符
const encodedCacheName = encodeURIComponent(cacheName)
return request.Get<Service.ResponseResult<string[]>>(`/coder/monitor/cache/getCacheKeys/${encodedCacheName}`)
}
/**
* Redis缓存内容
*/
export function getCacheValue(data: GetCacheValueBo) {
return request.Post<Service.ResponseResult<SysCacheVo>>('/coder/monitor/cache/getValue', data)
}
/**
* Redis指定名称缓存
*/
export function deleteCacheName(cacheName: string) {
// 对缓存名称进行URL编码处理冒号等特殊字符
const encodedCacheName = encodeURIComponent(cacheName)
return request.Post<Service.ResponseResult<void>>(`/coder/monitor/cache/deleteCacheName/${encodedCacheName}`)
}
/**
* Redis指定键名缓存
*/
export function deleteCacheKey(data: DeleteCacheKeyBo) {
return request.Post<Service.ResponseResult<void>>('/coder/monitor/cache/deleteCacheKey', data)
}
/**
* Redis所有信息
*/
export function deleteCacheAll() {
return request.Post<Service.ResponseResult<void>>('/coder/monitor/cache/deleteCacheAll')
}
// 重新导出类型供外部使用
export type {
DeleteCacheKeyBo,
GetCacheValueBo,
SysCacheVo,
} from './types'

33
src/service/api/monitor/cache/types.ts vendored Normal file
View File

@ -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
}

View File

@ -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<Service.ResponseResult<PageSysJobVo>>('/coder/sysJob/listPage', { params })
}
/**
*
*/
export function getSysJobList(params?: SysJobQueryBo) {
return request.Get<Service.ResponseResult<SysJobVo[]>>('/coder/sysJob/list', { params })
}
/**
* ID查询定时任务详情
*/
export function getSysJobById(id: string) {
return request.Get<Service.ResponseResult<SysJobVo>>(`/coder/sysJob/getById/${id}`)
}
/**
*
*/
export function addSysJob(data: SysJobForm) {
return request.Post<Service.ResponseResult<string>>('/coder/sysJob/add', data)
}
/**
*
*/
export function updateSysJob(data: SysJobForm) {
return request.Post<Service.ResponseResult<string>>('/coder/sysJob/update', data)
}
/**
*
*/
export function deleteSysJobById(id: string) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysJob/deleteById/${id}`)
}
/**
*
*/
export function batchDeleteSysJob(jobIds: string[]) {
return request.Post<Service.ResponseResult<string>>('/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<Service.ResponseResult<string>>(`/coder/sysJob/updateStatus/${id}/${jobStatus}/${policyStatus}`)
}
/**
*
*/
export function runSysJobNow(id: string) {
return request.Get<Service.ResponseResult<string>>(`/coder/sysJob/runNow/${id}`)
}

View File

@ -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
}

View File

@ -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<Service.ResponseResult<PageOnlineUserVo>>('/coder/sysUserOnline/listPage', { params })
}
/**
*
* @param userId ID
*/
export function logoutUser(userId: string) {
return request.Get<Service.ResponseResult<string>>(`/coder/sysUserOnline/logout/${userId}`)
}
/**
* 线
*/
export function getOnlineUserCount() {
return request.Get<Service.ResponseResult<OnlineUserCountVo>>('/coder/sysUserOnline/count')
}

View File

@ -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
}

View File

@ -0,0 +1,15 @@
import { request } from '@/service/http'
import type { RedisInfoVo } from './types'
/**
* Redis监控信息
*/
export function getRedisInformation() {
return request.Get<Service.ResponseResult<RedisInfoVo>>('/coder/monitor/redis/getRedisInformation')
}
// 重新导出类型供外部使用
export type {
RedisCommandStatVo,
RedisInfoVo,
} from './types'

View File

@ -0,0 +1,21 @@
/**
* Redis监控相关类型定义
*/
// Redis命令统计项
export interface RedisCommandStatVo {
/** 命令名称 */
name: string
/** 调用次数 */
value: string
}
// Redis监控信息
export interface RedisInfoVo {
/** Redis基本信息 */
info: Record<string, any>
/** 数据库大小 */
dbSize: number
/** 命令统计 */
commandStats: RedisCommandStatVo[]
}

View File

@ -0,0 +1,19 @@
import { request } from '@/service/http'
import type { ServerVo } from './types'
/**
*
*/
export function getServerInformation() {
return request.Get<Service.ResponseResult<ServerVo>>('/coder/monitor/server/getServerInformation')
}
// 重新导出类型供外部使用
export type {
CpuVo,
JvmVo,
MemVo,
ServerVo,
SysFileVo,
SysVo,
} from './types'

View File

@ -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[]
}

View File

@ -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<Service.ResponseResult<PersonalDataVo>>('/coder/sysLoginUser/getPersonalData')
}
/**
*
*/
export function updateBasicData(data: UpdatePersonalBo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysLoginUser/updateBasicData', data)
}
/**
*
*/
export function updatePassword(data: UpdatePasswordBo) {
return request.Post<Service.ResponseResult<string>>('/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<Service.ResponseResult<any>>(`/coder/file/uploadFile/${fileSize}/pictures/1`, formData)
}

View File

@ -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<Service.ResponseResult<PageDictTypeVo>>('/coder/sysDictType/listPage', {
params,
})
}
/**
*
*/
export function getAllDictTypes() {
return request.Get<Service.ResponseResult<DictTypeVo[]>>('/coder/sysDictType/list')
}
/**
* ID查询字典类型详情
*/
export function getDictTypeById(id: string) {
return request.Get<Service.ResponseResult<DictTypeVo>>(`/coder/sysDictType/getById/${id}`)
}
/**
*
*/
export function addDictType(data: DictTypeForm) {
return request.Post<Service.ResponseResult<string>>('/coder/sysDictType/add', data)
}
/**
*
*/
export function updateDictType(data: DictTypeForm) {
return request.Post<Service.ResponseResult<string>>('/coder/sysDictType/update', data)
}
/**
*
*/
export function deleteDictType(id: string) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysDictType/deleteById/${id}`)
}
/**
*
*/
export function batchDeleteDictType(ids: string[]) {
return request.Post<Service.ResponseResult<string>>('/coder/sysDictType/batchDelete', ids)
}
/**
*
*/
export function updateDictTypeStatus(dictId: string, dictStatus: string) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysDictType/updateStatus/${dictId}/${dictStatus}`)
}
/**
*
*/
export function getDictTypeOptions() {
return request.Get<Service.ResponseResult<DictTypeOption[]>>('/coder/sysDictType/listDictType')
}
/** ======================= 字典数据管理 API ======================= */
/**
*
*/
export function getDictDataList(params: DictDataQueryBo) {
return request.Get<Service.ResponseResult<PageDictDataVo>>('/coder/sysDictData/listPage', {
params,
})
}
/**
*
*/
export function getAllDictData() {
return request.Get<Service.ResponseResult<DictDataVo[]>>('/coder/sysDictData/list')
}
/**
* ID查询字典数据详情
*/
export function getDictDataById(id: string) {
return request.Get<Service.ResponseResult<DictDataVo>>(`/coder/sysDictData/getById/${id}`)
}
/**
*
*/
export function addDictData(data: DictDataForm) {
return request.Post<Service.ResponseResult<string>>('/coder/sysDictData/add', data)
}
/**
*
*/
export function getDictDataSorted(dictType: string) {
return request.Get<Service.ResponseResult<number>>(`/coder/sysDictData/getSorted/${dictType}`)
}
/**
*
*/
export function updateDictData(data: DictDataForm) {
return request.Post<Service.ResponseResult<string>>('/coder/sysDictData/update', data)
}
/**
*
*/
export function deleteDictData(id: string) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysDictData/deleteById/${id}`)
}
/**
*
*/
export function batchDeleteDictData(ids: string[]) {
return request.Post<Service.ResponseResult<string>>('/coder/sysDictData/batchDelete', ids)
}
/**
*
*/
export function updateDictDataStatus(dictId: string, dictStatus: string) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysDictData/updateStatus/${dictId}/${dictStatus}`)
}
/**
*
*/
export function getDictDataByType(dictType: string) {
return request.Get<Service.ResponseResult<DictDataOption[]>>(`/coder/sysDictData/listDataByType/${dictType}`)
}
/**
* Redis
*/
export function syncDictCache() {
return request.Get<Service.ResponseResult<string>>('/coder/sysDictData/listDictCacheRedis')
}

View File

@ -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

View File

@ -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<Service.ResponseResult<PageSysFileVo>>('/coder/sysFile/listPage', { params })
}
// 查询所有文件(不分页)
export function getAllSysFiles(params?: Omit<SysFileQueryBo, 'pageNo' | 'pageSize'>) {
return request.Get<Service.ResponseResult<SysFileVo[]>>('/coder/sysFile/list', { params })
}
// 根据ID查询文件
export function getSysFileById(id: number) {
return request.Get<Service.ResponseResult<SysFileVo>>(`/coder/sysFile/getById/${id}`)
}
// 新增文件记录
export function addSysFile(data: SysFileVo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysFile/add', data)
}
// 修改文件信息
export function updateSysFile(data: SysFileVo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysFile/update', data)
}
// 删除文件
export function deleteSysFile(id: number) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysFile/deleteById/${id}`)
}
// 批量删除文件
export function batchDeleteSysFiles(ids: number[]) {
return request.Post<Service.ResponseResult<string>>('/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<FileUploadResult>(`/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<FileUploadResult>(`/coder/file/uploadAnyFile/${fileSize}/${folderName}/${fileParam}`, formData)
}
// 兼容性导出 - 保持原有函数名以确保向后兼容
export const fetchSysFilePage = getSysFileList
export const fetchAllSysFiles = getAllSysFiles
export const fetchSysFileById = getSysFileById

View File

@ -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' },
]

View File

@ -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<Service.ResponseResult<Service.PageResult<LoginLogVo>>>('/coder/sysLoginLog/listPage', { params })
}
/**
*
*/
export function getLoginLogList() {
return request.Get<Service.ResponseResult<LoginLogVo[]>>('/coder/sysLoginLog/list')
}
/**
* ID查询登录日志详情
*/
export function getLoginLogById(id: number) {
return request.Get<Service.ResponseResult<LoginLogVo>>(`/coder/sysLoginLog/getById/${id}`)
}
/**
*
*/
export function addLoginLog(data: LoginLogForm) {
return request.Post<Service.ResponseResult<null>>('/coder/sysLoginLog/add', data)
}
/**
*
*/
export function updateLoginLog(data: LoginLogForm) {
return request.Post<Service.ResponseResult<null>>('/coder/sysLoginLog/update', data)
}
/**
*
*/
export function deleteLoginLog(id: number) {
return request.Post<Service.ResponseResult<null>>(`/coder/sysLoginLog/deleteById/${id}`)
}
/**
*
*/
export function batchDeleteLoginLog(ids: number[]) {
return request.Post<Service.ResponseResult<null>>('/coder/sysLoginLog/batchDelete', ids)
}

View File

@ -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
}

View File

@ -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<Service.ResponseResult<Service.PageResult<MenuVo>>>('/coder/sysMenu/listPage', { params })
}
/**
*
*/
export function getMenuList(params?: MenuQueryBo) {
return request.Get<Service.ResponseResult<MenuVo[]>>('/coder/sysMenu/list', { params })
}
/**
* ID查询菜单详情
*/
export function getMenuById(id: string) {
return request.Get<Service.ResponseResult<MenuVo>>(`/coder/sysMenu/getById/${id}`)
}
/**
*
*/
export function addMenu(data: MenuForm) {
return request.Post<Service.ResponseResult<null>>('/coder/sysMenu/add', data)
}
/**
*
*/
export function updateMenu(data: MenuForm) {
return request.Post<Service.ResponseResult<null>>('/coder/sysMenu/update', data)
}
/**
*
*/
export function deleteMenu(id: string) {
return request.Post<Service.ResponseResult<null>>(`/coder/sysMenu/deleteById/${id}`)
}
/**
*
*/
export function batchDeleteMenu(ids: string[]) {
return request.Post<Service.ResponseResult<null>>('/coder/sysMenu/batchDelete', ids)
}
/**
*
*/
export function updateMenuStatus(id: string, menuStatus: string) {
return request.Post<Service.ResponseResult<null>>(`/coder/sysMenu/updateStatus/${id}/${menuStatus}`)
}
/**
*
*/
export function updateMenuSpread(id: string, isSpread: string) {
return request.Post<Service.ResponseResult<null>>(`/coder/sysMenu/updateSpread/${id}/${isSpread}`)
}
/**
*
*/
export function getMenuCascaderList() {
return request.Get<Service.ResponseResult<MenuCascaderBo[]>>('/coder/sysMenu/cascaderList')
}
// 菜单路由管理相关API
/**
*
*/
export function getUserRoutes() {
return request.Get<Service.ResponseResult<MenuRouterBo[]>>('/coder/sysMenu/listRouters')
}
/**
* 使
*/
export function getMenuPermissionData() {
return request.Get<Service.ResponseResult<MenuNormalResponse>>('/coder/sysMenu/listMenuNormal')
}
/**
* ID查询菜单权限ID列表
*/
export function getMenuIdsByRoleId(roleId: string) {
return request.Get<Service.ResponseResult<string[]>>(`/coder/sysMenu/listMenuIdsByRoleId/${roleId}`)
}
/**
*
*/
export function saveRoleMenuPermission(roleId: string, menuIds: string[]) {
// 空数组时传递 ["-1"] 表示取消所有权限
// 保持字符串格式避免精度丢失
const menuIdsStr = menuIds.length > 0 ? menuIds : ['-1']
return request.Post<Service.ResponseResult<string>>('/coder/sysMenu/saveRoleMenu', {
roleId,
menuIds: menuIdsStr,
})
}
// 兼容性导出 - 保持原有函数名以确保向后兼容
export const fetchUserRoutes = getUserRoutes
export const fetchMenuPermissionData = getMenuPermissionData
export const fetchMenuIdsByRoleId = getMenuIdsByRoleId
export const saveRoleMenu = saveRoleMenuPermission

View File

@ -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[] // 字符串格式避免精度丢失
}

View File

@ -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<Service.ResponseResult<Service.PageResult<OperLogVo>>>('/coder/sysOperLog/listPage', { params })
}
/**
* ID查询操作日志详情
*/
export function getOperLogById(operId: number) {
return request.Get<Service.ResponseResult<OperLogVo>>(`/coder/sysOperLog/getById/${operId}`)
}
/**
*
*/
export function getOperLogDetailById(operId: number) {
return request.Get<Service.ResponseResult<OperLogVo>>(`/coder/sysOperLog/getDetailById/${operId}`)
}
/**
*
*/
export function deleteOperLog(operId: number) {
return request.Post<Service.ResponseResult<null>>(`/coder/sysOperLog/deleteById/${operId}`)
}
/**
*
*/
export function batchDeleteOperLog(operIds: number[]) {
return request.Post<Service.ResponseResult<null>>('/coder/sysOperLog/batchDelete', operIds)
}
/**
*
*/
export function clearOperLog() {
return request.Post<Service.ResponseResult<null>>('/coder/sysOperLog/clear')
}
/**
*
*/
export function getOperLogStatistics() {
return request.Get<Service.ResponseResult<Record<string, any>>>('/coder/sysOperLog/statistics')
}
/**
*
*/
export function getOperLogDashboard() {
return request.Get<Service.ResponseResult<Record<string, any>>>('/coder/sysOperLog/dashboard')
}

View File

@ -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
}

View File

@ -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<Service.ResponseResult<PageSysPictureVo>>('/coder/sysPicture/listPage', { params })
}
// 查询所有图片(不分页)
export function getAllSysPictures(params?: Omit<SysPictureQueryBo, 'pageNo' | 'pageSize'>) {
return request.Get<Service.ResponseResult<SysPictureVo[]>>('/coder/sysPicture/list', { params })
}
// 根据ID查询图片
export function getSysPictureById(id: number) {
return request.Get<Service.ResponseResult<SysPictureVo>>(`/coder/sysPicture/getById/${id}`)
}
// 新增图片记录
export function addSysPicture(data: SysPictureVo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysPicture/add', data)
}
// 修改图片信息
export function updateSysPicture(data: SysPictureVo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysPicture/update', data)
}
// 删除图片
export function deleteSysPicture(id: number) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysPicture/deleteById/${id}`)
}
// 批量删除图片
export function batchDeleteSysPictures(ids: number[]) {
return request.Post<Service.ResponseResult<string>>('/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<PictureUploadResult>(`/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<PictureUploadResult>(`/coder/file/uploadAnyFile/${fileSize}/pictures/${pictureType}`, formData)
}
// 兼容性导出 - 保持原有函数名以确保向后兼容
export const fetchSysPicturePage = getSysPictureList
export const fetchAllSysPictures = getAllSysPictures
export const fetchSysPictureById = getSysPictureById

View File

@ -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 },
]

View File

@ -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<Service.ResponseResult<Service.PageResult<RoleVo>>>('/coder/sysRole/listPage', { params })
}
// 获取所有角色列表
export function getRoleList() {
return request.Get<Service.ResponseResult<RoleVo[]>>('/coder/sysRole/list')
}
// 根据ID查询角色详情
export function getRoleById(id: number) {
return request.Get<Service.ResponseResult<RoleVo>>(`/coder/sysRole/getById/${id}`)
}
// 新增角色
export function addRole(data: RoleForm) {
return request.Post<Service.ResponseResult<string>>('/coder/sysRole/add', data)
}
// 修改角色
export function updateRole(data: RoleForm) {
return request.Post<Service.ResponseResult<string>>('/coder/sysRole/update', data)
}
// 删除角色
export function deleteRole(id: number) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysRole/deleteById/${id}`)
}
// 批量删除角色
export function batchDeleteRoles(ids: number[]) {
return request.Post<Service.ResponseResult<string>>('/coder/sysRole/batchDelete', ids)
}
// 修改角色状态
export function updateRoleStatus(roleId: number, roleStatus: string) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysRole/updateStatus/${roleId}/${roleStatus}`)
}
// 获取最新排序号
export function getRoleSorted() {
return request.Get<Service.ResponseResult<number>>('/coder/sysRole/getSorted')
}
// 获取角色下拉框数据
export function getRoleElSelect() {
return request.Get<Service.ResponseResult<RoleSelectVo[]>>('/coder/sysRole/listRoleElSelect')
}
// 查询正常角色穿梭框
export function getNormalRoleForUser(userId: number) {
return request.Get<Service.ResponseResult<RoleTransferVo>>(`/coder/sysRole/listNormalRole/${userId}`)
}
// 分配用户角色
export function assignUserRole(userId: number, roleIds: string) {
return request.Get<Service.ResponseResult<string>>(`/coder/sysRole/assignUserRole/${userId}/${roleIds}`)
}
// 兼容性导出 - 保持原有函数名以确保向后兼容
export const fetchRoleList = getRoleList
export const fetchRoleElSelect = getRoleElSelect
export const fetchNormalRoleForUser = getNormalRoleForUser
export const fetchRolePage = getRolePage

View File

@ -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[]
}

View File

@ -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<Service.ResponseResult<PageUserVo>>('/coder/sysLoginUser/listPage', { params })
}
// 查询所有用户(不分页)
export function getAllUsers(params?: Omit<UserQueryBo, 'pageNo' | 'pageSize'>) {
return request.Get<Service.ResponseResult<UserVo[]>>('/coder/sysLoginUser/list', { params })
}
// 根据ID查询用户
export function getUserById(id: number) {
return request.Get<Service.ResponseResult<UserVo>>(`/coder/sysLoginUser/getById/${id}`)
}
// 新增用户
export function addUser(data: UserVo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysLoginUser/add', data)
}
// 修改用户信息
export function updateUser(data: UserVo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysLoginUser/update', data)
}
// 删除用户
export function deleteUser(id: number) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysLoginUser/deleteById/${id}`)
}
// 批量删除用户
export function batchDeleteUsers(ids: number[]) {
return request.Post<Service.ResponseResult<string>>('/coder/sysLoginUser/batchDelete', ids)
}
// 修改用户状态
export function updateUserStatus(userId: number, userStatus: string) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysLoginUser/updateStatus/${userId}/${userStatus}`)
}
// 重置用户密码
export function resetUserPassword(id: number, password: string) {
return request.Post<Service.ResponseResult<string>>(`/coder/sysLoginUser/resetPwd/${id}/${password}`)
}
// 个人资料相关API
// 获取个人资料
export function getPersonalData() {
return request.Get<Service.ResponseResult<PersonalDataVo>>('/coder/sysLoginUser/getPersonalData')
}
// 修改个人资料
export function updatePersonalData(data: PersonalDataVo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysLoginUser/updateBasicData', data)
}
// 修改登录密码
export function updateUserPassword(data: PasswordUpdateBo) {
return request.Post<Service.ResponseResult<string>>('/coder/sysLoginUser/updateUserPwd', data)
}
// 用户数据导入导出相关API
// 下载用户导入模板
export function downloadExcelTemplate() {
const method = request.Get<Blob>('/coder/sysLoginUser/downloadExcelTemplate')
method.meta = {
isBlob: true,
}
return method
}
// 导出用户数据
export function exportExcelData(params?: UserQueryBo) {
const method = request.Get<Blob>('/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<Service.ResponseResult<{
total: number
success: number
failed: number
message: string
}>>('/coder/sysLoginUser/importExcelData', formData)
}
// 兼容性导出 - 保持原有函数名以确保向后兼容
export const fetchUserPage = getUserList
export const fetchAllUsers = getAllUsers
export const fetchUserById = getUserById

View File

@ -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
}

116
src/service/http/alova.ts Normal file
View File

@ -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<VueHookType>({
// 服务端判定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) => {
// 处理请求完成逻辑
},
}),
})
}

View File

@ -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]

145
src/service/http/handle.ts Normal file
View File

@ -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<string, any>, config: Required<Service.BackendConfig>) {
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)
}

15
src/service/http/index.ts Normal file
View File

@ -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: '',
})

137
src/store/app/index.ts Normal file
View File

@ -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,
},
})

24
src/store/app/theme.json Normal file
View File

@ -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"
}
}

141
src/store/auth.ts Normal file
View File

@ -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<Api.Login.Info>) {
if (this.userInfo) {
// 更新内存中的用户信息
this.userInfo = { ...this.userInfo, ...updates }
// 更新本地存储
local.set('userInfo', this.userInfo)
}
},
},
})

102
src/store/dict.ts Normal file
View File

@ -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<Record<string, DictDataOption[]>>({})
const loadingMap = ref<Record<string, boolean>>({})
const pendingMap: Record<string, Promise<DictDataOption[]>> = {}
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,
}
})

15
src/store/index.ts Normal file
View File

@ -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)
}

288
src/store/router/helper.ts Normal file
View File

@ -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: `
<div class="p-4">
<div class="text-center text-gray-500">
<h3></h3>
<p>组件路径: ${fullPath}</p>
<p></p>
</div>
</div>
`,
}),
{
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<number | null, AppRoute.Route>()
routes.forEach(route => routeMap.set(route.id, route))
// 找出有权限的路由
const authorizedRoutes = new Set<number | null>()
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
})
}

147
src/store/router/index.ts Normal file
View File

@ -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
},
},
})

116
src/store/tab.ts Normal file
View File

@ -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,
},
})