feat: 适配5.x后端,重构前端架构

主要变更:
1. 重构API目录结构,采用模块化设计,适配5.x后端接口规范
2. 优化布局组件,提升用户界面体验
3. 增强国际化支持,完善多语言配置
4. 新增权限控制功能,支持前端路由权限管理
5. 优化认证流程,支持JWT认证机制
6. 新增中间件和插件,提升系统扩展性
7. 优化状态管理,采用更清晰的模块化设计
8. 新增搜索框、广告等组件,丰富页面功能
9. 更改包管理工具从npm到pnpm,提升依赖管理效率
10. 完善配置系统,支持多环境配置

适配5.x后端特性:
- 采用新的API接口规范
- 支持JWT认证机制
- 适配后端权限控制体系
- 支持后端国际化接口
- 优化数据交互方式
This commit is contained in:
Mr.April
2026-04-07 11:01:11 +08:00
parent 1b83f1890b
commit d84d972e73
127 changed files with 103052 additions and 14661 deletions
+64
View File
@@ -0,0 +1,64 @@
import { useMemberStore } from '~/stores/member'
import { getMemberProfile } from '~/api/member'
import { refreshToken } from '~/api/auth'
export default defineNuxtPlugin(async () => {
// 仅在客户端执行
if (!import.meta.client) return
// 等待一下让 pinia 持久化插件完成初始化
await new Promise(resolve => setTimeout(resolve, 100))
const memberStore = useMemberStore()
// 如果没有 token 但有 refreshToken,尝试刷新
if (memberStore.refreshToken && !memberStore.token) {
try {
const response: any = await refreshToken(memberStore.refreshToken)
if (response.code === 0 && response.data) {
memberStore.token = response.data.access_token
memberStore.refreshToken = response.data.refresh_token
} else {
memberStore.logout()
}
} catch (error) {
memberStore.logout()
}
} else if (memberStore.token && memberStore.refreshToken) {
// 如果有 token 和 refreshToken,尝试获取用户信息
try {
const data: any = await getMemberProfile()
if (data && data.id) {
memberStore.info = data
} else {
memberStore.info = null
}
} catch (error: any) {
// 如果是 401 错误,尝试刷新 token
if (error?.message?.includes('401') || error?.response?.status === 401) {
try {
const refreshResponse: any = await refreshToken(memberStore.refreshToken)
if (refreshResponse.code === 0 && refreshResponse.data) {
memberStore.token = refreshResponse.data.access_token
memberStore.refreshToken = refreshResponse.data.refresh_token
// 刷新成功后重新获取用户信息
const userInfo: any = await getMemberProfile()
if (userInfo && userInfo.id) {
memberStore.info = userInfo
}
} else {
memberStore.logout()
}
} catch (refreshError) {
memberStore.logout()
}
} else {
memberStore.info = null
}
}
}
})
+103 -9
View File
@@ -1,4 +1,5 @@
import { createI18n } from 'vue-i18n'
import storage from '@/utils/storage'
// 定义语言文件模块接口
interface LanguageFileModule {
@@ -10,12 +11,26 @@ function isLanguageFileModule(module: any): module is LanguageFileModule {
return module && typeof module === 'object' && 'default' in module && typeof module.default === 'object'
}
// 递归处理嵌套结构
const flattenMessages = (obj: any, path: string = '') => {
let result: Record<string, string> = {}
for (const [key, value] of Object.entries(obj)) {
const newPath = path ? `${path}.${key}` : key
if (typeof value === 'object' && value !== null) {
Object.assign(result, flattenMessages(value, newPath))
} else {
result[newPath] = String(value)
}
}
return result
}
// 自动扫描加载所有语言文件
const loadAllLanguageFiles = () => {
const messages: Record<string, Record<string, any>> = {}
try {
// 加载所有语言文件
// 加载所有语言文件(包括子目录)
const languageFiles = import.meta.glob('~/lang/**/*.json', {
eager: true
}) as Record<string, LanguageFileModule>
@@ -28,16 +43,92 @@ const loadAllLanguageFiles = () => {
if (langIndex >= 0 && pathParts.length > langIndex + 1) {
const langCode = pathParts[langIndex + 1] ?? ''
const fileName = (pathParts[pathParts.length - 1] ?? '').replace('.json', '')
if (langCode && fileName) {
if (langCode) {
if (!messages[langCode]) {
messages[langCode] = {}
}
// 直接合并所有文件内容到对应语言下
messages[langCode] = {
...messages[langCode],
...module.default
// 只使用文件名作为键前缀,不包含目录名
const fileName = (pathParts[pathParts.length - 1] ?? '').replace('.json', '')
const fileKey = fileName
// 处理嵌套结构
const flattenedMessages = flattenMessages(module.default)
// 如果有文件键前缀,添加到所有消息键前
if (fileKey) {
const prefixedMessages: Record<string, string> = {}
for (const [key, value] of Object.entries(flattenedMessages)) {
prefixedMessages[`${fileKey}.${key}`] = value
}
messages[langCode] = {
...messages[langCode],
...prefixedMessages
}
} else {
// 没有前缀,直接合并
messages[langCode] = {
...messages[langCode],
...flattenedMessages
}
}
}
}
}
}
// 加载 apps 目录下的语言文件
const appsLanguageFiles = import.meta.glob('~/apps/**/lang/**/*.json', {
eager: true
}) as Record<string, LanguageFileModule>
// 处理 apps 语言文件
for (const [filePath, module] of Object.entries(appsLanguageFiles)) {
if (isLanguageFileModule(module)) {
const pathParts = filePath.split('/')
const appsIndex = pathParts.indexOf('apps')
const langIndex = pathParts.indexOf('lang')
if (appsIndex >= 0 && langIndex >= 0 && pathParts.length > langIndex + 1) {
const langCode = pathParts[langIndex + 1] ?? ''
if (langCode) {
if (!messages[langCode]) {
messages[langCode] = {}
}
// 提取模块名(apps目录下的子目录名)
const moduleName = pathParts[appsIndex + 1] ?? ''
// 只使用文件名作为键前缀,不包含目录名
const fileName = (pathParts[pathParts.length - 1] ?? '').replace('.json', '')
// 构建文件键,包含模块名前缀
let fileKey = moduleName
if (fileName) {
fileKey = `${moduleName}.${fileName}`
}
// 处理嵌套结构
const flattenedMessages = flattenMessages(module.default)
// 如果有文件键前缀,添加到所有消息键前
if (fileKey) {
const prefixedMessages: Record<string, string> = {}
for (const [key, value] of Object.entries(flattenedMessages)) {
prefixedMessages[`${fileKey}.${key}`] = value
}
messages[langCode] = {
...messages[langCode],
...prefixedMessages
}
} else {
// 没有前缀,直接合并
messages[langCode] = {
...messages[langCode],
...flattenedMessages
}
}
}
}
@@ -66,9 +157,12 @@ export default defineNuxtPlugin((NuxtApp) => {
// 加载所有语言消息
const messages = loadAllLanguageFiles()
// 从存储中读取语言设置,默认为 zh-cn
const savedLocale = storage.get('lang') ?? 'zh-cn'
const i18n = createI18n({
legacy: false, // 使用Composition API
locale: 'zh-cn', // 默认语言
locale: savedLocale, // 使用存储中的语言设置
fallbackLocale: 'zh-cn', // 回退语言
globalInjection: true, // 全局注入
messages: messages,
@@ -84,4 +178,4 @@ export default defineNuxtPlugin((NuxtApp) => {
getI18n: () => i18n
}
}
})
})
+3
View File
@@ -0,0 +1,3 @@
export default defineNuxtPlugin(() => {
// console.log('Icon plugin loaded')
})
+70
View File
@@ -0,0 +1,70 @@
import type { App, Directive, Plugin } from 'vue'
import { useMemberStore } from '~/stores/member'
/**
* 权限指令配置
*/
interface AuthDirectiveOptions {
permissions: string | string[]
}
/**
* 检查用户是否有权限
* @param permissions 权限码或权限码数组
* @returns 是否有权限
*/
export function hasAuth(permissions: string | string[]): boolean {
const memberStore = useMemberStore()
const userPermissions = memberStore.info?.permissions || []
// 如果用户拥有所有权限,直接返回 true
if (userPermissions.includes('*')) {
return true
}
// 处理字符串权限
if (typeof permissions === 'string') {
return userPermissions.includes(permissions)
}
// 处理数组权限,只要拥有任一权限就返回 true
return permissions.some(permission => userPermissions.includes(permission))
}
/**
* 权限指令
*/
const authDirective: Directive = {
mounted(el: HTMLElement, binding: any) {
const hasPermission = hasAuth(binding.value)
if (!hasPermission) {
el.style.display = 'none'
}
},
updated(el: HTMLElement, binding: any) {
const hasPermission = hasAuth(binding.value)
if (!hasPermission) {
el.style.display = 'none'
} else {
el.style.display = ''
}
}
}
/**
* 权限插件
*/
const permissionPlugin: Plugin = {
install(app: App) {
// 注册 v-auth 指令
app.directive('auth', authDirective)
// 注册全局 hasAuth 函数
app.config.globalProperties.$hasAuth = hasAuth
// 提供给组合式 API 使用
app.provide('hasAuth', hasAuth)
}
}
export default permissionPlugin
+11
View File
@@ -0,0 +1,11 @@
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { initDark } from '~/utils/dark'
export default defineNuxtPlugin(({ $pinia }) => {
// 仅在客户端使用持久化插件
if (import.meta.client) {
// @ts-ignore
$pinia.use(piniaPluginPersistedstate)
initDark()
}
})