修改eslint规则配置

This commit is contained in:
雷校云
2025-12-11 17:13:37 +08:00
parent 4098de60b0
commit 4be614663d
215 changed files with 11639 additions and 12679 deletions

View File

@@ -21,13 +21,13 @@ quoteProps: "as-needed"
# 不要求文件开头插入 @prettier 的 pragma 注释 # 不要求文件开头插入 @prettier 的 pragma 注释
requirePragma: false requirePragma: false
# 在语句末尾添加分号 # 在语句末尾添加分号
semi: true semi: false
# 使用双引号而不是单引号 # 使用双引号而不是单引号
singleQuote: false singleQuote: true
# 缩进使用 2 个空格 # 缩进使用 2 个空格
tabWidth: 2 tabWidth: 2
# 在多行元素的末尾添加逗号ES5 支持的对象、数组等) # 在多行元素的末尾添加逗号ES5 支持的对象、数组等)
trailingComma: "es5" trailingComma: "none"
# 使用空格而不是制表符缩进 # 使用空格而不是制表符缩进
useTabs: false useTabs: false
# Vue 文件中的 <script> 和 <style> 不增加额外的缩进 # Vue 文件中的 <script> 和 <style> 不增加额外的缩进

View File

@@ -1,77 +1,77 @@
// https://eslint.org/docs/latest/use/configure/configuration-files-new // https://eslint.org/docs/latest/use/configure/configuration-files-new
// 基础ESLint配置 // 基础ESLint配置
import eslint from "@eslint/js"; import eslint from '@eslint/js'
import globals from "globals"; import globals from 'globals'
// TypeScript支持 // TypeScript支持
import * as typescriptEslint from "typescript-eslint"; import * as typescriptEslint from 'typescript-eslint'
// Vue支持 // Vue支持
import pluginVue from "eslint-plugin-vue"; import pluginVue from 'eslint-plugin-vue'
import vueParser from "vue-eslint-parser"; import vueParser from 'vue-eslint-parser'
// 代码风格与格式化 // 代码风格与格式化
import configPrettier from "eslint-config-prettier"; import configPrettier from 'eslint-config-prettier'
import prettierPlugin from "eslint-plugin-prettier"; import prettierPlugin from 'eslint-plugin-prettier'
// 解析自动导入配置 // 解析自动导入配置
import fs from "node:fs"; import fs from 'node:fs'
let autoImportGlobals = {}; let autoImportGlobals = {}
try { try {
autoImportGlobals = autoImportGlobals =
JSON.parse(fs.readFileSync("./.eslintrc-auto-import.json", "utf-8")).globals || {}; JSON.parse(fs.readFileSync('./.eslintrc-auto-import.json', 'utf-8')).globals || {}
} catch (error) { } catch (error) {
// 文件不存在或解析错误时使用空对象 // 文件不存在或解析错误时使用空对象
console.warn("Could not load auto-import globals", error); console.warn('Could not load auto-import globals', error)
} }
// Element Plus组件 // Element Plus组件
const elementPlusComponents = { const elementPlusComponents = {
// Element Plus 组件添加为全局变量,避免 no-undef 报错 // Element Plus 组件添加为全局变量,避免 no-undef 报错
ElInput: "readonly", ElInput: 'readonly',
ElSelect: "readonly", ElSelect: 'readonly',
ElSwitch: "readonly", ElSwitch: 'readonly',
ElCascader: "readonly", ElCascader: 'readonly',
ElInputNumber: "readonly", ElInputNumber: 'readonly',
ElTimePicker: "readonly", ElTimePicker: 'readonly',
ElTimeSelect: "readonly", ElTimeSelect: 'readonly',
ElDatePicker: "readonly", ElDatePicker: 'readonly',
ElTreeSelect: "readonly", ElTreeSelect: 'readonly',
ElText: "readonly", ElText: 'readonly',
ElRadioGroup: "readonly", ElRadioGroup: 'readonly',
ElCheckboxGroup: "readonly", ElCheckboxGroup: 'readonly',
ElOption: "readonly", ElOption: 'readonly',
ElRadio: "readonly", ElRadio: 'readonly',
ElCheckbox: "readonly", ElCheckbox: 'readonly',
ElInputTag: "readonly", ElInputTag: 'readonly',
ElForm: "readonly", ElForm: 'readonly',
ElFormItem: "readonly", ElFormItem: 'readonly',
ElTable: "readonly", ElTable: 'readonly',
ElTableColumn: "readonly", ElTableColumn: 'readonly',
ElButton: "readonly", ElButton: 'readonly',
ElDialog: "readonly", ElDialog: 'readonly',
ElPagination: "readonly", ElPagination: 'readonly',
ElMessage: "readonly", ElMessage: 'readonly',
ElMessageBox: "readonly", ElMessageBox: 'readonly',
ElNotification: "readonly", ElNotification: 'readonly',
ElTree: "readonly", ElTree: 'readonly'
}; }
export default [ export default [
// 忽略文件配置 // 忽略文件配置
{ {
ignores: [ ignores: [
"**/node_modules/**", '**/node_modules/**',
"**/dist/**", '**/dist/**',
"**/*.min.*", '**/*.min.*',
"**/auto-imports.d.ts", '**/auto-imports.d.ts',
"**/components.d.ts", '**/components.d.ts'
], ]
}, },
// 基础 JavaScript 配置 // 基础 JavaScript 配置
eslint.configs.recommended, eslint.configs.recommended,
// Vue 推荐配置 // Vue 推荐配置
...pluginVue.configs["flat/recommended"], ...pluginVue.configs['flat/recommended'],
// TypeScript 推荐配置 // TypeScript 推荐配置
...typescriptEslint.configs.recommended, ...typescriptEslint.configs.recommended,
@@ -79,10 +79,10 @@ export default [
// 全局配置 // 全局配置
{ {
// 指定要检查的文件 // 指定要检查的文件
files: ["**/*.{js,mjs,cjs,ts,mts,cts,vue}"], files: ['**/*.{js,mjs,cjs,ts,mts,cts,vue}'],
languageOptions: { languageOptions: {
ecmaVersion: "latest", ecmaVersion: 'latest',
sourceType: "module", sourceType: 'module',
globals: { globals: {
...globals.browser, // 浏览器环境全局变量 ...globals.browser, // 浏览器环境全局变量
...globals.node, // Node.js 环境全局变量 ...globals.node, // Node.js 环境全局变量
@@ -90,145 +90,149 @@ export default [
...autoImportGlobals, // 自动导入的 API 函数 ...autoImportGlobals, // 自动导入的 API 函数
...elementPlusComponents, // Element Plus 组件 ...elementPlusComponents, // Element Plus 组件
// 全局类型定义,解决 TypeScript 中定义但 ESLint 不识别的问题 // 全局类型定义,解决 TypeScript 中定义但 ESLint 不识别的问题
PageQuery: "readonly", PageQuery: 'readonly',
PageResult: "readonly", PageResult: 'readonly',
OptionType: "readonly", OptionType: 'readonly',
ApiResponse: "readonly", ApiResponse: 'readonly',
ExcelResult: "readonly", ExcelResult: 'readonly',
TagView: "readonly", TagView: 'readonly',
AppSettings: "readonly", AppSettings: 'readonly',
__APP_INFO__: "readonly", __APP_INFO__: 'readonly'
}, }
}, },
plugins: { plugins: {
vue: pluginVue, vue: pluginVue,
"@typescript-eslint": typescriptEslint.plugin, '@typescript-eslint': typescriptEslint.plugin
}, },
rules: { rules: {
// 基础规则 // 基础规则
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// 添加这一行来强制使用单引号
quotes: ['error', 'single'],
semi: ['error', 'never'],
'comma-dangle': ['error', 'never'],
// ES6+ 规则 // ES6+ 规则SEWDvv
"prefer-const": "error", 'prefer-const': 'error',
"no-var": "error", 'no-var': 'error',
"object-shorthand": "error", 'object-shorthand': 'error',
// 最佳实践 // 最佳实践
eqeqeq: "off", eqeqeq: 'off',
"no-multi-spaces": "error", 'no-multi-spaces': 'error',
"no-multiple-empty-lines": ["error", { max: 1, maxBOF: 0, maxEOF: 0 }], 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }],
// 禁用与 TypeScript 冲突的规则 // 禁用与 TypeScript 冲突的规则
"no-unused-vars": "off", 'no-unused-vars': 'off',
"no-undef": "off", 'no-undef': 'off',
"no-redeclare": "off", 'no-redeclare': 'off',
"@typescript-eslint/ban-ts-comment": "off", '@typescript-eslint/ban-ts-comment': 'off'
}, }
}, },
// Vue 文件特定配置 // Vue 文件特定配置
{ {
files: ["**/*.vue"], files: ['**/*.vue'],
languageOptions: { languageOptions: {
parser: vueParser, parser: vueParser,
parserOptions: { parserOptions: {
ecmaVersion: "latest", ecmaVersion: 'latest',
sourceType: "module", sourceType: 'module',
parser: typescriptEslint.parser, parser: typescriptEslint.parser,
extraFileExtensions: [".vue"], extraFileExtensions: ['.vue'],
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname
}, }
}, },
rules: { rules: {
// Vue 规则 // Vue 规则
"vue/multi-word-component-names": "off", 'vue/multi-word-component-names': 'off',
"vue/no-v-html": "off", 'vue/no-v-html': 'off',
"vue/require-default-prop": "off", 'vue/require-default-prop': 'off',
"vue/require-explicit-emits": "error", 'vue/require-explicit-emits': 'error',
"vue/no-unused-vars": "error", 'vue/no-unused-vars': 'error',
"vue/no-mutating-props": "off", 'vue/no-mutating-props': 'off',
"vue/valid-v-for": "warn", 'vue/valid-v-for': 'warn',
"vue/no-template-shadow": "warn", 'vue/no-template-shadow': 'warn',
"vue/return-in-computed-property": "warn", 'vue/return-in-computed-property': 'warn',
"vue/block-order": [ 'vue/block-order': [
"error", 'error',
{ {
order: ["template", "script", "style"], order: ['template', 'script', 'style']
}, }
], ],
"vue/html-self-closing": [ 'vue/html-self-closing': [
"error", 'error',
{ {
html: { html: {
void: "always", void: 'always',
normal: "never", normal: 'never',
component: "always", component: 'always'
}, },
svg: "always", svg: 'always',
math: "always", math: 'always'
}, }
], ],
"vue/component-name-in-template-casing": ["error", "PascalCase"], 'vue/component-name-in-template-casing': ['error', 'PascalCase'],
"@typescript-eslint/no-explicit-any": "off", '@typescript-eslint/no-explicit-any': 'off'
}, }
}, },
// TypeScript 文件特定配置 // TypeScript 文件特定配置
{ {
files: ["**/*.{ts,tsx,mts,cts}"], files: ['**/*.{ts,tsx,mts,cts}'],
languageOptions: { languageOptions: {
parser: typescriptEslint.parser, parser: typescriptEslint.parser,
parserOptions: { parserOptions: {
ecmaVersion: "latest", ecmaVersion: 'latest',
sourceType: "module", sourceType: 'module',
project: "./tsconfig.json", project: './tsconfig.json',
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname
}, }
}, },
rules: { rules: {
// TypeScript 规则 // TypeScript 规则
"@typescript-eslint/no-explicit-any": "off", // 允许使用any类型方便开发 '@typescript-eslint/no-explicit-any': 'off', // 允许使用any类型方便开发
"@typescript-eslint/no-empty-function": "off", '@typescript-eslint/no-empty-function': 'off',
"@typescript-eslint/no-empty-object-type": "off", '@typescript-eslint/no-empty-object-type': 'off',
"@typescript-eslint/ban-ts-comment": "off", '@typescript-eslint/ban-ts-comment': 'off',
"@typescript-eslint/no-non-null-assertion": "off", '@typescript-eslint/no-non-null-assertion': 'off',
"@typescript-eslint/no-unused-vars": "warn", // 降级为警告 '@typescript-eslint/no-unused-vars': 'warn', // 降级为警告
"@typescript-eslint/no-unused-expressions": "warn", // 降级为警告 '@typescript-eslint/no-unused-expressions': 'warn', // 降级为警告
"@typescript-eslint/consistent-type-imports": "off", // 关闭强制使用type import '@typescript-eslint/consistent-type-imports': 'off', // 关闭强制使用type import
"@typescript-eslint/no-import-type-side-effects": "error", '@typescript-eslint/no-import-type-side-effects': 'error'
}, }
}, },
// .d.ts 文件配置 // .d.ts 文件配置
{ {
files: ["**/*.d.ts"], files: ['**/*.d.ts'],
rules: { rules: {
"@typescript-eslint/no-explicit-any": "off", '@typescript-eslint/no-explicit-any': 'off',
"@typescript-eslint/no-unused-vars": "off", '@typescript-eslint/no-unused-vars': 'off'
}, }
}, },
// CURD 组件配置 // CURD 组件配置
{ {
files: ["**/components/CURD/**/*.{ts,vue}"], files: ['**/components/CURD/**/*.{ts,vue}'],
rules: { rules: {
"no-unused-vars": "off", 'no-unused-vars': 'off',
"@typescript-eslint/no-unused-vars": "off", '@typescript-eslint/no-unused-vars': 'off',
"@typescript-eslint/no-explicit-any": "off", '@typescript-eslint/no-explicit-any': 'off'
}, }
}, },
// Prettier 集成(必须放在最后) // Prettier 集成(必须放在最后)
{ {
plugins: { plugins: {
prettier: prettierPlugin, // 将 Prettier 的输出作为 ESLint 的问题来报告 prettier: prettierPlugin // 将 Prettier 的输出作为 ESLint 的问题来报告
}, },
rules: { rules: {
...configPrettier.rules, ...configPrettier.rules,
"prettier/prettier": ["error", {}, { usePrettierrc: true }], 'prettier/prettier': ['error', {}, { usePrettierrc: true }],
"arrow-body-style": "off", 'arrow-body-style': 'off',
"prefer-arrow-callback": "off", 'prefer-arrow-callback': 'off'
}, }
}, }
]; ]

View File

@@ -6,12 +6,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useAppStore } from "@/store"; import { useAppStore } from '@/store'
import { defaultSettings } from "@/settings"; // import { defaultSettings } from '@/settings';
import { ThemeMode, ComponentSize } from "@/enums"; import { ComponentSize } from '@/enums'
const appStore = useAppStore(); const appStore = useAppStore()
const locale = computed(() => appStore.locale); const locale = computed(() => appStore.locale)
const size = computed(() => appStore.size as ComponentSize); const size = computed(() => appStore.size as ComponentSize)
</script> </script>

View File

@@ -1,17 +1,17 @@
import request from "@/utils/request"; import request from '@/utils/request'
/** /**
* AI 命令请求参数 * AI 命令请求参数
*/ */
export interface AiCommandRequest { export interface AiCommandRequest {
/** 用户输入的自然语言命令 */ /** 用户输入的自然语言命令 */
command: string; command: string
/** 当前页面路由(用于上下文) */ /** 当前页面路由(用于上下文) */
currentRoute?: string; currentRoute?: string
/** 当前激活的组件名称 */ /** 当前激活的组件名称 */
currentComponent?: string; currentComponent?: string
/** 额外上下文信息 */ /** 额外上下文信息 */
context?: Record<string, any>; context?: Record<string, any>
} }
/** /**
@@ -19,11 +19,11 @@ export interface AiCommandRequest {
*/ */
export interface FunctionCall { export interface FunctionCall {
/** 函数名称 */ /** 函数名称 */
name: string; name: string
/** 函数描述 */ /** 函数描述 */
description?: string; description?: string
/** 参数对象 */ /** 参数对象 */
arguments: Record<string, any>; arguments: Record<string, any>
} }
/** /**
@@ -31,19 +31,19 @@ export interface FunctionCall {
*/ */
export interface AiCommandResponse { export interface AiCommandResponse {
/** 解析日志ID用于关联执行记录 */ /** 解析日志ID用于关联执行记录 */
parseLogId?: string; parseLogId?: string
/** 是否成功解析 */ /** 是否成功解析 */
success: boolean; success: boolean
/** 解析后的函数调用列表 */ /** 解析后的函数调用列表 */
functionCalls: FunctionCall[]; functionCalls: FunctionCall[]
/** AI 的理解和说明 */ /** AI 的理解和说明 */
explanation?: string; explanation?: string
/** 置信度 (0-1) */ /** 置信度 (0-1) */
confidence?: number; confidence?: number
/** 错误信息 */ /** 错误信息 */
error?: string; error?: string
/** 原始 LLM 响应(用于调试) */ /** 原始 LLM 响应(用于调试) */
rawResponse?: string; rawResponse?: string
} }
/** /**
@@ -51,19 +51,19 @@ export interface AiCommandResponse {
*/ */
export interface AiExecuteRequest { export interface AiExecuteRequest {
/** 关联的解析日志ID */ /** 关联的解析日志ID */
parseLogId?: string; parseLogId?: string
/** 原始命令(用于审计) */ /** 原始命令(用于审计) */
originalCommand?: string; originalCommand?: string
/** 要执行的函数调用 */ /** 要执行的函数调用 */
functionCall: FunctionCall; functionCall: FunctionCall
/** 确认模式auto=自动执行, manual=需要用户确认 */ /** 确认模式auto=自动执行, manual=需要用户确认 */
confirmMode?: "auto" | "manual"; confirmMode?: 'auto' | 'manual'
/** 用户确认标志 */ /** 用户确认标志 */
userConfirmed?: boolean; userConfirmed?: boolean
/** 幂等性令牌(防止重复执行) */ /** 幂等性令牌(防止重复执行) */
idempotencyKey?: string; idempotencyKey?: string
/** 当前页面路由 */ /** 当前页面路由 */
currentRoute?: string; currentRoute?: string
} }
/** /**
@@ -71,67 +71,67 @@ export interface AiExecuteRequest {
*/ */
export interface AiExecuteResponse { export interface AiExecuteResponse {
/** 是否执行成功 */ /** 是否执行成功 */
success: boolean; success: boolean
/** 执行结果数据 */ /** 执行结果数据 */
data?: any; data?: any
/** 执行结果说明 */ /** 执行结果说明 */
message?: string; message?: string
/** 影响的记录数 */ /** 影响的记录数 */
affectedRows?: number; affectedRows?: number
/** 错误信息 */ /** 错误信息 */
error?: string; error?: string
/** 记录ID用于追踪 */ /** 记录ID用于追踪 */
recordId?: string; recordId?: string
/** 需要用户确认 */ /** 需要用户确认 */
requiresConfirmation?: boolean; requiresConfirmation?: boolean
/** 确认提示信息 */ /** 确认提示信息 */
confirmationPrompt?: string; confirmationPrompt?: string
} }
export interface AiCommandRecordPageQuery extends PageQuery { export interface AiCommandRecordPageQuery extends PageQuery {
keywords?: string; keywords?: string
executeStatus?: string; executeStatus?: string
parseSuccess?: boolean; parseSuccess?: boolean
userId?: number; userId?: number
isDangerous?: boolean; isDangerous?: boolean
provider?: string; provider?: string
model?: string; model?: string
functionName?: string; functionName?: string
createTime?: [string, string]; createTime?: [string, string]
} }
export interface AiCommandRecordVO { export interface AiCommandRecordVO {
id: string; id: string
userId: number; userId: number
username: string; username: string
originalCommand: string; originalCommand: string
provider?: string; provider?: string
model?: string; model?: string
parseSuccess?: boolean; parseSuccess?: boolean
functionCalls?: string; functionCalls?: string
explanation?: string; explanation?: string
confidence?: number; confidence?: number
parseErrorMessage?: string; parseErrorMessage?: string
inputTokens?: number; inputTokens?: number
outputTokens?: number; outputTokens?: number
totalTokens?: number; totalTokens?: number
parseTime?: number; parseTime?: number
functionName?: string; functionName?: string
functionArguments?: string; functionArguments?: string
executeStatus?: string; executeStatus?: string
executeResult?: string; executeResult?: string
executeErrorMessage?: string; executeErrorMessage?: string
affectedRows?: number; affectedRows?: number
isDangerous?: boolean; isDangerous?: boolean
requiresConfirmation?: boolean; requiresConfirmation?: boolean
userConfirmed?: boolean; userConfirmed?: boolean
executionTime?: number; executionTime?: number
ipAddress?: string; ipAddress?: string
userAgent?: string; userAgent?: string
currentRoute?: string; currentRoute?: string
createTime?: string; createTime?: string
updateTime?: string; updateTime?: string
remark?: string; remark?: string
} }
/** /**
@@ -146,10 +146,10 @@ class AiCommandApi {
*/ */
static parseCommand(data: AiCommandRequest): Promise<AiCommandResponse> { static parseCommand(data: AiCommandRequest): Promise<AiCommandResponse> {
return request<any, AiCommandResponse>({ return request<any, AiCommandResponse>({
url: "/api/v1/ai/command/parse", url: '/api/v1/ai/command/parse',
method: "post", method: 'post',
data, data
}); })
} }
/** /**
@@ -160,10 +160,10 @@ class AiCommandApi {
*/ */
static executeCommand(data: AiExecuteRequest): Promise<any> { static executeCommand(data: AiExecuteRequest): Promise<any> {
return request<any, any>({ return request<any, any>({
url: "/api/v1/ai/command/execute", url: '/api/v1/ai/command/execute',
method: "post", method: 'post',
data, data
}); })
} }
/** /**
@@ -171,10 +171,10 @@ class AiCommandApi {
*/ */
static getCommandRecordPage(queryParams: AiCommandRecordPageQuery) { static getCommandRecordPage(queryParams: AiCommandRecordPageQuery) {
return request<any, PageResult<AiCommandRecordVO[]>>({ return request<any, PageResult<AiCommandRecordVO[]>>({
url: "/api/v1/ai/command/records", url: '/api/v1/ai/command/records',
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
} }
/** /**
@@ -183,9 +183,9 @@ class AiCommandApi {
static rollbackCommand(recordId: string) { static rollbackCommand(recordId: string) {
return request({ return request({
url: `/api/v1/ai/command/rollback/${recordId}`, url: `/api/v1/ai/command/rollback/${recordId}`,
method: "post", method: 'post'
}); })
} }
} }
export default AiCommandApi; export default AiCommandApi

View File

@@ -1,86 +1,86 @@
import request from "@/utils/request"; import request from '@/utils/request'
const AUTH_BASE_URL = "/api/v1/auth"; const AUTH_BASE_URL = '/api/v1/auth'
const AuthAPI = { const AuthAPI = {
/** 登录接口*/ /** 登录接口*/
login(data: LoginFormData) { login(data: LoginFormData) {
const formData = new FormData(); const formData = new FormData()
formData.append("username", "admin"); formData.append('username', 'admin')
formData.append("password", data.password); formData.append('password', data.password)
formData.append("captchaKey", data.captchaKey); formData.append('captchaKey', data.captchaKey)
formData.append("captchaCode", data.captchaCode); formData.append('captchaCode', data.captchaCode)
return request<any, LoginResult>({ return request<any, LoginResult>({
url: `${AUTH_BASE_URL}/login`, url: `${AUTH_BASE_URL}/login`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}, },
/** 刷新 token 接口*/ /** 刷新 token 接口*/
refreshToken(refreshToken: string) { refreshToken(refreshToken: string) {
return request<any, LoginResult>({ return request<any, LoginResult>({
url: `${AUTH_BASE_URL}/refresh-token`, url: `${AUTH_BASE_URL}/refresh-token`,
method: "post", method: 'post',
params: { refreshToken }, params: { refreshToken },
headers: { headers: {
Authorization: "no-auth", Authorization: 'no-auth'
}, }
}); })
}, },
/** 退出登录接口 */ /** 退出登录接口 */
logout() { logout() {
return request({ return request({
url: `${AUTH_BASE_URL}/logout`, url: `${AUTH_BASE_URL}/logout`,
method: "delete", method: 'delete'
}); })
}, },
/** 获取验证码接口*/ /** 获取验证码接口*/
getCaptcha() { getCaptcha() {
return request<any, CaptchaInfo>({ return request<any, CaptchaInfo>({
url: `${AUTH_BASE_URL}/captcha`, url: `${AUTH_BASE_URL}/captcha`,
method: "get", method: 'get'
}); })
}, }
}; }
export default AuthAPI; export default AuthAPI
/** 登录表单数据 */ /** 登录表单数据 */
export interface LoginFormData { export interface LoginFormData {
/** 用户名 */ /** 用户名 */
username: string; username: string
/** 密码 */ /** 密码 */
password: string; password: string
/** 验证码缓存key */ /** 验证码缓存key */
captchaKey: string; captchaKey: string
/** 验证码 */ /** 验证码 */
captchaCode: string; captchaCode: string
/** 记住我 */ /** 记住我 */
rememberMe: boolean; rememberMe: boolean
} }
/** 登录响应 */ /** 登录响应 */
export interface LoginResult { export interface LoginResult {
/** 访问令牌 */ /** 访问令牌 */
accessToken: string; accessToken: string
/** 刷新令牌 */ /** 刷新令牌 */
refreshToken: string; refreshToken: string
/** 令牌类型 */ /** 令牌类型 */
tokenType: string; tokenType: string
/** 过期时间(秒) */ /** 过期时间(秒) */
expiresIn: number; expiresIn: number
} }
/** 验证码信息 */ /** 验证码信息 */
export interface CaptchaInfo { export interface CaptchaInfo {
/** 验证码缓存key */ /** 验证码缓存key */
captchaKey: string; captchaKey: string
/** 验证码图片Base64字符串 */ /** 验证码图片Base64字符串 */
captchaBase64: string; captchaBase64: string
} }

View File

@@ -1,5 +1,5 @@
import request from "@/utils/request"; import request from '@/utils/request'
const AUTH_BASE_URL = "/api2"; const AUTH_BASE_URL = '/api2'
/* /*
* 调账申请 * 调账申请
@@ -7,19 +7,19 @@ const AUTH_BASE_URL = "/api2";
// 新增调账申请 // 新增调账申请
export const FinanceLoan = (data: any) => { export const FinanceLoan = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("times", data.times); formData.append('times', data.times)
formData.append("ContractNo", data.ContractNo); formData.append('ContractNo', data.ContractNo)
formData.append("CustomerID", data.CustomerID); formData.append('CustomerID', data.CustomerID)
formData.append("amount", data.amount); formData.append('amount', data.amount)
formData.append("situation", data.situation); formData.append('situation', data.situation)
formData.append("personincharge", data.personincharge); formData.append('personincharge', data.personincharge)
return request({ return request({
url: `${AUTH_BASE_URL}/finance/loan`, url: `${AUTH_BASE_URL}/finance/loan`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }

View File

@@ -1,5 +1,5 @@
import request from "@/utils/request"; import request from '@/utils/request'
const AUTH_BASE_URL = "/api2"; const AUTH_BASE_URL = '/api2'
/* /*
* 部门管理 * 部门管理
@@ -7,57 +7,57 @@ const AUTH_BASE_URL = "/api2";
// 公司部门列表 // 公司部门列表
export const UserDepartment = (name: string) => { export const UserDepartment = (name: string) => {
const formData = new FormData(); const formData = new FormData()
formData.append("name", name); formData.append('name', name)
return request({ return request({
url: `${AUTH_BASE_URL}/user/department`, url: `${AUTH_BASE_URL}/user/department`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }
// 添加部门 // 添加部门
export const UserAddDepartment = (name: string) => { export const UserAddDepartment = (name: string) => {
const formData = new FormData(); const formData = new FormData()
formData.append("name", name); formData.append('name', name)
return request({ return request({
url: `${AUTH_BASE_URL}/user/add_department`, url: `${AUTH_BASE_URL}/user/add_department`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }
// 删除部门 // 删除部门
export const UserDeleteDepartment = (id: string) => { export const UserDeleteDepartment = (id: string) => {
const formData = new FormData(); const formData = new FormData()
formData.append("id", id); formData.append('id', id)
return request({ return request({
url: `${AUTH_BASE_URL}/user/delete_department`, url: `${AUTH_BASE_URL}/user/delete_department`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }
// 部门分页查询 // 部门分页查询
export const UserPersonlist = (data: any) => { export const UserPersonlist = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("per_page", data.pageSize); formData.append('per_page', data.pageSize)
formData.append("page", data.pageNum); formData.append('page', data.pageNum)
return request({ return request({
url: `${AUTH_BASE_URL}/user/personlist`, url: `${AUTH_BASE_URL}/user/personlist`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }

View File

@@ -1,5 +1,5 @@
import request from "@/utils/request"; import request from '@/utils/request'
const AUTH_BASE_URL = "/api2"; const AUTH_BASE_URL = '/api2'
/* /*
* 开票申请 * 开票申请
@@ -7,37 +7,37 @@ const AUTH_BASE_URL = "/api2";
// 开发票申请 // 开发票申请
export const FinanceIssueInvoice = (data: any) => { export const FinanceIssueInvoice = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("ContractNo", data.ContractNo); formData.append('ContractNo', data.ContractNo)
formData.append("personincharge", data.personincharge); formData.append('personincharge', data.personincharge)
formData.append("amount", data.amount); formData.append('amount', data.amount)
formData.append("type", data.type); formData.append('type', data.type)
formData.append("unit", data.unit); formData.append('unit', data.unit)
formData.append("number", data.number); formData.append('number', data.number)
formData.append("address_telephone", data.address_telephone); formData.append('address_telephone', data.address_telephone)
formData.append("bank", data.bank); formData.append('bank', data.bank)
formData.append("username", data.username); formData.append('username', data.username)
return request({ return request({
url: `${AUTH_BASE_URL}/finance/issue-invoice`, url: `${AUTH_BASE_URL}/finance/issue-invoice`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }
// 开票分页查询 // 开票分页查询
export const FinanceIssueDetail = (data: any) => { export const FinanceIssueDetail = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("page", data.pageNum); formData.append('page', data.pageNum)
formData.append("per_page", data.pageSize); formData.append('per_page', data.pageSize)
return request({ return request({
url: `${AUTH_BASE_URL}/finance/issue-Detail`, url: `${AUTH_BASE_URL}/finance/issue-Detail`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }

View File

@@ -1,29 +1,29 @@
import request from "@/utils/request"; import request from '@/utils/request'
/* /*
* 登录 * 登录
* */ * */
// const AUTH_BASE_URL = "http://8.137.99.82:8006"; // const AUTH_BASE_URL = "http://8.137.99.82:8006";
const AUTH_BASE_URL = "/api2"; const AUTH_BASE_URL = '/api2'
export const userLogin = (data: any) => { export const userLogin = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("username", data.username); formData.append('username', data.username)
formData.append("password", data.password); formData.append('password', data.password)
return request({ return request({
url: `${AUTH_BASE_URL}/user/login`, url: `${AUTH_BASE_URL}/user/login`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }
// 人员展示接口 // 人员展示接口
export const UserGetInfo = () => { export const UserGetInfo = () => {
return request({ return request({
url: `${AUTH_BASE_URL}/user/get_info`, url: `${AUTH_BASE_URL}/user/get_info`,
method: "post", method: 'post'
}); })
}; }

View File

@@ -1,5 +1,5 @@
import request from "@/utils/request"; import request from '@/utils/request'
const AUTH_BASE_URL = "/api2"; const AUTH_BASE_URL = '/api2'
/* /*
* 入职财务登记 * 入职财务登记
@@ -7,19 +7,19 @@ const AUTH_BASE_URL = "/api2";
// 入职财务 // 入职财务
export const FinanceUserRegister = (data: any) => { export const FinanceUserRegister = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("username", data.username); formData.append('username', data.username)
formData.append("card", data.card); formData.append('card', data.card)
formData.append("Dateofjoining", data.Dateofjoining); formData.append('Dateofjoining', data.Dateofjoining)
formData.append("position", data.position); formData.append('position', data.position)
formData.append("salary", data.salary); formData.append('salary', data.salary)
formData.append("personincharge", data.personincharge); formData.append('personincharge', data.personincharge)
return request({ return request({
url: `${AUTH_BASE_URL}/finance/user-register`, url: `${AUTH_BASE_URL}/finance/user-register`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }

View File

@@ -1,6 +1,6 @@
import { isFile, isString } from "@/utils/auxiliaryFunction"; import { isFile, isString } from '@/utils/auxiliaryFunction'
import request from "@/utils/request"; import request from '@/utils/request'
const AUTH_BASE_URL = "/api2"; const AUTH_BASE_URL = '/api2'
/* /*
* 人事管理 * 人事管理
@@ -8,92 +8,92 @@ const AUTH_BASE_URL = "/api2";
// 人员分页查询 // 人员分页查询
export const UserPersonnelList = (data: any) => { export const UserPersonnelList = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("page", data.pageNum); formData.append('page', data.pageNum)
formData.append("per_page", data.pageSize); formData.append('per_page', data.pageSize)
formData.append("username", data.username); formData.append('username', data.username)
formData.append("department", data.department); formData.append('department', data.department)
return request({ return request({
url: `${AUTH_BASE_URL}/user/personnel-list`, url: `${AUTH_BASE_URL}/user/personnel-list`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }
// 人事管理-人员添加 // 人事管理-人员添加
export const UserCreateUser = (data: any) => { export const UserCreateUser = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("username", data.username); formData.append('username', data.username)
formData.append("account", data.account); formData.append('account', data.account)
formData.append("password", data.password); formData.append('password', data.password)
formData.append("nation", data.nation); formData.append('nation', data.nation)
formData.append("IdCard", data.IdCard); formData.append('IdCard', data.IdCard)
formData.append("department", data.department); formData.append('department', data.department)
formData.append("mobilePhone", data.mobilePhone); formData.append('mobilePhone', data.mobilePhone)
formData.append("position", data.position); formData.append('position', data.position)
formData.append("team", data.team); formData.append('team', data.team)
formData.append("Dateofjoining", data.Dateofjoining); formData.append('Dateofjoining', data.Dateofjoining)
formData.append("Confirmationtime", data.Confirmationtime); formData.append('Confirmationtime', data.Confirmationtime)
formData.append("Practicingcertificatetime", data.Practicingcertificatetime); formData.append('Practicingcertificatetime', data.Practicingcertificatetime)
formData.append("AcademicResume", data.AcademicResume); formData.append('AcademicResume', data.AcademicResume)
formData.append("academic", JSON.stringify(data.academic)); formData.append('academic', JSON.stringify(data.academic))
formData.append("contract", data.contract); formData.append('contract', data.contract)
formData.append("ApplicationForm", data.ApplicationForm); formData.append('ApplicationForm', data.ApplicationForm)
return request({ return request({
url: `${AUTH_BASE_URL}/user/create-user`, url: `${AUTH_BASE_URL}/user/create-user`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }
// 人事管理-人员编辑 // 人事管理-人员编辑
export const UserEditorialStaff = (data: any) => { export const UserEditorialStaff = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("id", data.id); formData.append('id', data.id)
formData.append("username", data.username); formData.append('username', data.username)
formData.append("account", data.account); formData.append('account', data.account)
formData.append("password", data.password); formData.append('password', data.password)
formData.append("nation", data.nation); formData.append('nation', data.nation)
formData.append("IdCard", data.IdCard); formData.append('IdCard', data.IdCard)
formData.append("department", data.department); formData.append('department', data.department)
formData.append("mobilePhone", data.mobilePhone); formData.append('mobilePhone', data.mobilePhone)
formData.append("position", data.position); formData.append('position', data.position)
formData.append("team", data.team); formData.append('team', data.team)
formData.append("Dateofjoining", data.Dateofjoining); formData.append('Dateofjoining', data.Dateofjoining)
formData.append("Confirmationtime", data.Confirmationtime); formData.append('Confirmationtime', data.Confirmationtime)
formData.append("Practicingcertificatetime", data.Practicingcertificatetime); formData.append('Practicingcertificatetime', data.Practicingcertificatetime)
formData.append("academic", JSON.stringify(data.academic)); formData.append('academic', JSON.stringify(data.academic))
console.log(data.AcademicResume, data.contract, data.ApplicationForm); console.log(data.AcademicResume, data.contract, data.ApplicationForm)
if (isFile(data.AcademicResume)) formData.append("AcademicResume", data.AcademicResume); if (isFile(data.AcademicResume)) formData.append('AcademicResume', data.AcademicResume)
if (isFile(data.contract)) formData.append("contract", data.contract); if (isFile(data.contract)) formData.append('contract', data.contract)
if (isFile(data.ApplicationForm)) formData.append("ApplicationForm", data.ApplicationForm); if (isFile(data.ApplicationForm)) formData.append('ApplicationForm', data.ApplicationForm)
return request({ return request({
url: `${AUTH_BASE_URL}/user/editorial-staff`, url: `${AUTH_BASE_URL}/user/editorial-staff`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }
// 人员展示接口 // 人员展示接口
export const UserPersonnelDetails = (data: any) => { export const UserPersonnelDetails = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("account", data.account); formData.append('account', data.account)
return request({ return request({
url: `${AUTH_BASE_URL}/user/personnel-details`, url: `${AUTH_BASE_URL}/user/personnel-details`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }

View File

@@ -1,5 +1,5 @@
import request from "@/utils/request"; import request from '@/utils/request'
const AUTH_BASE_URL = "/api2"; const AUTH_BASE_URL = '/api2'
/* /*
* 收入确认 * 收入确认
@@ -7,19 +7,19 @@ const AUTH_BASE_URL = "/api2";
// 新增收入确认 // 新增收入确认
export const FinanceConfirm = (data: any) => { export const FinanceConfirm = (data: any) => {
const formData = new FormData(); const formData = new FormData()
formData.append("times", data.times); formData.append('times', data.times)
formData.append("ContractNo", data.ContractNo); formData.append('ContractNo', data.ContractNo)
formData.append("CustomerID", data.CustomerID); formData.append('CustomerID', data.CustomerID)
formData.append("amount", data.amount); formData.append('amount', data.amount)
formData.append("allocate", data.allocate); formData.append('allocate', data.allocate)
formData.append("personincharge", data.personincharge); formData.append('personincharge', data.personincharge)
return request({ return request({
url: `${AUTH_BASE_URL}/finance/confirm`, url: `${AUTH_BASE_URL}/finance/confirm`,
method: "post", method: 'post',
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}; }

View File

@@ -1,49 +1,49 @@
import request from "@/utils/request"; import request from '@/utils/request'
const GENERATOR_BASE_URL = "/api/v1/codegen"; const GENERATOR_BASE_URL = '/api/v1/codegen'
const GeneratorAPI = { const GeneratorAPI = {
/** 获取数据表分页列表 */ /** 获取数据表分页列表 */
getTablePage(params: TablePageQuery) { getTablePage(params: TablePageQuery) {
return request<any, PageResult<TablePageVO[]>>({ return request<any, PageResult<TablePageVO[]>>({
url: `${GENERATOR_BASE_URL}/table/page`, url: `${GENERATOR_BASE_URL}/table/page`,
method: "get", method: 'get',
params, params
}); })
}, },
/** 获取代码生成配置 */ /** 获取代码生成配置 */
getGenConfig(tableName: string) { getGenConfig(tableName: string) {
return request<any, GenConfigForm>({ return request<any, GenConfigForm>({
url: `${GENERATOR_BASE_URL}/${tableName}/config`, url: `${GENERATOR_BASE_URL}/${tableName}/config`,
method: "get", method: 'get'
}); })
}, },
/** 获取代码生成配置 */ /** 获取代码生成配置 */
saveGenConfig(tableName: string, data: GenConfigForm) { saveGenConfig(tableName: string, data: GenConfigForm) {
return request({ return request({
url: `${GENERATOR_BASE_URL}/${tableName}/config`, url: `${GENERATOR_BASE_URL}/${tableName}/config`,
method: "post", method: 'post',
data, data
}); })
}, },
/** 获取代码生成预览数据 */ /** 获取代码生成预览数据 */
getPreviewData(tableName: string, pageType?: "classic" | "curd") { getPreviewData(tableName: string, pageType?: 'classic' | 'curd') {
return request<any, GeneratorPreviewVO[]>({ return request<any, GeneratorPreviewVO[]>({
url: `${GENERATOR_BASE_URL}/${tableName}/preview`, url: `${GENERATOR_BASE_URL}/${tableName}/preview`,
method: "get", method: 'get',
params: pageType ? { pageType } : undefined, params: pageType ? { pageType } : undefined
}); })
}, },
/** 重置代码生成配置 */ /** 重置代码生成配置 */
resetGenConfig(tableName: string) { resetGenConfig(tableName: string) {
return request({ return request({
url: `${GENERATOR_BASE_URL}/${tableName}/config`, url: `${GENERATOR_BASE_URL}/${tableName}/config`,
method: "delete", method: 'delete'
}); })
}, },
/** /**
@@ -51,149 +51,149 @@ const GeneratorAPI = {
* @param url * @param url
* @param fileName * @param fileName
*/ */
download(tableName: string, pageType?: "classic" | "curd") { download(tableName: string, pageType?: 'classic' | 'curd') {
return request({ return request({
url: `${GENERATOR_BASE_URL}/${tableName}/download`, url: `${GENERATOR_BASE_URL}/${tableName}/download`,
method: "get", method: 'get',
params: pageType ? { pageType } : undefined, params: pageType ? { pageType } : undefined,
responseType: "blob", responseType: 'blob'
}).then((response) => { }).then((response) => {
const fileName = decodeURI( const fileName = decodeURI(
response.headers["content-disposition"].split(";")[1].split("=")[1] response.headers['content-disposition'].split(';')[1].split('=')[1]
); )
const blob = new Blob([response.data], { type: "application/zip" }); const blob = new Blob([response.data], { type: 'application/zip' })
const a = document.createElement("a"); const a = document.createElement('a')
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob)
a.href = url; a.href = url
a.download = fileName; a.download = fileName
a.click(); a.click()
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url)
}); })
}, }
}; }
export default GeneratorAPI; export default GeneratorAPI
/** 代码生成预览对象 */ /** 代码生成预览对象 */
export interface GeneratorPreviewVO { export interface GeneratorPreviewVO {
/** 文件生成路径 */ /** 文件生成路径 */
path: string; path: string
/** 文件名称 */ /** 文件名称 */
fileName: string; fileName: string
/** 文件内容 */ /** 文件内容 */
content: string; content: string
} }
/** 数据表分页查询参数 */ /** 数据表分页查询参数 */
export interface TablePageQuery extends PageQuery { export interface TablePageQuery extends PageQuery {
/** 关键字(表名) */ /** 关键字(表名) */
keywords?: string; keywords?: string
} }
/** 数据表分页对象 */ /** 数据表分页对象 */
export interface TablePageVO { export interface TablePageVO {
/** 表名称 */ /** 表名称 */
tableName: string; tableName: string
/** 表描述 */ /** 表描述 */
tableComment: string; tableComment: string
/** 存储引擎 */ /** 存储引擎 */
engine: string; engine: string
/** 字符集排序规则 */ /** 字符集排序规则 */
tableCollation: string; tableCollation: string
/** 创建时间 */ /** 创建时间 */
createTime: string; createTime: string
} }
/** 代码生成配置表单 */ /** 代码生成配置表单 */
export interface GenConfigForm { export interface GenConfigForm {
/** 主键 */ /** 主键 */
id?: string; id?: string
/** 表名 */ /** 表名 */
tableName?: string; tableName?: string
/** 业务名 */ /** 业务名 */
businessName?: string; businessName?: string
/** 模块名 */ /** 模块名 */
moduleName?: string; moduleName?: string
/** 包名 */ /** 包名 */
packageName?: string; packageName?: string
/** 实体名 */ /** 实体名 */
entityName?: string; entityName?: string
/** 作者 */ /** 作者 */
author?: string; author?: string
/** 上级菜单 */ /** 上级菜单 */
parentMenuId?: string; parentMenuId?: string
/** 后端应用名 */ /** 后端应用名 */
backendAppName?: string; backendAppName?: string
/** 前端应用名 */ /** 前端应用名 */
frontendAppName?: string; frontendAppName?: string
/** 字段配置列表 */ /** 字段配置列表 */
fieldConfigs?: FieldConfig[]; fieldConfigs?: FieldConfig[]
/** 页面类型 classic|curd */ /** 页面类型 classic|curd */
pageType?: "classic" | "curd"; pageType?: 'classic' | 'curd'
/** 要移除的表前缀,如 sys_ */ /** 要移除的表前缀,如 sys_ */
removeTablePrefix?: string; removeTablePrefix?: string
} }
/** 字段配置 */ /** 字段配置 */
export interface FieldConfig { export interface FieldConfig {
/** 主键 */ /** 主键 */
id?: string; id?: string
/** 列名 */ /** 列名 */
columnName?: string; columnName?: string
/** 列类型 */ /** 列类型 */
columnType?: string; columnType?: string
/** 字段名 */ /** 字段名 */
fieldName?: string; fieldName?: string
/** 字段类型 */ /** 字段类型 */
fieldType?: string; fieldType?: string
/** 字段描述 */ /** 字段描述 */
fieldComment?: string; fieldComment?: string
/** 是否在列表显示 */ /** 是否在列表显示 */
isShowInList?: number; isShowInList?: number
/** 是否在表单显示 */ /** 是否在表单显示 */
isShowInForm?: number; isShowInForm?: number
/** 是否在查询条件显示 */ /** 是否在查询条件显示 */
isShowInQuery?: number; isShowInQuery?: number
/** 是否必填 */ /** 是否必填 */
isRequired?: number; isRequired?: number
/** 表单类型 */ /** 表单类型 */
formType?: number; formType?: number
/** 查询类型 */ /** 查询类型 */
queryType?: number; queryType?: number
/** 字段长度 */ /** 字段长度 */
maxLength?: number; maxLength?: number
/** 字段排序 */ /** 字段排序 */
fieldSort?: number; fieldSort?: number
/** 字典类型 */ /** 字典类型 */
dictType?: string; dictType?: string
} }

View File

@@ -1,64 +1,64 @@
import request from "@/utils/request"; import request from '@/utils/request'
const FileAPI = { const FileAPI = {
/** 上传文件 (传入 FormData上传进度回调 */ /** 上传文件 (传入 FormData上传进度回调 */
upload(formData: FormData, onProgress?: (percent: number) => void) { upload(formData: FormData, onProgress?: (percent: number) => void) {
return request<any, FileInfo>({ return request<any, FileInfo>({
url: "/api/v1/files", url: '/api/v1/files',
method: "post", method: 'post',
data: formData, data: formData,
headers: { "Content-Type": "multipart/form-data" }, headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => { onUploadProgress: (progressEvent) => {
if (progressEvent.total) { if (progressEvent.total) {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total); const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
onProgress?.(percent); onProgress?.(percent)
} }
}, }
}); })
}, },
/** 上传文件(传入 File */ /** 上传文件(传入 File */
uploadFile(file: File) { uploadFile(file: File) {
const formData = new FormData(); const formData = new FormData()
formData.append("file", file); formData.append('file', file)
return request<any, FileInfo>({ return request<any, FileInfo>({
url: "/api/v1/files", url: '/api/v1/files',
method: "post", method: 'post',
data: formData, data: formData,
headers: { "Content-Type": "multipart/form-data" }, headers: { 'Content-Type': 'multipart/form-data' }
}); })
}, },
/** 删除文件 */ /** 删除文件 */
delete(filePath?: string) { delete(filePath?: string) {
return request({ return request({
url: "/api/v1/files", url: '/api/v1/files',
method: "delete", method: 'delete',
params: { filePath }, params: { filePath }
}); })
}, },
/** 下载文件 */ /** 下载文件 */
download(url: string, fileName?: string) { download(url: string, fileName?: string) {
return request({ return request({
url, url,
method: "get", method: 'get',
responseType: "blob", responseType: 'blob'
}).then((res) => { }).then((res) => {
const blob = new Blob([res.data]); const blob = new Blob([res.data])
const a = document.createElement("a"); const a = document.createElement('a')
const urlObject = window.URL.createObjectURL(blob); const urlObject = window.URL.createObjectURL(blob)
a.href = urlObject; a.href = urlObject
a.download = fileName || "下载文件"; a.download = fileName || '下载文件'
a.click(); a.click()
window.URL.revokeObjectURL(urlObject); window.URL.revokeObjectURL(urlObject)
}); })
}, }
}; }
export default FileAPI; export default FileAPI
export interface FileInfo { export interface FileInfo {
name: string; name: string
url: string; url: string
} }

View File

@@ -1,70 +1,70 @@
import request from "@/utils/request"; import request from '@/utils/request'
const CONFIG_BASE_URL = "/api/v1/config"; const CONFIG_BASE_URL = '/api/v1/config'
const ConfigAPI = { const ConfigAPI = {
/** 获取配置分页数据 */ /** 获取配置分页数据 */
getPage(queryParams?: ConfigPageQuery) { getPage(queryParams?: ConfigPageQuery) {
return request<any, PageResult<ConfigPageVO[]>>({ return request<any, PageResult<ConfigPageVO[]>>({
url: `${CONFIG_BASE_URL}/page`, url: `${CONFIG_BASE_URL}/page`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, },
/** 获取配置表单数据 */ /** 获取配置表单数据 */
getFormData(id: string) { getFormData(id: string) {
return request<any, ConfigForm>({ return request<any, ConfigForm>({
url: `${CONFIG_BASE_URL}/${id}/form`, url: `${CONFIG_BASE_URL}/${id}/form`,
method: "get", method: 'get'
}); })
}, },
/** 新增配置 */ /** 新增配置 */
create(data: ConfigForm) { create(data: ConfigForm) {
return request({ url: `${CONFIG_BASE_URL}`, method: "post", data }); return request({ url: `${CONFIG_BASE_URL}`, method: 'post', data })
}, },
/** 修改配置 */ /** 修改配置 */
update(id: string, data: ConfigForm) { update(id: string, data: ConfigForm) {
return request({ url: `${CONFIG_BASE_URL}/${id}`, method: "put", data }); return request({ url: `${CONFIG_BASE_URL}/${id}`, method: 'put', data })
}, },
/** 删除配置 */ /** 删除配置 */
deleteById(id: string) { deleteById(id: string) {
return request({ url: `${CONFIG_BASE_URL}/${id}`, method: "delete" }); return request({ url: `${CONFIG_BASE_URL}/${id}`, method: 'delete' })
}, },
/** 刷新配置缓存 */ /** 刷新配置缓存 */
refreshCache() { refreshCache() {
return request({ url: `${CONFIG_BASE_URL}/refresh`, method: "PUT" }); return request({ url: `${CONFIG_BASE_URL}/refresh`, method: 'PUT' })
}, }
}; }
export default ConfigAPI; export default ConfigAPI
export interface ConfigPageQuery extends PageQuery { export interface ConfigPageQuery extends PageQuery {
/** 搜索关键字 */ /** 搜索关键字 */
keywords?: string; keywords?: string
} }
export interface ConfigForm { export interface ConfigForm {
/** 主键 */ /** 主键 */
id?: string; id?: string
/** 配置名称 */ /** 配置名称 */
configName?: string; configName?: string
/** 配置键 */ /** 配置键 */
configKey?: string; configKey?: string
/** 配置值 */ /** 配置值 */
configValue?: string; configValue?: string
/** 描述、备注 */ /** 描述、备注 */
remark?: string; remark?: string
} }
export interface ConfigPageVO { export interface ConfigPageVO {
/** 主键 */ /** 主键 */
id?: string; id?: string
/** 配置名称 */ /** 配置名称 */
configName?: string; configName?: string
/** 配置键 */ /** 配置键 */
configKey?: string; configKey?: string
/** 配置值 */ /** 配置值 */
configValue?: string; configValue?: string
/** 描述、备注 */ /** 描述、备注 */
remark?: string; remark?: string
} }

View File

@@ -1,75 +1,75 @@
import request from "@/utils/request"; import request from '@/utils/request'
const DEPT_BASE_URL = "/api/v1/dept"; const DEPT_BASE_URL = '/api/v1/dept'
const DeptAPI = { const DeptAPI = {
/** 获取部门树形列表 */ /** 获取部门树形列表 */
getList(queryParams?: DeptQuery) { getList(queryParams?: DeptQuery) {
return request<any, DeptVO[]>({ url: `${DEPT_BASE_URL}`, method: "get", params: queryParams }); return request<any, DeptVO[]>({ url: `${DEPT_BASE_URL}`, method: 'get', params: queryParams })
}, },
/** 获取部门下拉数据源 */ /** 获取部门下拉数据源 */
getOptions() { getOptions() {
return request<any, OptionType[]>({ url: `${DEPT_BASE_URL}/options`, method: "get" }); return request<any, OptionType[]>({ url: `${DEPT_BASE_URL}/options`, method: 'get' })
}, },
/** 获取部门表单数据 */ /** 获取部门表单数据 */
getFormData(id: string) { getFormData(id: string) {
return request<any, DeptForm>({ url: `${DEPT_BASE_URL}/${id}/form`, method: "get" }); return request<any, DeptForm>({ url: `${DEPT_BASE_URL}/${id}/form`, method: 'get' })
}, },
/** 新增部门 */ /** 新增部门 */
create(data: DeptForm) { create(data: DeptForm) {
return request({ url: `${DEPT_BASE_URL}`, method: "post", data }); return request({ url: `${DEPT_BASE_URL}`, method: 'post', data })
}, },
/** 修改部门 */ /** 修改部门 */
update(id: string, data: DeptForm) { update(id: string, data: DeptForm) {
return request({ url: `${DEPT_BASE_URL}/${id}`, method: "put", data }); return request({ url: `${DEPT_BASE_URL}/${id}`, method: 'put', data })
}, },
/** 批量删除部门,多个以英文逗号(,)分割 */ /** 批量删除部门,多个以英文逗号(,)分割 */
deleteByIds(ids: string) { deleteByIds(ids: string) {
return request({ url: `${DEPT_BASE_URL}/${ids}`, method: "delete" }); return request({ url: `${DEPT_BASE_URL}/${ids}`, method: 'delete' })
}, }
}; }
export default DeptAPI; export default DeptAPI
export interface DeptQuery { export interface DeptQuery {
/** 搜索关键字 */ /** 搜索关键字 */
keywords?: string; keywords?: string
/** 状态 */ /** 状态 */
status?: number; status?: number
} }
export interface DeptVO { export interface DeptVO {
/** 子部门 */ /** 子部门 */
children?: DeptVO[]; children?: DeptVO[]
/** 创建时间 */ /** 创建时间 */
createTime?: Date; createTime?: Date
/** 部门ID */ /** 部门ID */
id?: string; id?: string
/** 部门名称 */ /** 部门名称 */
name?: string; name?: string
/** 部门编号 */ /** 部门编号 */
code?: string; code?: string
/** 父部门ID */ /** 父部门ID */
parentid?: string; parentid?: string
/** 排序 */ /** 排序 */
sort?: number; sort?: number
/** 状态(1:启用0:禁用) */ /** 状态(1:启用0:禁用) */
status?: number; status?: number
/** 修改时间 */ /** 修改时间 */
updateTime?: Date; updateTime?: Date
} }
export interface DeptForm { export interface DeptForm {
/** 部门ID(新增不填) */ /** 部门ID(新增不填) */
id?: string; id?: string
/** 部门名称 */ /** 部门名称 */
name?: string; name?: string
/** 部门编号 */ /** 部门编号 */
code?: string; code?: string
/** 父部门ID */ /** 父部门ID */
parentId: string; parentId: string
/** 排序 */ /** 排序 */
sort?: number; sort?: number
/** 状态(1:启用0禁用) */ /** 状态(1:启用0禁用) */
status?: number; status?: number
} }

View File

@@ -1,145 +1,145 @@
import request from "@/utils/request"; import request from '@/utils/request'
const DICT_BASE_URL = "/api/v1/dicts"; const DICT_BASE_URL = '/api/v1/dicts'
const DictAPI = { const DictAPI = {
/** 字典分页列表 */ /** 字典分页列表 */
getPage(queryParams: DictPageQuery) { getPage(queryParams: DictPageQuery) {
return request<any, PageResult<DictPageVO[]>>({ return request<any, PageResult<DictPageVO[]>>({
url: `${DICT_BASE_URL}/page`, url: `${DICT_BASE_URL}/page`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, },
/** 字典列表 */ /** 字典列表 */
getList() { getList() {
return request<any, OptionType[]>({ url: `${DICT_BASE_URL}`, method: "get" }); return request<any, OptionType[]>({ url: `${DICT_BASE_URL}`, method: 'get' })
}, },
/** 字典表单数据 */ /** 字典表单数据 */
getFormData(id: string) { getFormData(id: string) {
return request<any, DictForm>({ url: `${DICT_BASE_URL}/${id}/form`, method: "get" }); return request<any, DictForm>({ url: `${DICT_BASE_URL}/${id}/form`, method: 'get' })
}, },
/** 新增字典 */ /** 新增字典 */
create(data: DictForm) { create(data: DictForm) {
return request({ url: `${DICT_BASE_URL}`, method: "post", data }); return request({ url: `${DICT_BASE_URL}`, method: 'post', data })
}, },
/** 修改字典 */ /** 修改字典 */
update(id: string, data: DictForm) { update(id: string, data: DictForm) {
return request({ url: `${DICT_BASE_URL}/${id}`, method: "put", data }); return request({ url: `${DICT_BASE_URL}/${id}`, method: 'put', data })
}, },
/** 删除字典 */ /** 删除字典 */
deleteByIds(ids: string) { deleteByIds(ids: string) {
return request({ url: `${DICT_BASE_URL}/${ids}`, method: "delete" }); return request({ url: `${DICT_BASE_URL}/${ids}`, method: 'delete' })
}, },
/** 获取字典项分页列表 */ /** 获取字典项分页列表 */
getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) { getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) {
return request<any, PageResult<DictItemPageVO[]>>({ return request<any, PageResult<DictItemPageVO[]>>({
url: `${DICT_BASE_URL}/${dictCode}/items/page`, url: `${DICT_BASE_URL}/${dictCode}/items/page`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, },
/** 获取字典项列表 */ /** 获取字典项列表 */
getDictItems(dictCode: string) { getDictItems(dictCode: string) {
return request<any, DictItemOption[]>({ return request<any, DictItemOption[]>({
url: `${DICT_BASE_URL}/${dictCode}/items`, url: `${DICT_BASE_URL}/${dictCode}/items`,
method: "get", method: 'get'
}); })
}, },
/** 新增字典项 */ /** 新增字典项 */
createDictItem(dictCode: string, data: DictItemForm) { createDictItem(dictCode: string, data: DictItemForm) {
return request({ url: `${DICT_BASE_URL}/${dictCode}/items`, method: "post", data }); return request({ url: `${DICT_BASE_URL}/${dictCode}/items`, method: 'post', data })
}, },
/** 获取字典项表单数据 */ /** 获取字典项表单数据 */
getDictItemFormData(dictCode: string, id: string) { getDictItemFormData(dictCode: string, id: string) {
return request<any, DictItemForm>({ return request<any, DictItemForm>({
url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`, url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`,
method: "get", method: 'get'
}); })
}, },
/** 修改字典项 */ /** 修改字典项 */
updateDictItem(dictCode: string, id: string, data: DictItemForm) { updateDictItem(dictCode: string, id: string, data: DictItemForm) {
return request({ url: `${DICT_BASE_URL}/${dictCode}/items/${id}`, method: "put", data }); return request({ url: `${DICT_BASE_URL}/${dictCode}/items/${id}`, method: 'put', data })
}, },
/** 删除字典项 */ /** 删除字典项 */
deleteDictItems(dictCode: string, ids: string) { deleteDictItems(dictCode: string, ids: string) {
return request({ url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`, method: "delete" }); return request({ url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`, method: 'delete' })
}, }
}; }
export default DictAPI; export default DictAPI
export interface DictPageQuery extends PageQuery { export interface DictPageQuery extends PageQuery {
/** 搜索关键字 */ /** 搜索关键字 */
keywords?: string; keywords?: string
/** 状态(1:启用;0:禁用) */ /** 状态(1:启用;0:禁用) */
status?: number; status?: number
} }
export interface DictPageVO { export interface DictPageVO {
/** 字典ID */ /** 字典ID */
id: string; id: string
/** 字典名称 */ /** 字典名称 */
name: string; name: string
/** 字典编码 */ /** 字典编码 */
dictCode: string; dictCode: string
/** 状态(1:启用;0:禁用) */ /** 状态(1:启用;0:禁用) */
status: number; status: number
} }
export interface DictForm { export interface DictForm {
/** 字典ID(新增不填) */ /** 字典ID(新增不填) */
id?: string; id?: string
/** 字典名称 */ /** 字典名称 */
name?: string; name?: string
/** 字典编码 */ /** 字典编码 */
dictCode?: string; dictCode?: string
/** 状态(1:启用;0:禁用) */ /** 状态(1:启用;0:禁用) */
status?: number; status?: number
/** 备注 */ /** 备注 */
remark?: string; remark?: string
} }
export interface DictItemPageQuery extends PageQuery { export interface DictItemPageQuery extends PageQuery {
/** 搜索关键字 */ /** 搜索关键字 */
keywords?: string; keywords?: string
/** 字典编码 */ /** 字典编码 */
dictCode?: string; dictCode?: string
} }
export interface DictItemPageVO { export interface DictItemPageVO {
/** 字典项ID */ /** 字典项ID */
id: string; id: string
/** 字典编码 */ /** 字典编码 */
dictCode: string; dictCode: string
/** 字典项值 */ /** 字典项值 */
value: string; value: string
/** 字典项标签 */ /** 字典项标签 */
label: string; label: string
/** 状态(1:启用;0:禁用) */ /** 状态(1:启用;0:禁用) */
status: number; status: number
/** 排序 */ /** 排序 */
sort?: number; sort?: number
} }
export interface DictItemForm { export interface DictItemForm {
/** 字典项ID(新增不填) */ /** 字典项ID(新增不填) */
id?: string; id?: string
/** 字典编码 */ /** 字典编码 */
dictCode?: string; dictCode?: string
/** 字典项值 */ /** 字典项值 */
value?: string; value?: string
/** 字典项标签 */ /** 字典项标签 */
label?: string; label?: string
/** 状态(1:启用;0:禁用) */ /** 状态(1:启用;0:禁用) */
status?: number; status?: number
/** 排序 */ /** 排序 */
sort?: number; sort?: number
/** 标签类型 */ /** 标签类型 */
tagType?: "success" | "warning" | "info" | "primary" | "danger" | ""; tagType?: 'success' | 'warning' | 'info' | 'primary' | 'danger' | ''
} }
export interface DictItemOption { export interface DictItemOption {
/** 值 */ /** 值 */
value: number | string; value: number | string
/** 标签 */ /** 标签 */
label: string; label: string
/** 标签类型 */ /** 标签类型 */
tagType?: "" | "success" | "info" | "warning" | "danger"; tagType?: '' | 'success' | 'info' | 'warning' | 'danger'
[key: string]: any; [key: string]: any
} }

View File

@@ -1,89 +1,89 @@
import request from "@/utils/request"; import request from '@/utils/request'
const LOG_BASE_URL = "/api/v1/logs"; const LOG_BASE_URL = '/api/v1/logs'
const LogAPI = { const LogAPI = {
/** 获取日志分页列表 */ /** 获取日志分页列表 */
getPage(queryParams: LogPageQuery) { getPage(queryParams: LogPageQuery) {
return request<any, PageResult<LogPageVO[]>>({ return request<any, PageResult<LogPageVO[]>>({
url: `${LOG_BASE_URL}/page`, url: `${LOG_BASE_URL}/page`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, },
/** 获取访问趋势 */ /** 获取访问趋势 */
getVisitTrend(queryParams: VisitTrendQuery) { getVisitTrend(queryParams: VisitTrendQuery) {
return request<any, VisitTrendVO>({ return request<any, VisitTrendVO>({
url: `${LOG_BASE_URL}/visit-trend`, url: `${LOG_BASE_URL}/visit-trend`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, },
/** 获取访问统计 */ /** 获取访问统计 */
getVisitStats() { getVisitStats() {
return request<any, VisitStatsVO>({ url: `${LOG_BASE_URL}/visit-stats`, method: "get" }); return request<any, VisitStatsVO>({ url: `${LOG_BASE_URL}/visit-stats`, method: 'get' })
}, }
}; }
export default LogAPI; export default LogAPI
export interface LogPageQuery extends PageQuery { export interface LogPageQuery extends PageQuery {
/** 搜索关键字 */ /** 搜索关键字 */
keywords?: string; keywords?: string
/** 操作时间 */ /** 操作时间 */
createTime?: [string, string]; createTime?: [string, string]
} }
export interface LogPageVO { export interface LogPageVO {
/** 主键 */ /** 主键 */
id: string; id: string
/** 日志模块 */ /** 日志模块 */
module: string; module: string
/** 日志内容 */ /** 日志内容 */
content: string; content: string
/** 请求路径 */ /** 请求路径 */
requestUri: string; requestUri: string
/** 请求方法 */ /** 请求方法 */
method: string; method: string
/** IP 地址 */ /** IP 地址 */
ip: string; ip: string
/** 地区 */ /** 地区 */
region: string; region: string
/** 浏览器 */ /** 浏览器 */
browser: string; browser: string
/** 终端系统 */ /** 终端系统 */
os: string; os: string
/** 执行时间(毫秒) */ /** 执行时间(毫秒) */
executionTime: number; executionTime: number
/** 操作人 */ /** 操作人 */
operator: string; operator: string
} }
export interface VisitTrendVO { export interface VisitTrendVO {
/** 日期列表 */ /** 日期列表 */
dates: string[]; dates: string[]
/** 浏览量(PV) */ /** 浏览量(PV) */
pvList: number[]; pvList: number[]
/** 访客数(UV) */ /** 访客数(UV) */
uvList: number[]; uvList: number[]
/** IP数 */ /** IP数 */
ipList: number[]; ipList: number[]
} }
export interface VisitTrendQuery { export interface VisitTrendQuery {
/** 开始日期 */ /** 开始日期 */
startDate: string; startDate: string
/** 结束日期 */ /** 结束日期 */
endDate: string; endDate: string
} }
export interface VisitStatsVO { export interface VisitStatsVO {
/** 今日访客数(UV) */ /** 今日访客数(UV) */
todayUvCount: number; todayUvCount: number
/** 总访客数 */ /** 总访客数 */
totalUvCount: number; totalUvCount: number
/** 访客数同比增长率(相对于昨天同一时间段的增长率) */ /** 访客数同比增长率(相对于昨天同一时间段的增长率) */
uvGrowthRate: number; uvGrowthRate: number
/** 今日浏览量(PV) */ /** 今日浏览量(PV) */
todayPvCount: number; todayPvCount: number
/** 总浏览量 */ /** 总浏览量 */
totalPvCount: number; totalPvCount: number
/** 同比增长率(相对于昨天同一时间段的增长率) */ /** 同比增长率(相对于昨天同一时间段的增长率) */
pvGrowthRate: number; pvGrowthRate: number
} }

View File

@@ -1,135 +1,135 @@
import request from "@/utils/request"; import request from '@/utils/request'
const MENU_BASE_URL = "/api/v1/menus"; const MENU_BASE_URL = '/api/v1/menus'
const MenuAPI = { const MenuAPI = {
/** 获取当前用户的路由列表 */ /** 获取当前用户的路由列表 */
getRoutes() { getRoutes() {
return request<any, RouteVO[]>({ url: `${MENU_BASE_URL}/routes`, method: "get" }); return request<any, RouteVO[]>({ url: `${MENU_BASE_URL}/routes`, method: 'get' })
}, },
/** 获取菜单树形列表 */ /** 获取菜单树形列表 */
getList(queryParams: MenuQuery) { getList(queryParams: MenuQuery) {
return request<any, MenuVO[]>({ url: `${MENU_BASE_URL}`, method: "get", params: queryParams }); return request<any, MenuVO[]>({ url: `${MENU_BASE_URL}`, method: 'get', params: queryParams })
}, },
/** 获取菜单下拉数据源 */ /** 获取菜单下拉数据源 */
getOptions(onlyParent?: boolean) { getOptions(onlyParent?: boolean) {
return request<any, OptionType[]>({ return request<any, OptionType[]>({
url: `${MENU_BASE_URL}/options`, url: `${MENU_BASE_URL}/options`,
method: "get", method: 'get',
params: { onlyParent }, params: { onlyParent }
}); })
}, },
/** 获取菜单表单数据 */ /** 获取菜单表单数据 */
getFormData(id: string) { getFormData(id: string) {
return request<any, MenuForm>({ url: `${MENU_BASE_URL}/${id}/form`, method: "get" }); return request<any, MenuForm>({ url: `${MENU_BASE_URL}/${id}/form`, method: 'get' })
}, },
/** 新增菜单 */ /** 新增菜单 */
create(data: MenuForm) { create(data: MenuForm) {
return request({ url: `${MENU_BASE_URL}`, method: "post", data }); return request({ url: `${MENU_BASE_URL}`, method: 'post', data })
}, },
/** 修改菜单 */ /** 修改菜单 */
update(id: string, data: MenuForm) { update(id: string, data: MenuForm) {
return request({ url: `${MENU_BASE_URL}/${id}`, method: "put", data }); return request({ url: `${MENU_BASE_URL}/${id}`, method: 'put', data })
}, },
/** 删除菜单 */ /** 删除菜单 */
deleteById(id: string) { deleteById(id: string) {
return request({ url: `${MENU_BASE_URL}/${id}`, method: "delete" }); return request({ url: `${MENU_BASE_URL}/${id}`, method: 'delete' })
}, }
}; }
export default MenuAPI; export default MenuAPI
export interface MenuQuery { export interface MenuQuery {
/** 搜索关键字 */ /** 搜索关键字 */
keywords?: string; keywords?: string
} }
import type { MenuTypeEnum } from "@/enums/system/menu-enum"; import type { MenuTypeEnum } from '@/enums/system/menu-enum'
export interface MenuVO { export interface MenuVO {
/** 子菜单 */ /** 子菜单 */
children?: MenuVO[]; children?: MenuVO[]
/** 组件路径 */ /** 组件路径 */
component?: string; component?: string
/** ICON */ /** ICON */
icon?: string; icon?: string
/** 菜单ID */ /** 菜单ID */
id?: string; id?: string
/** 菜单名称 */ /** 菜单名称 */
name?: string; name?: string
/** 父菜单ID */ /** 父菜单ID */
parentId?: string; parentId?: string
/** 按钮权限标识 */ /** 按钮权限标识 */
perm?: string; perm?: string
/** 跳转路径 */ /** 跳转路径 */
redirect?: string; redirect?: string
/** 路由名称 */ /** 路由名称 */
routeName?: string; routeName?: string
/** 路由相对路径 */ /** 路由相对路径 */
routePath?: string; routePath?: string
/** 菜单排序(数字越小排名越靠前) */ /** 菜单排序(数字越小排名越靠前) */
sort?: number; sort?: number
/** 菜单类型 */ /** 菜单类型 */
type?: MenuTypeEnum; type?: MenuTypeEnum
/** 是否可见(1:显示;0:隐藏) */ /** 是否可见(1:显示;0:隐藏) */
visible?: number; visible?: number
} }
export interface MenuForm { export interface MenuForm {
/** 菜单ID */ /** 菜单ID */
id?: string; id?: string
/** 父菜单ID */ /** 父菜单ID */
parentId?: string; parentId?: string
/** 菜单名称 */ /** 菜单名称 */
name?: string; name?: string
/** 是否可见(1-是 0-否) */ /** 是否可见(1-是 0-否) */
visible: number; visible: number
/** ICON */ /** ICON */
icon?: string; icon?: string
/** 排序 */ /** 排序 */
sort?: number; sort?: number
/** 路由名称 */ /** 路由名称 */
routeName?: string; routeName?: string
/** 路由路径 */ /** 路由路径 */
routePath?: string; routePath?: string
/** 组件路径 */ /** 组件路径 */
component?: string; component?: string
/** 跳转路由路径 */ /** 跳转路由路径 */
redirect?: string; redirect?: string
/** 菜单类型 */ /** 菜单类型 */
type?: MenuTypeEnum; type?: MenuTypeEnum
/** 权限标识 */ /** 权限标识 */
perm?: string; perm?: string
/** 【菜单】是否开启页面缓存 */ /** 【菜单】是否开启页面缓存 */
keepAlive?: number; keepAlive?: number
/** 【目录】只有一个子路由是否始终显示 */ /** 【目录】只有一个子路由是否始终显示 */
alwaysShow?: number; alwaysShow?: number
/** 其他参数 */ /** 其他参数 */
params?: KeyValue[]; params?: KeyValue[]
} }
interface KeyValue { interface KeyValue {
key: string; key: string
value: string; value: string
} }
export interface RouteVO { export interface RouteVO {
/** 子路由列表 */ /** 子路由列表 */
children: RouteVO[]; children: RouteVO[]
/** 组件路径 */ /** 组件路径 */
component?: string; component?: string
/** 路由属性 */ /** 路由属性 */
meta?: Meta; meta?: Meta
/** 路由名称 */ /** 路由名称 */
name?: string; name?: string
/** 路由路径 */ /** 路由路径 */
path?: string; path?: string
/** 跳转链接 */ /** 跳转链接 */
redirect?: string; redirect?: string
} }
export interface Meta { export interface Meta {
/** 【目录】只有一个子路由是否始终显示 */ /** 【目录】只有一个子路由是否始终显示 */
alwaysShow?: boolean; alwaysShow?: boolean
/** 是否隐藏(true-是 false-否) */ /** 是否隐藏(true-是 false-否) */
hidden?: boolean; hidden?: boolean
/** ICON */ /** ICON */
icon?: string; icon?: string
/** 【菜单】是否开启页面缓存 */ /** 【菜单】是否开启页面缓存 */
keepAlive?: boolean; keepAlive?: boolean
/** 路由title */ /** 路由title */
title?: string; title?: string
} }

View File

@@ -1,121 +1,121 @@
import request from "@/utils/request"; import request from '@/utils/request'
const NOTICE_BASE_URL = "/api/v1/notices"; const NOTICE_BASE_URL = '/api/v1/notices'
const NoticeAPI = { const NoticeAPI = {
/** 获取通知公告分页数据 */ /** 获取通知公告分页数据 */
getPage(queryParams?: NoticePageQuery) { getPage(queryParams?: NoticePageQuery) {
return request<any, PageResult<NoticePageVO[]>>({ return request<any, PageResult<NoticePageVO[]>>({
url: `${NOTICE_BASE_URL}/page`, url: `${NOTICE_BASE_URL}/page`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, },
/** 获取通知公告表单数据 */ /** 获取通知公告表单数据 */
getFormData(id: string) { getFormData(id: string) {
return request<any, NoticeForm>({ url: `${NOTICE_BASE_URL}/${id}/form`, method: "get" }); return request<any, NoticeForm>({ url: `${NOTICE_BASE_URL}/${id}/form`, method: 'get' })
}, },
/** 添加通知公告 */ /** 添加通知公告 */
create(data: NoticeForm) { create(data: NoticeForm) {
return request({ url: `${NOTICE_BASE_URL}`, method: "post", data }); return request({ url: `${NOTICE_BASE_URL}`, method: 'post', data })
}, },
/** 更新通知公告 */ /** 更新通知公告 */
update(id: string, data: NoticeForm) { update(id: string, data: NoticeForm) {
return request({ url: `${NOTICE_BASE_URL}/${id}`, method: "put", data }); return request({ url: `${NOTICE_BASE_URL}/${id}`, method: 'put', data })
}, },
/** 批量删除通知公告,多个以英文逗号(,)分割 */ /** 批量删除通知公告,多个以英文逗号(,)分割 */
deleteByIds(ids: string) { deleteByIds(ids: string) {
return request({ url: `${NOTICE_BASE_URL}/${ids}`, method: "delete" }); return request({ url: `${NOTICE_BASE_URL}/${ids}`, method: 'delete' })
}, },
/** 发布通知 */ /** 发布通知 */
publish(id: string) { publish(id: string) {
return request({ url: `${NOTICE_BASE_URL}/${id}/publish`, method: "put" }); return request({ url: `${NOTICE_BASE_URL}/${id}/publish`, method: 'put' })
}, },
/** 撤回通知 */ /** 撤回通知 */
revoke(id: string) { revoke(id: string) {
return request({ url: `${NOTICE_BASE_URL}/${id}/revoke`, method: "put" }); return request({ url: `${NOTICE_BASE_URL}/${id}/revoke`, method: 'put' })
}, },
/** 查看通知 */ /** 查看通知 */
getDetail(id: string) { getDetail(id: string) {
return request<any, NoticeDetailVO>({ url: `${NOTICE_BASE_URL}/${id}/detail`, method: "get" }); return request<any, NoticeDetailVO>({ url: `${NOTICE_BASE_URL}/${id}/detail`, method: 'get' })
}, },
/** 全部已读 */ /** 全部已读 */
readAll() { readAll() {
return request({ url: `${NOTICE_BASE_URL}/read-all`, method: "put" }); return request({ url: `${NOTICE_BASE_URL}/read-all`, method: 'put' })
}, },
/** 获取我的通知分页列表 */ /** 获取我的通知分页列表 */
getMyNoticePage(queryParams?: NoticePageQuery) { getMyNoticePage(queryParams?: NoticePageQuery) {
return request<any, PageResult<NoticePageVO[]>>({ return request<any, PageResult<NoticePageVO[]>>({
url: `${NOTICE_BASE_URL}/my-page`, url: `${NOTICE_BASE_URL}/my-page`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, }
}; }
export default NoticeAPI; export default NoticeAPI
export interface NoticePageQuery extends PageQuery { export interface NoticePageQuery extends PageQuery {
/** 标题 */ /** 标题 */
title?: string; title?: string
/** 发布状态(0:草稿;1:已发布;2:已撤回) */ /** 发布状态(0:草稿;1:已发布;2:已撤回) */
publishStatus?: number; publishStatus?: number
/** 是否已读(1:是;0:否) */ /** 是否已读(1:是;0:否) */
isRead?: number; isRead?: number
} }
export interface NoticeForm { export interface NoticeForm {
/** 通知ID(新增不填) */ /** 通知ID(新增不填) */
id?: string; id?: string
/** 标题 */ /** 标题 */
title?: string; title?: string
/** 内容 */ /** 内容 */
content?: string; content?: string
/** 类型 */ /** 类型 */
type?: number; type?: number
/** 优先级/级别 */ /** 优先级/级别 */
level?: string; level?: string
/** 目标类型 */ /** 目标类型 */
targetType?: number; targetType?: number
/** 目标用户ID(多个以英文逗号(,)分割) */ /** 目标用户ID(多个以英文逗号(,)分割) */
targetUserIds?: string; targetUserIds?: string
} }
export interface NoticePageVO { export interface NoticePageVO {
/** 通知ID */ /** 通知ID */
id: string; id: string
/** 标题 */ /** 标题 */
title?: string; title?: string
/** 内容 */ /** 内容 */
content?: string; content?: string
/** 类型 */ /** 类型 */
type?: number; type?: number
/** 发布人ID */ /** 发布人ID */
publisherId?: bigint; publisherId?: bigint
/** 优先级 */ /** 优先级 */
priority?: number; priority?: number
/** 目标类型 */ /** 目标类型 */
targetType?: number; targetType?: number
/** 发布状态 */ /** 发布状态 */
publishStatus?: number; publishStatus?: number
/** 发布时间 */ /** 发布时间 */
publishTime?: Date; publishTime?: Date
/** 撤回时间 */ /** 撤回时间 */
revokeTime?: Date; revokeTime?: Date
} }
export interface NoticeDetailVO { export interface NoticeDetailVO {
/** 通知ID */ /** 通知ID */
id?: string; id?: string
/** 标题 */ /** 标题 */
title?: string; title?: string
/** 内容 */ /** 内容 */
content?: string; content?: string
/** 类型 */ /** 类型 */
type?: number; type?: number
/** 发布人名称 */ /** 发布人名称 */
publisherName?: string; publisherName?: string
/** 优先级/级别 */ /** 优先级/级别 */
level?: string; level?: string
/** 发布时间 */ /** 发布时间 */
publishTime?: Date; publishTime?: Date
/** 发布状态 */ /** 发布状态 */
publishStatus?: number; publishStatus?: number
} }

View File

@@ -1,79 +1,79 @@
import request from "@/utils/request"; import request from '@/utils/request'
const ROLE_BASE_URL = "/api/v1/roles"; const ROLE_BASE_URL = '/api/v1/roles'
const RoleAPI = { const RoleAPI = {
/** 获取角色分页数据 */ /** 获取角色分页数据 */
getPage(queryParams?: RolePageQuery) { getPage(queryParams?: RolePageQuery) {
return request<any, PageResult<RolePageVO[]>>({ return request<any, PageResult<RolePageVO[]>>({
url: `${ROLE_BASE_URL}/page`, url: `${ROLE_BASE_URL}/page`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, },
/** 获取角色下拉数据源 */ /** 获取角色下拉数据源 */
getOptions() { getOptions() {
return request<any, OptionType[]>({ url: `${ROLE_BASE_URL}/options`, method: "get" }); return request<any, OptionType[]>({ url: `${ROLE_BASE_URL}/options`, method: 'get' })
}, },
/** 获取角色的菜单ID集合 */ /** 获取角色的菜单ID集合 */
getRoleMenuIds(roleId: string) { getRoleMenuIds(roleId: string) {
return request<any, string[]>({ url: `${ROLE_BASE_URL}/${roleId}/menuIds`, method: "get" }); return request<any, string[]>({ url: `${ROLE_BASE_URL}/${roleId}/menuIds`, method: 'get' })
}, },
/** 分配菜单权限 */ /** 分配菜单权限 */
updateRoleMenus(roleId: string, data: number[]) { updateRoleMenus(roleId: string, data: number[]) {
return request({ url: `${ROLE_BASE_URL}/${roleId}/menus`, method: "put", data }); return request({ url: `${ROLE_BASE_URL}/${roleId}/menus`, method: 'put', data })
}, },
/** 获取角色表单数据 */ /** 获取角色表单数据 */
getFormData(id: string) { getFormData(id: string) {
return request<any, RoleForm>({ url: `${ROLE_BASE_URL}/${id}/form`, method: "get" }); return request<any, RoleForm>({ url: `${ROLE_BASE_URL}/${id}/form`, method: 'get' })
}, },
/** 新增角色 */ /** 新增角色 */
create(data: RoleForm) { create(data: RoleForm) {
return request({ url: `${ROLE_BASE_URL}`, method: "post", data }); return request({ url: `${ROLE_BASE_URL}`, method: 'post', data })
}, },
/** 更新角色 */ /** 更新角色 */
update(id: string, data: RoleForm) { update(id: string, data: RoleForm) {
return request({ url: `${ROLE_BASE_URL}/${id}`, method: "put", data }); return request({ url: `${ROLE_BASE_URL}/${id}`, method: 'put', data })
}, },
/** 批量删除角色,多个以英文逗号(,)分割 */ /** 批量删除角色,多个以英文逗号(,)分割 */
deleteByIds(ids: string) { deleteByIds(ids: string) {
return request({ url: `${ROLE_BASE_URL}/${ids}`, method: "delete" }); return request({ url: `${ROLE_BASE_URL}/${ids}`, method: 'delete' })
}, }
}; }
export default RoleAPI; export default RoleAPI
export interface RolePageQuery extends PageQuery { export interface RolePageQuery extends PageQuery {
/** 搜索关键字 */ /** 搜索关键字 */
keywords?: string; keywords?: string
} }
export interface RolePageVO { export interface RolePageVO {
/** 角色ID */ /** 角色ID */
id?: string; id?: string
/** 角色编码 */ /** 角色编码 */
code?: string; code?: string
/** 角色名称 */ /** 角色名称 */
name?: string; name?: string
/** 排序 */ /** 排序 */
sort?: number; sort?: number
/** 角色状态 */ /** 角色状态 */
status?: number; status?: number
/** 创建时间 */ /** 创建时间 */
createTime?: Date; createTime?: Date
/** 修改时间 */ /** 修改时间 */
updateTime?: Date; updateTime?: Date
} }
export interface RoleForm { export interface RoleForm {
/** 角色ID */ /** 角色ID */
id?: string; id?: string
/** 角色编码 */ /** 角色编码 */
code?: string; code?: string
/** 数据权限 */ /** 数据权限 */
dataScope?: number; dataScope?: number
/** 角色名称 */ /** 角色名称 */
name?: string; name?: string
/** 排序 */ /** 排序 */
sort?: number; sort?: number
/** 角色状态(1-正常0-停用) */ /** 角色状态(1-正常0-停用) */
status?: number; status?: number
} }

View File

@@ -1,6 +1,6 @@
import request from "@/utils/request"; import request from '@/utils/request'
const USER_BASE_URL = "/api/v1/users"; const USER_BASE_URL = '/api/v1/users'
const UserAPI = { const UserAPI = {
/** /**
@@ -11,8 +11,8 @@ const UserAPI = {
getInfo() { getInfo() {
return request<any, UserInfo>({ return request<any, UserInfo>({
url: `${USER_BASE_URL}/me`, url: `${USER_BASE_URL}/me`,
method: "get", method: 'get'
}); })
}, },
/** /**
@@ -23,9 +23,9 @@ const UserAPI = {
getPage(queryParams: UserPageQuery) { getPage(queryParams: UserPageQuery) {
return request<any, PageResult<UserPageVO[]>>({ return request<any, PageResult<UserPageVO[]>>({
url: `${USER_BASE_URL}/page`, url: `${USER_BASE_URL}/page`,
method: "get", method: 'get',
params: queryParams, params: queryParams
}); })
}, },
/** /**
@@ -37,8 +37,8 @@ const UserAPI = {
getFormData(userId: string) { getFormData(userId: string) {
return request<any, UserForm>({ return request<any, UserForm>({
url: `${USER_BASE_URL}/${userId}/form`, url: `${USER_BASE_URL}/${userId}/form`,
method: "get", method: 'get'
}); })
}, },
/** /**
@@ -49,9 +49,9 @@ const UserAPI = {
create(data: UserForm) { create(data: UserForm) {
return request({ return request({
url: `${USER_BASE_URL}`, url: `${USER_BASE_URL}`,
method: "post", method: 'post',
data, data
}); })
}, },
/** /**
@@ -63,9 +63,9 @@ const UserAPI = {
update(id: string, data: UserForm) { update(id: string, data: UserForm) {
return request({ return request({
url: `${USER_BASE_URL}/${id}`, url: `${USER_BASE_URL}/${id}`,
method: "put", method: 'put',
data, data
}); })
}, },
/** /**
@@ -77,9 +77,9 @@ const UserAPI = {
resetPassword(id: string, password: string) { resetPassword(id: string, password: string) {
return request({ return request({
url: `${USER_BASE_URL}/${id}/password/reset`, url: `${USER_BASE_URL}/${id}/password/reset`,
method: "put", method: 'put',
params: { password }, params: { password }
}); })
}, },
/** /**
@@ -90,17 +90,17 @@ const UserAPI = {
deleteByIds(ids: string | number) { deleteByIds(ids: string | number) {
return request({ return request({
url: `${USER_BASE_URL}/${ids}`, url: `${USER_BASE_URL}/${ids}`,
method: "delete", method: 'delete'
}); })
}, },
/** 下载用户导入模板 */ /** 下载用户导入模板 */
downloadTemplate() { downloadTemplate() {
return request({ return request({
url: `${USER_BASE_URL}/template`, url: `${USER_BASE_URL}/template`,
method: "get", method: 'get',
responseType: "blob", responseType: 'blob'
}); })
}, },
/** /**
@@ -111,10 +111,10 @@ const UserAPI = {
export(queryParams: UserPageQuery) { export(queryParams: UserPageQuery) {
return request({ return request({
url: `${USER_BASE_URL}/export`, url: `${USER_BASE_URL}/export`,
method: "get", method: 'get',
params: queryParams, params: queryParams,
responseType: "blob", responseType: 'blob'
}); })
}, },
/** /**
@@ -124,79 +124,79 @@ const UserAPI = {
* @param file 导入文件 * @param file 导入文件
*/ */
import(deptId: string, file: File) { import(deptId: string, file: File) {
const formData = new FormData(); const formData = new FormData()
formData.append("file", file); formData.append('file', file)
return request<any, ExcelResult>({ return request<any, ExcelResult>({
url: `${USER_BASE_URL}/import`, url: `${USER_BASE_URL}/import`,
method: "post", method: 'post',
params: { deptId }, params: { deptId },
data: formData, data: formData,
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data'
}, }
}); })
}, },
/** 获取个人中心用户信息 */ /** 获取个人中心用户信息 */
getProfile() { getProfile() {
return request<any, UserProfileVO>({ return request<any, UserProfileVO>({
url: `${USER_BASE_URL}/profile`, url: `${USER_BASE_URL}/profile`,
method: "get", method: 'get'
}); })
}, },
/** 修改个人中心用户信息 */ /** 修改个人中心用户信息 */
updateProfile(data: UserProfileForm) { updateProfile(data: UserProfileForm) {
return request({ return request({
url: `${USER_BASE_URL}/profile`, url: `${USER_BASE_URL}/profile`,
method: "put", method: 'put',
data, data
}); })
}, },
/** 修改个人中心用户密码 */ /** 修改个人中心用户密码 */
changePassword(data: PasswordChangeForm) { changePassword(data: PasswordChangeForm) {
return request({ return request({
url: `${USER_BASE_URL}/password`, url: `${USER_BASE_URL}/password`,
method: "put", method: 'put',
data, data
}); })
}, },
/** 发送短信验证码(绑定或更换手机号)*/ /** 发送短信验证码(绑定或更换手机号)*/
sendMobileCode(mobile: string) { sendMobileCode(mobile: string) {
return request({ return request({
url: `${USER_BASE_URL}/mobile/code`, url: `${USER_BASE_URL}/mobile/code`,
method: "post", method: 'post',
params: { mobile }, params: { mobile }
}); })
}, },
/** 绑定或更换手机号 */ /** 绑定或更换手机号 */
bindOrChangeMobile(data: MobileUpdateForm) { bindOrChangeMobile(data: MobileUpdateForm) {
return request({ return request({
url: `${USER_BASE_URL}/mobile`, url: `${USER_BASE_URL}/mobile`,
method: "put", method: 'put',
data, data
}); })
}, },
/** 发送邮箱验证码(绑定或更换邮箱)*/ /** 发送邮箱验证码(绑定或更换邮箱)*/
sendEmailCode(email: string) { sendEmailCode(email: string) {
return request({ return request({
url: `${USER_BASE_URL}/email/code`, url: `${USER_BASE_URL}/email/code`,
method: "post", method: 'post',
params: { email }, params: { email }
}); })
}, },
/** 绑定或更换邮箱 */ /** 绑定或更换邮箱 */
bindOrChangeEmail(data: EmailUpdateForm) { bindOrChangeEmail(data: EmailUpdateForm) {
return request({ return request({
url: `${USER_BASE_URL}/email`, url: `${USER_BASE_URL}/email`,
method: "put", method: 'put',
data, data
}); })
}, },
/** /**
@@ -205,32 +205,32 @@ const UserAPI = {
getOptions() { getOptions() {
return request<any, OptionType[]>({ return request<any, OptionType[]>({
url: `${USER_BASE_URL}/options`, url: `${USER_BASE_URL}/options`,
method: "get", method: 'get'
}); })
}, }
}; }
export default UserAPI; export default UserAPI
/** 登录用户信息 */ /** 登录用户信息 */
export interface UserInfo { export interface UserInfo {
/** 用户ID */ /** 用户ID */
userId?: string; userId?: string
/** 用户名 */ /** 用户名 */
username?: string; username?: string
/** 昵称 */ /** 昵称 */
nickname?: string; nickname?: string
/** 头像URL */ /** 头像URL */
avatar?: string; avatar?: string
/** 角色 */ /** 角色 */
roles: string[]; roles: string[]
/** 权限 */ /** 权限 */
perms: string[]; perms: string[]
} }
/** /**
@@ -238,147 +238,147 @@ export interface UserInfo {
*/ */
export interface UserPageQuery extends PageQuery { export interface UserPageQuery extends PageQuery {
/** 搜索关键字 */ /** 搜索关键字 */
keywords?: string; keywords?: string
/** 用户状态 */ /** 用户状态 */
status?: number; status?: number
/** 部门ID */ /** 部门ID */
deptId?: string; deptId?: string
/** 开始时间 */ /** 开始时间 */
createTime?: [string, string]; createTime?: [string, string]
} }
/** 用户分页对象 */ /** 用户分页对象 */
export interface UserPageVO { export interface UserPageVO {
/** 用户ID */ /** 用户ID */
id: string; id: string
/** 用户头像URL */ /** 用户头像URL */
avatar?: string; avatar?: string
/** 创建时间 */ /** 创建时间 */
createTime?: Date; createTime?: Date
/** 部门名称 */ /** 部门名称 */
deptName?: string; deptName?: string
/** 用户邮箱 */ /** 用户邮箱 */
email?: string; email?: string
/** 性别 */ /** 性别 */
gender?: number; gender?: number
/** 手机号 */ /** 手机号 */
mobile?: string; mobile?: string
/** 用户昵称 */ /** 用户昵称 */
nickname?: string; nickname?: string
/** 角色名称,多个使用英文逗号(,)分割 */ /** 角色名称,多个使用英文逗号(,)分割 */
roleNames?: string; roleNames?: string
/** 用户状态(1:启用;0:禁用) */ /** 用户状态(1:启用;0:禁用) */
status?: number; status?: number
/** 用户名 */ /** 用户名 */
username?: string; username?: string
} }
/** 用户表单类型 */ /** 用户表单类型 */
export interface UserForm { export interface UserForm {
/** 用户ID */ /** 用户ID */
id?: string; id?: string
/** 用户头像 */ /** 用户头像 */
avatar?: string; avatar?: string
/** 部门ID */ /** 部门ID */
deptId?: string; deptId?: string
/** 邮箱 */ /** 邮箱 */
email?: string; email?: string
/** 性别 */ /** 性别 */
gender?: number; gender?: number
/** 手机号 */ /** 手机号 */
mobile?: string; mobile?: string
/** 昵称 */ /** 昵称 */
nickname?: string; nickname?: string
/** 角色ID集合 */ /** 角色ID集合 */
roleIds?: number[]; roleIds?: number[]
/** 用户状态(1:正常;0:禁用) */ /** 用户状态(1:正常;0:禁用) */
status?: number; status?: number
/** 用户名 */ /** 用户名 */
username?: string; username?: string
} }
/** 个人中心用户信息 */ /** 个人中心用户信息 */
export interface UserProfileVO { export interface UserProfileVO {
/** 用户ID */ /** 用户ID */
id?: string; id?: string
/** 用户名 */ /** 用户名 */
username?: string; username?: string
/** 昵称 */ /** 昵称 */
nickname?: string; nickname?: string
/** 头像URL */ /** 头像URL */
avatar?: string; avatar?: string
/** 性别 */ /** 性别 */
gender?: number; gender?: number
/** 手机号 */ /** 手机号 */
mobile?: string; mobile?: string
/** 邮箱 */ /** 邮箱 */
email?: string; email?: string
/** 部门名称 */ /** 部门名称 */
deptName?: string; deptName?: string
/** 角色名称,多个使用英文逗号(,)分割 */ /** 角色名称,多个使用英文逗号(,)分割 */
roleNames?: string; roleNames?: string
/** 创建时间 */ /** 创建时间 */
createTime?: Date; createTime?: Date
} }
/** 个人中心用户信息表单 */ /** 个人中心用户信息表单 */
export interface UserProfileForm { export interface UserProfileForm {
/** 用户ID */ /** 用户ID */
id?: string; id?: string
/** 用户名 */ /** 用户名 */
username?: string; username?: string
/** 昵称 */ /** 昵称 */
nickname?: string; nickname?: string
/** 头像URL */ /** 头像URL */
avatar?: string; avatar?: string
/** 性别 */ /** 性别 */
gender?: number; gender?: number
/** 手机号 */ /** 手机号 */
mobile?: string; mobile?: string
/** 邮箱 */ /** 邮箱 */
email?: string; email?: string
} }
/** 修改密码表单 */ /** 修改密码表单 */
export interface PasswordChangeForm { export interface PasswordChangeForm {
/** 原密码 */ /** 原密码 */
oldPassword?: string; oldPassword?: string
/** 新密码 */ /** 新密码 */
newPassword?: string; newPassword?: string
/** 确认新密码 */ /** 确认新密码 */
confirmPassword?: string; confirmPassword?: string
} }
/** 修改手机表单 */ /** 修改手机表单 */
export interface MobileUpdateForm { export interface MobileUpdateForm {
/** 手机号 */ /** 手机号 */
mobile?: string; mobile?: string
/** 验证码 */ /** 验证码 */
code?: string; code?: string
} }
/** 修改邮箱表单 */ /** 修改邮箱表单 */
export interface EmailUpdateForm { export interface EmailUpdateForm {
/** 邮箱 */ /** 邮箱 */
email?: string; email?: string
/** 验证码 */ /** 验证码 */
code?: string; code?: string
} }

View File

@@ -107,89 +107,89 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount } from "vue"; import { onBeforeUnmount } from 'vue'
import { useRouter } from "vue-router"; import { useRouter } from 'vue-router'
import { ElMessage } from "element-plus"; import { ElMessage } from 'element-plus'
import AiCommandApi from "@/api/ai"; import AiCommandApi from '@/api/ai'
type ToolFunctionCall = { type ToolFunctionCall = {
name: string; name: string
arguments: Record<string, any>; arguments: Record<string, any>
}; }
// 统一的动作描述(区分“跳转”、“跳转+执行”、“仅执行”三种场景) // 统一的动作描述(区分“跳转”、“跳转+执行”、“仅执行”三种场景)
type AiAction = type AiAction =
| { | {
type: "navigate"; type: 'navigate'
path: string; path: string
pageName: string; pageName: string
query?: string; query?: string
} }
| { | {
type: "navigate-and-execute"; type: 'navigate-and-execute'
path: string; path: string
pageName: string; pageName: string
query?: string; query?: string
functionCall: ToolFunctionCall; functionCall: ToolFunctionCall
} }
| { | {
type: "execute"; type: 'execute'
functionName: string; functionName: string
functionCall: ToolFunctionCall; functionCall: ToolFunctionCall
}; }
type AiResponse = { type AiResponse = {
explanation: string; explanation: string
action: AiAction | null; action: AiAction | null
}; }
const router = useRouter(); const router = useRouter()
// 状态管理 // 状态管理
const dialogVisible = ref(false); const dialogVisible = ref(false)
const command = ref(""); const command = ref('')
const loading = ref(false); const loading = ref(false)
const response = ref<AiResponse | null>(null); const response = ref<AiResponse | null>(null)
// 快捷命令示例 // 快捷命令示例
const examples = [ const examples = [
"修改test用户的姓名为测试人员", '修改test用户的姓名为测试人员',
"获取姓名为张三的用户信息", '获取姓名为张三的用户信息',
"跳转到用户管理", '跳转到用户管理',
"打开角色管理页面", '打开角色管理页面'
]; ]
// 打开对话框 // 打开对话框
const handleOpen = () => { const handleOpen = () => {
dialogVisible.value = true; dialogVisible.value = true
command.value = ""; command.value = ''
response.value = null; response.value = null
}; }
// 关闭对话框 // 关闭对话框
const handleClose = () => { const handleClose = () => {
dialogVisible.value = false; dialogVisible.value = false
command.value = ""; command.value = ''
response.value = null; response.value = null
}; }
// 执行命令 // 执行命令
const handleExecute = async () => { const handleExecute = async () => {
const rawCommand = command.value.trim(); const rawCommand = command.value.trim()
if (!rawCommand) { if (!rawCommand) {
ElMessage.warning("请输入命令"); ElMessage.warning('请输入命令')
return; return
} }
// 优先检测无需调用 AI 的纯跳转命令 // 优先检测无需调用 AI 的纯跳转命令
const directNavigation = tryDirectNavigate(rawCommand); const directNavigation = tryDirectNavigate(rawCommand)
if (directNavigation && directNavigation.action) { if (directNavigation && directNavigation.action) {
response.value = directNavigation; response.value = directNavigation
await executeAction(directNavigation.action); await executeAction(directNavigation.action)
return; return
} }
loading.value = true; loading.value = true
try { try {
// 调用 AI API 解析命令 // 调用 AI API 解析命令
@@ -198,368 +198,366 @@ const handleExecute = async () => {
currentRoute: router.currentRoute.value.path, currentRoute: router.currentRoute.value.path,
currentComponent: router.currentRoute.value.name as string, currentComponent: router.currentRoute.value.name as string,
context: { context: {
userRoles: [], userRoles: []
}, }
}); })
if (!result.success) { if (!result.success) {
ElMessage.error(result.error || "命令解析失败"); ElMessage.error(result.error || '命令解析失败')
return; return
} }
// 解析 AI 返回的操作类型 // 解析 AI 返回的操作类型
const action = parseAction(result, rawCommand); const action = parseAction(result, rawCommand)
response.value = { response.value = {
explanation: result.explanation ?? "命令解析成功,准备执行操作", explanation: result.explanation ?? '命令解析成功,准备执行操作',
action, action
}; }
// 等待用户确认后执行 // 等待用户确认后执行
if (action) { if (action) {
await executeAction(action); await executeAction(action)
} }
} catch (error: any) { } catch (error: any) {
console.error("AI 命令执行失败:", error); console.error('AI 命令执行失败:', error)
ElMessage.error(error.message || "命令执行失败"); ElMessage.error(error.message || '命令执行失败')
} finally { } finally {
loading.value = false; loading.value = false
} }
}; }
// 路由配置映射表(支持扩展) // 路由配置映射表(支持扩展)
const routeConfig = [ const routeConfig = [
{ keywords: ["用户", "user", "user list"], path: "/system/user", name: "用户管理" }, { keywords: ['用户', 'user', 'user list'], path: '/system/user', name: '用户管理' },
{ keywords: ["角色", "role"], path: "/system/role", name: "角色管理" }, { keywords: ['角色', 'role'], path: '/system/role', name: '角色管理' },
{ keywords: ["菜单", "menu"], path: "/system/menu", name: "菜单管理" }, { keywords: ['菜单', 'menu'], path: '/system/menu', name: '菜单管理' },
{ keywords: ["部门", "dept"], path: "/system/dept", name: "部门管理" }, { keywords: ['部门', 'dept'], path: '/system/dept', name: '部门管理' },
{ keywords: ["字典", "dict"], path: "/system/dict", name: "字典管理" }, { keywords: ['字典', 'dict'], path: '/system/dict', name: '字典管理' },
{ keywords: ["日志", "log"], path: "/system/log", name: "系统日志" }, { keywords: ['日志', 'log'], path: '/system/log', name: '系统日志' }
]; ]
// 根据函数名推断路由(如 getUserInfo -> /system/user // 根据函数名推断路由(如 getUserInfo -> /system/user
const normalizeText = (text: string) => text.replace(/\s+/g, " ").trim().toLowerCase(); const normalizeText = (text: string) => text.replace(/\s+/g, ' ').trim().toLowerCase()
const inferRouteFromFunction = (functionName: string) => { const inferRouteFromFunction = (functionName: string) => {
const fnLower = normalizeText(functionName); const fnLower = normalizeText(functionName)
for (const config of routeConfig) { for (const config of routeConfig) {
// 检查函数名是否包含关键词(如 getUserInfo 包含 user // 检查函数名是否包含关键词(如 getUserInfo 包含 user
if (config.keywords.some((kw) => fnLower.includes(kw.toLowerCase()))) { if (config.keywords.some((kw) => fnLower.includes(kw.toLowerCase()))) {
return { path: config.path, name: config.name }; return { path: config.path, name: config.name }
} }
} }
return null; return null
}; }
// 根据命令文本匹配路由 // 根据命令文本匹配路由
const matchRouteFromCommand = (cmd: string) => { const matchRouteFromCommand = (cmd: string) => {
const normalized = normalizeText(cmd); const normalized = normalizeText(cmd)
for (const config of routeConfig) { for (const config of routeConfig) {
if (config.keywords.some((kw) => normalized.includes(kw.toLowerCase()))) { if (config.keywords.some((kw) => normalized.includes(kw.toLowerCase()))) {
return { path: config.path, name: config.name }; return { path: config.path, name: config.name }
} }
} }
return null; return null
}; }
const extractKeywordFromCommand = (cmd: string): string => { const extractKeywordFromCommand = (cmd: string): string => {
const normalized = normalizeText(cmd); const normalized = normalizeText(cmd)
// 从 routeConfig 动态获取所有数据类型关键词 // 从 routeConfig 动态获取所有数据类型关键词
const allKeywords = routeConfig.flatMap((config) => const allKeywords = routeConfig.flatMap((config) => config.keywords.map((kw) => kw.toLowerCase()))
config.keywords.map((kw) => kw.toLowerCase()) const keywordsPattern = allKeywords.join('|')
);
const keywordsPattern = allKeywords.join("|");
const patterns = [ const patterns = [
new RegExp(`(?:查询|获取|搜索|查找|找).*?([^\\s,。]+?)(?:的)?(?:${keywordsPattern})`, "i"), new RegExp(`(?:查询|获取|搜索|查找|找).*?([^\\s,。]+?)(?:的)?(?:${keywordsPattern})`, 'i'),
new RegExp(`(?:${keywordsPattern}).*?([^\\s,。]+?)(?:的|信息|详情)?`, "i"), new RegExp(`(?:${keywordsPattern}).*?([^\\s,。]+?)(?:的|信息|详情)?`, 'i'),
new RegExp( new RegExp(
`(?:姓名为|名字叫|叫做|名称为|名是|为)([^\\s,。]+?)(?:的)?(?:${keywordsPattern})?`, `(?:姓名为|名字叫|叫做|名称为|名是|为)([^\\s,。]+?)(?:的)?(?:${keywordsPattern})?`,
"i" 'i'
), ),
new RegExp(`([^\\s,。]+?)(?:的)?(?:${keywordsPattern})(?:信息|详情)?`, "i"), new RegExp(`([^\\s,。]+?)(?:的)?(?:${keywordsPattern})(?:信息|详情)?`, 'i')
]; ]
for (const pattern of patterns) { for (const pattern of patterns) {
const match = normalized.match(pattern); const match = normalized.match(pattern)
if (match && match[1]) { if (match && match[1]) {
let extracted = match[1].trim(); let extracted = match[1].trim()
extracted = extracted.replace(/姓名为|名字叫|叫做|名称为|名是|为|的|信息|详情/g, ""); extracted = extracted.replace(/姓名为|名字叫|叫做|名称为|名是|为|的|信息|详情/g, '')
if ( if (
extracted && extracted &&
!allKeywords.some((type) => extracted.toLowerCase().includes(type.toLowerCase())) !allKeywords.some((type) => extracted.toLowerCase().includes(type.toLowerCase()))
) { ) {
return extracted; return extracted
} }
} }
} }
return ""; return ''
}; }
const tryDirectNavigate = (rawCommand: string): AiResponse | null => { const tryDirectNavigate = (rawCommand: string): AiResponse | null => {
const navigationIntents = ["跳转", "打开", "进入", "前往", "去", "浏览", "查看"]; const navigationIntents = ['跳转', '打开', '进入', '前往', '去', '浏览', '查看']
const operationIntents = [ const operationIntents = [
"修改", '修改',
"更新", '更新',
"变更", '变更',
"删除", '删除',
"添加", '添加',
"创建", '创建',
"设置", '设置',
"获取", '获取',
"查询", '查询',
"搜索", '搜索'
]; ]
const hasNavigationIntent = navigationIntents.some((keyword) => rawCommand.includes(keyword)); const hasNavigationIntent = navigationIntents.some((keyword) => rawCommand.includes(keyword))
const hasOperationIntent = operationIntents.some((keyword) => rawCommand.includes(keyword)); const hasOperationIntent = operationIntents.some((keyword) => rawCommand.includes(keyword))
if (!hasNavigationIntent || hasOperationIntent) { if (!hasNavigationIntent || hasOperationIntent) {
return null; return null
} }
const routeInfo = matchRouteFromCommand(rawCommand); const routeInfo = matchRouteFromCommand(rawCommand)
if (!routeInfo) { if (!routeInfo) {
return null; return null
} }
const keyword = extractKeywordFromCommand(rawCommand); const keyword = extractKeywordFromCommand(rawCommand)
const action: AiAction = { const action: AiAction = {
type: "navigate", type: 'navigate',
path: routeInfo.path, path: routeInfo.path,
pageName: routeInfo.name, pageName: routeInfo.name,
query: keyword || undefined, query: keyword || undefined
}; }
return { return {
explanation: `检测到跳转命令,正在前往 ${routeInfo.name}`, explanation: `检测到跳转命令,正在前往 ${routeInfo.name}`,
action, action
}; }
}; }
// 解析 AI 返回的操作类型 // 解析 AI 返回的操作类型
const parseAction = (result: any, rawCommand: string): AiAction | null => { const parseAction = (result: any, rawCommand: string): AiAction | null => {
const cmd = normalizeText(rawCommand); const cmd = normalizeText(rawCommand)
const primaryCall = result.functionCalls?.[0]; const primaryCall = result.functionCalls?.[0]
const functionName = primaryCall?.name; const functionName = primaryCall?.name
// 优先从函数名推断路由,其次从命令文本匹配 // 优先从函数名推断路由,其次从命令文本匹配
let routeInfo = functionName ? inferRouteFromFunction(functionName) : null; let routeInfo = functionName ? inferRouteFromFunction(functionName) : null
if (!routeInfo) { if (!routeInfo) {
routeInfo = matchRouteFromCommand(cmd); routeInfo = matchRouteFromCommand(cmd)
} }
const routePath = routeInfo?.path || ""; const routePath = routeInfo?.path || ''
const pageName = routeInfo?.name || ""; const pageName = routeInfo?.name || ''
const keyword = extractKeywordFromCommand(cmd); const keyword = extractKeywordFromCommand(cmd)
if (primaryCall && functionName) { if (primaryCall && functionName) {
const fnNameLower = functionName.toLowerCase(); const fnNameLower = functionName.toLowerCase()
// 1) 查询类函数query/search/list/get-> 跳转并执行筛选操作 // 1) 查询类函数query/search/list/get-> 跳转并执行筛选操作
const isQueryFunction = const isQueryFunction =
fnNameLower.includes("query") || fnNameLower.includes('query') ||
fnNameLower.includes("search") || fnNameLower.includes('search') ||
fnNameLower.includes("list") || fnNameLower.includes('list') ||
fnNameLower.includes("get"); fnNameLower.includes('get')
if (isQueryFunction) { if (isQueryFunction) {
// 统一使用 keywords 参数(约定大于配置) // 统一使用 keywords 参数(约定大于配置)
const args = (primaryCall.arguments || {}) as Record<string, unknown>; const args = (primaryCall.arguments || {}) as Record<string, unknown>
const keywords = const keywords =
typeof args.keywords === "string" && args.keywords.trim().length > 0 typeof args.keywords === 'string' && args.keywords.trim().length > 0
? args.keywords ? args.keywords
: keyword; : keyword
if (routePath) { if (routePath) {
return { return {
type: "navigate-and-execute", type: 'navigate-and-execute',
path: routePath, path: routePath,
pageName, pageName,
functionCall: primaryCall, functionCall: primaryCall,
query: keywords || undefined, query: keywords || undefined
}; }
} }
} }
// 2) 其他操作类函数(修改/删除/创建/更新等)-> 跳转并执行 // 2) 其他操作类函数(修改/删除/创建/更新等)-> 跳转并执行
const isModifyFunction = const isModifyFunction =
fnNameLower.includes("update") || fnNameLower.includes('update') ||
fnNameLower.includes("modify") || fnNameLower.includes('modify') ||
fnNameLower.includes("edit") || fnNameLower.includes('edit') ||
fnNameLower.includes("delete") || fnNameLower.includes('delete') ||
fnNameLower.includes("remove") || fnNameLower.includes('remove') ||
fnNameLower.includes("create") || fnNameLower.includes('create') ||
fnNameLower.includes("add") || fnNameLower.includes('add') ||
fnNameLower.includes("save"); fnNameLower.includes('save')
if (isModifyFunction && routePath) { if (isModifyFunction && routePath) {
return { return {
type: "navigate-and-execute", type: 'navigate-and-execute',
path: routePath, path: routePath,
pageName, pageName,
functionCall: primaryCall, functionCall: primaryCall
}; }
} }
// 3) 其他未匹配的函数,如果有路由则跳转,否则执行 // 3) 其他未匹配的函数,如果有路由则跳转,否则执行
if (routePath) { if (routePath) {
return { return {
type: "navigate-and-execute", type: 'navigate-and-execute',
path: routePath, path: routePath,
pageName, pageName,
functionCall: primaryCall, functionCall: primaryCall
}; }
} }
return { return {
type: "execute", type: 'execute',
functionName, functionName,
functionCall: primaryCall, functionCall: primaryCall
}; }
} }
// 4) 无函数调用,仅跳转 // 4) 无函数调用,仅跳转
if (routePath) { if (routePath) {
return { return {
type: "navigate", type: 'navigate',
path: routePath, path: routePath,
pageName, pageName,
query: keyword || undefined, query: keyword || undefined
}; }
} }
return null; return null
}; }
// 定时器引用(用于清理) // 定时器引用(用于清理)
let navigationTimer: ReturnType<typeof setTimeout> | null = null; let navigationTimer: ReturnType<typeof setTimeout> | null = null
let executeTimer: ReturnType<typeof setTimeout> | null = null; let executeTimer: ReturnType<typeof setTimeout> | null = null
// 执行操作 // 执行操作
const executeAction = async (action: AiAction) => { const executeAction = async (action: AiAction) => {
// 🎯 新增:跳转并执行操作 // 🎯 新增:跳转并执行操作
if (action.type === "navigate-and-execute") { if (action.type === 'navigate-and-execute') {
ElMessage.success(`正在跳转到 ${action.pageName} 并执行操作...`); ElMessage.success(`正在跳转到 ${action.pageName} 并执行操作...`)
// 清理之前的定时器 // 清理之前的定时器
if (navigationTimer) { if (navigationTimer) {
clearTimeout(navigationTimer); clearTimeout(navigationTimer)
} }
// 跳转并传递待执行的操作信息 // 跳转并传递待执行的操作信息
navigationTimer = setTimeout(() => { navigationTimer = setTimeout(() => {
navigationTimer = null; navigationTimer = null
const queryParams: any = { const queryParams: any = {
// 通过 URL 参数传递 AI 操作信息 // 通过 URL 参数传递 AI 操作信息
aiAction: encodeURIComponent( aiAction: encodeURIComponent(
JSON.stringify({ JSON.stringify({
functionName: action.functionCall.name, functionName: action.functionCall.name,
arguments: action.functionCall.arguments, arguments: action.functionCall.arguments,
timestamp: Date.now(), timestamp: Date.now()
}) })
), )
}; }
// 如果有查询关键字,也一并传递 // 如果有查询关键字,也一并传递
if (action.query) { if (action.query) {
queryParams.keywords = action.query; queryParams.keywords = action.query
queryParams.autoSearch = "true"; queryParams.autoSearch = 'true'
} }
router.push({ router.push({
path: action.path, path: action.path,
query: queryParams, query: queryParams
}); })
// 关闭对话框 // 关闭对话框
handleClose(); handleClose()
}, 800); }, 800)
return; return
} }
if (action.type === "navigate") { if (action.type === 'navigate') {
// 检查是否已经在目标页面 // 检查是否已经在目标页面
const currentPath = router.currentRoute.value.path; const currentPath = router.currentRoute.value.path
if (currentPath === action.path) { if (currentPath === action.path) {
// 如果已经在目标页面 // 如果已经在目标页面
if (action.query) { if (action.query) {
// 有查询关键字,直接在当前页面执行搜索 // 有查询关键字,直接在当前页面执行搜索
ElMessage.info(`您已在 ${action.pageName} 页面,为您执行搜索:${action.query}`); ElMessage.info(`您已在 ${action.pageName} 页面,为您执行搜索:${action.query}`)
// 触发路由更新,让页面执行搜索 // 触发路由更新,让页面执行搜索
router.replace({ router.replace({
path: action.path, path: action.path,
query: { query: {
keywords: action.query, keywords: action.query,
autoSearch: "true", autoSearch: 'true',
_t: Date.now().toString(), // 添加时间戳强制刷新 _t: Date.now().toString() // 添加时间戳强制刷新
}, }
}); })
} else { } else {
// 没有查询关键字,只是跳转,给出提示 // 没有查询关键字,只是跳转,给出提示
ElMessage.warning(`您已经在 ${action.pageName} 页面了`); ElMessage.warning(`您已经在 ${action.pageName} 页面了`)
} }
// 关闭对话框 // 关闭对话框
handleClose(); handleClose()
return; return
} }
// 不在目标页面,正常跳转 // 不在目标页面,正常跳转
ElMessage.success(`正在跳转到 ${action.pageName}...`); ElMessage.success(`正在跳转到 ${action.pageName}...`)
// 清理之前的定时器 // 清理之前的定时器
if (navigationTimer) { if (navigationTimer) {
clearTimeout(navigationTimer); clearTimeout(navigationTimer)
} }
// 延迟一下让用户看到提示 // 延迟一下让用户看到提示
navigationTimer = setTimeout(() => { navigationTimer = setTimeout(() => {
navigationTimer = null; navigationTimer = null
// 跳转并传递查询参数 // 跳转并传递查询参数
router.push({ router.push({
path: action.path, path: action.path,
query: action.query query: action.query
? { ? {
keywords: action.query, // 传递关键字参数 keywords: action.query, // 传递关键字参数
autoSearch: "true", // 标记自动搜索 autoSearch: 'true' // 标记自动搜索
} }
: undefined, : undefined
}); })
// 关闭对话框 // 关闭对话框
handleClose(); handleClose()
}, 800); }, 800)
} else if (action.type === "execute") { } else if (action.type === 'execute') {
// 执行函数调用 // 执行函数调用
ElMessage.info("功能开发中,请前往 AI 命令助手页面体验完整功能"); ElMessage.info('功能开发中,请前往 AI 命令助手页面体验完整功能')
// 清理之前的定时器 // 清理之前的定时器
if (executeTimer) { if (executeTimer) {
clearTimeout(executeTimer); clearTimeout(executeTimer)
} }
// 可以跳转到完整的 AI 命令页面 // 可以跳转到完整的 AI 命令页面
executeTimer = setTimeout(() => { executeTimer = setTimeout(() => {
executeTimer = null; executeTimer = null
router.push("/function/ai-command"); router.push('/function/ai-command')
handleClose(); handleClose()
}, 1000); }, 1000)
} }
}; }
// 组件卸载时清理定时器 // 组件卸载时清理定时器
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (navigationTimer) { if (navigationTimer) {
clearTimeout(navigationTimer); clearTimeout(navigationTimer)
navigationTimer = null; navigationTimer = null
} }
if (executeTimer) { if (executeTimer) {
clearTimeout(executeTimer); clearTimeout(executeTimer)
executeTimer = null; executeTimer = null
} }
}); })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -6,33 +6,33 @@
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ defineOptions({
name: "AppLink", name: 'AppLink',
inheritAttrs: false, inheritAttrs: false
}); })
import { isExternal } from "@/utils/index"; import { isExternal } from '@/utils/index'
const props = defineProps({ const props = defineProps({
to: { to: {
type: Object, type: Object,
required: true, required: true
}, }
}); })
const isExternalLink = computed(() => { const isExternalLink = computed(() => {
return isExternal(props.to.path || ""); return isExternal(props.to.path || '')
}); })
const linkType = computed(() => (isExternalLink.value ? "a" : "router-link")); const linkType = computed(() => (isExternalLink.value ? 'a' : 'router-link'))
const linkProps = (to: any) => { const linkProps = (to: any) => {
if (isExternalLink.value) { if (isExternalLink.value) {
return { return {
href: to.path, href: to.path,
target: "_blank", target: '_blank',
rel: "noopener noreferrer", rel: 'noopener noreferrer'
}; }
} }
return { to }; return { to }
}; }
</script> </script>

View File

@@ -15,65 +15,65 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouteLocationMatched } from "vue-router"; import { RouteLocationMatched } from 'vue-router'
import { compile } from "path-to-regexp"; import { compile } from 'path-to-regexp'
import router from "@/router"; import router from '@/router'
import { translateRouteTitle } from "@/utils/i18n"; import { translateRouteTitle } from '@/utils/i18n'
const currentRoute = useRoute(); const currentRoute = useRoute()
const pathCompile = (path: string) => { const pathCompile = (path: string) => {
const { params } = currentRoute; const { params } = currentRoute
const toPath = compile(path); const toPath = compile(path)
return toPath(params); return toPath(params)
}; }
const breadcrumbs = ref<Array<RouteLocationMatched>>([]); const breadcrumbs = ref<Array<RouteLocationMatched>>([])
function getBreadcrumb() { function getBreadcrumb() {
let matched = currentRoute.matched.filter((item) => item.meta && item.meta.title); let matched = currentRoute.matched.filter((item) => item.meta && item.meta.title)
const first = matched[0]; const first = matched[0]
if (!isDashboard(first)) { if (!isDashboard(first)) {
matched = [{ path: "/dashboard", meta: { title: "dashboard" } } as any].concat(matched); matched = [{ path: '/dashboard', meta: { title: 'dashboard' } } as any].concat(matched)
} }
breadcrumbs.value = matched.filter((item) => { breadcrumbs.value = matched.filter((item) => {
return item.meta && item.meta.title && item.meta.breadcrumb !== false; return item.meta && item.meta.title && item.meta.breadcrumb !== false
}); })
} }
function isDashboard(route: RouteLocationMatched) { function isDashboard(route: RouteLocationMatched) {
const name = route && route.name; const name = route && route.name
if (!name) { if (!name) {
return false; return false
} }
return name.toString().trim().toLocaleLowerCase() === "Dashboard".toLocaleLowerCase(); return name.toString().trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
} }
function handleLink(item: any) { function handleLink(item: any) {
const { redirect, path } = item; const { redirect, path } = item
if (redirect) { if (redirect) {
router.push(redirect).catch((err) => { router.push(redirect).catch((err) => {
console.warn(err); console.warn(err)
}); })
return; return
} }
router.push(pathCompile(path)).catch((err) => { router.push(pathCompile(path)).catch((err) => {
console.warn(err); console.warn(err)
}); })
} }
watch( watch(
() => currentRoute.path, () => currentRoute.path,
(path) => { (path) => {
if (path.startsWith("/redirect/")) { if (path.startsWith('/redirect/')) {
return; return
} }
getBreadcrumb(); getBreadcrumb()
} }
); )
onBeforeMount(() => { onBeforeMount(() => {
getBreadcrumb(); getBreadcrumb()
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -125,7 +125,7 @@
<!-- 格式化为价格 --> <!-- 格式化为价格 -->
<template v-else-if="col.templet === 'price'"> <template v-else-if="col.templet === 'price'">
<template v-if="col.prop"> <template v-if="col.prop">
{{ `${col.priceFormat ?? ""}${scope.row[col.prop]}` }} {{ `${col.priceFormat ?? ''}${scope.row[col.prop]}` }}
</template> </template>
</template> </template>
<!-- 格式化为百分比 --> <!-- 格式化为百分比 -->
@@ -150,9 +150,9 @@
<template v-if="col.prop"> <template v-if="col.prop">
{{ {{
scope.row[col.prop] scope.row[col.prop]
? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss") ? useDateFormat(scope.row[col.prop], col.dateFormat ?? 'YYYY-MM-DD HH:mm:ss')
.value .value
: "" : ''
}} }}
</template> </template>
</template> </template>
@@ -168,7 +168,7 @@
name: btn.name, name: btn.name,
row: scope.row, row: scope.row,
column: scope.column, column: scope.column,
$index: scope.$index, $index: scope.$index
}) })
" "
> >
@@ -322,8 +322,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { hasPerm } from "@/utils/auth"; import { hasPerm } from '@/utils/auth'
import { useDateFormat, useThrottleFn } from "@vueuse/core"; import { useDateFormat, useThrottleFn } from '@vueuse/core'
import { import {
genFileId, genFileId,
type FormInstance, type FormInstance,
@@ -331,331 +331,331 @@ import {
type UploadInstance, type UploadInstance,
type UploadRawFile, type UploadRawFile,
type UploadUserFile, type UploadUserFile,
type TableInstance, type TableInstance
} from "element-plus"; } from 'element-plus'
import ExcelJS from "exceljs"; import ExcelJS from 'exceljs'
import { reactive, ref, computed } from "vue"; import { reactive, ref, computed } from 'vue'
import type { IContentConfig, IObject, IOperateData } from "./types"; import type { IContentConfig, IObject, IOperateData } from './types'
import type { IToolsButton } from "./types"; import type { IToolsButton } from './types'
// 定义接收的属性 // 定义接收的属性
const props = defineProps<{ contentConfig: IContentConfig }>(); const props = defineProps<{ contentConfig: IContentConfig }>()
// 定义自定义事件 // 定义自定义事件
const emit = defineEmits<{ const emit = defineEmits<{
addClick: []; addClick: []
exportClick: []; exportClick: []
searchClick: []; searchClick: []
toolbarClick: [name: string]; toolbarClick: [name: string]
editClick: [row: IObject]; editClick: [row: IObject]
filterChange: [data: IObject]; filterChange: [data: IObject]
operateClick: [data: IOperateData]; operateClick: [data: IOperateData]
}>(); }>()
// 表格工具栏按钮配置 // 表格工具栏按钮配置
const config = computed(() => props.contentConfig); const config = computed(() => props.contentConfig)
const buttonConfig = reactive<Record<string, IObject>>({ const buttonConfig = reactive<Record<string, IObject>>({
add: { text: "新增", attrs: { icon: "plus", type: "success" }, perm: "add" }, add: { text: '新增', attrs: { icon: 'plus', type: 'success' }, perm: 'add' },
delete: { text: "删除", attrs: { icon: "delete", type: "danger" }, perm: "delete" }, delete: { text: '删除', attrs: { icon: 'delete', type: 'danger' }, perm: 'delete' },
import: { text: "导入", attrs: { icon: "upload", type: "" }, perm: "import" }, import: { text: '导入', attrs: { icon: 'upload', type: '' }, perm: 'import' },
export: { text: "导出", attrs: { icon: "download", type: "" }, perm: "export" }, export: { text: '导出', attrs: { icon: 'download', type: '' }, perm: 'export' },
refresh: { text: "刷新", attrs: { icon: "refresh", type: "" }, perm: "*:*:*" }, refresh: { text: '刷新', attrs: { icon: 'refresh', type: '' }, perm: '*:*:*' },
filter: { text: "筛选列", attrs: { icon: "operation", type: "" }, perm: "*:*:*" }, filter: { text: '筛选列', attrs: { icon: 'operation', type: '' }, perm: '*:*:*' },
search: { text: "搜索", attrs: { icon: "search", type: "" }, perm: "search" }, search: { text: '搜索', attrs: { icon: 'search', type: '' }, perm: 'search' },
imports: { text: "批量导入", attrs: { icon: "upload", type: "" }, perm: "imports" }, imports: { text: '批量导入', attrs: { icon: 'upload', type: '' }, perm: 'imports' },
exports: { text: "批量导出", attrs: { icon: "download", type: "" }, perm: "exports" }, exports: { text: '批量导出', attrs: { icon: 'download', type: '' }, perm: 'exports' },
view: { text: "查看", attrs: { icon: "view", type: "primary" }, perm: "view" }, view: { text: '查看', attrs: { icon: 'view', type: 'primary' }, perm: 'view' },
edit: { text: "编辑", attrs: { icon: "edit", type: "primary" }, perm: "edit" }, edit: { text: '编辑', attrs: { icon: 'edit', type: 'primary' }, perm: 'edit' }
}); })
// 主键 // 主键
const pk = props.contentConfig.pk ?? "id"; const pk = props.contentConfig.pk ?? 'id'
// 权限名称前缀 // 权限名称前缀
const authPrefix = computed(() => props.contentConfig.permPrefix); const authPrefix = computed(() => props.contentConfig.permPrefix)
// 获取按钮权限标识 // 获取按钮权限标识
function getButtonPerm(action: string): string | null { function getButtonPerm(action: string): string | null {
// 如果action已经包含完整路径(包含冒号),则直接使用 // 如果action已经包含完整路径(包含冒号),则直接使用
if (action.includes(":")) { if (action.includes(':')) {
return action; return action
} }
// 否则使用权限前缀组合 // 否则使用权限前缀组合
return authPrefix.value ? `${authPrefix.value}:${action}` : null; return authPrefix.value ? `${authPrefix.value}:${action}` : null
} }
// 检查是否有权限 // 检查是否有权限
function hasButtonPerm(action: string): boolean { function hasButtonPerm(action: string): boolean {
const perm = getButtonPerm(action); const perm = getButtonPerm(action)
// 如果没有设置权限标识,则默认具有权限 // 如果没有设置权限标识,则默认具有权限
if (!perm) return true; if (!perm) return true
return hasPerm(perm); return hasPerm(perm)
} }
// 创建工具栏按钮 // 创建工具栏按钮
function createToolbar(toolbar: Array<string | IToolsButton>, attr = {}) { function createToolbar(toolbar: Array<string | IToolsButton>, attr = {}) {
return toolbar.map((item) => { return toolbar.map((item) => {
const isString = typeof item === "string"; const isString = typeof item === 'string'
return { return {
name: isString ? item : item?.name || "", name: isString ? item : item?.name || '',
text: isString ? buttonConfig[item].text : item?.text, text: isString ? buttonConfig[item].text : item?.text,
attrs: { attrs: {
...attr, ...attr,
...(isString ? buttonConfig[item].attrs : item?.attrs), ...(isString ? buttonConfig[item].attrs : item?.attrs)
}, },
render: isString ? undefined : (item?.render ?? undefined), render: isString ? undefined : (item?.render ?? undefined),
perm: isString perm: isString
? getButtonPerm(buttonConfig[item].perm) ? getButtonPerm(buttonConfig[item].perm)
: item?.perm : item?.perm
? getButtonPerm(item.perm as string) ? getButtonPerm(item.perm as string)
: "*:*:*", : '*:*:*'
}; }
}); })
} }
// 左侧工具栏按钮 // 左侧工具栏按钮
const toolbarLeftBtn = computed(() => { const toolbarLeftBtn = computed(() => {
if (!config.value.toolbar || config.value.toolbar.length === 0) return []; if (!config.value.toolbar || config.value.toolbar.length === 0) return []
return createToolbar(config.value.toolbar, {}); return createToolbar(config.value.toolbar, {})
}); })
// 右侧工具栏按钮 // 右侧工具栏按钮
const toolbarRightBtn = computed(() => { const toolbarRightBtn = computed(() => {
if (!config.value.defaultToolbar || config.value.defaultToolbar.length === 0) return []; if (!config.value.defaultToolbar || config.value.defaultToolbar.length === 0) return []
return createToolbar(config.value.defaultToolbar, { circle: true }); return createToolbar(config.value.defaultToolbar, { circle: true })
}); })
// 表格操作工具栏 // 表格操作工具栏
const tableToolbar = config.value.cols[config.value.cols.length - 1].operat ?? ["edit", "delete"]; const tableToolbar = config.value.cols[config.value.cols.length - 1].operat ?? ['edit', 'delete']
const tableToolbarBtn = createToolbar(tableToolbar, { link: true, size: "small" }); const tableToolbarBtn = createToolbar(tableToolbar, { link: true, size: 'small' })
// 表格列 // 表格列
const cols = ref( const cols = ref(
props.contentConfig.cols.map((col) => { props.contentConfig.cols.map((col) => {
if (col.initFn) { if (col.initFn) {
col.initFn(col); col.initFn(col)
} }
if (col.show === undefined) { if (col.show === undefined) {
col.show = true; col.show = true
} }
if (col.prop !== undefined && col.columnKey === undefined && col["column-key"] === undefined) { if (col.prop !== undefined && col.columnKey === undefined && col['column-key'] === undefined) {
col.columnKey = col.prop; col.columnKey = col.prop
} }
if ( if (
col.type === "selection" && col.type === 'selection' &&
col.reserveSelection === undefined && col.reserveSelection === undefined &&
col["reserve-selection"] === undefined col['reserve-selection'] === undefined
) { ) {
// 配合表格row-key实现跨页多选 // 配合表格row-key实现跨页多选
col.reserveSelection = true; col.reserveSelection = true
} }
return col; return col
}) })
); )
// 加载状态 // 加载状态
const loading = ref(false); const loading = ref(false)
// 列表数据 // 列表数据
const pageData = ref<IObject[]>([]); const pageData = ref<IObject[]>([])
// 显示分页 // 显示分页
const showPagination = props.contentConfig.pagination !== false; const showPagination = props.contentConfig.pagination !== false
// 分页配置 // 分页配置
const defaultPagination = { const defaultPagination = {
background: true, background: true,
layout: "total, sizes, prev, pager, next, jumper", layout: 'total, sizes, prev, pager, next, jumper',
pageSize: 20, pageSize: 20,
pageSizes: [10, 20, 30, 50], pageSizes: [10, 20, 30, 50],
total: 0, total: 0,
currentPage: 1, currentPage: 1
}; }
const pagination = reactive( const pagination = reactive(
typeof props.contentConfig.pagination === "object" typeof props.contentConfig.pagination === 'object'
? { ...defaultPagination, ...props.contentConfig.pagination } ? { ...defaultPagination, ...props.contentConfig.pagination }
: defaultPagination : defaultPagination
); )
// 分页相关的请求参数 // 分页相关的请求参数
const request = props.contentConfig.request ?? { const request = props.contentConfig.request ?? {
pageName: "pageNum", pageName: 'pageNum',
limitName: "pageSize", limitName: 'pageSize'
}; }
const tableRef = ref<TableInstance>(); const tableRef = ref<TableInstance>()
// 行选中 // 行选中
const selectionData = ref<IObject[]>([]); const selectionData = ref<IObject[]>([])
// 删除ID集合 用于批量删除 // 删除ID集合 用于批量删除
const removeIds = ref<(number | string)[]>([]); const removeIds = ref<(number | string)[]>([])
function handleSelectionChange(selection: any[]) { function handleSelectionChange(selection: any[]) {
selectionData.value = selection; selectionData.value = selection
removeIds.value = selection.map((item) => item[pk]); removeIds.value = selection.map((item) => item[pk])
} }
// 获取行选中 // 获取行选中
function getSelectionData() { function getSelectionData() {
return selectionData.value; return selectionData.value
} }
// 刷新 // 刷新
function handleRefresh(isRestart = false) { function handleRefresh(isRestart = false) {
fetchPageData(lastFormData, isRestart); fetchPageData(lastFormData, isRestart)
} }
// 删除 // 删除
function handleDelete(id?: number | string) { function handleDelete(id?: number | string) {
const ids = [id || removeIds.value].join(","); const ids = [id || removeIds.value].join(',')
if (!ids) { if (!ids) {
ElMessage.warning("请勾选删除项"); ElMessage.warning('请勾选删除项')
return; return
} }
ElMessageBox.confirm("确认删除?", "警告", { ElMessageBox.confirm('确认删除?', '警告', {
confirmButtonText: "确定", confirmButtonText: '确定',
cancelButtonText: "取消", cancelButtonText: '取消',
type: "warning", type: 'warning'
}) })
.then(function () { .then(function () {
if (props.contentConfig.deleteAction) { if (props.contentConfig.deleteAction) {
props.contentConfig props.contentConfig
.deleteAction(ids) .deleteAction(ids)
.then(() => { .then(() => {
ElMessage.success("删除成功"); ElMessage.success('删除成功')
removeIds.value = []; removeIds.value = []
//清空选中项 //清空选中项
tableRef.value?.clearSelection(); tableRef.value?.clearSelection()
handleRefresh(true); handleRefresh(true)
}) })
.catch(() => {}); .catch(() => {})
} else { } else {
ElMessage.error("未配置deleteAction"); ElMessage.error('未配置deleteAction')
} }
}) })
.catch(() => {}); .catch(() => {})
} }
// 导出表单 // 导出表单
const fields: string[] = []; const fields: string[] = []
cols.value.forEach((item) => { cols.value.forEach((item) => {
if (item.prop !== undefined) { if (item.prop !== undefined) {
fields.push(item.prop); fields.push(item.prop)
} }
}); })
const enum ExportsOriginEnum { const enum ExportsOriginEnum {
CURRENT = "current", CURRENT = 'current',
SELECTED = "selected", SELECTED = 'selected',
REMOTE = "remote", REMOTE = 'remote'
} }
const exportsModalVisible = ref(false); const exportsModalVisible = ref(false)
const exportsFormRef = ref<FormInstance>(); const exportsFormRef = ref<FormInstance>()
const exportsFormData = reactive({ const exportsFormData = reactive({
filename: "", filename: '',
sheetname: "", sheetname: '',
fields, fields,
origin: ExportsOriginEnum.CURRENT, origin: ExportsOriginEnum.CURRENT
}); })
const exportsFormRules: FormRules = { const exportsFormRules: FormRules = {
fields: [{ required: true, message: "请选择字段" }], fields: [{ required: true, message: '请选择字段' }],
origin: [{ required: true, message: "请选择数据源" }], origin: [{ required: true, message: '请选择数据源' }]
}; }
// 打开导出弹窗 // 打开导出弹窗
function handleOpenExportsModal() { function handleOpenExportsModal() {
exportsModalVisible.value = true; exportsModalVisible.value = true
} }
// 导出确认 // 导出确认
const handleExportsSubmit = useThrottleFn(() => { const handleExportsSubmit = useThrottleFn(() => {
exportsFormRef.value?.validate((valid: boolean) => { exportsFormRef.value?.validate((valid: boolean) => {
if (valid) { if (valid) {
handleExports(); handleExports()
handleCloseExportsModal(); handleCloseExportsModal()
} }
}); })
}, 3000); }, 3000)
// 关闭导出弹窗 // 关闭导出弹窗
function handleCloseExportsModal() { function handleCloseExportsModal() {
exportsModalVisible.value = false; exportsModalVisible.value = false
exportsFormRef.value?.resetFields(); exportsFormRef.value?.resetFields()
nextTick(() => { nextTick(() => {
exportsFormRef.value?.clearValidate(); exportsFormRef.value?.clearValidate()
}); })
} }
// 导出 // 导出
function handleExports() { function handleExports() {
const filename = exportsFormData.filename const filename = exportsFormData.filename
? exportsFormData.filename ? exportsFormData.filename
: props.contentConfig.permPrefix || "export"; : props.contentConfig.permPrefix || 'export'
const sheetname = exportsFormData.sheetname ? exportsFormData.sheetname : "sheet"; const sheetname = exportsFormData.sheetname ? exportsFormData.sheetname : 'sheet'
const workbook = new ExcelJS.Workbook(); const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet(sheetname); const worksheet = workbook.addWorksheet(sheetname)
const columns: Partial<ExcelJS.Column>[] = []; const columns: Partial<ExcelJS.Column>[] = []
cols.value.forEach((col) => { cols.value.forEach((col) => {
if (col.label && col.prop && exportsFormData.fields.includes(col.prop)) { if (col.label && col.prop && exportsFormData.fields.includes(col.prop)) {
columns.push({ header: col.label, key: col.prop }); columns.push({ header: col.label, key: col.prop })
} }
}); })
worksheet.columns = columns; worksheet.columns = columns
if (exportsFormData.origin === ExportsOriginEnum.REMOTE) { if (exportsFormData.origin === ExportsOriginEnum.REMOTE) {
if (props.contentConfig.exportsAction) { if (props.contentConfig.exportsAction) {
props.contentConfig.exportsAction(lastFormData).then((res) => { props.contentConfig.exportsAction(lastFormData).then((res) => {
worksheet.addRows(res); worksheet.addRows(res)
workbook.xlsx workbook.xlsx
.writeBuffer() .writeBuffer()
.then((buffer) => { .then((buffer) => {
saveXlsx(buffer, filename as string); saveXlsx(buffer, filename as string)
}) })
.catch((error) => console.log(error)); .catch((error) => console.log(error))
}); })
} else { } else {
ElMessage.error("未配置exportsAction"); ElMessage.error('未配置exportsAction')
} }
} else { } else {
worksheet.addRows( worksheet.addRows(
exportsFormData.origin === ExportsOriginEnum.SELECTED ? selectionData.value : pageData.value exportsFormData.origin === ExportsOriginEnum.SELECTED ? selectionData.value : pageData.value
); )
workbook.xlsx workbook.xlsx
.writeBuffer() .writeBuffer()
.then((buffer) => { .then((buffer) => {
saveXlsx(buffer, filename as string); saveXlsx(buffer, filename as string)
}) })
.catch((error) => console.log(error)); .catch((error) => console.log(error))
} }
} }
// 导入表单 // 导入表单
let isFileImport = false; let isFileImport = false
const uploadRef = ref<UploadInstance>(); const uploadRef = ref<UploadInstance>()
const importModalVisible = ref(false); const importModalVisible = ref(false)
const importFormRef = ref<FormInstance>(); const importFormRef = ref<FormInstance>()
const importFormData = reactive<{ const importFormData = reactive<{
files: UploadUserFile[]; files: UploadUserFile[]
}>({ }>({
files: [], files: []
}); })
const importFormRules: FormRules = { const importFormRules: FormRules = {
files: [{ required: true, message: "请选择文件" }], files: [{ required: true, message: '请选择文件' }]
}; }
// 打开导入弹窗 // 打开导入弹窗
function handleOpenImportModal(isFile: boolean = false) { function handleOpenImportModal(isFile: boolean = false) {
importModalVisible.value = true; importModalVisible.value = true
isFileImport = isFile; isFileImport = isFile
} }
// 覆盖前一个文件 // 覆盖前一个文件
function handleFileExceed(files: File[]) { function handleFileExceed(files: File[]) {
uploadRef.value!.clearFiles(); uploadRef.value!.clearFiles()
const file = files[0] as UploadRawFile; const file = files[0] as UploadRawFile
file.uid = genFileId(); file.uid = genFileId()
uploadRef.value!.handleStart(file); uploadRef.value!.handleStart(file)
} }
// 下载导入模板 // 下载导入模板
function handleDownloadTemplate() { function handleDownloadTemplate() {
const importTemplate = props.contentConfig.importTemplate; const importTemplate = props.contentConfig.importTemplate
if (typeof importTemplate === "string") { if (typeof importTemplate === 'string') {
window.open(importTemplate); window.open(importTemplate)
} else if (typeof importTemplate === "function") { } else if (typeof importTemplate === 'function') {
importTemplate().then((response) => { importTemplate().then((response) => {
const fileData = response.data; const fileData = response.data
const fileName = decodeURI( const fileName = decodeURI(
response.headers["content-disposition"].split(";")[1].split("=")[1] response.headers['content-disposition'].split(';')[1].split('=')[1]
); )
saveXlsx(fileData, fileName); saveXlsx(fileData, fileName)
}); })
} else { } else {
ElMessage.error("未配置importTemplate"); ElMessage.error('未配置importTemplate')
} }
} }
// 导入确认 // 导入确认
@@ -663,142 +663,142 @@ const handleImportSubmit = useThrottleFn(() => {
importFormRef.value?.validate((valid: boolean) => { importFormRef.value?.validate((valid: boolean) => {
if (valid) { if (valid) {
if (isFileImport) { if (isFileImport) {
handleImport(); handleImport()
} else { } else {
handleImports(); handleImports()
} }
} }
}); })
}, 3000); }, 3000)
// 关闭导入弹窗 // 关闭导入弹窗
function handleCloseImportModal() { function handleCloseImportModal() {
importModalVisible.value = false; importModalVisible.value = false
importFormRef.value?.resetFields(); importFormRef.value?.resetFields()
nextTick(() => { nextTick(() => {
importFormRef.value?.clearValidate(); importFormRef.value?.clearValidate()
}); })
} }
// 文件导入 // 文件导入
function handleImport() { function handleImport() {
const importAction = props.contentConfig.importAction; const importAction = props.contentConfig.importAction
if (importAction === undefined) { if (importAction === undefined) {
ElMessage.error("未配置importAction"); ElMessage.error('未配置importAction')
return; return
} }
importAction(importFormData.files[0].raw as File).then(() => { importAction(importFormData.files[0].raw as File).then(() => {
ElMessage.success("导入数据成功"); ElMessage.success('导入数据成功')
handleCloseImportModal(); handleCloseImportModal()
handleRefresh(true); handleRefresh(true)
}); })
} }
// 导入 // 导入
function handleImports() { function handleImports() {
const importsAction = props.contentConfig.importsAction; const importsAction = props.contentConfig.importsAction
if (importsAction === undefined) { if (importsAction === undefined) {
ElMessage.error("未配置importsAction"); ElMessage.error('未配置importsAction')
return; return
} }
// 获取选择的文件 // 获取选择的文件
const file = importFormData.files[0].raw as File; const file = importFormData.files[0].raw as File
// 创建Workbook实例 // 创建Workbook实例
const workbook = new ExcelJS.Workbook(); const workbook = new ExcelJS.Workbook()
// 使用FileReader对象来读取文件内容 // 使用FileReader对象来读取文件内容
const fileReader = new FileReader(); const fileReader = new FileReader()
// 二进制字符串的形式加载文件 // 二进制字符串的形式加载文件
fileReader.readAsArrayBuffer(file); fileReader.readAsArrayBuffer(file)
fileReader.onload = (ev) => { fileReader.onload = (ev) => {
if (ev.target !== null && ev.target.result !== null) { if (ev.target !== null && ev.target.result !== null) {
const result = ev.target.result as ArrayBuffer; const result = ev.target.result as ArrayBuffer
// 从 buffer中加载数据解析 // 从 buffer中加载数据解析
workbook.xlsx workbook.xlsx
.load(result) .load(result)
.then((workbook) => { .then((workbook) => {
// 解析后的数据 // 解析后的数据
const data = []; const data = []
// 获取第一个worksheet内容 // 获取第一个worksheet内容
const worksheet = workbook.getWorksheet(1); const worksheet = workbook.getWorksheet(1)
if (worksheet) { if (worksheet) {
// 获取第一行的标题 // 获取第一行的标题
const fields: any[] = []; const fields: any[] = []
worksheet.getRow(1).eachCell((cell) => { worksheet.getRow(1).eachCell((cell) => {
fields.push(cell.value); fields.push(cell.value)
}); })
// 遍历工作表的每一行(从第二行开始,因为第一行通常是标题行) // 遍历工作表的每一行(从第二行开始,因为第一行通常是标题行)
for (let rowNumber = 2; rowNumber <= worksheet.rowCount; rowNumber++) { for (let rowNumber = 2; rowNumber <= worksheet.rowCount; rowNumber++) {
const rowData: IObject = {}; const rowData: IObject = {}
const row = worksheet.getRow(rowNumber); const row = worksheet.getRow(rowNumber)
// 遍历当前行的每个单元格 // 遍历当前行的每个单元格
row.eachCell((cell, colNumber) => { row.eachCell((cell, colNumber) => {
// 获取标题对应的键,并将当前单元格的值存储到相应的属性名中 // 获取标题对应的键,并将当前单元格的值存储到相应的属性名中
rowData[fields[colNumber - 1]] = cell.value; rowData[fields[colNumber - 1]] = cell.value
}); })
// 将当前行的数据对象添加到数组中 // 将当前行的数据对象添加到数组中
data.push(rowData); data.push(rowData)
} }
} }
if (data.length === 0) { if (data.length === 0) {
ElMessage.error("未解析到数据"); ElMessage.error('未解析到数据')
return; return
} }
importsAction(data).then(() => { importsAction(data).then(() => {
ElMessage.success("导入数据成功"); ElMessage.success('导入数据成功')
handleCloseImportModal(); handleCloseImportModal()
handleRefresh(true); handleRefresh(true)
}); })
}) })
.catch((error) => console.log(error)); .catch((error) => console.log(error))
} else { } else {
ElMessage.error("读取文件失败"); ElMessage.error('读取文件失败')
} }
}; }
} }
// 操作栏 // 操作栏
function handleToolbar(name: string) { function handleToolbar(name: string) {
switch (name) { switch (name) {
case "refresh": case 'refresh':
handleRefresh(); handleRefresh()
break; break
case "exports": case 'exports':
handleOpenExportsModal(); handleOpenExportsModal()
break; break
case "imports": case 'imports':
handleOpenImportModal(); handleOpenImportModal()
break; break
case "search": case 'search':
emit("searchClick"); emit('searchClick')
break; break
case "add": case 'add':
emit("addClick"); emit('addClick')
break; break
case "delete": case 'delete':
handleDelete(); handleDelete()
break; break
case "import": case 'import':
handleOpenImportModal(true); handleOpenImportModal(true)
break; break
case "export": case 'export':
emit("exportClick"); emit('exportClick')
break; break
default: default:
emit("toolbarClick", name); emit('toolbarClick', name)
break; break
} }
} }
// 操作列 // 操作列
function handleOperate(data: IOperateData) { function handleOperate(data: IOperateData) {
switch (data.name) { switch (data.name) {
case "delete": case 'delete':
if (props.contentConfig?.deleteAction) { if (props.contentConfig?.deleteAction) {
handleDelete(data.row[pk]); handleDelete(data.row[pk])
} else { } else {
emit("operateClick", data); emit('operateClick', data)
} }
break; break
default: default:
emit("operateClick", data); emit('operateClick', data)
break; break
} }
} }
@@ -808,55 +808,55 @@ function handleModify(field: string, value: boolean | string | number, row: Reco
props.contentConfig.modifyAction({ props.contentConfig.modifyAction({
[pk]: row[pk], [pk]: row[pk],
field, field,
value, value
}); })
} else { } else {
ElMessage.error("未配置modifyAction"); ElMessage.error('未配置modifyAction')
} }
} }
// 分页切换 // 分页切换
function handleSizeChange(value: number) { function handleSizeChange(value: number) {
pagination.pageSize = value; pagination.pageSize = value
handleRefresh(); handleRefresh()
} }
function handleCurrentChange(value: number) { function handleCurrentChange(value: number) {
pagination.currentPage = value; pagination.currentPage = value
handleRefresh(); handleRefresh()
} }
// 远程数据筛选 // 远程数据筛选
let filterParams: IObject = {}; let filterParams: IObject = {}
function handleFilterChange(newFilters: any) { function handleFilterChange(newFilters: any) {
const filters: IObject = {}; const filters: IObject = {}
for (const key in newFilters) { for (const key in newFilters) {
const col = cols.value.find((col) => { const col = cols.value.find((col) => {
return col.columnKey === key || col["column-key"] === key; return col.columnKey === key || col['column-key'] === key
}); })
if (col && col.filterJoin !== undefined) { if (col && col.filterJoin !== undefined) {
filters[key] = newFilters[key].join(col.filterJoin); filters[key] = newFilters[key].join(col.filterJoin)
} else { } else {
filters[key] = newFilters[key]; filters[key] = newFilters[key]
} }
} }
filterParams = { ...filterParams, ...filters }; filterParams = { ...filterParams, ...filters }
emit("filterChange", filterParams); emit('filterChange', filterParams)
} }
// 获取筛选条件 // 获取筛选条件
function getFilterParams() { function getFilterParams() {
return filterParams; return filterParams
} }
// 获取分页数据 // 获取分页数据
let lastFormData = {}; let lastFormData = {}
function fetchPageData(formData: IObject = {}, isRestart = false) { function fetchPageData(formData: IObject = {}, isRestart = false) {
loading.value = true; loading.value = true
// 上一次搜索条件 // 上一次搜索条件
lastFormData = formData; lastFormData = formData
// 重置页码 // 重置页码
if (isRestart) { if (isRestart) {
pagination.currentPage = 1; pagination.currentPage = 1
} }
props.contentConfig props.contentConfig
.indexAction( .indexAction(
@@ -864,63 +864,62 @@ function fetchPageData(formData: IObject = {}, isRestart = false) {
? { ? {
[request.pageName]: pagination.currentPage, [request.pageName]: pagination.currentPage,
[request.limitName]: pagination.pageSize, [request.limitName]: pagination.pageSize,
...formData, ...formData
} }
: formData : formData
) )
.then((data) => { .then((data) => {
if (showPagination) { if (showPagination) {
if (props.contentConfig.parseData) { if (props.contentConfig.parseData) {
data = props.contentConfig.parseData(data); data = props.contentConfig.parseData(data)
} }
pagination.total = data.total; pagination.total = data.total
pageData.value = data.list; pageData.value = data.list
} else { } else {
pageData.value = data; pageData.value = data
} }
}) })
.finally(() => { .finally(() => {
loading.value = false; loading.value = false
}); })
} }
fetchPageData(); fetchPageData()
// 导出Excel // 导出Excel
function exportPageData(formData: IObject = {}) { function exportPageData(formData: IObject = {}) {
if (props.contentConfig.exportAction) { if (props.contentConfig.exportAction) {
props.contentConfig.exportAction(formData).then((response) => { props.contentConfig.exportAction(formData).then((response) => {
const fileData = response.data; const fileData = response.data
const fileName = decodeURI( const fileName = decodeURI(
response.headers["content-disposition"].split(";")[1].split("=")[1] response.headers['content-disposition'].split(';')[1].split('=')[1]
); )
saveXlsx(fileData, fileName); saveXlsx(fileData, fileName)
}); })
} else { } else {
ElMessage.error("未配置exportAction"); ElMessage.error('未配置exportAction')
} }
} }
// 浏览器保存文件 // 浏览器保存文件
function saveXlsx(fileData: any, fileName: string) { function saveXlsx(fileData: any, fileName: string) {
const fileType = const fileType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType }); const blob = new Blob([fileData], { type: fileType })
const downloadUrl = window.URL.createObjectURL(blob); const downloadUrl = window.URL.createObjectURL(blob)
const downloadLink = document.createElement("a"); const downloadLink = document.createElement('a')
downloadLink.href = downloadUrl; downloadLink.href = downloadUrl
downloadLink.download = fileName; downloadLink.download = fileName
document.body.appendChild(downloadLink); document.body.appendChild(downloadLink)
downloadLink.click(); downloadLink.click()
document.body.removeChild(downloadLink); document.body.removeChild(downloadLink)
window.URL.revokeObjectURL(downloadUrl); window.URL.revokeObjectURL(downloadUrl)
} }
// 暴露的属性和方法 // 暴露的属性和方法
defineExpose({ fetchPageData, exportPageData, getFilterParams, getSelectionData, handleRefresh }); defineExpose({ fetchPageData, exportPageData, getFilterParams, getSelectionData, handleRefresh })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -15,7 +15,7 @@
<!-- Label --> <!-- Label -->
<template #label> <template #label>
<span> <span>
{{ item?.label || "" }} {{ item?.label || '' }}
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)"> <el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
<QuestionFilled class="w-4 h-4 mx-1" /> <QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip> </el-tooltip>
@@ -60,7 +60,7 @@
<template #footer> <template #footer>
<el-button v-if="!formDisable" type="primary" @click="handleSubmit"> </el-button> <el-button v-if="!formDisable" type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleClose">{{ !formDisable ? " " : "关闭" }}</el-button> <el-button @click="handleClose">{{ !formDisable ? ' ' : '关闭' }}</el-button>
</template> </template>
</el-drawer> </el-drawer>
</template> </template>
@@ -79,7 +79,7 @@
<el-form-item :label="item.label" :prop="item.prop"> <el-form-item :label="item.label" :prop="item.prop">
<template #label> <template #label>
<span> <span>
{{ item?.label || "" }} {{ item?.label || '' }}
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)"> <el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
<QuestionFilled class="w-4 h-4 mx-1" /> <QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip> </el-tooltip>
@@ -125,7 +125,7 @@
<template #footer> <template #footer>
<el-button v-if="!formDisable" type="primary" @click="handleSubmit"> </el-button> <el-button v-if="!formDisable" type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleClose">{{ !formDisable ? " " : "关闭" }}</el-button> <el-button @click="handleClose">{{ !formDisable ? ' ' : '关闭' }}</el-button>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
@@ -133,114 +133,114 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useThrottleFn } from "@vueuse/core"; import { useThrottleFn } from '@vueuse/core'
import type { FormInstance, FormRules } from "element-plus"; import type { FormInstance, FormRules } from 'element-plus'
import type { IComponentType, IModalConfig, IObject } from "./types"; import type { IComponentType, IModalConfig, IObject } from './types'
import InputTag from "@/components/InputTag/index.vue"; import InputTag from '@/components/InputTag/index.vue'
import IconSelect from "@/components/IconSelect/index.vue"; import IconSelect from '@/components/IconSelect/index.vue'
defineSlots<{ [key: string]: (_args: any) => any }>(); defineSlots<{ [key: string]: (_args: any) => any }>()
// 定义接收的属性 // 定义接收的属性
const props = defineProps<{ modalConfig: IModalConfig }>(); const props = defineProps<{ modalConfig: IModalConfig }>()
// 自定义事件 // 自定义事件
const emit = defineEmits<{ submitClick: []; customSubmit: [queryParams: IObject] }>(); const emit = defineEmits<{ submitClick: []; customSubmit: [queryParams: IObject] }>()
// 组件映射表 // 组件映射表
const componentMap = new Map<IComponentType, any>([ const componentMap = new Map<IComponentType, any>([
// @ts-ignore // @ts-ignore
["input", markRaw(ElInput)], // @ts-ignore ['input', markRaw(ElInput)], // @ts-ignore
["select", markRaw(ElSelect)], // @ts-ignore ['select', markRaw(ElSelect)], // @ts-ignore
["switch", markRaw(ElSwitch)], // @ts-ignore ['switch', markRaw(ElSwitch)], // @ts-ignore
["cascader", markRaw(ElCascader)], // @ts-ignore ['cascader', markRaw(ElCascader)], // @ts-ignore
["input-number", markRaw(ElInputNumber)], // @ts-ignore ['input-number', markRaw(ElInputNumber)], // @ts-ignore
["input-tag", markRaw(InputTag)], // @ts-ignore ['input-tag', markRaw(InputTag)], // @ts-ignore
["time-picker", markRaw(ElTimePicker)], // @ts-ignore ['time-picker', markRaw(ElTimePicker)], // @ts-ignore
["time-select", markRaw(ElTimeSelect)], // @ts-ignore ['time-select', markRaw(ElTimeSelect)], // @ts-ignore
["date-picker", markRaw(ElDatePicker)], // @ts-ignore ['date-picker', markRaw(ElDatePicker)], // @ts-ignore
["tree-select", markRaw(ElTreeSelect)], // @ts-ignore" ['tree-select', markRaw(ElTreeSelect)], // @ts-ignore"
["custom-tag", markRaw(InputTag)], // @ts-ignore ['custom-tag', markRaw(InputTag)], // @ts-ignore
["text", markRaw(ElText)], // @ts-ignore ['text', markRaw(ElText)], // @ts-ignore
["radio", markRaw(ElRadioGroup)], // @ts-ignore" ['radio', markRaw(ElRadioGroup)], // @ts-ignore"
["checkbox", markRaw(ElCheckboxGroup)], // @ts-ignore" ['checkbox', markRaw(ElCheckboxGroup)], // @ts-ignore"
["icon-select", markRaw(IconSelect)], // @ts-ignore" ['icon-select', markRaw(IconSelect)], // @ts-ignore"
["custom", ""], ['custom', '']
]); ])
const childrenMap = new Map<IComponentType, any>([ const childrenMap = new Map<IComponentType, any>([
// @ts-ignore // @ts-ignore
["select", markRaw(ElOption)], // @ts-ignore ['select', markRaw(ElOption)], // @ts-ignore
["radio", markRaw(ElRadio)], // @ts-ignore" ['radio', markRaw(ElRadio)], // @ts-ignore"
["checkbox", markRaw(ElCheckbox)], ['checkbox', markRaw(ElCheckbox)]
]); ])
const pk = props.modalConfig.pk ?? "id"; // 主键名,用于表单数据处理 const pk = props.modalConfig.pk ?? 'id' // 主键名,用于表单数据处理
const modalVisible = ref(false); // 弹窗显示状态 const modalVisible = ref(false) // 弹窗显示状态
const formRef = ref<FormInstance>(); // 表单实例 const formRef = ref<FormInstance>() // 表单实例
const formItems = reactive(props.modalConfig.formItems ?? []); // 表单配置项 const formItems = reactive(props.modalConfig.formItems ?? []) // 表单配置项
const formData = reactive<IObject>({}); // 表单数据 const formData = reactive<IObject>({}) // 表单数据
const formRules: FormRules = {}; // 表单验证规则 const formRules: FormRules = {} // 表单验证规则
const formDisable = ref(false); // 表单禁用状态 const formDisable = ref(false) // 表单禁用状态
// 获取tooltip提示框属性 // 获取tooltip提示框属性
const getTooltipProps = (tips: string | IObject) => { const getTooltipProps = (tips: string | IObject) => {
return typeof tips === "string" ? { content: tips } : tips; return typeof tips === 'string' ? { content: tips } : tips
}; }
// 隐藏弹窗 // 隐藏弹窗
const handleClose = () => { const handleClose = () => {
modalVisible.value = false; modalVisible.value = false
formRef.value?.resetFields(); formRef.value?.resetFields()
}; }
// 设置表单值 // 设置表单值
const setFormData = (data: IObject) => { const setFormData = (data: IObject) => {
for (const key in formData) { for (const key in formData) {
if (Object.prototype.hasOwnProperty.call(formData, key) && key in data) { if (Object.prototype.hasOwnProperty.call(formData, key) && key in data) {
formData[key] = data[key]; formData[key] = data[key]
} }
} }
if (Object.prototype.hasOwnProperty.call(data, pk)) { if (Object.prototype.hasOwnProperty.call(data, pk)) {
formData[pk] = data[pk]; formData[pk] = data[pk]
} }
}; }
// 表单提交 // 表单提交
const handleSubmit = useThrottleFn(() => { const handleSubmit = useThrottleFn(() => {
formRef.value?.validate((valid: boolean) => { formRef.value?.validate((valid: boolean) => {
if (!valid) return; if (!valid) return
if (typeof props.modalConfig.beforeSubmit === "function") { if (typeof props.modalConfig.beforeSubmit === 'function') {
props.modalConfig.beforeSubmit(formData); props.modalConfig.beforeSubmit(formData)
} }
if (!props.modalConfig?.formAction) { if (!props.modalConfig?.formAction) {
emit("customSubmit", formData); emit('customSubmit', formData)
handleClose(); handleClose()
return; return
} }
props.modalConfig.formAction(formData).then(() => { props.modalConfig.formAction(formData).then(() => {
if (props.modalConfig.component === "drawer") { if (props.modalConfig.component === 'drawer') {
ElMessage.success(`${props.modalConfig.drawer?.title}成功`); ElMessage.success(`${props.modalConfig.drawer?.title}成功`)
} else { } else {
ElMessage.success(`${props.modalConfig.dialog?.title}成功`); ElMessage.success(`${props.modalConfig.dialog?.title}成功`)
} }
emit("submitClick"); emit('submitClick')
handleClose(); handleClose()
}); })
}); })
}, 3000); }, 3000)
onMounted(() => { onMounted(() => {
formItems.forEach((item) => { formItems.forEach((item) => {
if (item.initFn) { if (item.initFn) {
item.initFn(item); item.initFn(item)
} }
formRules[item.prop] = item?.rules ?? []; formRules[item.prop] = item?.rules ?? []
props.modalConfig.form = { labelWidth: "auto", ...props.modalConfig?.form }; props.modalConfig.form = { labelWidth: 'auto', ...props.modalConfig?.form }
if (["input-tag", "custom-tag", "cascader"].includes(item.type)) { if (['input-tag', 'custom-tag', 'cascader'].includes(item.type)) {
formData[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : []; formData[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : []
} else if (item.type === "input-number") { } else if (item.type === 'input-number') {
formData[item.prop] = item.initialValue ?? null; formData[item.prop] = item.initialValue ?? null
} else { } else {
formData[item.prop] = item.initialValue ?? ""; formData[item.prop] = item.initialValue ?? ''
} }
}); })
}); })
// 暴露的属性和方法 // 暴露的属性和方法
defineExpose({ defineExpose({
@@ -253,13 +253,13 @@ defineExpose({
setFormItemData: (key: string, value: any) => (formData[key] = value), setFormItemData: (key: string, value: any) => (formData[key] = value),
// 禁用表单 // 禁用表单
handleDisabled: (disable: boolean) => { handleDisabled: (disable: boolean) => {
formDisable.value = disable; formDisable.value = disable
props.modalConfig.form = { props.modalConfig.form = {
...props.modalConfig.form, ...props.modalConfig.form,
disabled: disable, disabled: disable
}; }
}, }
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -11,7 +11,7 @@
<!-- Label --> <!-- Label -->
<template #label> <template #label>
<span class="flex-y-center"> <span class="flex-y-center">
{{ item?.label || "" }} {{ item?.label || '' }}
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)"> <el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
<QuestionFilled class="w-4 h-4 mx-1" /> <QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip> </el-tooltip>
@@ -55,7 +55,7 @@
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable && formItems.length > showNumber"> <template v-if="isExpandable && formItems.length > showNumber">
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand"> <el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
{{ isExpand ? "收起" : "展开" }} {{ isExpand ? '收起' : '展开' }}
<component :is="isExpand ? ArrowUp : ArrowDown" class="w-4 h-4 ml-2" /> <component :is="isExpand ? ArrowUp : ArrowDown" class="w-4 h-4 ml-2" />
</el-link> </el-link>
</template> </template>
@@ -66,96 +66,96 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { IObject, IForm, ISearchConfig, ISearchComponent } from "./types"; import type { IObject, IForm, ISearchConfig, ISearchComponent } from './types'
import { ArrowUp, ArrowDown } from "@element-plus/icons-vue"; import { ArrowUp, ArrowDown } from '@element-plus/icons-vue'
import type { FormInstance } from "element-plus"; import type { FormInstance } from 'element-plus'
import InputTag from "@/components/InputTag/index.vue"; import InputTag from '@/components/InputTag/index.vue'
// 定义接收的属性 // 定义接收的属性
const props = defineProps<{ searchConfig: ISearchConfig }>(); const props = defineProps<{ searchConfig: ISearchConfig }>()
// 自定义事件 // 自定义事件
const emit = defineEmits<{ const emit = defineEmits<{
queryClick: [queryParams: IObject]; queryClick: [queryParams: IObject]
resetClick: [queryParams: IObject]; resetClick: [queryParams: IObject]
}>(); }>()
// 组件映射表 // 组件映射表
const componentMap = new Map<ISearchComponent, any>([ const componentMap = new Map<ISearchComponent, any>([
// @ts-ignore // @ts-ignore
["input", markRaw(ElInput)], // @ts-ignore ['input', markRaw(ElInput)], // @ts-ignore
["select", markRaw(ElSelect)], // @ts-ignore ['select', markRaw(ElSelect)], // @ts-ignore
["cascader", markRaw(ElCascader)], // @ts-ignore ['cascader', markRaw(ElCascader)], // @ts-ignore
["input-number", markRaw(ElInputNumber)], // @ts-ignore ['input-number', markRaw(ElInputNumber)], // @ts-ignore
["date-picker", markRaw(ElDatePicker)], // @ts-ignore ['date-picker', markRaw(ElDatePicker)], // @ts-ignore
["time-picker", markRaw(ElTimePicker)], // @ts-ignore ['time-picker', markRaw(ElTimePicker)], // @ts-ignore
["time-select", markRaw(ElTimeSelect)], // @ts-ignore ['time-select', markRaw(ElTimeSelect)], // @ts-ignore
["tree-select", markRaw(ElTreeSelect)], // @ts-ignore ['tree-select', markRaw(ElTreeSelect)], // @ts-ignore
["input-tag", markRaw(ElInputTag)], // @ts-ignore ['input-tag', markRaw(ElInputTag)], // @ts-ignore
["custom-tag", markRaw(InputTag)], ['custom-tag', markRaw(InputTag)]
]); ])
// 存储表单实例 // 存储表单实例
const queryFormRef = ref<FormInstance>(); const queryFormRef = ref<FormInstance>()
// 存储查询参数 // 存储查询参数
const queryParams = reactive<IObject>({}); const queryParams = reactive<IObject>({})
// 是否显示 // 是否显示
const visible = ref(true); const visible = ref(true)
// 响应式的formItems // 响应式的formItems
const formItems = reactive(props.searchConfig?.formItems ?? []); const formItems = reactive(props.searchConfig?.formItems ?? [])
// 是否可展开/收缩 // 是否可展开/收缩
const isExpandable = ref(props.searchConfig?.isExpandable ?? true); const isExpandable = ref(props.searchConfig?.isExpandable ?? true)
// 是否已展开 // 是否已展开
const isExpand = ref(false); const isExpand = ref(false)
// 表单项展示数量,若可展开,超出展示数量的表单项隐藏 // 表单项展示数量,若可展开,超出展示数量的表单项隐藏
const showNumber = computed(() => const showNumber = computed(() =>
isExpandable.value ? (props.searchConfig?.showNumber ?? 3) : formItems.length isExpandable.value ? (props.searchConfig?.showNumber ?? 3) : formItems.length
); )
// 卡片组件自定义属性(阴影、自定义边距样式等) // 卡片组件自定义属性(阴影、自定义边距样式等)
const cardAttrs = computed<IObject>(() => { const cardAttrs = computed<IObject>(() => {
return { shadow: "never", style: { "margin-bottom": "12px" }, ...props.searchConfig?.cardAttrs }; return { shadow: 'never', style: { 'margin-bottom': '12px' }, ...props.searchConfig?.cardAttrs }
}); })
// 表单组件自定义属性label位置、宽度、对齐方式等 // 表单组件自定义属性label位置、宽度、对齐方式等
const formAttrs = computed<IForm>(() => { const formAttrs = computed<IForm>(() => {
return { inline: true, ...props.searchConfig?.form }; return { inline: true, ...props.searchConfig?.form }
}); })
// 是否使用自适应网格布局 // 是否使用自适应网格布局
const isGrid = computed(() => const isGrid = computed(() =>
props.searchConfig?.grid props.searchConfig?.grid
? "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 4xl:grid-cols-6 gap-5" ? 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 4xl:grid-cols-6 gap-5'
: "flex flex-wrap gap-x-8 gap-y-4" : 'flex flex-wrap gap-x-8 gap-y-4'
); )
// 获取tooltip提示框属性 // 获取tooltip提示框属性
const getTooltipProps = (tips: string | IObject) => { const getTooltipProps = (tips: string | IObject) => {
return typeof tips === "string" ? { content: tips } : tips; return typeof tips === 'string' ? { content: tips } : tips
}; }
// 查询/重置操作 // 查询/重置操作
const handleQuery = () => emit("queryClick", queryParams); const handleQuery = () => emit('queryClick', queryParams)
const handleReset = () => { const handleReset = () => {
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields()
emit("resetClick", queryParams); emit('resetClick', queryParams)
}; }
onMounted(() => { onMounted(() => {
formItems.forEach((item) => { formItems.forEach((item) => {
if (item?.initFn) { if (item?.initFn) {
item.initFn(item); item.initFn(item)
} }
if (["input-tag", "custom-tag", "cascader"].includes(item?.type ?? "")) { if (['input-tag', 'custom-tag', 'cascader'].includes(item?.type ?? '')) {
queryParams[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : []; queryParams[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : []
} else if (item.type === "input-number") { } else if (item.type === 'input-number') {
queryParams[item.prop] = item.initialValue ?? null; queryParams[item.prop] = item.initialValue ?? null
} else { } else {
queryParams[item.prop] = item.initialValue ?? ""; queryParams[item.prop] = item.initialValue ?? ''
} }
}); })
}); })
// 暴露的属性和方法 // 暴露的属性和方法
defineExpose({ defineExpose({
// 获取分页数据 // 获取分页数据
getQueryParams: () => queryParams, getQueryParams: () => queryParams,
// 显示/隐藏 SearchForm // 显示/隐藏 SearchForm
toggleVisible: () => (visible.value = !visible.value), toggleVisible: () => (visible.value = !visible.value)
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,223 +1,223 @@
import type { DialogProps, DrawerProps, FormItemRule, PaginationProps } from "element-plus"; import type { DialogProps, DrawerProps, FormItemRule, PaginationProps } from 'element-plus'
import type { FormProps, TableProps, ColProps, ButtonProps, CardProps } from "element-plus"; import type { FormProps, TableProps, ColProps, ButtonProps, CardProps } from 'element-plus'
import type PageContent from "./PageContent.vue"; import type PageContent from './PageContent.vue'
import type PageModal from "./PageModal.vue"; import type PageModal from './PageModal.vue'
import type PageSearch from "./PageSearch.vue"; import type PageSearch from './PageSearch.vue'
import type { CSSProperties } from "vue"; import type { CSSProperties } from 'vue'
export type PageSearchInstance = InstanceType<typeof PageSearch>; export type PageSearchInstance = InstanceType<typeof PageSearch>
export type PageContentInstance = InstanceType<typeof PageContent>; export type PageContentInstance = InstanceType<typeof PageContent>
export type PageModalInstance = InstanceType<typeof PageModal>; export type PageModalInstance = InstanceType<typeof PageModal>
export type IObject = Record<string, any>; export type IObject = Record<string, any>
type DateComponent = "date-picker" | "time-picker" | "time-select" | "custom-tag" | "input-tag"; type DateComponent = 'date-picker' | 'time-picker' | 'time-select' | 'custom-tag' | 'input-tag'
type InputComponent = "input" | "select" | "input-number" | "cascader" | "tree-select"; type InputComponent = 'input' | 'select' | 'input-number' | 'cascader' | 'tree-select'
type OtherComponent = "text" | "radio" | "checkbox" | "switch" | "icon-select" | "custom"; type OtherComponent = 'text' | 'radio' | 'checkbox' | 'switch' | 'icon-select' | 'custom'
export type ISearchComponent = DateComponent | InputComponent | "custom"; export type ISearchComponent = DateComponent | InputComponent | 'custom'
export type IComponentType = DateComponent | InputComponent | OtherComponent; export type IComponentType = DateComponent | InputComponent | OtherComponent
type ToolbarLeft = "add" | "delete" | "import" | "export"; type ToolbarLeft = 'add' | 'delete' | 'import' | 'export'
type ToolbarRight = "refresh" | "filter" | "imports" | "exports" | "search"; type ToolbarRight = 'refresh' | 'filter' | 'imports' | 'exports' | 'search'
type ToolbarTable = "edit" | "view" | "delete"; type ToolbarTable = 'edit' | 'view' | 'delete'
export type IToolsButton = { export type IToolsButton = {
name: string; // 按钮名称 name: string // 按钮名称
text?: string; // 按钮文本 text?: string // 按钮文本
perm?: Array<string> | string; // 权限标识(可以是完整权限字符串如'sys:user:add'或操作权限如'add') perm?: Array<string> | string // 权限标识(可以是完整权限字符串如'sys:user:add'或操作权限如'add')
attrs?: Partial<ButtonProps> & { style?: CSSProperties }; // 按钮属性 attrs?: Partial<ButtonProps> & { style?: CSSProperties } // 按钮属性
render?: (row: IObject) => boolean; // 条件渲染 render?: (row: IObject) => boolean // 条件渲染
}; }
export type IToolsDefault = ToolbarLeft | ToolbarRight | ToolbarTable | IToolsButton; export type IToolsDefault = ToolbarLeft | ToolbarRight | ToolbarTable | IToolsButton
export interface IOperateData { export interface IOperateData {
name: string; name: string
row: IObject; row: IObject
column: IObject; column: IObject
$index: number; $index: number
} }
export interface ISearchConfig { export interface ISearchConfig {
// 权限前缀(如sys:user用于组成权限标识),不提供则不进行权限校验 // 权限前缀(如sys:user用于组成权限标识),不提供则不进行权限校验
permPrefix?: string; permPrefix?: string
// 标签冒号(默认false) // 标签冒号(默认false)
colon?: boolean; colon?: boolean
// 表单项(默认:[]) // 表单项(默认:[])
formItems?: IFormItems<ISearchComponent>; formItems?: IFormItems<ISearchComponent>
// 是否开启展开和收缩(默认true) // 是否开启展开和收缩(默认true)
isExpandable?: boolean; isExpandable?: boolean
// 默认展示的表单项数量(默认3) // 默认展示的表单项数量(默认3)
showNumber?: number; showNumber?: number
// 卡片属性 // 卡片属性
cardAttrs?: Partial<CardProps> & { style?: CSSProperties }; cardAttrs?: Partial<CardProps> & { style?: CSSProperties }
// form组件属性 // form组件属性
form?: IForm; form?: IForm
// 自适应网格布局(使用时表单不要添加 style: { width: "200px" }) // 自适应网格布局(使用时表单不要添加 style: { width: "200px" })
grid?: boolean | "left" | "right"; grid?: boolean | 'left' | 'right'
} }
export interface IContentConfig<T = any> { export interface IContentConfig<T = any> {
// 权限前缀(如sys:user用于组成权限标识),不提供则不进行权限校验 // 权限前缀(如sys:user用于组成权限标识),不提供则不进行权限校验
permPrefix?: string; permPrefix?: string
// table组件属性 // table组件属性
table?: Omit<TableProps<any>, "data">; table?: Omit<TableProps<any>, 'data'>
// 分页组件位置(默认left) // 分页组件位置(默认left)
pagePosition?: "left" | "right"; pagePosition?: 'left' | 'right'
// pagination组件属性 // pagination组件属性
pagination?: pagination?:
| boolean | boolean
| Partial< | Partial<
Omit< Omit<
PaginationProps, PaginationProps,
"v-model:page-size" | "v-model:current-page" | "total" | "currentPage" 'v-model:page-size' | 'v-model:current-page' | 'total' | 'currentPage'
> >
>; >
// 列表的网络请求函数(需返回promise) // 列表的网络请求函数(需返回promise)
indexAction: (queryParams: T) => Promise<any>; indexAction: (queryParams: T) => Promise<any>
// 默认的分页相关的请求参数 // 默认的分页相关的请求参数
request?: { request?: {
pageName: string; pageName: string
limitName: string; limitName: string
}; }
// 数据格式解析的回调函数 // 数据格式解析的回调函数
parseData?: (res: any) => { parseData?: (res: any) => {
total: number; total: number
list: IObject[]; list: IObject[]
[key: string]: any; [key: string]: any
}; }
// 修改属性的网络请求函数(需返回promise) // 修改属性的网络请求函数(需返回promise)
modifyAction?: (data: { modifyAction?: (data: {
[key: string]: any; [key: string]: any
field: string; field: string
value: boolean | string | number; value: boolean | string | number
}) => Promise<any>; }) => Promise<any>
// 删除的网络请求函数(需返回promise) // 删除的网络请求函数(需返回promise)
deleteAction?: (ids: string) => Promise<any>; deleteAction?: (ids: string) => Promise<any>
// 后端导出的网络请求函数(需返回promise) // 后端导出的网络请求函数(需返回promise)
exportAction?: (queryParams: T) => Promise<any>; exportAction?: (queryParams: T) => Promise<any>
// 前端全量导出的网络请求函数(需返回promise) // 前端全量导出的网络请求函数(需返回promise)
exportsAction?: (queryParams: T) => Promise<IObject[]>; exportsAction?: (queryParams: T) => Promise<IObject[]>
// 导入模板 // 导入模板
importTemplate?: string | (() => Promise<any>); importTemplate?: string | (() => Promise<any>)
// 后端导入的网络请求函数(需返回promise) // 后端导入的网络请求函数(需返回promise)
importAction?: (file: File) => Promise<any>; importAction?: (file: File) => Promise<any>
// 前端导入的网络请求函数(需返回promise) // 前端导入的网络请求函数(需返回promise)
importsAction?: (data: IObject[]) => Promise<any>; importsAction?: (data: IObject[]) => Promise<any>
// 主键名(默认为id) // 主键名(默认为id)
pk?: string; pk?: string
// 表格工具栏(默认:add,delete,export,也可自定义) // 表格工具栏(默认:add,delete,export,也可自定义)
toolbar?: Array<ToolbarLeft | IToolsButton>; toolbar?: Array<ToolbarLeft | IToolsButton>
// 表格工具栏右侧图标(默认:refresh,filter,imports,exports,search) // 表格工具栏右侧图标(默认:refresh,filter,imports,exports,search)
defaultToolbar?: Array<ToolbarRight | IToolsButton>; defaultToolbar?: Array<ToolbarRight | IToolsButton>
// table组件列属性(额外的属性templet,operat,slotName) // table组件列属性(额外的属性templet,operat,slotName)
cols: Array<{ cols: Array<{
type?: "default" | "selection" | "index" | "expand"; type?: 'default' | 'selection' | 'index' | 'expand'
label?: string; label?: string
prop?: string; prop?: string
width?: string | number; width?: string | number
align?: "left" | "center" | "right"; align?: 'left' | 'center' | 'right'
columnKey?: string; columnKey?: string
reserveSelection?: boolean; reserveSelection?: boolean
// 列是否显示 // 列是否显示
show?: boolean; show?: boolean
// 模板 // 模板
templet?: templet?:
| "image" | 'image'
| "list" | 'list'
| "url" | 'url'
| "switch" | 'switch'
| "input" | 'input'
| "price" | 'price'
| "percent" | 'percent'
| "icon" | 'icon'
| "date" | 'date'
| "tool" | 'tool'
| "custom"; | 'custom'
// image模板相关参数 // image模板相关参数
imageWidth?: number; imageWidth?: number
imageHeight?: number; imageHeight?: number
// list模板相关参数 // list模板相关参数
selectList?: IObject; selectList?: IObject
// switch模板相关参数 // switch模板相关参数
activeValue?: boolean | string | number; activeValue?: boolean | string | number
inactiveValue?: boolean | string | number; inactiveValue?: boolean | string | number
activeText?: string; activeText?: string
inactiveText?: string; inactiveText?: string
// input模板相关参数 // input模板相关参数
inputType?: string; inputType?: string
// price模板相关参数 // price模板相关参数
priceFormat?: string; priceFormat?: string
// date模板相关参数 // date模板相关参数
dateFormat?: string; dateFormat?: string
// tool模板相关参数 // tool模板相关参数
operat?: Array<ToolbarTable | IToolsButton>; operat?: Array<ToolbarTable | IToolsButton>
// filter值拼接符 // filter值拼接符
filterJoin?: string; filterJoin?: string
[key: string]: any; [key: string]: any
// 初始化数据函数 // 初始化数据函数
initFn?: (item: IObject) => void; initFn?: (item: IObject) => void
}>; }>
} }
export interface IModalConfig<T = any> { export interface IModalConfig<T = any> {
// 权限前缀(如sys:user用于组成权限标识),不提供则不进行权限校验 // 权限前缀(如sys:user用于组成权限标识),不提供则不进行权限校验
permPrefix?: string; permPrefix?: string
// 标签冒号(默认false) // 标签冒号(默认false)
colon?: boolean; colon?: boolean
// 主键名(主要用于编辑数据,默认为id) // 主键名(主要用于编辑数据,默认为id)
pk?: string; pk?: string
// 组件类型(默认dialog) // 组件类型(默认dialog)
component?: "dialog" | "drawer"; component?: 'dialog' | 'drawer'
// dialog组件属性 // dialog组件属性
dialog?: Partial<Omit<DialogProps, "modelValue">>; dialog?: Partial<Omit<DialogProps, 'modelValue'>>
// drawer组件属性 // drawer组件属性
drawer?: Partial<Omit<DrawerProps, "modelValue">>; drawer?: Partial<Omit<DrawerProps, 'modelValue'>>
// form组件属性 // form组件属性
form?: IForm; form?: IForm
// 表单项 // 表单项
formItems: IFormItems<IComponentType>; formItems: IFormItems<IComponentType>
// 提交之前处理 // 提交之前处理
beforeSubmit?: (data: T) => void; beforeSubmit?: (data: T) => void
// 提交的网络请求函数(需返回promise) // 提交的网络请求函数(需返回promise)
formAction?: (data: T) => Promise<any>; formAction?: (data: T) => Promise<any>
} }
export type IForm = Partial<Omit<FormProps, "model" | "rules">>; export type IForm = Partial<Omit<FormProps, 'model' | 'rules'>>
// 表单项 // 表单项
export type IFormItems<T = IComponentType> = Array<{ export type IFormItems<T = IComponentType> = Array<{
// 组件类型(如input,select,radio,custom等) // 组件类型(如input,select,radio,custom等)
type: T; type: T
// 标签提示 // 标签提示
tips?: string | IObject; tips?: string | IObject
// 标签文本 // 标签文本
label: string; label: string
// 键名 // 键名
prop: string; prop: string
// 组件属性 // 组件属性
attrs?: IObject; attrs?: IObject
// 组件可选项(只适用于select,radio,checkbox组件) // 组件可选项(只适用于select,radio,checkbox组件)
options?: Array<{ label: string; value: any; [key: string]: any }> | Ref<any[]>; options?: Array<{ label: string; value: any; [key: string]: any }> | Ref<any[]>
// 验证规则 // 验证规则
rules?: FormItemRule[]; rules?: FormItemRule[]
// 初始值 // 初始值
initialValue?: any; initialValue?: any
// 插槽名(适用于自定义组件设置类型为custom) // 插槽名(适用于自定义组件设置类型为custom)
slotName?: string; slotName?: string
// 是否隐藏 // 是否隐藏
hidden?: boolean; hidden?: boolean
// layout组件Col属性 // layout组件Col属性
col?: Partial<ColProps>; col?: Partial<ColProps>
// 组件事件 // 组件事件
events?: Record<string, (...args: any) => void>; events?: Record<string, (...args: any) => void>
// 初始化数据函数扩展 // 初始化数据函数扩展
initFn?: (item: IObject) => void; initFn?: (item: IObject) => void
}>; }>
export interface IPageForm { export interface IPageForm {
// 主键名(主要用于编辑数据,默认为id) // 主键名(主要用于编辑数据,默认为id)
pk?: string; pk?: string
// form组件属性 // form组件属性
form?: IForm; form?: IForm
// 表单项 // 表单项
formItems: IFormItems<IComponentType>; formItems: IFormItems<IComponentType>
} }

View File

@@ -1,30 +1,30 @@
import { ref } from "vue"; import { ref } from 'vue'
import type { IObject, PageContentInstance, PageModalInstance, PageSearchInstance } from "./types"; import type { IObject, PageContentInstance, PageModalInstance, PageSearchInstance } from './types'
function usePage() { function usePage() {
const searchRef = ref<PageSearchInstance>(); const searchRef = ref<PageSearchInstance>()
const contentRef = ref<PageContentInstance>(); const contentRef = ref<PageContentInstance>()
const addModalRef = ref<PageModalInstance>(); const addModalRef = ref<PageModalInstance>()
const editModalRef = ref<PageModalInstance>(); const editModalRef = ref<PageModalInstance>()
// 搜索 // 搜索
function handleQueryClick(queryParams: IObject) { function handleQueryClick(queryParams: IObject) {
const filterParams = contentRef.value?.getFilterParams(); const filterParams = contentRef.value?.getFilterParams()
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true)
} }
// 重置 // 重置
function handleResetClick(queryParams: IObject) { function handleResetClick(queryParams: IObject) {
const filterParams = contentRef.value?.getFilterParams(); const filterParams = contentRef.value?.getFilterParams()
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true)
} }
// 新增 // 新增
function handleAddClick(RefImpl?: Ref<PageModalInstance>) { function handleAddClick(RefImpl?: Ref<PageModalInstance>) {
if (RefImpl) { if (RefImpl) {
RefImpl?.value.setModalVisible(); RefImpl?.value.setModalVisible()
RefImpl?.value.handleDisabled(false); RefImpl?.value.handleDisabled(false)
} else { } else {
addModalRef.value?.setModalVisible(); addModalRef.value?.setModalVisible()
addModalRef.value?.handleDisabled(false); addModalRef.value?.handleDisabled(false)
} }
} }
// 编辑 // 编辑
@@ -34,15 +34,15 @@ function usePage() {
RefImpl?: Ref<PageModalInstance> RefImpl?: Ref<PageModalInstance>
) { ) {
if (RefImpl) { if (RefImpl) {
RefImpl.value?.setModalVisible(); RefImpl.value?.setModalVisible()
RefImpl.value?.handleDisabled(false); RefImpl.value?.handleDisabled(false)
const from = await (callback?.(row) ?? Promise.resolve(row)); const from = await (callback?.(row) ?? Promise.resolve(row))
RefImpl.value?.setFormData(from ? from : row); RefImpl.value?.setFormData(from ? from : row)
} else { } else {
editModalRef.value?.setModalVisible(); editModalRef.value?.setModalVisible()
editModalRef.value?.handleDisabled(false); editModalRef.value?.handleDisabled(false)
const from = await (callback?.(row) ?? Promise.resolve(row)); const from = await (callback?.(row) ?? Promise.resolve(row))
editModalRef.value?.setFormData(from ? from : row); editModalRef.value?.setFormData(from ? from : row)
} }
} }
// 查看 // 查看
@@ -52,37 +52,37 @@ function usePage() {
RefImpl?: Ref<PageModalInstance> RefImpl?: Ref<PageModalInstance>
) { ) {
if (RefImpl) { if (RefImpl) {
RefImpl.value?.setModalVisible(); RefImpl.value?.setModalVisible()
RefImpl.value?.handleDisabled(true); RefImpl.value?.handleDisabled(true)
const from = await (callback?.(row) ?? Promise.resolve(row)); const from = await (callback?.(row) ?? Promise.resolve(row))
RefImpl.value?.setFormData(from ? from : row); RefImpl.value?.setFormData(from ? from : row)
} else { } else {
editModalRef.value?.setModalVisible(); editModalRef.value?.setModalVisible()
editModalRef.value?.handleDisabled(true); editModalRef.value?.handleDisabled(true)
const from = await (callback?.(row) ?? Promise.resolve(row)); const from = await (callback?.(row) ?? Promise.resolve(row))
editModalRef.value?.setFormData(from ? from : row); editModalRef.value?.setFormData(from ? from : row)
} }
} }
// 表单提交 // 表单提交
function handleSubmitClick() { function handleSubmitClick() {
//根据检索条件刷新列表数据 //根据检索条件刷新列表数据
const queryParams = searchRef.value?.getQueryParams(); const queryParams = searchRef.value?.getQueryParams()
contentRef.value?.fetchPageData(queryParams, true); contentRef.value?.fetchPageData(queryParams, true)
} }
// 导出 // 导出
function handleExportClick() { function handleExportClick() {
// 根据检索条件导出数据 // 根据检索条件导出数据
const queryParams = searchRef.value?.getQueryParams(); const queryParams = searchRef.value?.getQueryParams()
contentRef.value?.exportPageData(queryParams); contentRef.value?.exportPageData(queryParams)
} }
// 搜索显隐 // 搜索显隐
function handleSearchClick() { function handleSearchClick() {
searchRef.value?.toggleVisible(); searchRef.value?.toggleVisible()
} }
// 涮选数据 // 涮选数据
function handleFilterChange(filterParams: IObject) { function handleFilterChange(filterParams: IObject) {
const queryParams = searchRef.value?.getQueryParams(); const queryParams = searchRef.value?.getQueryParams()
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true)
} }
return { return {
@@ -98,8 +98,8 @@ function usePage() {
handleSubmitClick, handleSubmitClick,
handleExportClick, handleExportClick,
handleSearchClick, handleSearchClick,
handleFilterChange, handleFilterChange
}; }
} }
export default usePage; export default usePage

View File

@@ -7,9 +7,9 @@
defineProps({ defineProps({
padding: { padding: {
type: String, type: String,
default: "p-2", default: 'p-2'
}, }
}); })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.el { .el {

View File

@@ -9,20 +9,20 @@
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ defineOptions({
name: "CopyButton", name: 'CopyButton',
inheritAttrs: false, inheritAttrs: false
}); })
const props = defineProps({ const props = defineProps({
text: { text: {
type: String, type: String,
default: "", default: ''
}, },
style: { style: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, }
}); })
function handleClipboard() { function handleClipboard() {
if (navigator.clipboard && navigator.clipboard.writeText) { if (navigator.clipboard && navigator.clipboard.writeText) {
@@ -30,32 +30,32 @@ function handleClipboard() {
navigator.clipboard navigator.clipboard
.writeText(props.text) .writeText(props.text)
.then(() => { .then(() => {
ElMessage.success("Copy successfully"); ElMessage.success('Copy successfully')
}) })
.catch((error) => { .catch((error) => {
ElMessage.warning("Copy failed"); ElMessage.warning('Copy failed')
console.log("[CopyButton] Copy failed", error); console.log('[CopyButton] Copy failed', error)
}); })
} else { } else {
// 兼容性处理useClipboard 有兼容性问题) // 兼容性处理useClipboard 有兼容性问题)
const input = document.createElement("input"); const input = document.createElement('input')
input.style.position = "absolute"; input.style.position = 'absolute'
input.style.left = "-9999px"; input.style.left = '-9999px'
input.setAttribute("value", props.text); input.setAttribute('value', props.text)
document.body.appendChild(input); document.body.appendChild(input)
input.select(); input.select()
try { try {
const successful = document.execCommand("copy"); const successful = document.execCommand('copy')
if (successful) { if (successful) {
ElMessage.success("Copy successfully!"); ElMessage.success('Copy successfully!')
} else { } else {
ElMessage.warning("Copy failed!"); ElMessage.warning('Copy failed!')
} }
} catch (err) { } catch (err) {
ElMessage.error("Copy failed."); ElMessage.error('Copy failed.')
console.log("[CopyButton] Copy failed.", err); console.log('[CopyButton] Copy failed.', err)
} finally { } finally {
document.body.removeChild(input); document.body.removeChild(input)
} }
} }
} }

View File

@@ -21,19 +21,19 @@
</el-dropdown> </el-dropdown>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSettingsStore } from "@/store"; import { useSettingsStore } from '@/store'
import { ThemeMode } from "@/enums"; import { ThemeMode } from '@/enums'
import { Moon, Sunny } from "@element-plus/icons-vue"; import { Moon, Sunny } from '@element-plus/icons-vue'
const { t } = useI18n(); const { t } = useI18n()
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
const theneList = [ const theneList = [
{ label: t("login.light"), value: ThemeMode.LIGHT, component: Sunny }, { label: t('login.light'), value: ThemeMode.LIGHT, component: Sunny },
{ label: t("login.dark"), value: ThemeMode.DARK, component: Moon }, { label: t('login.dark'), value: ThemeMode.DARK, component: Moon }
]; ]
const handleDarkChange = (theme: ThemeMode) => { const handleDarkChange = (theme: ThemeMode) => {
settingsStore.updateTheme(theme); settingsStore.updateTheme(theme)
}; }
</script> </script>

View File

@@ -7,21 +7,21 @@
</template> </template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useDictStore } from "@/store"; import { useDictStore } from '@/store'
const props = defineProps({ const props = defineProps({
code: String, // 字典编码 code: String, // 字典编码
modelValue: [String, Number], // 字典项的值 modelValue: [String, Number], // 字典项的值
size: { size: {
type: String, type: String,
default: "default", // 标签大小 default: 'default' // 标签大小
}, }
}); })
const label = ref(""); const label = ref('')
const tagType = ref<"success" | "warning" | "info" | "primary" | "danger" | undefined>(); // 标签类型 const tagType = ref<'success' | 'warning' | 'info' | 'primary' | 'danger' | undefined>() // 标签类型
const tagSize = ref<"default" | "large" | "small">(props.size as "default" | "large" | "small"); // 标签大小 const tagSize = ref<'default' | 'large' | 'small'>(props.size as 'default' | 'large' | 'small') // 标签大小
const dictStore = useDictStore(); const dictStore = useDictStore()
/** /**
* 根据字典项的值获取对应的 label 和 tagType * 根据字典项的值获取对应的 label 和 tagType
* @param dictCode 字典编码 * @param dictCode 字典编码
@@ -30,37 +30,37 @@ const dictStore = useDictStore();
*/ */
const getLabelAndTagByValue = async (dictCode: string, value: any) => { const getLabelAndTagByValue = async (dictCode: string, value: any) => {
// 按需加载字典数据 // 按需加载字典数据
await dictStore.loadDictItems(dictCode); await dictStore.loadDictItems(dictCode)
// 从缓存中获取字典数据 // 从缓存中获取字典数据
const dictItems = dictStore.getDictItems(dictCode); const dictItems = dictStore.getDictItems(dictCode)
// 查找对应的字典项 // 查找对应的字典项
const dictItem = dictItems.find((item) => item.value == value); const dictItem = dictItems.find((item) => item.value == value)
return { return {
label: dictItem?.label || "", label: dictItem?.label || '',
tagType: dictItem?.tagType, tagType: dictItem?.tagType
}; }
}; }
/** /**
* 更新 label 和 tagType * 更新 label 和 tagType
*/ */
const updateLabelAndTag = async () => { const updateLabelAndTag = async () => {
if (!props.code || props.modelValue === undefined) return; if (!props.code || props.modelValue === undefined) return
const { label: newLabel, tagType: newTagType } = await getLabelAndTagByValue( const { label: newLabel, tagType: newTagType } = await getLabelAndTagByValue(
props.code, props.code,
props.modelValue props.modelValue
); )
label.value = newLabel; label.value = newLabel
tagType.value = newTagType as typeof tagType.value; tagType.value = newTagType as typeof tagType.value
}; }
// 初始化或code变化时更新标签和标签样式 // 初始化或code变化时更新标签和标签样式
watch( watch(
[() => props.code, () => props.modelValue], [() => props.code, () => props.modelValue],
async () => { async () => {
if (props.code) { if (props.code) {
await updateLabelAndTag(); await updateLabelAndTag()
} }
}, },
{ immediate: true } { immediate: true }
); )
</script> </script>

View File

@@ -42,82 +42,80 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useDictStore } from "@/store"; import { useDictStore } from '@/store'
const dictStore = useDictStore(); const dictStore = useDictStore()
const props = defineProps({ const props = defineProps({
code: { code: {
type: String, type: String,
required: true, required: true
}, },
modelValue: { modelValue: {
type: [String, Number, Array], type: [String, Number, Array],
required: false, required: false
}, },
type: { type: {
type: String, type: String,
default: "select", default: 'select',
validator: (value: string) => ["select", "radio", "checkbox"].includes(value), validator: (value: string) => ['select', 'radio', 'checkbox'].includes(value)
}, },
placeholder: { placeholder: {
type: String, type: String,
default: "请选择", default: '请选择'
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
style: { style: {
type: Object, type: Object,
default: () => { default: () => {
return { return {
width: "300px", width: '300px'
}; }
}, }
}, }
}); })
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(['update:modelValue'])
const options = ref<Array<{ label: string; value: string | number }>>([]); const options = ref<Array<{ label: string; value: string | number }>>([])
const selectedValue = ref<any>( const selectedValue = ref<any>(
typeof props.modelValue === "string" || typeof props.modelValue === "number" typeof props.modelValue === 'string' || typeof props.modelValue === 'number'
? props.modelValue ? props.modelValue
: Array.isArray(props.modelValue) : Array.isArray(props.modelValue)
? props.modelValue ? props.modelValue
: undefined : undefined
); )
// 监听 modelValue 和 options 的变化 // 监听 modelValue 和 options 的变化
watch( watch(
[() => props.modelValue, () => options.value], [() => props.modelValue, () => options.value],
([newValue, newOptions]) => { ([newValue, newOptions]) => {
if (newOptions.length > 0 && newValue !== undefined) { if (newOptions.length > 0 && newValue !== undefined) {
if (props.type === "checkbox") { if (props.type === 'checkbox') {
selectedValue.value = Array.isArray(newValue) ? newValue : []; selectedValue.value = Array.isArray(newValue) ? newValue : []
} else { } else {
const matchedOption = newOptions.find( const matchedOption = newOptions.find((option) => String(option.value) === String(newValue))
(option) => String(option.value) === String(newValue) selectedValue.value = matchedOption?.value
);
selectedValue.value = matchedOption?.value;
} }
} else { } else {
selectedValue.value = undefined; selectedValue.value = undefined
} }
}, },
{ immediate: true } { immediate: true }
); )
// 监听 selectedValue 的变化并触发 update:modelValue // 监听 selectedValue 的变化并触发 update:modelValue
function handleChange(val: any) { function handleChange(val: any) {
emit("update:modelValue", val); emit('update:modelValue', val)
} }
// 获取字典数据 // 获取字典数据
onMounted(async () => { onMounted(async () => {
await dictStore.loadDictItems(props.code); await dictStore.loadDictItems(props.code)
options.value = dictStore.getDictItems(props.code); options.value = dictStore.getDictItems(props.code)
}); })
</script> </script>

View File

@@ -15,15 +15,15 @@
<script setup lang="ts"> <script setup lang="ts">
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。 // 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from "echarts/core"; import * as echarts from 'echarts/core'
// 引入柱状、折线和饼图常用图表 // 引入柱状、折线和饼图常用图表
import { BarChart, LineChart, PieChart } from "echarts/charts"; import { BarChart, LineChart, PieChart } from 'echarts/charts'
// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件, // 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,
import { GridComponent, TooltipComponent, LegendComponent } from "echarts/components"; import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步 // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from "echarts/renderers"; import { CanvasRenderer } from 'echarts/renderers'
import { useResizeObserver } from "@vueuse/core"; import { useResizeObserver } from '@vueuse/core'
// 按需注册组件 // 按需注册组件
echarts.use([ echarts.use([
@@ -33,49 +33,49 @@ echarts.use([
PieChart, PieChart,
GridComponent, GridComponent,
TooltipComponent, TooltipComponent,
LegendComponent, LegendComponent
]); ])
const props = defineProps<{ const props = defineProps<{
options: echarts.EChartsCoreOption; options: echarts.EChartsCoreOption
width?: string; width?: string
height?: string; height?: string
}>(); }>()
const chartRef = ref<HTMLDivElement | null>(null); const chartRef = ref<HTMLDivElement | null>(null)
let chartInstance: echarts.ECharts | null = null; let chartInstance: echarts.ECharts | null = null
// 初始化图表 // 初始化图表
const initChart = () => { const initChart = () => {
if (chartRef.value) { if (chartRef.value) {
chartInstance = echarts.init(chartRef.value); chartInstance = echarts.init(chartRef.value)
if (props.options) { if (props.options) {
chartInstance.setOption(props.options); chartInstance.setOption(props.options)
} }
} }
}; }
// 监听尺寸变化,自动调整 // 监听尺寸变化,自动调整
useResizeObserver(chartRef, () => { useResizeObserver(chartRef, () => {
chartInstance?.resize(); chartInstance?.resize()
}); })
// 监听 options 变化,更新图表 // 监听 options 变化,更新图表
watch( watch(
() => props.options, () => props.options,
(newOptions) => { (newOptions) => {
if (chartInstance && newOptions) { if (chartInstance && newOptions) {
chartInstance.setOption(newOptions); chartInstance.setOption(newOptions)
} }
}, },
{ deep: true } { deep: true }
); )
onMounted(() => { onMounted(() => {
nextTick(() => initChart()); nextTick(() => initChart())
}); })
onBeforeUnmount(() => { onBeforeUnmount(() => {
chartInstance?.dispose(); chartInstance?.dispose()
}); })
</script> </script>

View File

@@ -5,7 +5,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { isFullscreen, toggle } = useFullscreen(); const { isFullscreen, toggle } = useFullscreen()
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -5,23 +5,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useSettingsStore } from "@/store"; import { useSettingsStore } from '@/store'
import { ThemeMode, SidebarColor } from "@/enums/settings/theme-enum"; import { ThemeMode, SidebarColor } from '@/enums/settings/theme-enum'
import { LayoutMode } from "@/enums/settings/layout-enum"; import { LayoutMode } from '@/enums/settings/layout-enum'
defineProps({ defineProps({
isActive: { type: Boolean, required: true }, isActive: { type: Boolean, required: true }
}); })
const emit = defineEmits(["toggleClick"]); const emit = defineEmits(['toggleClick'])
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
const layout = computed(() => settingsStore.layout); const layout = computed(() => settingsStore.layout)
const hamburgerClass = computed(() => { const hamburgerClass = computed(() => {
// 如果暗黑主题 // 如果暗黑主题
if (settingsStore.theme === ThemeMode.DARK) { if (settingsStore.theme === ThemeMode.DARK) {
return "hamburger--white"; return 'hamburger--white'
} }
// 如果是混合布局 && 侧边栏配色方案是经典蓝 // 如果是混合布局 && 侧边栏配色方案是经典蓝
@@ -29,15 +29,15 @@ const hamburgerClass = computed(() => {
layout.value === LayoutMode.MIX && layout.value === LayoutMode.MIX &&
settingsStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE settingsStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE
) { ) {
return "hamburger--white"; return 'hamburger--white'
} }
// 默认返回空字符串 // 默认返回空字符串
return ""; return ''
}); })
function toggleClick() { function toggleClick() {
emit("toggleClick"); emit('toggleClick')
} }
</script> </script>

View File

@@ -1,11 +1,10 @@
<template> <template>
<el-input v-model="input" placeholder="Please input" /> <el-input v-model="input" placeholder="Please input" />
<el-input-number v-model="num" :min="1" :max="10" @change="handleChange"/> <el-input-number v-model="num" :min="1" :max="10" @change="handleChange" />
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent, ref} from 'vue' import { defineComponent, ref } from 'vue'
export default defineComponent({ export default defineComponent({
setup() { setup() {
@@ -14,15 +13,14 @@ export default defineComponent({
console.log(value) console.log(value)
} }
return { return {
input:ref(''), input: ref(''),
num, num,
handleChange, handleChange
} }
}, }
}) })
</script> </script>
<style scoped> <style scoped>
a { a {
color: #42b983; color: #42b983;

View File

@@ -27,7 +27,7 @@
<el-icon <el-icon
:style="{ :style="{
transform: popoverVisible ? 'rotate(180deg)' : 'rotate(0)', transform: popoverVisible ? 'rotate(180deg)' : 'rotate(0)',
transition: 'transform .5s', transition: 'transform .5s'
}" }"
> >
<ArrowDown @click.stop="togglePopover" /> <ArrowDown @click.stop="togglePopover" />
@@ -81,100 +81,100 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as ElementPlusIconsVue from "@element-plus/icons-vue"; import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: String, type: String,
default: "", default: ''
}, },
width: { width: {
type: String, type: String,
default: "500px", default: '500px'
}, }
}); })
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(['update:modelValue'])
const iconSelectRef = ref(); const iconSelectRef = ref()
const popoverContentRef = ref(); const popoverContentRef = ref()
const popoverVisible = ref(false); const popoverVisible = ref(false)
const activeTab = ref("svg"); const activeTab = ref('svg')
const svgIcons = ref<string[]>([]); const svgIcons = ref<string[]>([])
const elementIcons = ref<string[]>(Object.keys(ElementPlusIconsVue)); const elementIcons = ref<string[]>(Object.keys(ElementPlusIconsVue))
const selectedIcon = defineModel("modelValue", { const selectedIcon = defineModel('modelValue', {
type: String, type: String,
required: true, required: true,
default: "", default: ''
}); })
const filterText = ref(""); const filterText = ref('')
const filteredSvgIcons = ref<string[]>([]); const filteredSvgIcons = ref<string[]>([])
const filteredElementIcons = ref<string[]>(elementIcons.value); const filteredElementIcons = ref<string[]>(elementIcons.value)
const isElementIcon = computed(() => { const isElementIcon = computed(() => {
return selectedIcon.value && selectedIcon.value.startsWith("el-icon"); return selectedIcon.value && selectedIcon.value.startsWith('el-icon')
}); })
function loadIcons() { function loadIcons() {
const icons = import.meta.glob("../../assets/icons/*.svg"); const icons = import.meta.glob('../../assets/icons/*.svg')
for (const path in icons) { for (const path in icons) {
const iconName = path.replace(/.*\/(.*)\.svg$/, "$1"); const iconName = path.replace(/.*\/(.*)\.svg$/, '$1')
svgIcons.value.push(iconName); svgIcons.value.push(iconName)
} }
filteredSvgIcons.value = svgIcons.value; filteredSvgIcons.value = svgIcons.value
} }
function handleTabClick(tabPane: any) { function handleTabClick(tabPane: any) {
activeTab.value = tabPane.props.name; activeTab.value = tabPane.props.name
filterIcons(); filterIcons()
} }
function filterIcons() { function filterIcons() {
if (activeTab.value === "svg") { if (activeTab.value === 'svg') {
filteredSvgIcons.value = filterText.value filteredSvgIcons.value = filterText.value
? svgIcons.value.filter((icon) => icon.toLowerCase().includes(filterText.value.toLowerCase())) ? svgIcons.value.filter((icon) => icon.toLowerCase().includes(filterText.value.toLowerCase()))
: svgIcons.value; : svgIcons.value
} else { } else {
filteredElementIcons.value = filterText.value filteredElementIcons.value = filterText.value
? elementIcons.value.filter((icon) => ? elementIcons.value.filter((icon) =>
icon.toLowerCase().includes(filterText.value.toLowerCase()) icon.toLowerCase().includes(filterText.value.toLowerCase())
) )
: elementIcons.value; : elementIcons.value
} }
} }
function selectIcon(icon: string) { function selectIcon(icon: string) {
const iconName = activeTab.value === "element" ? "el-icon-" + icon : icon; const iconName = activeTab.value === 'element' ? 'el-icon-' + icon : icon
emit("update:modelValue", iconName); emit('update:modelValue', iconName)
popoverVisible.value = false; popoverVisible.value = false
} }
function togglePopover() { function togglePopover() {
popoverVisible.value = !popoverVisible.value; popoverVisible.value = !popoverVisible.value
} }
onClickOutside(iconSelectRef, () => (popoverVisible.value = false), { onClickOutside(iconSelectRef, () => (popoverVisible.value = false), {
ignore: [popoverContentRef], ignore: [popoverContentRef]
}); })
/** /**
* 清空已选图标 * 清空已选图标
*/ */
function clearSelectedIcon() { function clearSelectedIcon() {
selectedIcon.value = ""; selectedIcon.value = ''
} }
onMounted(() => { onMounted(() => {
loadIcons(); loadIcons()
if (selectedIcon.value) { if (selectedIcon.value) {
if (elementIcons.value.includes(selectedIcon.value.replace("el-icon-", ""))) { if (elementIcons.value.includes(selectedIcon.value.replace('el-icon-', ''))) {
activeTab.value = "element"; activeTab.value = 'element'
} else { } else {
activeTab.value = "svg"; activeTab.value = 'svg'
} }
} }
}); })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -20,54 +20,54 @@
@blur.stop.prevent="handleInputConfirm" @blur.stop.prevent="handleInputConfirm"
/> />
<el-button v-else v-bind="config.buttonAttrs" @click="showInput"> <el-button v-else v-bind="config.buttonAttrs" @click="showInput">
{{ config.buttonAttrs.btnText ? config.buttonAttrs.btnText : "+ New Tag" }} {{ config.buttonAttrs.btnText ? config.buttonAttrs.btnText : '+ New Tag' }}
</el-button> </el-button>
</div> </div>
</el-scrollbar> </el-scrollbar>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { InputInstance } from "element-plus"; import type { InputInstance } from 'element-plus'
const inputValue = ref(""); const inputValue = ref('')
const inputVisible = ref(false); const inputVisible = ref(false)
const inputRef = ref<InputInstance>(); const inputRef = ref<InputInstance>()
// 定义 model用于与父组件的 v-model绑定 // 定义 model用于与父组件的 v-model绑定
const tags = defineModel<string[]>(); const tags = defineModel<string[]>()
defineProps({ defineProps({
config: { config: {
type: Object as () => { type: Object as () => {
buttonAttrs: Record<string, any>; buttonAttrs: Record<string, any>
inputAttrs: Record<string, any>; inputAttrs: Record<string, any>
tagAttrs: Record<string, any>; tagAttrs: Record<string, any>
}, },
default: () => ({ default: () => ({
buttonAttrs: {}, buttonAttrs: {},
inputAttrs: {}, inputAttrs: {},
tagAttrs: {}, tagAttrs: {}
}), })
}, }
}); })
const handleClose = (tag: string) => { const handleClose = (tag: string) => {
if (tags.value) { if (tags.value) {
const newTags = tags.value.filter((t) => t !== tag); const newTags = tags.value.filter((t) => t !== tag)
tags.value = [...newTags]; tags.value = [...newTags]
} }
}; }
const showInput = () => { const showInput = () => {
inputVisible.value = true; inputVisible.value = true
nextTick(() => inputRef.value?.focus()); nextTick(() => inputRef.value?.focus())
}; }
const handleInputConfirm = () => { const handleInputConfirm = () => {
if (inputValue.value) { if (inputValue.value) {
const newTags = [...(tags.value || []), inputValue.value]; const newTags = [...(tags.value || []), inputValue.value]
tags.value = newTags; tags.value = newTags
} }
inputVisible.value = false; inputVisible.value = false
inputValue.value = ""; inputValue.value = ''
}; }
</script> </script>

View File

@@ -17,23 +17,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useAppStore } from "@/store/modules/app-store"; import { useAppStore } from '@/store/modules/app-store'
import { LanguageEnum } from "@/enums/settings/locale-enum"; import { LanguageEnum } from '@/enums/settings/locale-enum'
defineProps({ defineProps({
size: { size: {
type: String, type: String,
required: false, required: false
}, }
}); })
const langOptions = [ const langOptions = [
{ label: "中文", value: LanguageEnum.ZH_CN }, { label: '中文', value: LanguageEnum.ZH_CN },
{ label: "English", value: LanguageEnum.EN }, { label: 'English', value: LanguageEnum.EN }
]; ]
const appStore = useAppStore(); const appStore = useAppStore()
const { locale, t } = useI18n(); const { locale, t } = useI18n()
/** /**
* 处理语言切换 * 处理语言切换
@@ -41,9 +41,9 @@ const { locale, t } = useI18n();
* @param lang 语言zh-cn、en * @param lang 语言zh-cn、en
*/ */
function handleLanguageChange(lang: string) { function handleLanguageChange(lang: string) {
locale.value = lang; locale.value = lang
appStore.changeLanguage(lang); appStore.changeLanguage(lang)
ElMessage.success(t("langSelect.message.success")); ElMessage.success(t('langSelect.message.success'))
} }
</script> </script>

View File

@@ -71,8 +71,8 @@
:class="[ :class="[
'search-result__item', 'search-result__item',
{ {
'search-result__item--active': index === activeIndex, 'search-result__item--active': index === activeIndex
}, }
]" ]"
@click="navigateToRoute(item)" @click="navigateToRoute(item)"
> >
@@ -124,193 +124,191 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import router from "@/router"; import router from '@/router'
import { usePermissionStore } from "@/store"; import { usePermissionStore } from '@/store'
import { isExternal } from "@/utils"; import { isExternal } from '@/utils'
import { RouteRecordRaw, LocationQueryRaw } from "vue-router"; import { RouteRecordRaw, LocationQueryRaw } from 'vue-router'
import { Clock, Close, Delete } from "@element-plus/icons-vue"; import { Clock, Close, Delete } from '@element-plus/icons-vue'
const HISTORY_KEY = "menu_search_history"; const HISTORY_KEY = 'menu_search_history'
const MAX_HISTORY = 5; const MAX_HISTORY = 5
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore()
const isModalVisible = ref(false); const isModalVisible = ref(false)
const searchKeyword = ref(""); const searchKeyword = ref('')
const searchInputRef = ref(); const searchInputRef = ref()
const excludedRoutes = ref(["/redirect", "/login", "/401", "/404"]); const excludedRoutes = ref(['/redirect', '/login', '/401', '/404'])
const menuItems = ref<SearchItem[]>([]); const menuItems = ref<SearchItem[]>([])
const searchResults = ref<SearchItem[]>([]); const searchResults = ref<SearchItem[]>([])
const activeIndex = ref(-1); const activeIndex = ref(-1)
const searchHistory = ref<SearchItem[]>([]); const searchHistory = ref<SearchItem[]>([])
interface SearchItem { interface SearchItem {
title: string; title: string
path: string; path: string
name?: string; name?: string
icon?: string; icon?: string
redirect?: string; redirect?: string
params?: LocationQueryRaw; params?: LocationQueryRaw
} }
// 从本地存储加载搜索历史 // 从本地存储加载搜索历史
function loadSearchHistory() { function loadSearchHistory() {
const historyStr = localStorage.getItem(HISTORY_KEY); const historyStr = localStorage.getItem(HISTORY_KEY)
if (historyStr) { if (historyStr) {
try { try {
searchHistory.value = JSON.parse(historyStr); searchHistory.value = JSON.parse(historyStr)
} catch { } catch {
searchHistory.value = []; searchHistory.value = []
} }
} }
} }
// 保存搜索历史到本地存储 // 保存搜索历史到本地存储
function saveSearchHistory() { function saveSearchHistory() {
localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory.value)); localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory.value))
} }
// 添加项目到搜索历史 // 添加项目到搜索历史
function addToHistory(item: SearchItem) { function addToHistory(item: SearchItem) {
// 检查是否已存在 // 检查是否已存在
const index = searchHistory.value.findIndex((i) => i.path === item.path); const index = searchHistory.value.findIndex((i) => i.path === item.path)
// 如果存在则移除 // 如果存在则移除
if (index !== -1) { if (index !== -1) {
searchHistory.value.splice(index, 1); searchHistory.value.splice(index, 1)
} }
// 添加到历史开头 // 添加到历史开头
searchHistory.value.unshift(item); searchHistory.value.unshift(item)
// 限制历史记录数量 // 限制历史记录数量
if (searchHistory.value.length > MAX_HISTORY) { if (searchHistory.value.length > MAX_HISTORY) {
searchHistory.value = searchHistory.value.slice(0, MAX_HISTORY); searchHistory.value = searchHistory.value.slice(0, MAX_HISTORY)
} }
// 保存到本地存储 // 保存到本地存储
saveSearchHistory(); saveSearchHistory()
} }
// 移除历史记录项 // 移除历史记录项
function removeHistoryItem(index: number) { function removeHistoryItem(index: number) {
searchHistory.value.splice(index, 1); searchHistory.value.splice(index, 1)
saveSearchHistory(); saveSearchHistory()
} }
// 清空历史记录 // 清空历史记录
function clearHistory() { function clearHistory() {
searchHistory.value = []; searchHistory.value = []
localStorage.removeItem(HISTORY_KEY); localStorage.removeItem(HISTORY_KEY)
} }
// 注册全局快捷键 // 注册全局快捷键
function handleKeyDown(e: KeyboardEvent) { function handleKeyDown(e: KeyboardEvent) {
// 判断是否为Ctrl+K组合键 // 判断是否为Ctrl+K组合键
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
e.preventDefault(); // 阻止默认行为 e.preventDefault() // 阻止默认行为
openSearchModal(); openSearchModal()
} }
} }
// 添加键盘事件监听 // 添加键盘事件监听
onMounted(() => { onMounted(() => {
loadRoutes(permissionStore.routes); loadRoutes(permissionStore.routes)
loadSearchHistory(); loadSearchHistory()
document.addEventListener("keydown", handleKeyDown); document.addEventListener('keydown', handleKeyDown)
}); })
// 移除键盘事件监听 // 移除键盘事件监听
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener("keydown", handleKeyDown); document.removeEventListener('keydown', handleKeyDown)
}); })
// 打开搜索模态框 // 打开搜索模态框
function openSearchModal() { function openSearchModal() {
searchKeyword.value = ""; searchKeyword.value = ''
activeIndex.value = -1; activeIndex.value = -1
isModalVisible.value = true; isModalVisible.value = true
setTimeout(() => { setTimeout(() => {
searchInputRef.value.focus(); searchInputRef.value.focus()
}, 100); }, 100)
} }
// 关闭搜索模态框 // 关闭搜索模态框
function closeSearchModal() { function closeSearchModal() {
isModalVisible.value = false; isModalVisible.value = false
} }
// 更新搜索结果 // 更新搜索结果
function updateSearchResults() { function updateSearchResults() {
activeIndex.value = -1; activeIndex.value = -1
if (searchKeyword.value) { if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase(); const keyword = searchKeyword.value.toLowerCase()
searchResults.value = menuItems.value.filter((item) => searchResults.value = menuItems.value.filter((item) =>
item.title.toLowerCase().includes(keyword) item.title.toLowerCase().includes(keyword)
); )
} else { } else {
searchResults.value = []; searchResults.value = []
} }
} }
// 显示搜索结果 // 显示搜索结果
const displayResults = computed(() => searchResults.value); const displayResults = computed(() => searchResults.value)
// 执行搜索 // 执行搜索
function selectActiveResult() { function selectActiveResult() {
if (displayResults.value.length > 0 && activeIndex.value >= 0) { if (displayResults.value.length > 0 && activeIndex.value >= 0) {
navigateToRoute(displayResults.value[activeIndex.value]); navigateToRoute(displayResults.value[activeIndex.value])
} }
} }
// 导航搜索结果 // 导航搜索结果
function navigateResults(direction: string) { function navigateResults(direction: string) {
if (displayResults.value.length === 0) return; if (displayResults.value.length === 0) return
if (direction === "up") { if (direction === 'up') {
activeIndex.value = activeIndex.value =
activeIndex.value <= 0 ? displayResults.value.length - 1 : activeIndex.value - 1; activeIndex.value <= 0 ? displayResults.value.length - 1 : activeIndex.value - 1
} else if (direction === "down") { } else if (direction === 'down') {
activeIndex.value = activeIndex.value =
activeIndex.value >= displayResults.value.length - 1 ? 0 : activeIndex.value + 1; activeIndex.value >= displayResults.value.length - 1 ? 0 : activeIndex.value + 1
} }
} }
// 跳转到 // 跳转到
function navigateToRoute(item: SearchItem) { function navigateToRoute(item: SearchItem) {
closeSearchModal(); closeSearchModal()
// 添加到历史记录 // 添加到历史记录
addToHistory(item); addToHistory(item)
if (isExternal(item.path)) { if (isExternal(item.path)) {
window.open(item.path, "_blank"); window.open(item.path, '_blank')
} else { } else {
router.push({ path: item.path, query: item.params }); router.push({ path: item.path, query: item.params })
} }
} }
function loadRoutes(routes: RouteRecordRaw[], parentPath = "") { function loadRoutes(routes: RouteRecordRaw[], parentPath = '') {
routes.forEach((route) => { routes.forEach((route) => {
const path = route.path.startsWith("/") const path = route.path.startsWith('/')
? route.path ? route.path
: `${parentPath}${parentPath.endsWith("/") ? "" : "/"}${route.path}`; : `${parentPath}${parentPath.endsWith('/') ? '' : '/'}${route.path}`
if (excludedRoutes.value.includes(route.path) || isExternal(route.path)) return; if (excludedRoutes.value.includes(route.path) || isExternal(route.path)) return
if (route.children) { if (route.children) {
loadRoutes(route.children, path); loadRoutes(route.children, path)
} else if (route.meta?.title) { } else if (route.meta?.title) {
const title = route.meta.title === "dashboard" ? "首页" : route.meta.title; const title = route.meta.title === 'dashboard' ? '首页' : route.meta.title
menuItems.value.push({ menuItems.value.push({
title, title,
path, path,
name: typeof route.name === "string" ? route.name : undefined, name: typeof route.name === 'string' ? route.name : undefined,
icon: route.meta.icon, icon: route.meta.icon,
redirect: typeof route.redirect === "string" ? route.redirect : undefined, redirect: typeof route.redirect === 'string' ? route.redirect : undefined,
params: route.meta.params params: route.meta.params ? JSON.parse(JSON.stringify(toRaw(route.meta.params))) : undefined
? JSON.parse(JSON.stringify(toRaw(route.meta.params))) })
: undefined,
});
} }
}); })
} }
</script> </script>
@@ -481,14 +479,14 @@ function loadRoutes(routes: RouteRecordRaw[], parentPath = "") {
left: 1px; left: 1px;
height: 50%; height: 50%;
pointer-events: none; pointer-events: none;
content: ""; content: '';
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0)); background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
} }
} }
.esc-btn { .esc-btn {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 11px; font-size: 11px;
} }

View File

@@ -79,85 +79,85 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import NoticeAPI, { NoticePageVO, NoticeDetailVO } from "@/api/system/notice-api"; import NoticeAPI, { NoticePageVO, NoticeDetailVO } from '@/api/system/notice-api'
import router from "@/router"; import router from '@/router'
const noticeList = ref<NoticePageVO[]>([]); const noticeList = ref<NoticePageVO[]>([])
const noticeDialogVisible = ref(false); const noticeDialogVisible = ref(false)
const noticeDetail = ref<NoticeDetailVO | null>(null); const noticeDetail = ref<NoticeDetailVO | null>(null)
import { useStomp } from "@/composables/websocket/useStomp"; import { useStomp } from '@/composables/websocket/useStomp'
const { subscribe, unsubscribe, isConnected } = useStomp(); const { subscribe, unsubscribe, isConnected } = useStomp()
watch( watch(
() => isConnected.value, () => isConnected.value,
(connected) => { (connected) => {
if (connected) { if (connected) {
subscribe("/user/queue/message", (message: any) => { subscribe('/user/queue/message', (message: any) => {
console.log("收到通知消息:", message); console.log('收到通知消息:', message)
const data = JSON.parse(message.body); const data = JSON.parse(message.body)
const id = data.id; const id = data.id
if (!noticeList.value.some((notice) => notice.id == id)) { if (!noticeList.value.some((notice) => notice.id == id)) {
noticeList.value.unshift({ noticeList.value.unshift({
id, id,
title: data.title, title: data.title,
type: data.type, type: data.type,
publishTime: data.publishTime, publishTime: data.publishTime
}); })
ElNotification({ ElNotification({
title: "您收到一条新的通知消息!", title: '您收到一条新的通知消息!',
message: data.title, message: data.title,
type: "success", type: 'success',
position: "bottom-right", position: 'bottom-right'
}); })
} }
}); })
} }
} }
); )
/** /**
* 获取我的通知公告 * 获取我的通知公告
*/ */
function featchMyNotice() { function featchMyNotice() {
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then((data) => { NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then((data) => {
noticeList.value = data.list; noticeList.value = data.list
}); })
} }
// 阅读通知公告 // 阅读通知公告
function handleReadNotice(id: string) { function handleReadNotice(id: string) {
NoticeAPI.getDetail(id).then((data) => { NoticeAPI.getDetail(id).then((data) => {
noticeDialogVisible.value = true; noticeDialogVisible.value = true
noticeDetail.value = data; noticeDetail.value = data
// 标记为已读 // 标记为已读
const index = noticeList.value.findIndex((notice) => notice.id === id); const index = noticeList.value.findIndex((notice) => notice.id === id)
if (index >= 0) { if (index >= 0) {
noticeList.value.splice(index, 1); noticeList.value.splice(index, 1)
} }
}); })
} }
// 查看更多 // 查看更多
function handleViewMoreNotice() { function handleViewMoreNotice() {
router.push({ name: "MyNotice" }); router.push({ name: 'MyNotice' })
} }
// 全部已读 // 全部已读
function handleMarkAllAsRead() { function handleMarkAllAsRead() {
NoticeAPI.readAll().then(() => { NoticeAPI.readAll().then(() => {
noticeList.value = []; noticeList.value = []
}); })
} }
onMounted(() => { onMounted(() => {
featchMyNotice(); featchMyNotice()
}); })
onBeforeUnmount(() => { onBeforeUnmount(() => {
unsubscribe("/user/queue/message"); unsubscribe('/user/queue/message')
}); })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -16,52 +16,52 @@
<script setup lang="ts"> <script setup lang="ts">
interface Props { interface Props {
listDataLength: number; listDataLength: number
prop?: string; prop?: string
label?: string; label?: string
fixed?: string; fixed?: string
align?: string; align?: string
width?: number; width?: number
showOverflowTooltip?: boolean; showOverflowTooltip?: boolean
minWidth?: number; minWidth?: number
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
label: "操作", label: '操作',
fixed: "right", fixed: 'right',
align: "center", align: 'center',
minWidth: 80, minWidth: 80
}); })
const count = ref(0); const count = ref(0)
const operationWidth = ref(props.minWidth || 80); const operationWidth = ref(props.minWidth || 80)
// 计算操作列宽度 // 计算操作列宽度
const calculateWidth = () => { const calculateWidth = () => {
count.value++; count.value++
if (count.value !== props.listDataLength) return; if (count.value !== props.listDataLength) return
const maxWidth = getOperationMaxWidth(); const maxWidth = getOperationMaxWidth()
operationWidth.value = Math.max(maxWidth, props.minWidth); operationWidth.value = Math.max(maxWidth, props.minWidth)
count.value = 0; count.value = 0
}; }
// 计算最终宽度 // 计算最终宽度
const finalWidth = computed(() => { const finalWidth = computed(() => {
return props.width || operationWidth.value || props.minWidth; return props.width || operationWidth.value || props.minWidth
}); })
// 自适应宽度指令 // 自适应宽度指令
const vAutoWidth = { const vAutoWidth = {
mounted() { mounted() {
// 初次挂载的时候计算一次 // 初次挂载的时候计算一次
calculateWidth(); calculateWidth()
}, },
updated() { updated() {
// 数据更新时重新计算一次 // 数据更新时重新计算一次
calculateWidth(); calculateWidth()
}, }
}; }
/** /**
* 获取按钮数量和宽带来获取操作组的最大宽度 * 获取按钮数量和宽带来获取操作组的最大宽度
@@ -69,23 +69,23 @@ const vAutoWidth = {
* @returns {number} 返回操作组的最大宽度 * @returns {number} 返回操作组的最大宽度
*/ */
const getOperationMaxWidth = () => { const getOperationMaxWidth = () => {
const el = document.getElementsByClassName("operation-buttons"); const el = document.getElementsByClassName('operation-buttons')
// 取操作组的最大宽度 // 取操作组的最大宽度
let maxWidth = 0; let maxWidth = 0
let totalWidth: any = 0; let totalWidth: any = 0
Array.prototype.forEach.call(el, (item) => { Array.prototype.forEach.call(el, (item) => {
// 获取每个item的dom // 获取每个item的dom
const buttons = item.querySelectorAll(".el-button"); const buttons = item.querySelectorAll('.el-button')
// 获取每行按钮的总宽度 // 获取每行按钮的总宽度
totalWidth = Array.from(buttons).reduce((acc, button: any) => { totalWidth = Array.from(buttons).reduce((acc, button: any) => {
return acc + button.scrollWidth + 22; // 每个按钮的宽度加上预留宽度 return acc + button.scrollWidth + 22 // 每个按钮的宽度加上预留宽度
}, 0); }, 0)
// 获取最大的宽度 // 获取最大的宽度
if (totalWidth > maxWidth) maxWidth = totalWidth; if (totalWidth > maxWidth) maxWidth = totalWidth
}); })
return maxWidth; return maxWidth
}; }
</script> </script>

View File

@@ -19,64 +19,64 @@
const props = defineProps({ const props = defineProps({
total: { total: {
type: Number as PropType<number>, type: Number as PropType<number>,
default: 0, default: 0
}, },
pageSizes: { pageSizes: {
type: Array as PropType<number[]>, type: Array as PropType<number[]>,
default() { default() {
return [10, 20, 30, 50]; return [10, 20, 30, 50]
}, }
}, },
layout: { layout: {
type: String, type: String,
default: "total, sizes, prev, pager, next, jumper", default: 'total, sizes, prev, pager, next, jumper'
}, },
background: { background: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
autoScroll: { autoScroll: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
hidden: { hidden: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}); })
const emit = defineEmits(["pagination"]); const emit = defineEmits(['pagination'])
const currentPage = defineModel("page", { const currentPage = defineModel('page', {
type: Number, type: Number,
required: true, required: true,
default: 1, default: 1
}); })
const pageSize = defineModel("limit", { const pageSize = defineModel('limit', {
type: Number, type: Number,
required: true, required: true,
default: 10, default: 10
}); })
watch( watch(
() => props.total, () => props.total,
(newVal: number) => { (newVal: number) => {
const lastPage = Math.ceil(newVal / pageSize.value); const lastPage = Math.ceil(newVal / pageSize.value)
if (newVal > 0 && currentPage.value > lastPage) { if (newVal > 0 && currentPage.value > lastPage) {
currentPage.value = lastPage; currentPage.value = lastPage
emit("pagination", { page: currentPage.value, limit: pageSize.value }); emit('pagination', { page: currentPage.value, limit: pageSize.value })
} }
} }
); )
function handleSizeChange(val: number) { function handleSizeChange(val: number) {
currentPage.value = 1; currentPage.value = 1
emit("pagination", { page: currentPage.value, limit: val }); emit('pagination', { page: currentPage.value, limit: val })
} }
function handleCurrentChange(val: number) { function handleCurrentChange(val: number) {
emit("pagination", { page: val, limit: pageSize.value }); emit('pagination', { page: val, limit: pageSize.value })
} }
</script> </script>

View File

@@ -20,21 +20,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ComponentSize } from "@/enums/settings/layout-enum"; import { ComponentSize } from '@/enums/settings/layout-enum'
import { useAppStore } from "@/store/modules/app-store"; import { useAppStore } from '@/store/modules/app-store'
const { t } = useI18n(); const { t } = useI18n()
const sizeOptions = computed(() => { const sizeOptions = computed(() => {
return [ return [
{ label: t("sizeSelect.default"), value: ComponentSize.DEFAULT }, { label: t('sizeSelect.default'), value: ComponentSize.DEFAULT },
{ label: t("sizeSelect.large"), value: ComponentSize.LARGE }, { label: t('sizeSelect.large'), value: ComponentSize.LARGE },
{ label: t("sizeSelect.small"), value: ComponentSize.SMALL }, { label: t('sizeSelect.small'), value: ComponentSize.SMALL }
]; ]
}); })
const appStore = useAppStore(); const appStore = useAppStore()
function handleSizeChange(size: string) { function handleSizeChange(size: string) {
appStore.changeSize(size); appStore.changeSize(size)
ElMessage.success(t("sizeSelect.message.success")); ElMessage.success(t('sizeSelect.message.success'))
} }
</script> </script>

View File

@@ -20,7 +20,7 @@
<el-icon <el-icon
:style="{ :style="{
transform: popoverVisible ? 'rotate(180deg)' : 'rotate(0)', transform: popoverVisible ? 'rotate(180deg)' : 'rotate(0)',
transition: 'transform .5s', transition: 'transform .5s'
}" }"
> >
<ArrowDown /> <ArrowDown />
@@ -142,198 +142,198 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed } from "vue"; import { ref, reactive, computed } from 'vue'
import { useResizeObserver } from "@vueuse/core"; import { useResizeObserver } from '@vueuse/core'
import type { FormInstance, PopoverProps, TableInstance } from "element-plus"; import type { FormInstance, PopoverProps, TableInstance } from 'element-plus'
// 对象类型 // 对象类型
export type IObject = Record<string, any>; export type IObject = Record<string, any>
// 定义接收的属性 // 定义接收的属性
export interface ISelectConfig<T = any> { export interface ISelectConfig<T = any> {
// 宽度 // 宽度
width?: string; width?: string
// 占位符 // 占位符
placeholder?: string; placeholder?: string
// popover组件属性 // popover组件属性
popover?: Partial<Omit<PopoverProps, "visible" | "v-model:visible">>; popover?: Partial<Omit<PopoverProps, 'visible' | 'v-model:visible'>>
// 列表的网络请求函数(需返回promise) // 列表的网络请求函数(需返回promise)
indexAction: (_queryParams: T) => Promise<any>; indexAction: (_queryParams: T) => Promise<any>
// 主键名(跨页选择必填,默认为id) // 主键名(跨页选择必填,默认为id)
pk?: string; pk?: string
// 多选 // 多选
multiple?: boolean; multiple?: boolean
// 表单项 // 表单项
formItems: Array<{ formItems: Array<{
// 组件类型(如input,select等) // 组件类型(如input,select等)
type?: "input" | "select" | "tree-select" | "date-picker"; type?: 'input' | 'select' | 'tree-select' | 'date-picker'
// 标签文本 // 标签文本
label: string; label: string
// 键名 // 键名
prop: string; prop: string
// 组件属性 // 组件属性
attrs?: IObject; attrs?: IObject
// 初始值 // 初始值
initialValue?: any; initialValue?: any
// 可选项(适用于select组件) // 可选项(适用于select组件)
options?: { label: string; value: any }[]; options?: { label: string; value: any }[]
}>; }>
// 列选项 // 列选项
tableColumns: Array<{ tableColumns: Array<{
type?: "default" | "selection" | "index" | "expand"; type?: 'default' | 'selection' | 'index' | 'expand'
label?: string; label?: string
prop?: string; prop?: string
width?: string | number; width?: string | number
[key: string]: any; [key: string]: any
}>; }>
} }
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
selectConfig: ISelectConfig; selectConfig: ISelectConfig
text?: string; text?: string
}>(), }>(),
{ {
text: "", text: ''
} }
); )
// 自定义事件 // 自定义事件
const emit = defineEmits<{ const emit = defineEmits<{
confirmClick: [selection: any[]]; confirmClick: [selection: any[]]
}>(); }>()
// 主键 // 主键
const pk = props.selectConfig.pk ?? "id"; const pk = props.selectConfig.pk ?? 'id'
// 是否多选 // 是否多选
const isMultiple = props.selectConfig.multiple === true; const isMultiple = props.selectConfig.multiple === true
// 宽度 // 宽度
const width = props.selectConfig.width ?? "100%"; const width = props.selectConfig.width ?? '100%'
// 占位符 // 占位符
const placeholder = props.selectConfig.placeholder ?? "请选择"; const placeholder = props.selectConfig.placeholder ?? '请选择'
// 是否显示弹出框 // 是否显示弹出框
const popoverVisible = ref(false); const popoverVisible = ref(false)
// 加载状态 // 加载状态
const loading = ref(false); const loading = ref(false)
// 数据总数 // 数据总数
const total = ref(0); const total = ref(0)
// 列表数据 // 列表数据
const pageData = ref<IObject[]>([]); const pageData = ref<IObject[]>([])
// 每页条数 // 每页条数
const pageSize = 10; const pageSize = 10
// 搜索参数 // 搜索参数
const queryParams = reactive<{ const queryParams = reactive<{
pageNum: number; pageNum: number
pageSize: number; pageSize: number
[key: string]: any; [key: string]: any
}>({ }>({
pageNum: 1, pageNum: 1,
pageSize, pageSize
}); })
// 计算popover的宽度 // 计算popover的宽度
const tableSelectRef = ref(); const tableSelectRef = ref()
const popoverWidth = ref(width); const popoverWidth = ref(width)
useResizeObserver(tableSelectRef, (entries) => { useResizeObserver(tableSelectRef, (entries) => {
popoverWidth.value = `${entries[0].contentRect.width}px`; popoverWidth.value = `${entries[0].contentRect.width}px`
}); })
// 表单操作 // 表单操作
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>()
// 初始化搜索条件 // 初始化搜索条件
for (const item of props.selectConfig.formItems) { for (const item of props.selectConfig.formItems) {
queryParams[item.prop] = item.initialValue ?? ""; queryParams[item.prop] = item.initialValue ?? ''
} }
// 重置操作 // 重置操作
function handleReset() { function handleReset() {
formRef.value?.resetFields(); formRef.value?.resetFields()
fetchPageData(true); fetchPageData(true)
} }
// 查询操作 // 查询操作
function handleQuery() { function handleQuery() {
fetchPageData(true); fetchPageData(true)
} }
// 获取分页数据 // 获取分页数据
function fetchPageData(isRestart = false) { function fetchPageData(isRestart = false) {
loading.value = true; loading.value = true
if (isRestart) { if (isRestart) {
queryParams.pageNum = 1; queryParams.pageNum = 1
queryParams.pageSize = pageSize; queryParams.pageSize = pageSize
} }
props.selectConfig props.selectConfig
.indexAction(queryParams) .indexAction(queryParams)
.then((data) => { .then((data) => {
total.value = data.total; total.value = data.total
pageData.value = data.list; pageData.value = data.list
}) })
.finally(() => { .finally(() => {
loading.value = false; loading.value = false
}); })
} }
// 列表操作 // 列表操作
const tableRef = ref<TableInstance>(); const tableRef = ref<TableInstance>()
// 数据刷新后是否保留选项 // 数据刷新后是否保留选项
for (const item of props.selectConfig.tableColumns) { for (const item of props.selectConfig.tableColumns) {
if (item.type === "selection") { if (item.type === 'selection') {
item.reserveSelection = true; item.reserveSelection = true
break; break
} }
} }
// 选择 // 选择
const selectedItems = ref<IObject[]>([]); const selectedItems = ref<IObject[]>([])
const confirmText = computed(() => { const confirmText = computed(() => {
return selectedItems.value.length > 0 ? `已选(${selectedItems.value.length})` : "确 定"; return selectedItems.value.length > 0 ? `已选(${selectedItems.value.length})` : '确 定'
}); })
function handleSelect(selection: any[]) { function handleSelect(selection: any[]) {
if (isMultiple || selection.length === 0) { if (isMultiple || selection.length === 0) {
// 多选 // 多选
selectedItems.value = selection; selectedItems.value = selection
} else { } else {
// 单选 // 单选
selectedItems.value = [selection[selection.length - 1]]; selectedItems.value = [selection[selection.length - 1]]
tableRef.value?.clearSelection(); tableRef.value?.clearSelection()
tableRef.value?.toggleRowSelection(selectedItems.value[0], true); tableRef.value?.toggleRowSelection(selectedItems.value[0], true)
tableRef.value?.setCurrentRow(selectedItems.value[0]); tableRef.value?.setCurrentRow(selectedItems.value[0])
} }
} }
function handleSelectAll(selection: any[]) { function handleSelectAll(selection: any[]) {
if (isMultiple) { if (isMultiple) {
selectedItems.value = selection; selectedItems.value = selection
} }
} }
// 分页 // 分页
function handlePagination() { function handlePagination() {
fetchPageData(); fetchPageData()
} }
// 弹出框 // 弹出框
const isInit = ref(false); const isInit = ref(false)
// 显示 // 显示
function handleShow() { function handleShow() {
if (isInit.value === false) { if (isInit.value === false) {
isInit.value = true; isInit.value = true
fetchPageData(); fetchPageData()
} }
} }
// 确定 // 确定
function handleConfirm() { function handleConfirm() {
if (selectedItems.value.length === 0) { if (selectedItems.value.length === 0) {
ElMessage.error("请选择数据"); ElMessage.error('请选择数据')
return; return
} }
popoverVisible.value = false; popoverVisible.value = false
emit("confirmClick", selectedItems.value); emit('confirmClick', selectedItems.value)
} }
// 清空 // 清空
function handleClear() { function handleClear() {
tableRef.value?.clearSelection(); tableRef.value?.clearSelection()
selectedItems.value = []; selectedItems.value = []
} }
// 关闭 // 关闭
function handleClose() { function handleClose() {
popoverVisible.value = false; popoverVisible.value = false
} }
const popoverContentRef = ref(); const popoverContentRef = ref()
/* onClickOutside(tableSelectRef, () => (popoverVisible.value = false), { /* onClickOutside(tableSelectRef, () => (popoverVisible.value = false), {
ignore: [popoverContentRef], ignore: [popoverContentRef],
}); */ }); */

View File

@@ -41,55 +41,55 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useElementHover } from "@vueuse/core"; import { useElementHover } from '@vueuse/core'
const emit = defineEmits(["close"]); const emit = defineEmits(['close'])
interface Props { interface Props {
/** 滚动文本内容(必填) */ /** 滚动文本内容(必填) */
text: string; text: string
/** 滚动速度,数值越小滚动越慢 */ /** 滚动速度,数值越小滚动越慢 */
speed?: number; speed?: number
/** 滚动方向:左侧或右侧 */ /** 滚动方向:左侧或右侧 */
direction?: "left" | "right"; direction?: 'left' | 'right'
/** 样式类型 */ /** 样式类型 */
type?: "default" | "success" | "warning" | "danger" | "info"; type?: 'default' | 'success' | 'warning' | 'danger' | 'info'
/** 是否显示关闭按钮 */ /** 是否显示关闭按钮 */
showClose?: boolean; showClose?: boolean
/** 是否启用打字机效果 */ /** 是否启用打字机效果 */
typewriter?: boolean; typewriter?: boolean
/** 打字机效果的速度,数值越小打字越快 */ /** 打字机效果的速度,数值越小打字越快 */
typewriterSpeed?: number; typewriterSpeed?: number
} }
// 定义组件属性及默认值 // 定义组件属性及默认值
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
speed: 70, speed: 70,
direction: "left", direction: 'left',
type: "default", type: 'default',
showClose: false, showClose: false,
typewriter: false, typewriter: false,
typewriterSpeed: 100, typewriterSpeed: 100
}); })
// 容器元素引用 // 容器元素引用
const containerRef = ref<HTMLElement | null>(null); const containerRef = ref<HTMLElement | null>(null)
// 使用 vueuse 的 useElementHover 检测鼠标悬停状态 // 使用 vueuse 的 useElementHover 检测鼠标悬停状态
const isHovered = useElementHover(containerRef); const isHovered = useElementHover(containerRef)
// 滚动内容元素引用 // 滚动内容元素引用
const scrollContent = ref<HTMLElement | null>(null); const scrollContent = ref<HTMLElement | null>(null)
// 动画持续时间(秒) // 动画持续时间(秒)
const animationDuration = ref(0); const animationDuration = ref(0)
/** /**
* 打字机效果相关状态 * 打字机效果相关状态
*/ */
// 当前已显示的文本内容 // 当前已显示的文本内容
const currentText = ref(""); const currentText = ref('')
// 打字机定时器引用,用于清理 // 打字机定时器引用,用于清理
let typewriterTimer: ReturnType<typeof setTimeout> | null = null; let typewriterTimer: ReturnType<typeof setTimeout> | null = null
// 打字机效果是否已完成 // 打字机效果是否已完成
const isTypewriterComplete = ref(false); const isTypewriterComplete = ref(false)
/** /**
* 计算是否应该滚动 * 计算是否应该滚动
@@ -99,10 +99,10 @@ const isTypewriterComplete = ref(false);
*/ */
const shouldScroll = computed(() => { const shouldScroll = computed(() => {
if (props.typewriter) { if (props.typewriter) {
return !isHovered.value && isTypewriterComplete.value; return !isHovered.value && isTypewriterComplete.value
} }
return !isHovered.value; return !isHovered.value
}); })
/** /**
* 计算最终显示的内容 * 计算最终显示的内容
@@ -110,7 +110,7 @@ const shouldScroll = computed(() => {
* 否则直接显示完整文本 * 否则直接显示完整文本
* 注意:内容支持 HTML使用时需注意 XSS 风险 * 注意:内容支持 HTML使用时需注意 XSS 风险
*/ */
const sanitizedContent = computed(() => (props.typewriter ? currentText.value : props.text)); const sanitizedContent = computed(() => (props.typewriter ? currentText.value : props.text))
/** /**
* 计算滚动样式 * 计算滚动样式
@@ -118,10 +118,10 @@ const sanitizedContent = computed(() => (props.typewriter ? currentText.value :
* 这些值通过 CSS 变量传递给样式 * 这些值通过 CSS 变量传递给样式
*/ */
const scrollStyle = computed(() => ({ const scrollStyle = computed(() => ({
"--animation-duration": `${animationDuration.value}s`, '--animation-duration': `${animationDuration.value}s`,
"--animation-play-state": shouldScroll.value ? "running" : "paused", '--animation-play-state': shouldScroll.value ? 'running' : 'paused',
"--animation-direction": props.direction === "left" ? "normal" : "reverse", '--animation-direction': props.direction === 'left' ? 'normal' : 'reverse'
})); }))
/** /**
* 计算动画持续时间 * 计算动画持续时间
@@ -130,71 +130,71 @@ const scrollStyle = computed(() => ({
*/ */
const calculateDuration = () => { const calculateDuration = () => {
if (scrollContent.value) { if (scrollContent.value) {
const contentWidth = scrollContent.value.scrollWidth / 2; const contentWidth = scrollContent.value.scrollWidth / 2
animationDuration.value = contentWidth / props.speed; animationDuration.value = contentWidth / props.speed
} }
}; }
/** /**
* 处理关闭按钮点击事件 * 处理关闭按钮点击事件
* 触发 close 事件,并直接销毁当前组件 * 触发 close 事件,并直接销毁当前组件
*/ */
const handleRightIconClick = () => { const handleRightIconClick = () => {
emit("close"); emit('close')
// 获取当前组件的DOM元素 // 获取当前组件的DOM元素
if (containerRef.value) { if (containerRef.value) {
// 从DOM中移除元素 // 从DOM中移除元素
containerRef.value.remove(); containerRef.value.remove()
} }
}; }
/** /**
* 启动打字机效果 * 启动打字机效果
* 逐字显示文本内容,完成后设置状态以开始滚动 * 逐字显示文本内容,完成后设置状态以开始滚动
*/ */
const startTypewriter = () => { const startTypewriter = () => {
let index = 0; let index = 0
currentText.value = ""; currentText.value = ''
isTypewriterComplete.value = false; // 重置状态 isTypewriterComplete.value = false // 重置状态
// 递归函数,逐字添加文本 // 递归函数,逐字添加文本
const type = () => { const type = () => {
if (index < props.text.length) { if (index < props.text.length) {
// 添加一个字符 // 添加一个字符
currentText.value += props.text[index]; currentText.value += props.text[index]
index++; index++
// 设置下一个字符的延迟 // 设置下一个字符的延迟
typewriterTimer = setTimeout(type, props.typewriterSpeed); typewriterTimer = setTimeout(type, props.typewriterSpeed)
} else { } else {
// 所有字符都已添加,设置完成状态 // 所有字符都已添加,设置完成状态
isTypewriterComplete.value = true; isTypewriterComplete.value = true
} }
}; }
// 开始打字过程 // 开始打字过程
type(); type()
}; }
onMounted(() => { onMounted(() => {
// 计算初始动画持续时间 // 计算初始动画持续时间
calculateDuration(); calculateDuration()
// 监听窗口大小变化,重新计算动画持续时间 // 监听窗口大小变化,重新计算动画持续时间
window.addEventListener("resize", calculateDuration); window.addEventListener('resize', calculateDuration)
// 如果启用了打字机效果,开始打字 // 如果启用了打字机效果,开始打字
if (props.typewriter) { if (props.typewriter) {
startTypewriter(); startTypewriter()
} }
}); })
onUnmounted(() => { onUnmounted(() => {
// 移除事件监听 // 移除事件监听
window.removeEventListener("resize", calculateDuration); window.removeEventListener('resize', calculateDuration)
// 清除打字机定时器 // 清除打字机定时器
if (typewriterTimer) { if (typewriterTimer) {
clearTimeout(typewriterTimer); clearTimeout(typewriterTimer)
} }
}); })
/** /**
* 监听文本内容变化 * 监听文本内容变化
@@ -206,13 +206,13 @@ watch(
if (props.typewriter) { if (props.typewriter) {
// 清除现有定时器 // 清除现有定时器
if (typewriterTimer) { if (typewriterTimer) {
clearTimeout(typewriterTimer); clearTimeout(typewriterTimer)
} }
// 重新开始打字效果 // 重新开始打字效果
startTypewriter(); startTypewriter()
} }
} }
); )
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -386,7 +386,7 @@ watch(
// 添加打字机效果的光标样式 // 添加打字机效果的光标样式
.text-scroll-content .scroll-item { .text-scroll-content .scroll-item {
&::after { &::after {
content: ""; content: '';
opacity: 0; opacity: 0;
animation: none; animation: none;
} }
@@ -394,7 +394,7 @@ watch(
// 仅在启用打字机效果时显示光标 // 仅在启用打字机效果时显示光标
.text-scroll-container[typewriter] .text-scroll-content .scroll-item::after { .text-scroll-container[typewriter] .text-scroll-content .scroll-item::after {
content: "|"; content: '|';
opacity: 0; opacity: 0;
animation: cursor 1s infinite; animation: cursor 1s infinite;
} }

View File

@@ -50,10 +50,10 @@ import {
UploadUserFile, UploadUserFile,
UploadFile, UploadFile,
UploadFiles, UploadFiles,
UploadRequestOptions, UploadRequestOptions
} from "element-plus"; } from 'element-plus'
import FileAPI, { FileInfo } from "@/api/file-api"; import FileAPI, { FileInfo } from '@/api/file-api'
const props = defineProps({ const props = defineProps({
/** /**
@@ -62,43 +62,43 @@ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: () => { default: () => {
return {}; return {}
}, }
}, },
/** /**
* 上传文件的参数名 * 上传文件的参数名
*/ */
name: { name: {
type: String, type: String,
default: "file", default: 'file'
}, },
/** /**
* 文件上传数量限制 * 文件上传数量限制
*/ */
limit: { limit: {
type: Number, type: Number,
default: 10, default: 10
}, },
/** /**
* 单个文件上传大小限制(单位MB) * 单个文件上传大小限制(单位MB)
*/ */
maxFileSize: { maxFileSize: {
type: Number, type: Number,
default: 10, default: 10
}, },
/** /**
* 上传文件类型 * 上传文件类型
*/ */
accept: { accept: {
type: String, type: String,
default: "*", default: '*'
}, },
/** /**
* 上传按钮文本 * 上传按钮文本
*/ */
uploadBtnText: { uploadBtnText: {
type: String, type: String,
default: "上传文件", default: '上传文件'
}, },
/** /**
@@ -108,37 +108,37 @@ const props = defineProps({
type: Object, type: Object,
default: () => { default: () => {
return { return {
width: "300px", width: '300px'
}; }
}, }
}, }
}); })
const modelValue = defineModel("modelValue", { const modelValue = defineModel('modelValue', {
type: [Array] as PropType<FileInfo[]>, type: [Array] as PropType<FileInfo[]>,
required: true, required: true,
default: () => [], default: () => []
}); })
const fileList = ref([] as UploadFile[]); const fileList = ref([] as UploadFile[])
// 监听 modelValue 转换用于显示的 fileList // 监听 modelValue 转换用于显示的 fileList
watch( watch(
modelValue, modelValue,
(value) => { (value) => {
fileList.value = value.map((item) => { fileList.value = value.map((item) => {
const name = item.name ? item.name : item.url?.substring(item.url.lastIndexOf("/") + 1); const name = item.name ? item.name : item.url?.substring(item.url.lastIndexOf('/') + 1)
return { return {
name, name,
url: item.url, url: item.url,
status: "success", status: 'success',
uid: getUid(), uid: getUid()
} as UploadFile; } as UploadFile
}); })
}, },
{ {
immediate: true, immediate: true
} }
); )
/** /**
* 上传前校验 * 上传前校验
@@ -146,10 +146,10 @@ watch(
function handleBeforeUpload(file: UploadRawFile) { function handleBeforeUpload(file: UploadRawFile) {
// 限制文件大小 // 限制文件大小
if (file.size > props.maxFileSize * 1024 * 1024) { if (file.size > props.maxFileSize * 1024 * 1024) {
ElMessage.warning("上传文件不能大于" + props.maxFileSize + "M"); ElMessage.warning('上传文件不能大于' + props.maxFileSize + 'M')
return false; return false
} }
return true; return true
} }
/* /*
@@ -157,101 +157,101 @@ function handleBeforeUpload(file: UploadRawFile) {
*/ */
function handleUpload(options: UploadRequestOptions) { function handleUpload(options: UploadRequestOptions) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const file = options.file; const file = options.file
const formData = new FormData(); const formData = new FormData()
formData.append(props.name, file); formData.append(props.name, file)
// 处理附加参数 // 处理附加参数
Object.keys(props.data).forEach((key) => { Object.keys(props.data).forEach((key) => {
formData.append(key, props.data[key]); formData.append(key, props.data[key])
}); })
FileAPI.upload(formData, (percent) => { FileAPI.upload(formData, (percent) => {
const uid = file.uid; const uid = file.uid
const fileItem = fileList.value.find((file) => file.uid === uid); const fileItem = fileList.value.find((file) => file.uid === uid)
if (fileItem) { if (fileItem) {
fileItem.percentage = percent; fileItem.percentage = percent
} }
}) })
.then((res) => { .then((res) => {
resolve(res); resolve(res)
}) })
.catch((err) => { .catch((err) => {
reject(err); reject(err)
}); })
}); })
} }
/** /**
* 上传文件超出限制 * 上传文件超出限制
*/ */
function handleExceed() { function handleExceed() {
ElMessage.warning(`最多只能上传${props.limit}个文件`); ElMessage.warning(`最多只能上传${props.limit}个文件`)
} }
/** /**
* 上传成功 * 上传成功
*/ */
const handleSuccess = (response: any, uploadFile: UploadFile, files: UploadFiles) => { const handleSuccess = (response: any, uploadFile: UploadFile, files: UploadFiles) => {
ElMessage.success("上传成功"); ElMessage.success('上传成功')
//只有当状态为success或者fail代表文件上传全部完成了失败也算完成 //只有当状态为success或者fail代表文件上传全部完成了失败也算完成
if ( if (
files.every((file: UploadFile) => { files.every((file: UploadFile) => {
return file.status === "success" || file.status === "fail"; return file.status === 'success' || file.status === 'fail'
}) })
) { ) {
const fileInfos = [] as FileInfo[]; const fileInfos = [] as FileInfo[]
files.map((file: UploadFile) => { files.map((file: UploadFile) => {
if (file.status === "success") { if (file.status === 'success') {
//只取携带response的才是刚上传的 //只取携带response的才是刚上传的
const res = file.response as FileInfo; const res = file.response as FileInfo
if (res) { if (res) {
fileInfos.push({ name: res.name, url: res.url } as FileInfo); fileInfos.push({ name: res.name, url: res.url } as FileInfo)
} }
} else { } else {
//失败上传 从fileList删掉不展示 //失败上传 从fileList删掉不展示
fileList.value.splice( fileList.value.splice(
fileList.value.findIndex((e) => e.uid === file.uid), fileList.value.findIndex((e) => e.uid === file.uid),
1 1
); )
} }
}); })
if (fileInfos.length > 0) { if (fileInfos.length > 0) {
modelValue.value = [...modelValue.value, ...fileInfos]; modelValue.value = [...modelValue.value, ...fileInfos]
} }
} }
}; }
/** /**
* 上传失败 * 上传失败
*/ */
const handleError = (_error: any) => { const handleError = (_error: any) => {
console.error(_error); console.error(_error)
ElMessage.error("上传失败"); ElMessage.error('上传失败')
}; }
/** /**
* 删除文件 * 删除文件
*/ */
function handleRemove(fileUrl: string) { function handleRemove(fileUrl: string) {
FileAPI.delete(fileUrl).then(() => { FileAPI.delete(fileUrl).then(() => {
modelValue.value = modelValue.value.filter((file) => file.url !== fileUrl); modelValue.value = modelValue.value.filter((file) => file.url !== fileUrl)
}); })
} }
/** /**
* 下载文件 * 下载文件
*/ */
function handleDownload(file: UploadUserFile) { function handleDownload(file: UploadUserFile) {
const { url, name } = file; const { url, name } = file
if (url) { if (url) {
FileAPI.download(url, name); FileAPI.download(url, name)
} }
} }
/** 获取一个不重复的id */ /** 获取一个不重复的id */
function getUid(): number { function getUid(): number {
// 时间戳左移13位相当于乘以8192 + 4位随机数 // 时间戳左移13位相当于乘以8192 + 4位随机数
return (Date.now() << 13) | Math.floor(Math.random() * 8192); return (Date.now() << 13) | Math.floor(Math.random() * 8192)
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -39,8 +39,8 @@
/> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { UploadRawFile, UploadRequestOptions, UploadUserFile } from "element-plus"; import { UploadRawFile, UploadRequestOptions, UploadUserFile } from 'element-plus'
import FileAPI, { FileInfo } from "@/api/file-api"; import FileAPI, { FileInfo } from '@/api/file-api'
const props = defineProps({ const props = defineProps({
/** /**
@@ -49,61 +49,61 @@ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: () => { default: () => {
return {}; return {}
}, }
}, },
/** /**
* 上传文件的参数名 * 上传文件的参数名
*/ */
name: { name: {
type: String, type: String,
default: "file", default: 'file'
}, },
/** /**
* 文件上传数量限制 * 文件上传数量限制
*/ */
limit: { limit: {
type: Number, type: Number,
default: 10, default: 10
}, },
/** /**
* 单个文件的最大允许大小 * 单个文件的最大允许大小
*/ */
maxFileSize: { maxFileSize: {
type: Number, type: Number,
default: 10, default: 10
}, },
/** /**
* 上传文件类型 * 上传文件类型
*/ */
accept: { accept: {
type: String, type: String,
default: "image/*", // 默认支持所有图片格式 ,如果需要指定格式,格式如下:'.png,.jpg,.jpeg,.gif,.bmp' default: 'image/*' // 默认支持所有图片格式 ,如果需要指定格式,格式如下:'.png,.jpg,.jpeg,.gif,.bmp'
}, }
}); })
const previewVisible = ref(false); // 是否显示预览 const previewVisible = ref(false) // 是否显示预览
const previewImageIndex = ref(0); // 预览图片的索引 const previewImageIndex = ref(0) // 预览图片的索引
const modelValue = defineModel("modelValue", { const modelValue = defineModel('modelValue', {
type: [Array] as PropType<string[]>, type: [Array] as PropType<string[]>,
default: () => [], default: () => []
}); })
const fileList = ref<UploadUserFile[]>([]); const fileList = ref<UploadUserFile[]>([])
/** /**
* 删除图片 * 删除图片
*/ */
function handleRemove(imageUrl: string) { function handleRemove(imageUrl: string) {
FileAPI.delete(imageUrl).then(() => { FileAPI.delete(imageUrl).then(() => {
const index = modelValue.value.indexOf(imageUrl); const index = modelValue.value.indexOf(imageUrl)
if (index !== -1) { if (index !== -1) {
// 直接修改数组避免触发整体更新 // 直接修改数组避免触发整体更新
modelValue.value.splice(index, 1); modelValue.value.splice(index, 1)
fileList.value.splice(index, 1); // 同步更新 fileList fileList.value.splice(index, 1) // 同步更新 fileList
} }
}); })
} }
/** /**
@@ -111,33 +111,33 @@ function handleRemove(imageUrl: string) {
*/ */
function handleBeforeUpload(file: UploadRawFile) { function handleBeforeUpload(file: UploadRawFile) {
// 校验文件类型:虽然 accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符合 accept 的规则 // 校验文件类型:虽然 accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符合 accept 的规则
const acceptTypes = props.accept.split(",").map((type) => type.trim()); const acceptTypes = props.accept.split(',').map((type) => type.trim())
// 检查文件格式是否符合 accept // 检查文件格式是否符合 accept
const isValidType = acceptTypes.some((type) => { const isValidType = acceptTypes.some((type) => {
if (type === "image/*") { if (type === 'image/*') {
// 如果是 image/*,检查 MIME 类型是否以 "image/" 开头 // 如果是 image/*,检查 MIME 类型是否以 "image/" 开头
return file.type.startsWith("image/"); return file.type.startsWith('image/')
} else if (type.startsWith(".")) { } else if (type.startsWith('.')) {
// 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾 // 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾
return file.name.toLowerCase().endsWith(type); return file.name.toLowerCase().endsWith(type)
} else { } else {
// 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹配 // 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹配
return file.type === type; return file.type === type
} }
}); })
if (!isValidType) { if (!isValidType) {
ElMessage.warning(`上传文件的格式不正确,仅支持:${props.accept}`); ElMessage.warning(`上传文件的格式不正确,仅支持:${props.accept}`)
return false; return false
} }
// 限制文件大小 // 限制文件大小
if (file.size > props.maxFileSize * 1024 * 1024) { if (file.size > props.maxFileSize * 1024 * 1024) {
ElMessage.warning("上传图片不能大于" + props.maxFileSize + "M"); ElMessage.warning('上传图片不能大于' + props.maxFileSize + 'M')
return false; return false
} }
return true; return true
} }
/* /*
@@ -145,71 +145,71 @@ function handleBeforeUpload(file: UploadRawFile) {
*/ */
function handleUpload(options: UploadRequestOptions) { function handleUpload(options: UploadRequestOptions) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const file = options.file; const file = options.file
const formData = new FormData(); const formData = new FormData()
formData.append(props.name, file); formData.append(props.name, file)
// 处理附加参数 // 处理附加参数
Object.keys(props.data).forEach((key) => { Object.keys(props.data).forEach((key) => {
formData.append(key, props.data[key]); formData.append(key, props.data[key])
}); })
FileAPI.upload(formData) FileAPI.upload(formData)
.then((data) => { .then((data) => {
resolve(data); resolve(data)
}) })
.catch((error) => { .catch((error) => {
reject(error); reject(error)
}); })
}); })
} }
/** /**
* 上传文件超出限制 * 上传文件超出限制
*/ */
function handleExceed() { function handleExceed() {
ElMessage.warning("最多只能上传" + props.limit + "张图片"); ElMessage.warning('最多只能上传' + props.limit + '张图片')
} }
/** /**
* 上传成功回调 * 上传成功回调
*/ */
const handleSuccess = (fileInfo: FileInfo, uploadFile: UploadUserFile) => { const handleSuccess = (fileInfo: FileInfo, uploadFile: UploadUserFile) => {
ElMessage.success("上传成功"); ElMessage.success('上传成功')
const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid); const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid)
if (index !== -1) { if (index !== -1) {
fileList.value[index].url = fileInfo.url; fileList.value[index].url = fileInfo.url
fileList.value[index].status = "success"; fileList.value[index].status = 'success'
modelValue.value[index] = fileInfo.url; modelValue.value[index] = fileInfo.url
} }
}; }
/** /**
* 上传失败回调 * 上传失败回调
*/ */
const handleError = (error: any) => { const handleError = (error: any) => {
console.log("handleError"); console.log('handleError')
ElMessage.error("上传失败: " + error.message); ElMessage.error('上传失败: ' + error.message)
}; }
/** /**
* 预览图片 * 预览图片
*/ */
const handlePreviewImage = (imageUrl: string) => { const handlePreviewImage = (imageUrl: string) => {
previewImageIndex.value = modelValue.value.findIndex((url) => url === imageUrl); previewImageIndex.value = modelValue.value.findIndex((url) => url === imageUrl)
previewVisible.value = true; previewVisible.value = true
}; }
/** /**
* 关闭预览 * 关闭预览
*/ */
const handlePreviewClose = () => { const handlePreviewClose = () => {
previewVisible.value = false; previewVisible.value = false
}; }
onMounted(() => { onMounted(() => {
fileList.value = modelValue.value.map((url) => ({ url }) as UploadUserFile); fileList.value = modelValue.value.map((url) => ({ url }) as UploadUserFile)
}); })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@@ -32,8 +32,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { UploadRawFile, UploadRequestOptions } from "element-plus"; import { UploadRawFile, UploadRequestOptions } from 'element-plus'
import FileAPI, { FileInfo } from "@/api/file-api"; import FileAPI, { FileInfo } from '@/api/file-api'
const props = defineProps({ const props = defineProps({
/** /**
@@ -42,22 +42,22 @@ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: () => { default: () => {
return {}; return {}
}, }
}, },
/** /**
* 上传文件的参数名 * 上传文件的参数名
*/ */
name: { name: {
type: String, type: String,
default: "file", default: 'file'
}, },
/** /**
* 最大文件大小单位M * 最大文件大小单位M
*/ */
maxFileSize: { maxFileSize: {
type: Number, type: Number,
default: 10, default: 10
}, },
/** /**
@@ -65,7 +65,7 @@ const props = defineProps({
*/ */
accept: { accept: {
type: String, type: String,
default: "image/*", default: 'image/*'
}, },
/** /**
@@ -75,50 +75,50 @@ const props = defineProps({
type: Object, type: Object,
default: () => { default: () => {
return { return {
width: "150px", width: '150px',
height: "150px", height: '150px'
}; }
}, }
}, }
}); })
const modelValue = defineModel("modelValue", { const modelValue = defineModel('modelValue', {
type: String, type: String,
default: () => "", default: () => ''
}); })
/** /**
* 限制用户上传文件的格式和大小 * 限制用户上传文件的格式和大小
*/ */
function handleBeforeUpload(file: UploadRawFile) { function handleBeforeUpload(file: UploadRawFile) {
// 校验文件类型:虽然 accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符合 accept 的规则 // 校验文件类型:虽然 accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符合 accept 的规则
const acceptTypes = props.accept.split(",").map((type) => type.trim()); const acceptTypes = props.accept.split(',').map((type) => type.trim())
// 检查文件格式是否符合 accept // 检查文件格式是否符合 accept
const isValidType = acceptTypes.some((type) => { const isValidType = acceptTypes.some((type) => {
if (type === "image/*") { if (type === 'image/*') {
// 如果是 image/*,检查 MIME 类型是否以 "image/" 开头 // 如果是 image/*,检查 MIME 类型是否以 "image/" 开头
return file.type.startsWith("image/"); return file.type.startsWith('image/')
} else if (type.startsWith(".")) { } else if (type.startsWith('.')) {
// 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾 // 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾
return file.name.toLowerCase().endsWith(type); return file.name.toLowerCase().endsWith(type)
} else { } else {
// 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹配 // 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹配
return file.type === type; return file.type === type
} }
}); })
if (!isValidType) { if (!isValidType) {
ElMessage.warning(`上传文件的格式不正确,仅支持:${props.accept}`); ElMessage.warning(`上传文件的格式不正确,仅支持:${props.accept}`)
return false; return false
} }
// 限制文件大小 // 限制文件大小
if (file.size > props.maxFileSize * 1024 * 1024) { if (file.size > props.maxFileSize * 1024 * 1024) {
ElMessage.warning("上传图片不能大于" + props.maxFileSize + "M"); ElMessage.warning('上传图片不能大于' + props.maxFileSize + 'M')
return false; return false
} }
return true; return true
} }
/* /*
@@ -126,38 +126,38 @@ function handleBeforeUpload(file: UploadRawFile) {
*/ */
function handleUpload(options: UploadRequestOptions) { function handleUpload(options: UploadRequestOptions) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const file = options.file; const file = options.file
const formData = new FormData(); const formData = new FormData()
formData.append(props.name, file); formData.append(props.name, file)
// 处理附加参数 // 处理附加参数
Object.keys(props.data).forEach((key) => { Object.keys(props.data).forEach((key) => {
formData.append(key, props.data[key]); formData.append(key, props.data[key])
}); })
FileAPI.upload(formData) FileAPI.upload(formData)
.then((data) => { .then((data) => {
resolve(data); resolve(data)
}) })
.catch((error) => { .catch((error) => {
reject(error); reject(error)
}); })
}); })
} }
/** /**
* 预览图片 * 预览图片
*/ */
function handlePreview() { function handlePreview() {
console.log("预览图片,停止冒泡"); console.log('预览图片,停止冒泡')
} }
/** /**
* 删除图片 * 删除图片
*/ */
function handleDelete() { function handleDelete() {
modelValue.value = ""; modelValue.value = ''
} }
/** /**
@@ -166,17 +166,17 @@ function handleDelete() {
* @param fileInfo 上传成功后的文件信息 * @param fileInfo 上传成功后的文件信息
*/ */
const onSuccess = (fileInfo: FileInfo) => { const onSuccess = (fileInfo: FileInfo) => {
ElMessage.success("上传成功"); ElMessage.success('上传成功')
modelValue.value = fileInfo.url; modelValue.value = fileInfo.url
}; }
/** /**
* 上传失败回调 * 上传失败回调
*/ */
const onError = (error: any) => { const onError = (error: any) => {
console.log("onError"); console.log('onError')
ElMessage.error("上传失败: " + error.message); ElMessage.error('上传失败: ' + error.message)
}; }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -29,59 +29,59 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import "@wangeditor-next/editor/dist/css/style.css"; import '@wangeditor-next/editor/dist/css/style.css'
import { Toolbar, Editor } from "@wangeditor-next/editor-for-vue"; import { Toolbar, Editor } from '@wangeditor-next/editor-for-vue'
import { IToolbarConfig, IEditorConfig } from "@wangeditor-next/editor"; import { IToolbarConfig, IEditorConfig } from '@wangeditor-next/editor'
// 文件上传 API // 文件上传 API
import FileAPI from "@/api/file-api"; import FileAPI from '@/api/file-api'
// 上传图片回调函数类型 // 上传图片回调函数类型
type InsertFnType = (_url: string, _alt: string, _href: string) => void; type InsertFnType = (_url: string, _alt: string, _href: string) => void
defineProps({ defineProps({
height: { height: {
type: String, type: String,
default: "500px", default: '500px'
}, }
}); })
// 双向绑定 // 双向绑定
const modelValue = defineModel("modelValue", { const modelValue = defineModel('modelValue', {
type: String, type: String,
required: false, required: false
}); })
// 编辑器实例,必须用 shallowRef重要 // 编辑器实例,必须用 shallowRef重要
const editorRef = shallowRef(); const editorRef = shallowRef()
// 工具栏配置 // 工具栏配置
const toolbarConfig = ref<Partial<IToolbarConfig>>({}); const toolbarConfig = ref<Partial<IToolbarConfig>>({})
// 编辑器配置 // 编辑器配置
const editorConfig = ref<Partial<IEditorConfig>>({ const editorConfig = ref<Partial<IEditorConfig>>({
placeholder: "请输入内容...", placeholder: '请输入内容...',
MENU_CONF: { MENU_CONF: {
uploadImage: { uploadImage: {
customUpload(file: File, insertFn: InsertFnType) { customUpload(file: File, insertFn: InsertFnType) {
// 上传图片 // 上传图片
FileAPI.uploadFile(file).then((res) => { FileAPI.uploadFile(file).then((res) => {
// 插入图片 // 插入图片
insertFn(res.url, res.name, res.url); insertFn(res.url, res.name, res.url)
}); })
}, }
} as any, } as any
}, }
}); })
// 记录 editor 实例,重要! // 记录 editor 实例,重要!
const handleCreated = (editor: any) => { const handleCreated = (editor: any) => {
editorRef.value = editor; editorRef.value = editor
}; }
// 组件销毁时,也及时销毁编辑器,重要! // 组件销毁时,也及时销毁编辑器,重要!
onBeforeUnmount(() => { onBeforeUnmount(() => {
const editor = editorRef.value; const editor = editorRef.value
if (editor == null) return; if (editor == null) return
editor.destroy(); editor.destroy()
}); })
</script> </script>

View File

@@ -1,19 +1,19 @@
import type { InternalAxiosRequestConfig } from "axios"; import type { InternalAxiosRequestConfig } from 'axios'
import { useUserStoreHook } from "@/store/modules/user-store"; import { useUserStoreHook } from '@/store/modules/user-store'
import { AuthStorage, redirectToLogin } from "@/utils/auth"; import { AuthStorage, redirectToLogin } from '@/utils/auth'
/** /**
* 重试请求的回调函数类型 * 重试请求的回调函数类型
*/ */
type RetryCallback = () => void; type RetryCallback = () => void
/** /**
* Token刷新组合式函数 * Token刷新组合式函数
*/ */
export function useTokenRefresh() { export function useTokenRefresh() {
// Token 刷新相关状态 // Token 刷新相关状态
let isRefreshingToken = false; let isRefreshingToken = false
const pendingRequests: RetryCallback[] = []; const pendingRequests: RetryCallback[] = []
/** /**
* 刷新 Token 并重试请求 * 刷新 Token 并重试请求
@@ -25,19 +25,19 @@ export function useTokenRefresh() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 封装需要重试的请求 // 封装需要重试的请求
const retryRequest = () => { const retryRequest = () => {
const newToken = AuthStorage.getAccessToken(); const newToken = AuthStorage.getAccessToken()
if (newToken && config.headers) { if (newToken && config.headers) {
config.headers.Authorization = `Bearer ${newToken}`; config.headers.Authorization = `Bearer ${newToken}`
} }
httpRequest(config).then(resolve).catch(reject); httpRequest(config).then(resolve).catch(reject)
}; }
// 将请求加入等待队列 // 将请求加入等待队列
pendingRequests.push(retryRequest); pendingRequests.push(retryRequest)
// 如果没有正在刷新,则开始刷新流程 // 如果没有正在刷新,则开始刷新流程
if (!isRefreshingToken) { if (!isRefreshingToken) {
isRefreshingToken = true; isRefreshingToken = true
useUserStoreHook() useUserStoreHook()
.refreshToken() .refreshToken()
@@ -45,33 +45,33 @@ export function useTokenRefresh() {
// 刷新成功,重试所有等待的请求 // 刷新成功,重试所有等待的请求
pendingRequests.forEach((callback) => { pendingRequests.forEach((callback) => {
try { try {
callback(); callback()
} catch (error) { } catch (error) {
console.error("Retry request error:", error); console.error('Retry request error:', error)
} }
}); })
// 清空队列 // 清空队列
pendingRequests.length = 0; pendingRequests.length = 0
}) })
.catch(async (error) => { .catch(async (error) => {
console.error("Token refresh failed:", error); console.error('Token refresh failed:', error)
// 刷新失败,先 reject 所有等待的请求,再清空队列 // 刷新失败,先 reject 所有等待的请求,再清空队列
const failedRequests = [...pendingRequests]; const failedRequests = [...pendingRequests]
pendingRequests.length = 0; pendingRequests.length = 0
// 拒绝所有等待的请求 // 拒绝所有等待的请求
failedRequests.forEach(() => { failedRequests.forEach(() => {
reject(new Error("Token refresh failed")); reject(new Error('Token refresh failed'))
}); })
// 跳转登录页 // 跳转登录页
await redirectToLogin("登录状态已失效,请重新登录"); await redirectToLogin('登录状态已失效,请重新登录')
}) })
.finally(() => { .finally(() => {
isRefreshingToken = false; isRefreshingToken = false
}); })
} }
}); })
} }
/** /**
@@ -80,12 +80,12 @@ export function useTokenRefresh() {
function getRefreshStatus() { function getRefreshStatus() {
return { return {
isRefreshing: isRefreshingToken, isRefreshing: isRefreshingToken,
pendingCount: pendingRequests.length, pendingCount: pendingRequests.length
}; }
} }
return { return {
refreshTokenAndRetry, refreshTokenAndRetry,
getRefreshStatus, getRefreshStatus
}; }
} }

View File

@@ -1,14 +1,14 @@
export { useStomp } from "./websocket/useStomp"; export { useStomp } from './websocket/useStomp'
export { useDictSync } from "./websocket/useDictSync"; export { useDictSync } from './websocket/useDictSync'
export type { DictMessage } from "./websocket/useDictSync"; export type { DictMessage } from './websocket/useDictSync'
export { useOnlineCount } from "./websocket/useOnlineCount"; export { useOnlineCount } from './websocket/useOnlineCount'
export { useTokenRefresh } from "./auth/useTokenRefresh"; export { useTokenRefresh } from './auth/useTokenRefresh'
export { useLayout } from "./layout/useLayout"; export { useLayout } from './layout/useLayout'
export { useLayoutMenu } from "./layout/useLayoutMenu"; export { useLayoutMenu } from './layout/useLayoutMenu'
export { useDeviceDetection } from "./layout/useDeviceDetection"; export { useDeviceDetection } from './layout/useDeviceDetection'
export { useAiAction } from "./useAiAction"; export { useAiAction } from './useAiAction'
export type { UseAiActionOptions, AiActionHandler } from "./useAiAction"; export type { UseAiActionOptions, AiActionHandler } from './useAiAction'
export { useTableSelection } from "./useTableSelection"; export { useTableSelection } from './useTableSelection'

View File

@@ -1,40 +1,40 @@
import { watchEffect, computed } from "vue"; import { watchEffect, computed } from 'vue'
import { useWindowSize } from "@vueuse/core"; import { useWindowSize } from '@vueuse/core'
import { useAppStore } from "@/store"; import { useAppStore } from '@/store'
import { DeviceEnum } from "@/enums/settings/device-enum"; import { DeviceEnum } from '@/enums/settings/device-enum'
/** /**
* 设备检测和响应式处理 * 设备检测和响应式处理
* 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态 * 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态
*/ */
export function useDeviceDetection() { export function useDeviceDetection() {
const appStore = useAppStore(); const appStore = useAppStore()
const { width } = useWindowSize(); const { width } = useWindowSize()
// 桌面设备断点 // 桌面设备断点
const DESKTOP_BREAKPOINT = 992; const DESKTOP_BREAKPOINT = 992
// 计算设备类型 // 计算设备类型
const isDesktop = computed(() => width.value >= DESKTOP_BREAKPOINT); const isDesktop = computed(() => width.value >= DESKTOP_BREAKPOINT)
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE); const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE)
// 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态 // 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态
watchEffect(() => { watchEffect(() => {
const deviceType = isDesktop.value ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE; const deviceType = isDesktop.value ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE
// 更新设备类型 // 更新设备类型
appStore.toggleDevice(deviceType); appStore.toggleDevice(deviceType)
// 根据设备类型调整侧边栏状态 // 根据设备类型调整侧边栏状态
if (isDesktop.value) { if (isDesktop.value) {
appStore.openSideBar(); appStore.openSideBar()
} else { } else {
appStore.closeSideBar(); appStore.closeSideBar()
} }
}); })
return { return {
isDesktop, isDesktop,
isMobile, isMobile
}; }
} }

View File

@@ -1,51 +1,51 @@
import { useAppStore, useSettingsStore } from "@/store"; import { useAppStore, useSettingsStore } from '@/store'
import { defaultSettings } from "@/settings"; import { defaultSettings } from '@/settings'
/** /**
* 布局相关的通用逻辑 * 布局相关的通用逻辑
*/ */
export function useLayout() { export function useLayout() {
const appStore = useAppStore(); const appStore = useAppStore()
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
// 计算当前布局模式 // 计算当前布局模式
const currentLayout = computed(() => settingsStore.layout); const currentLayout = computed(() => settingsStore.layout)
// 侧边栏展开状态 // 侧边栏展开状态
const isSidebarOpen = computed(() => appStore.sidebar.opened); const isSidebarOpen = computed(() => appStore.sidebar.opened)
// 是否显示标签视图 // 是否显示标签视图
const isShowTagsView = computed(() => settingsStore.showTagsView); const isShowTagsView = computed(() => settingsStore.showTagsView)
// 是否显示设置面板 // 是否显示设置面板
const isShowSettings = computed(() => defaultSettings.showSettings); const isShowSettings = computed(() => defaultSettings.showSettings)
// 是否显示Logo // 是否显示Logo
const isShowLogo = computed(() => settingsStore.showAppLogo); const isShowLogo = computed(() => settingsStore.showAppLogo)
// 是否移动设备 // 是否移动设备
const isMobile = computed(() => appStore.device === "mobile"); const isMobile = computed(() => appStore.device === 'mobile')
// 布局CSS类 // 布局CSS类
const layoutClass = computed(() => ({ const layoutClass = computed(() => ({
hideSidebar: !appStore.sidebar.opened, hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened, openSidebar: appStore.sidebar.opened,
mobile: appStore.device === "mobile", mobile: appStore.device === 'mobile',
[`layout-${settingsStore.layout}`]: true, [`layout-${settingsStore.layout}`]: true
})); }))
/** /**
* 处理切换侧边栏的展开/收起状态 * 处理切换侧边栏的展开/收起状态
*/ */
function toggleSidebar() { function toggleSidebar() {
appStore.toggleSidebar(); appStore.toggleSidebar()
} }
/** /**
* 关闭侧边栏(移动端) * 关闭侧边栏(移动端)
*/ */
function closeSidebar() { function closeSidebar() {
appStore.closeSideBar(); appStore.closeSideBar()
} }
return { return {
@@ -57,6 +57,6 @@ export function useLayout() {
isMobile, isMobile,
layoutClass, layoutClass,
toggleSidebar, toggleSidebar,
closeSidebar, closeSidebar
}; }
} }

View File

@@ -1,39 +1,39 @@
import { useRoute } from "vue-router"; import { useRoute } from 'vue-router'
import { useAppStore, usePermissionStore } from "@/store"; import { useAppStore, usePermissionStore } from '@/store'
/** /**
* 布局菜单处理逻辑 * 布局菜单处理逻辑
*/ */
export function useLayoutMenu() { export function useLayoutMenu() {
const route = useRoute(); const route = useRoute()
const appStore = useAppStore(); const appStore = useAppStore()
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore()
// 顶部菜单激活路径 // 顶部菜单激活路径
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); const activeTopMenuPath = computed(() => appStore.activeTopMenuPath)
// 常规路由(左侧菜单或顶部菜单) // 常规路由(左侧菜单或顶部菜单)
const routes = computed(() => permissionStore.routes); const routes = computed(() => permissionStore.routes)
// 混合布局左侧菜单路由 // 混合布局左侧菜单路由
const sideMenuRoutes = computed(() => permissionStore.mixLayoutSideMenus); const sideMenuRoutes = computed(() => permissionStore.mixLayoutSideMenus)
// 当前激活的菜单 // 当前激活的菜单
const activeMenu = computed(() => { const activeMenu = computed(() => {
const { meta, path } = route; const { meta, path } = route
// 如果设置了activeMenu则使用 // 如果设置了activeMenu则使用
if (meta?.activeMenu) { if (meta?.activeMenu) {
return meta.activeMenu; return meta.activeMenu
} }
return path; return path
}); })
return { return {
routes, routes,
sideMenuRoutes, sideMenuRoutes,
activeMenu, activeMenu,
activeTopMenuPath, activeTopMenuPath
}; }
} }

View File

@@ -1,7 +1,7 @@
import { useRoute } from "vue-router"; import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, onBeforeUnmount, nextTick } from "vue"; import { onMounted, onBeforeUnmount, nextTick } from 'vue'
import AiCommandApi from "@/api/ai"; import AiCommandApi from '@/api/ai'
/** /**
* AI 操作处理器(简化版) * AI 操作处理器(简化版)
@@ -12,29 +12,29 @@ export type AiActionHandler<T = any> =
| ((args: T) => Promise<void> | void) | ((args: T) => Promise<void> | void)
| { | {
/** 执行函数 */ /** 执行函数 */
execute: (args: T) => Promise<void> | void; execute: (args: T) => Promise<void> | void
/** 是否需要确认(默认 true */ /** 是否需要确认(默认 true */
needConfirm?: boolean; needConfirm?: boolean
/** 确认消息(支持函数或字符串) */ /** 确认消息(支持函数或字符串) */
confirmMessage?: string | ((args: T) => string); confirmMessage?: string | ((args: T) => string)
/** 成功消息(支持函数或字符串) */ /** 成功消息(支持函数或字符串) */
successMessage?: string | ((args: T) => string); successMessage?: string | ((args: T) => string)
/** 是否调用后端 API默认 false如果为 true 则自动调用 executeCommand */ /** 是否调用后端 API默认 false如果为 true 则自动调用 executeCommand */
callBackendApi?: boolean; callBackendApi?: boolean
}; }
/** /**
* AI 操作配置 * AI 操作配置
*/ */
export interface UseAiActionOptions { export interface UseAiActionOptions {
/** 操作映射表:函数名 -> 处理器 */ /** 操作映射表:函数名 -> 处理器 */
actionHandlers?: Record<string, AiActionHandler>; actionHandlers?: Record<string, AiActionHandler>
/** 数据刷新函数(操作完成后调用) */ /** 数据刷新函数(操作完成后调用) */
onRefresh?: () => Promise<void> | void; onRefresh?: () => Promise<void> | void
/** 自动搜索处理函数 */ /** 自动搜索处理函数 */
onAutoSearch?: (keywords: string) => void; onAutoSearch?: (keywords: string) => void
/** 当前路由路径(用于执行命令时传递) */ /** 当前路由路径(用于执行命令时传递) */
currentRoute?: string; currentRoute?: string
} }
/** /**
@@ -46,101 +46,101 @@ export interface UseAiActionOptions {
* - 配置化的操作处理器 * - 配置化的操作处理器
*/ */
export function useAiAction(options: UseAiActionOptions = {}) { export function useAiAction(options: UseAiActionOptions = {}) {
const route = useRoute(); const route = useRoute()
const { actionHandlers = {}, onRefresh, onAutoSearch, currentRoute = route.path } = options; const { actionHandlers = {}, onRefresh, onAutoSearch, currentRoute = route.path } = options
// 用于跟踪是否已卸载,防止在卸载后执行回调 // 用于跟踪是否已卸载,防止在卸载后执行回调
let isUnmounted = false; let isUnmounted = false
/** /**
* 执行 AI 操作(统一处理确认、执行、反馈流程) * 执行 AI 操作(统一处理确认、执行、反馈流程)
*/ */
async function executeAiAction(action: any) { async function executeAiAction(action: any) {
if (isUnmounted) return; if (isUnmounted) return
// 兼容两种入参:{ functionName, arguments } 或 { functionCall: { name, arguments } } // 兼容两种入参:{ functionName, arguments } 或 { functionCall: { name, arguments } }
const fnCall = action.functionCall ?? { const fnCall = action.functionCall ?? {
name: action.functionName, name: action.functionName,
arguments: action.arguments, arguments: action.arguments
}; }
if (!fnCall?.name) { if (!fnCall?.name) {
ElMessage.warning("未识别的 AI 操作"); ElMessage.warning('未识别的 AI 操作')
return; return
} }
// 查找对应的处理器 // 查找对应的处理器
const handler = actionHandlers[fnCall.name]; const handler = actionHandlers[fnCall.name]
if (!handler) { if (!handler) {
ElMessage.warning(`暂不支持操作: ${fnCall.name}`); ElMessage.warning(`暂不支持操作: ${fnCall.name}`)
return; return
} }
try { try {
// 判断处理器类型(函数 or 配置对象) // 判断处理器类型(函数 or 配置对象)
const isSimpleFunction = typeof handler === "function"; const isSimpleFunction = typeof handler === 'function'
if (isSimpleFunction) { if (isSimpleFunction) {
// 简单函数形式:直接执行 // 简单函数形式:直接执行
await handler(fnCall.arguments); await handler(fnCall.arguments)
} else { } else {
// 配置对象形式:统一处理确认、执行、反馈 // 配置对象形式:统一处理确认、执行、反馈
const config = handler; const config = handler
// 1. 确认阶段(默认需要确认) // 1. 确认阶段(默认需要确认)
if (config.needConfirm !== false) { if (config.needConfirm !== false) {
const confirmMsg = const confirmMsg =
typeof config.confirmMessage === "function" typeof config.confirmMessage === 'function'
? config.confirmMessage(fnCall.arguments) ? config.confirmMessage(fnCall.arguments)
: config.confirmMessage || "确认执行此操作吗?"; : config.confirmMessage || '确认执行此操作吗?'
await ElMessageBox.confirm(confirmMsg, "AI 助手操作确认", { await ElMessageBox.confirm(confirmMsg, 'AI 助手操作确认', {
confirmButtonText: "确认执行", confirmButtonText: '确认执行',
cancelButtonText: "取消", cancelButtonText: '取消',
type: "warning", type: 'warning',
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true
}); })
} }
// 2. 执行阶段 // 2. 执行阶段
if (config.callBackendApi) { if (config.callBackendApi) {
// 自动调用后端 API // 自动调用后端 API
await AiCommandApi.executeCommand({ await AiCommandApi.executeCommand({
originalCommand: action.originalCommand || "", originalCommand: action.originalCommand || '',
confirmMode: "manual", confirmMode: 'manual',
userConfirmed: true, userConfirmed: true,
currentRoute, currentRoute,
functionCall: { functionCall: {
name: fnCall.name, name: fnCall.name,
arguments: fnCall.arguments, arguments: fnCall.arguments
}, }
}); })
} else { } else {
// 执行自定义函数 // 执行自定义函数
await config.execute(fnCall.arguments); await config.execute(fnCall.arguments)
} }
// 3. 成功反馈 // 3. 成功反馈
const successMsg = const successMsg =
typeof config.successMessage === "function" typeof config.successMessage === 'function'
? config.successMessage(fnCall.arguments) ? config.successMessage(fnCall.arguments)
: config.successMessage || "操作执行成功"; : config.successMessage || '操作执行成功'
ElMessage.success(successMsg); ElMessage.success(successMsg)
} }
// 4. 刷新数据 // 4. 刷新数据
if (onRefresh) { if (onRefresh) {
await onRefresh(); await onRefresh()
} }
} catch (error: any) { } catch (error: any) {
// 处理取消操作 // 处理取消操作
if (error === "cancel") { if (error === 'cancel') {
ElMessage.info("已取消操作"); ElMessage.info('已取消操作')
return; return
} }
console.error("AI 操作执行失败:", error); console.error('AI 操作执行失败:', error)
ElMessage.error(error.message || "操作执行失败"); ElMessage.error(error.message || '操作执行失败')
} }
} }
@@ -151,31 +151,31 @@ export function useAiAction(options: UseAiActionOptions = {}) {
functionName: string, functionName: string,
args: any, args: any,
options: { options: {
originalCommand?: string; originalCommand?: string
confirmMode?: "auto" | "manual"; confirmMode?: 'auto' | 'manual'
needConfirm?: boolean; needConfirm?: boolean
confirmMessage?: string; confirmMessage?: string
} = {} } = {}
) { ) {
const { const {
originalCommand = "", originalCommand = '',
confirmMode = "manual", confirmMode = 'manual',
needConfirm = false, needConfirm = false,
confirmMessage, confirmMessage
} = options; } = options
// 如果需要确认,先显示确认对话框 // 如果需要确认,先显示确认对话框
if (needConfirm && confirmMessage) { if (needConfirm && confirmMessage) {
try { try {
await ElMessageBox.confirm(confirmMessage, "AI 助手操作确认", { await ElMessageBox.confirm(confirmMessage, 'AI 助手操作确认', {
confirmButtonText: "确认执行", confirmButtonText: '确认执行',
cancelButtonText: "取消", cancelButtonText: '取消',
type: "warning", type: 'warning',
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true
}); })
} catch { } catch {
ElMessage.info("已取消操作"); ElMessage.info('已取消操作')
return; return
} }
} }
@@ -187,14 +187,14 @@ export function useAiAction(options: UseAiActionOptions = {}) {
currentRoute, currentRoute,
functionCall: { functionCall: {
name: functionName, name: functionName,
arguments: args, arguments: args
}, }
}); })
ElMessage.success("操作执行成功"); ElMessage.success('操作执行成功')
} catch (error: any) { } catch (error: any) {
if (error !== "cancel") { if (error !== 'cancel') {
throw error; throw error
} }
} }
} }
@@ -204,9 +204,9 @@ export function useAiAction(options: UseAiActionOptions = {}) {
*/ */
function handleAutoSearch(keywords: string) { function handleAutoSearch(keywords: string) {
if (onAutoSearch) { if (onAutoSearch) {
onAutoSearch(keywords); onAutoSearch(keywords)
} else { } else {
ElMessage.info(`AI 助手已为您自动搜索:${keywords}`); ElMessage.info(`AI 助手已为您自动搜索:${keywords}`)
} }
} }
@@ -217,54 +217,54 @@ export function useAiAction(options: UseAiActionOptions = {}) {
* 页面数据加载应由组件的 onMounted 钩子自行处理 * 页面数据加载应由组件的 onMounted 钩子自行处理
*/ */
async function init() { async function init() {
if (isUnmounted) return; if (isUnmounted) return
// 检查是否有 AI 助手传递的参数 // 检查是否有 AI 助手传递的参数
const keywords = route.query.keywords as string; const keywords = route.query.keywords as string
const autoSearch = route.query.autoSearch as string; const autoSearch = route.query.autoSearch as string
const aiActionParam = route.query.aiAction as string; const aiActionParam = route.query.aiAction as string
// 如果没有任何 AI 参数,直接返回 // 如果没有任何 AI 参数,直接返回
if (!keywords && !autoSearch && !aiActionParam) { if (!keywords && !autoSearch && !aiActionParam) {
return; return
} }
// 在 nextTick 中执行,确保页面数据已加载 // 在 nextTick 中执行,确保页面数据已加载
nextTick(async () => { nextTick(async () => {
if (isUnmounted) return; if (isUnmounted) return
// 1. 处理自动搜索 // 1. 处理自动搜索
if (autoSearch === "true" && keywords) { if (autoSearch === 'true' && keywords) {
handleAutoSearch(keywords); handleAutoSearch(keywords)
} }
// 2. 处理 AI 操作 // 2. 处理 AI 操作
if (aiActionParam) { if (aiActionParam) {
try { try {
const aiAction = JSON.parse(decodeURIComponent(aiActionParam)); const aiAction = JSON.parse(decodeURIComponent(aiActionParam))
await executeAiAction(aiAction); await executeAiAction(aiAction)
} catch (error) { } catch (error) {
console.error("解析 AI 操作失败:", error); console.error('解析 AI 操作失败:', error)
ElMessage.error("AI 操作参数解析失败"); ElMessage.error('AI 操作参数解析失败')
} }
} }
}); })
} }
// 组件挂载时自动初始化 // 组件挂载时自动初始化
onMounted(() => { onMounted(() => {
init(); init()
}); })
// 组件卸载时清理 // 组件卸载时清理
onBeforeUnmount(() => { onBeforeUnmount(() => {
isUnmounted = true; isUnmounted = true
}); })
return { return {
executeAiAction, executeAiAction,
executeCommand, executeCommand,
handleAutoSearch, handleAutoSearch,
init, init
}; }
} }

View File

@@ -1,4 +1,4 @@
import { computed, ref } from "vue"; import { computed, ref } from 'vue'
/** /**
* 表格行选择 Composable * 表格行选择 Composable
@@ -16,21 +16,21 @@ export function useTableSelection<T extends { id: string | number }>() {
/** /**
* 选中的数据项ID列表 * 选中的数据项ID列表
*/ */
const selectedIds = ref<(string | number)[]>([]); const selectedIds = ref<(string | number)[]>([])
/** /**
* 表格选中项变化处理 * 表格选中项变化处理
* @param selection 选中的行数据列表 * @param selection 选中的行数据列表
*/ */
function handleSelectionChange(selection: T[]): void { function handleSelectionChange(selection: T[]): void {
selectedIds.value = selection.map((item) => item.id); selectedIds.value = selection.map((item) => item.id)
} }
/** /**
* 清空选择 * 清空选择
*/ */
function clearSelection(): void { function clearSelection(): void {
selectedIds.value = []; selectedIds.value = []
} }
/** /**
@@ -39,18 +39,18 @@ export function useTableSelection<T extends { id: string | number }>() {
* @returns 是否被选中 * @returns 是否被选中
*/ */
function isSelected(id: string | number): boolean { function isSelected(id: string | number): boolean {
return selectedIds.value.includes(id); return selectedIds.value.includes(id)
} }
/** /**
* 获取选中的数量 * 获取选中的数量
*/ */
const selectedCount = computed(() => selectedIds.value.length); const selectedCount = computed(() => selectedIds.value.length)
/** /**
* 是否有选中项 * 是否有选中项
*/ */
const hasSelection = computed(() => selectedIds.value.length > 0); const hasSelection = computed(() => selectedIds.value.length > 0)
return { return {
selectedIds, selectedIds,
@@ -58,6 +58,6 @@ export function useTableSelection<T extends { id: string | number }>() {
hasSelection, hasSelection,
handleSelectionChange, handleSelectionChange,
clearSelection, clearSelection,
isSelected, isSelected
}; }
} }

View File

@@ -1,37 +1,37 @@
import { useDictStoreHook } from "@/store/modules/dict-store"; import { useDictStoreHook } from '@/store/modules/dict-store'
import { useStomp } from "./useStomp"; import { useStomp } from './useStomp'
import type { IMessage } from "@stomp/stompjs"; import type { IMessage } from '@stomp/stompjs'
/** /**
* 字典变更消息结构 * 字典变更消息结构
*/ */
export interface DictChangeMessage { export interface DictChangeMessage {
/** 字典编码 */ /** 字典编码 */
dictCode: string; dictCode: string
/** 时间戳 */ /** 时间戳 */
timestamp: number; timestamp: number
} }
/** /**
* 字典消息别名(向后兼容) * 字典消息别名(向后兼容)
*/ */
export type DictMessage = DictChangeMessage; export type DictMessage = DictChangeMessage
/** /**
* 字典变更事件回调函数类型 * 字典变更事件回调函数类型
*/ */
export type DictChangeCallback = (message: DictChangeMessage) => void; export type DictChangeCallback = (message: DictChangeMessage) => void
/** /**
* 全局单例实例 * 全局单例实例
*/ */
let singletonInstance: ReturnType<typeof createDictSyncComposable> | null = null; let singletonInstance: ReturnType<typeof createDictSyncComposable> | null = null
/** /**
* 创建字典同步组合式函数(内部工厂函数) * 创建字典同步组合式函数(内部工厂函数)
*/ */
function createDictSyncComposable() { function createDictSyncComposable() {
const dictStore = useDictStoreHook(); const dictStore = useDictStoreHook()
// 使用优化后的 useStomp // 使用优化后的 useStomp
const stomp = useStomp({ const stomp = useStomp({
@@ -40,100 +40,100 @@ function createDictSyncComposable() {
useExponentialBackoff: false, useExponentialBackoff: false,
maxReconnectAttempts: 3, maxReconnectAttempts: 3,
autoRestoreSubscriptions: true, // 自动恢复订阅 autoRestoreSubscriptions: true, // 自动恢复订阅
debug: false, debug: false
}); })
// 字典主题地址 // 字典主题地址
const DICT_TOPIC = "/topic/dict"; const DICT_TOPIC = '/topic/dict'
// 消息回调函数列表 // 消息回调函数列表
const messageCallbacks = ref<DictChangeCallback[]>([]); const messageCallbacks = ref<DictChangeCallback[]>([])
// 订阅 ID用于取消订阅 // 订阅 ID用于取消订阅
let subscriptionId: string | null = null; let subscriptionId: string | null = null
/** /**
* 处理字典变更事件 * 处理字典变更事件
*/ */
const handleDictChangeMessage = (message: IMessage) => { const handleDictChangeMessage = (message: IMessage) => {
if (!message.body) { if (!message.body) {
return; return
} }
try { try {
const data = JSON.parse(message.body) as DictChangeMessage; const data = JSON.parse(message.body) as DictChangeMessage
const { dictCode } = data; const { dictCode } = data
if (!dictCode) { if (!dictCode) {
console.warn("[DictSync] 收到无效的字典变更消息:缺少 dictCode"); console.warn('[DictSync] 收到无效的字典变更消息:缺少 dictCode')
return; return
} }
console.log(`[DictSync] 字典 "${dictCode}" 已更新,清除本地缓存`); console.log(`[DictSync] 字典 "${dictCode}" 已更新,清除本地缓存`)
// 清除缓存,等待按需加载 // 清除缓存,等待按需加载
dictStore.removeDictItem(dictCode); dictStore.removeDictItem(dictCode)
// 执行所有注册的回调函数 // 执行所有注册的回调函数
messageCallbacks.value.forEach((callback) => { messageCallbacks.value.forEach((callback) => {
try { try {
callback(data); callback(data)
} catch (error) { } catch (error) {
console.error("[DictSync] 回调函数执行失败:", error); console.error('[DictSync] 回调函数执行失败:', error)
} }
}); })
} catch (error) { } catch (error) {
console.error("[DictSync] 解析字典变更消息失败:", error); console.error('[DictSync] 解析字典变更消息失败:', error)
} }
}; }
/** /**
* 初始化 WebSocket 连接并订阅字典主题 * 初始化 WebSocket 连接并订阅字典主题
*/ */
const initialize = () => { const initialize = () => {
// 检查是否配置了 WebSocket 端点 // 检查是否配置了 WebSocket 端点
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT; const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT
if (!wsEndpoint) { if (!wsEndpoint) {
console.log("[DictSync] 未配置 WebSocket 端点,跳过字典同步功能"); console.log('[DictSync] 未配置 WebSocket 端点,跳过字典同步功能')
return; return
} }
console.log("[DictSync] 初始化字典同步服务..."); console.log('[DictSync] 初始化字典同步服务...')
// 建立 WebSocket 连接 // 建立 WebSocket 连接
stomp.connect(); stomp.connect()
// 订阅字典主题useStomp 会自动处理重连后的订阅恢复) // 订阅字典主题useStomp 会自动处理重连后的订阅恢复)
subscriptionId = stomp.subscribe(DICT_TOPIC, handleDictChangeMessage); subscriptionId = stomp.subscribe(DICT_TOPIC, handleDictChangeMessage)
if (subscriptionId) { if (subscriptionId) {
console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`); console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`)
} else { } else {
console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`); console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`)
} }
}; }
/** /**
* 关闭 WebSocket 连接并清理资源 * 关闭 WebSocket 连接并清理资源
*/ */
const cleanup = () => { const cleanup = () => {
console.log("[DictSync] 清理字典同步服务..."); console.log('[DictSync] 清理字典同步服务...')
// 取消订阅(如果有的话) // 取消订阅(如果有的话)
if (subscriptionId) { if (subscriptionId) {
stomp.unsubscribe(subscriptionId); stomp.unsubscribe(subscriptionId)
subscriptionId = null; subscriptionId = null
} }
// 也可以通过主题地址取消订阅 // 也可以通过主题地址取消订阅
stomp.unsubscribeDestination(DICT_TOPIC); stomp.unsubscribeDestination(DICT_TOPIC)
// 断开连接 // 断开连接
stomp.disconnect(); stomp.disconnect()
// 清空回调列表 // 清空回调列表
messageCallbacks.value = []; messageCallbacks.value = []
}; }
/** /**
* 注册字典变更回调函数 * 注册字典变更回调函数
@@ -142,16 +142,16 @@ function createDictSyncComposable() {
* @returns 返回一个取消注册的函数 * @returns 返回一个取消注册的函数
*/ */
const onDictChange = (callback: DictChangeCallback) => { const onDictChange = (callback: DictChangeCallback) => {
messageCallbacks.value.push(callback); messageCallbacks.value.push(callback)
// 返回取消注册的函数 // 返回取消注册的函数
return () => { return () => {
const index = messageCallbacks.value.indexOf(callback); const index = messageCallbacks.value.indexOf(callback)
if (index !== -1) { if (index !== -1) {
messageCallbacks.value.splice(index, 1); messageCallbacks.value.splice(index, 1)
} }
}; }
}; }
return { return {
// 状态 // 状态
@@ -169,8 +169,8 @@ function createDictSyncComposable() {
onDictMessage: onDictChange, onDictMessage: onDictChange,
// 用于测试和调试 // 用于测试和调试
handleDictChangeMessage, handleDictChangeMessage
}; }
} }
/** /**
@@ -199,7 +199,7 @@ function createDictSyncComposable() {
*/ */
export function useDictSync() { export function useDictSync() {
if (!singletonInstance) { if (!singletonInstance) {
singletonInstance = createDictSyncComposable(); singletonInstance = createDictSyncComposable()
} }
return singletonInstance; return singletonInstance
} }

View File

@@ -1,28 +1,28 @@
import { ref, watch, onMounted, onUnmounted, getCurrentInstance } from "vue"; import { ref, watch, onMounted, onUnmounted, getCurrentInstance } from 'vue'
import { useStomp } from "./useStomp"; import { useStomp } from './useStomp'
import { registerWebSocketInstance } from "@/plugins/websocket"; import { registerWebSocketInstance } from '@/plugins/websocket'
import { AuthStorage } from "@/utils/auth"; import { AuthStorage } from '@/utils/auth'
/** /**
* 在线用户数量消息结构 * 在线用户数量消息结构
*/ */
interface OnlineCountMessage { interface OnlineCountMessage {
count?: number; count?: number
timestamp?: number; timestamp?: number
} }
/** /**
* 全局单例实例 * 全局单例实例
*/ */
let globalInstance: ReturnType<typeof createOnlineCountComposable> | null = null; let globalInstance: ReturnType<typeof createOnlineCountComposable> | null = null
/** /**
* 创建在线用户计数组合式函数(内部工厂函数) * 创建在线用户计数组合式函数(内部工厂函数)
*/ */
function createOnlineCountComposable() { function createOnlineCountComposable() {
// ==================== 状态管理 ==================== // ==================== 状态管理 ====================
const onlineUserCount = ref(0); const onlineUserCount = ref(0)
const lastUpdateTime = ref(0); const lastUpdateTime = ref(0)
// ==================== WebSocket 客户端 ==================== // ==================== WebSocket 客户端 ====================
const stomp = useStomp({ const stomp = useStomp({
@@ -31,124 +31,124 @@ function createOnlineCountComposable() {
connectionTimeout: 10000, connectionTimeout: 10000,
useExponentialBackoff: true, useExponentialBackoff: true,
autoRestoreSubscriptions: true, // 自动恢复订阅 autoRestoreSubscriptions: true, // 自动恢复订阅
debug: false, debug: false
}); })
// 在线用户计数主题 // 在线用户计数主题
const ONLINE_COUNT_TOPIC = "/topic/online-count"; const ONLINE_COUNT_TOPIC = '/topic/online-count'
// 订阅 ID // 订阅 ID
let subscriptionId: string | null = null; let subscriptionId: string | null = null
// 注册到全局实例管理器 // 注册到全局实例管理器
registerWebSocketInstance("onlineCount", stomp); registerWebSocketInstance('onlineCount', stomp)
/** /**
* 处理在线用户数量消息 * 处理在线用户数量消息
*/ */
const handleOnlineCountMessage = (message: any) => { const handleOnlineCountMessage = (message: any) => {
try { try {
const data = message.body; const data = message.body
const jsonData = JSON.parse(data) as OnlineCountMessage; const jsonData = JSON.parse(data) as OnlineCountMessage
// 支持两种消息格式 // 支持两种消息格式
// 1. 直接是数字: 42 // 1. 直接是数字: 42
// 2. 对象格式: { count: 42, timestamp: 1234567890 } // 2. 对象格式: { count: 42, timestamp: 1234567890 }
const count = typeof jsonData === "number" ? jsonData : jsonData.count; const count = typeof jsonData === 'number' ? jsonData : jsonData.count
if (count !== undefined && !isNaN(count)) { if (count !== undefined && !isNaN(count)) {
onlineUserCount.value = count; onlineUserCount.value = count
lastUpdateTime.value = Date.now(); lastUpdateTime.value = Date.now()
console.log(`[useOnlineCount] 在线用户数更新: ${count}`); console.log(`[useOnlineCount] 在线用户数更新: ${count}`)
} else { } else {
console.warn("[useOnlineCount] 收到无效的在线用户数:", data); console.warn('[useOnlineCount] 收到无效的在线用户数:', data)
} }
} catch (error) { } catch (error) {
console.error("[useOnlineCount] 解析在线用户数失败:", error); console.error('[useOnlineCount] 解析在线用户数失败:', error)
} }
}; }
/** /**
* 订阅在线用户计数主题 * 订阅在线用户计数主题
*/ */
const subscribeToOnlineCount = () => { const subscribeToOnlineCount = () => {
if (subscriptionId) { if (subscriptionId) {
console.log("[useOnlineCount] 已存在订阅,跳过"); console.log('[useOnlineCount] 已存在订阅,跳过')
return; return
} }
// 订阅在线用户计数主题useStomp 会处理重连后的订阅恢复) // 订阅在线用户计数主题useStomp 会处理重连后的订阅恢复)
subscriptionId = stomp.subscribe(ONLINE_COUNT_TOPIC, handleOnlineCountMessage); subscriptionId = stomp.subscribe(ONLINE_COUNT_TOPIC, handleOnlineCountMessage)
if (subscriptionId) { if (subscriptionId) {
console.log(`[useOnlineCount] 已订阅主题: ${ONLINE_COUNT_TOPIC}`); console.log(`[useOnlineCount] 已订阅主题: ${ONLINE_COUNT_TOPIC}`)
} else { } else {
console.log(`[useOnlineCount] 暂存订阅配置,等待连接建立后自动订阅`); console.log(`[useOnlineCount] 暂存订阅配置,等待连接建立后自动订阅`)
} }
}; }
/** /**
* 初始化 WebSocket 连接并订阅在线用户主题 * 初始化 WebSocket 连接并订阅在线用户主题
*/ */
const initialize = () => { const initialize = () => {
// 检查 WebSocket 端点是否配置 // 检查 WebSocket 端点是否配置
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT; const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT
if (!wsEndpoint) { if (!wsEndpoint) {
console.log("[useOnlineCount] 未配置 WebSocket 端点,跳过初始化"); console.log('[useOnlineCount] 未配置 WebSocket 端点,跳过初始化')
return; return
} }
// 检查令牌有效性 // 检查令牌有效性
const accessToken = AuthStorage.getAccessToken(); const accessToken = AuthStorage.getAccessToken()
if (!accessToken) { if (!accessToken) {
console.log("[useOnlineCount] 未检测到有效令牌,跳过初始化"); console.log('[useOnlineCount] 未检测到有效令牌,跳过初始化')
return; return
} }
console.log("[useOnlineCount] 初始化在线用户计数服务..."); console.log('[useOnlineCount] 初始化在线用户计数服务...')
// 建立 WebSocket 连接 // 建立 WebSocket 连接
stomp.connect(); stomp.connect()
// 订阅主题 // 订阅主题
subscribeToOnlineCount(); subscribeToOnlineCount()
}; }
/** /**
* 关闭 WebSocket 连接并清理资源 * 关闭 WebSocket 连接并清理资源
*/ */
const cleanup = () => { const cleanup = () => {
console.log("[useOnlineCount] 清理在线用户计数服务..."); console.log('[useOnlineCount] 清理在线用户计数服务...')
// 取消订阅 // 取消订阅
if (subscriptionId) { if (subscriptionId) {
stomp.unsubscribe(subscriptionId); stomp.unsubscribe(subscriptionId)
subscriptionId = null; subscriptionId = null
} }
// 也可以通过主题地址取消订阅 // 也可以通过主题地址取消订阅
stomp.unsubscribeDestination(ONLINE_COUNT_TOPIC); stomp.unsubscribeDestination(ONLINE_COUNT_TOPIC)
// 断开连接 // 断开连接
stomp.disconnect(); stomp.disconnect()
// 重置状态 // 重置状态
onlineUserCount.value = 0; onlineUserCount.value = 0
lastUpdateTime.value = 0; lastUpdateTime.value = 0
}; }
// 监听连接状态变化 // 监听连接状态变化
watch( watch(
stomp.isConnected, stomp.isConnected,
(connected) => { (connected) => {
if (connected) { if (connected) {
console.log("[useOnlineCount] WebSocket 已连接"); console.log('[useOnlineCount] WebSocket 已连接')
} else { } else {
console.log("[useOnlineCount] WebSocket 已断开"); console.log('[useOnlineCount] WebSocket 已断开')
} }
}, },
{ immediate: false } { immediate: false }
); )
return { return {
// 状态 // 状态
@@ -163,8 +163,8 @@ function createOnlineCountComposable() {
// 别名方法(向后兼容) // 别名方法(向后兼容)
initWebSocket: initialize, initWebSocket: initialize,
closeWebSocket: cleanup, closeWebSocket: cleanup
}; }
} }
/** /**
@@ -187,31 +187,31 @@ function createOnlineCountComposable() {
* ``` * ```
*/ */
export function useOnlineCount(options: { autoInit?: boolean } = {}) { export function useOnlineCount(options: { autoInit?: boolean } = {}) {
const { autoInit = true } = options; const { autoInit = true } = options
// 获取或创建单例实例 // 获取或创建单例实例
if (!globalInstance) { if (!globalInstance) {
globalInstance = createOnlineCountComposable(); globalInstance = createOnlineCountComposable()
} }
// 只在组件上下文中且 autoInit 为 true 时使用生命周期钩子 // 只在组件上下文中且 autoInit 为 true 时使用生命周期钩子
const instance = getCurrentInstance(); const instance = getCurrentInstance()
if (autoInit && instance) { if (autoInit && instance) {
onMounted(() => { onMounted(() => {
// 只有在未连接时才尝试初始化 // 只有在未连接时才尝试初始化
if (!globalInstance!.isConnected.value) { if (!globalInstance!.isConnected.value) {
console.log("[useOnlineCount] 组件挂载,初始化 WebSocket 连接"); console.log('[useOnlineCount] 组件挂载,初始化 WebSocket 连接')
globalInstance!.initialize(); globalInstance!.initialize()
} else { } else {
console.log("[useOnlineCount] WebSocket 已连接,跳过初始化"); console.log('[useOnlineCount] WebSocket 已连接,跳过初始化')
} }
}); })
// 注意:不在卸载时关闭连接,保持全局连接 // 注意:不在卸载时关闭连接,保持全局连接
onUnmounted(() => { onUnmounted(() => {
console.log("[useOnlineCount] 组件卸载(保持 WebSocket 连接)"); console.log('[useOnlineCount] 组件卸载(保持 WebSocket 连接)')
}); })
} }
return globalInstance; return globalInstance
} }

View File

@@ -1,43 +1,43 @@
import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs"; import { Client, type IMessage, type StompSubscription } from '@stomp/stompjs'
import { AuthStorage } from "@/utils/auth"; import { AuthStorage } from '@/utils/auth'
export interface UseStompOptions { export interface UseStompOptions {
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */ /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
brokerURL?: string; brokerURL?: string
/** 用于鉴权的 token不传时使用 getAccessToken() 的返回值 */ /** 用于鉴权的 token不传时使用 getAccessToken() 的返回值 */
token?: string; token?: string
/** 重连延迟,单位毫秒,默认为 15000 */ /** 重连延迟,单位毫秒,默认为 15000 */
reconnectDelay?: number; reconnectDelay?: number
/** 连接超时时间,单位毫秒,默认为 10000 */ /** 连接超时时间,单位毫秒,默认为 10000 */
connectionTimeout?: number; connectionTimeout?: number
/** 是否开启指数退避重连策略 */ /** 是否开启指数退避重连策略 */
useExponentialBackoff?: boolean; useExponentialBackoff?: boolean
/** 最大重连次数,默认为 3 */ /** 最大重连次数,默认为 3 */
maxReconnectAttempts?: number; maxReconnectAttempts?: number
/** 最大重连延迟,单位毫秒,默认为 60000 */ /** 最大重连延迟,单位毫秒,默认为 60000 */
maxReconnectDelay?: number; maxReconnectDelay?: number
/** 是否开启调试日志 */ /** 是否开启调试日志 */
debug?: boolean; debug?: boolean
/** 是否在重连时自动恢复订阅,默认为 true */ /** 是否在重连时自动恢复订阅,默认为 true */
autoRestoreSubscriptions?: boolean; autoRestoreSubscriptions?: boolean
} }
/** /**
* 订阅配置信息 * 订阅配置信息
*/ */
interface SubscriptionConfig { interface SubscriptionConfig {
destination: string; destination: string
callback: (message: IMessage) => void; callback: (message: IMessage) => void
} }
/** /**
* 连接状态枚举 * 连接状态枚举
*/ */
enum ConnectionState { enum ConnectionState {
DISCONNECTED = "DISCONNECTED", DISCONNECTED = 'DISCONNECTED',
CONNECTING = "CONNECTING", CONNECTING = 'CONNECTING',
CONNECTED = "CONNECTED", CONNECTED = 'CONNECTED',
RECONNECTING = "RECONNECTING", RECONNECTING = 'RECONNECTING'
} }
/** /**
@@ -54,7 +54,7 @@ enum ConnectionState {
*/ */
export function useStomp(options: UseStompOptions = {}) { export function useStomp(options: UseStompOptions = {}) {
// ==================== 配置初始化 ==================== // ==================== 配置初始化 ====================
const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || ""; const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || ''
const config = { const config = {
brokerURL: ref(options.brokerURL ?? defaultBrokerURL), brokerURL: ref(options.brokerURL ?? defaultBrokerURL),
@@ -64,27 +64,27 @@ export function useStomp(options: UseStompOptions = {}) {
maxReconnectAttempts: options.maxReconnectAttempts ?? 3, maxReconnectAttempts: options.maxReconnectAttempts ?? 3,
maxReconnectDelay: options.maxReconnectDelay ?? 60000, maxReconnectDelay: options.maxReconnectDelay ?? 60000,
autoRestoreSubscriptions: options.autoRestoreSubscriptions ?? true, autoRestoreSubscriptions: options.autoRestoreSubscriptions ?? true,
debug: options.debug ?? false, debug: options.debug ?? false
}; }
// ==================== 状态管理 ==================== // ==================== 状态管理 ====================
const connectionState = ref<ConnectionState>(ConnectionState.DISCONNECTED); const connectionState = ref<ConnectionState>(ConnectionState.DISCONNECTED)
const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED); const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED)
const reconnectAttempts = ref(0); const reconnectAttempts = ref(0)
// ==================== 定时器管理 ==================== // ==================== 定时器管理 ====================
let reconnectTimer: ReturnType<typeof setTimeout> | null = null; let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let connectionTimeoutTimer: ReturnType<typeof setTimeout> | null = null; let connectionTimeoutTimer: ReturnType<typeof setTimeout> | null = null
// ==================== 订阅管理 ==================== // ==================== 订阅管理 ====================
// 活动订阅:存储当前 STOMP 订阅对象 // 活动订阅:存储当前 STOMP 订阅对象
const activeSubscriptions = new Map<string, StompSubscription>(); const activeSubscriptions = new Map<string, StompSubscription>()
// 订阅配置注册表:用于自动恢复订阅 // 订阅配置注册表:用于自动恢复订阅
const subscriptionRegistry = new Map<string, SubscriptionConfig>(); const subscriptionRegistry = new Map<string, SubscriptionConfig>()
// ==================== 客户端实例 ==================== // ==================== 客户端实例 ====================
const stompClient = ref<Client | null>(null); const stompClient = ref<Client | null>(null)
let isManualDisconnect = false; let isManualDisconnect = false
// ==================== 工具函数 ==================== // ==================== 工具函数 ====================
@@ -93,50 +93,50 @@ export function useStomp(options: UseStompOptions = {}) {
*/ */
const clearAllTimers = () => { const clearAllTimers = () => {
if (reconnectTimer) { if (reconnectTimer) {
clearTimeout(reconnectTimer); clearTimeout(reconnectTimer)
reconnectTimer = null; reconnectTimer = null
} }
if (connectionTimeoutTimer) { if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer); clearTimeout(connectionTimeoutTimer)
connectionTimeoutTimer = null; connectionTimeoutTimer = null
} }
}; }
/** /**
* 日志输出(支持调试模式控制) * 日志输出(支持调试模式控制)
*/ */
const log = (...args: any[]) => { const log = (...args: any[]) => {
if (config.debug) { if (config.debug) {
console.log("[useStomp]", ...args); console.log('[useStomp]', ...args)
} }
}; }
const logWarn = (...args: any[]) => { const logWarn = (...args: any[]) => {
console.warn("[useStomp]", ...args); console.warn('[useStomp]', ...args)
}; }
const logError = (...args: any[]) => { const logError = (...args: any[]) => {
console.error("[useStomp]", ...args); console.error('[useStomp]', ...args)
}; }
/** /**
* 恢复所有订阅 * 恢复所有订阅
*/ */
const restoreSubscriptions = () => { const restoreSubscriptions = () => {
if (!config.autoRestoreSubscriptions || subscriptionRegistry.size === 0) { if (!config.autoRestoreSubscriptions || subscriptionRegistry.size === 0) {
return; return
} }
log(`开始恢复 ${subscriptionRegistry.size} 个订阅...`); log(`开始恢复 ${subscriptionRegistry.size} 个订阅...`)
for (const [destination, subscriptionConfig] of subscriptionRegistry.entries()) { for (const [destination, subscriptionConfig] of subscriptionRegistry.entries()) {
try { try {
performSubscribe(destination, subscriptionConfig.callback); performSubscribe(destination, subscriptionConfig.callback)
} catch (error) { } catch (error) {
logError(`恢复订阅 ${destination} 失败:`, error); logError(`恢复订阅 ${destination} 失败:`, error)
} }
} }
}; }
/** /**
* 初始化 STOMP 客户端 * 初始化 STOMP 客户端
@@ -144,82 +144,82 @@ export function useStomp(options: UseStompOptions = {}) {
const initializeClient = () => { const initializeClient = () => {
// 如果客户端已存在且处于活动状态,直接返回 // 如果客户端已存在且处于活动状态,直接返回
if (stompClient.value && (stompClient.value.active || stompClient.value.connected)) { if (stompClient.value && (stompClient.value.active || stompClient.value.connected)) {
log("STOMP 客户端已存在且处于活动状态,跳过初始化"); log('STOMP 客户端已存在且处于活动状态,跳过初始化')
return; return
} }
// 检查 WebSocket 端点是否配置 // 检查 WebSocket 端点是否配置
if (!config.brokerURL.value) { if (!config.brokerURL.value) {
logWarn("WebSocket 连接失败: 未配置 WebSocket 端点 URL"); logWarn('WebSocket 连接失败: 未配置 WebSocket 端点 URL')
return; return
} }
// 每次连接前重新获取最新令牌 // 每次连接前重新获取最新令牌
const accessToken = AuthStorage.getAccessToken(); const accessToken = AuthStorage.getAccessToken()
if (!accessToken) { if (!accessToken) {
logWarn("WebSocket 连接失败:授权令牌为空,请先登录"); logWarn('WebSocket 连接失败:授权令牌为空,请先登录')
return; return
} }
// 清理旧客户端 // 清理旧客户端
if (stompClient.value) { if (stompClient.value) {
try { try {
stompClient.value.deactivate(); stompClient.value.deactivate()
} catch (error) { } catch (error) {
logWarn("清理旧客户端时出错:", error); logWarn('清理旧客户端时出错:', error)
} }
stompClient.value = null; stompClient.value = null
} }
// 创建 STOMP 客户端 // 创建 STOMP 客户端
stompClient.value = new Client({ stompClient.value = new Client({
brokerURL: config.brokerURL.value, brokerURL: config.brokerURL.value,
connectHeaders: { connectHeaders: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`
}, },
debug: config.debug ? (msg) => console.log("[STOMP]", msg) : () => {}, debug: config.debug ? (msg) => console.log('[STOMP]', msg) : () => {},
reconnectDelay: 0, // 禁用内置重连,使用自定义重连逻辑 reconnectDelay: 0, // 禁用内置重连,使用自定义重连逻辑
heartbeatIncoming: 4000, heartbeatIncoming: 4000,
heartbeatOutgoing: 4000, heartbeatOutgoing: 4000
}); })
// ==================== 事件监听器 ==================== // ==================== 事件监听器 ====================
// 连接成功 // 连接成功
stompClient.value.onConnect = () => { stompClient.value.onConnect = () => {
connectionState.value = ConnectionState.CONNECTED; connectionState.value = ConnectionState.CONNECTED
reconnectAttempts.value = 0; reconnectAttempts.value = 0
clearAllTimers(); clearAllTimers()
log("✅ WebSocket 连接已建立"); log('✅ WebSocket 连接已建立')
// 自动恢复订阅 // 自动恢复订阅
restoreSubscriptions(); restoreSubscriptions()
}; }
// 连接断开 // 连接断开
stompClient.value.onDisconnect = () => { stompClient.value.onDisconnect = () => {
connectionState.value = ConnectionState.DISCONNECTED; connectionState.value = ConnectionState.DISCONNECTED
log("❌ WebSocket 连接已断开"); log('❌ WebSocket 连接已断开')
// 清空活动订阅(但保留订阅配置用于恢复) // 清空活动订阅(但保留订阅配置用于恢复)
activeSubscriptions.clear(); activeSubscriptions.clear()
// 如果不是手动断开且未达到最大重连次数,则尝试重连 // 如果不是手动断开且未达到最大重连次数,则尝试重连
if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) { if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) {
scheduleReconnect(); scheduleReconnect()
} }
}; }
// WebSocket 关闭 // WebSocket 关闭
stompClient.value.onWebSocketClose = (event) => { stompClient.value.onWebSocketClose = (event) => {
connectionState.value = ConnectionState.DISCONNECTED; connectionState.value = ConnectionState.DISCONNECTED
log(`WebSocket 已关闭: code=${event?.code}, reason=${event?.reason}`); log(`WebSocket 已关闭: code=${event?.code}, reason=${event?.reason}`)
// 如果是手动断开,不重连 // 如果是手动断开,不重连
if (isManualDisconnect) { if (isManualDisconnect) {
log("手动断开连接,不进行重连"); log('手动断开连接,不进行重连')
return; return
} }
// 对于异常关闭,尝试重连 // 对于异常关闭,尝试重连
@@ -228,29 +228,29 @@ export function useStomp(options: UseStompOptions = {}) {
[1000, 1006, 1008, 1011].includes(event.code) && [1000, 1006, 1008, 1011].includes(event.code) &&
reconnectAttempts.value < config.maxReconnectAttempts reconnectAttempts.value < config.maxReconnectAttempts
) { ) {
log("检测到连接异常关闭,将尝试重连"); log('检测到连接异常关闭,将尝试重连')
scheduleReconnect(); scheduleReconnect()
} }
}; }
// STOMP 错误 // STOMP 错误
stompClient.value.onStompError = (frame) => { stompClient.value.onStompError = (frame) => {
logError("STOMP 错误:", frame.headers, frame.body); logError('STOMP 错误:', frame.headers, frame.body)
connectionState.value = ConnectionState.DISCONNECTED; connectionState.value = ConnectionState.DISCONNECTED
// 检查是否是授权错误 // 检查是否是授权错误
const isAuthError = const isAuthError =
frame.headers?.message?.includes("Unauthorized") || frame.headers?.message?.includes('Unauthorized') ||
frame.body?.includes("Unauthorized") || frame.body?.includes('Unauthorized') ||
frame.body?.includes("Token") || frame.body?.includes('Token') ||
frame.body?.includes("401"); frame.body?.includes('401')
if (isAuthError) { if (isAuthError) {
logWarn("WebSocket 授权错误,停止重连"); logWarn('WebSocket 授权错误,停止重连')
isManualDisconnect = true; // 授权错误不进行重连 isManualDisconnect = true // 授权错误不进行重连
} }
}; }
}; }
/** /**
* 调度重连任务 * 调度重连任务
@@ -258,17 +258,17 @@ export function useStomp(options: UseStompOptions = {}) {
const scheduleReconnect = () => { const scheduleReconnect = () => {
// 如果正在连接或手动断开,不重连 // 如果正在连接或手动断开,不重连
if (connectionState.value === ConnectionState.CONNECTING || isManualDisconnect) { if (connectionState.value === ConnectionState.CONNECTING || isManualDisconnect) {
return; return
} }
// 检查是否达到最大重连次数 // 检查是否达到最大重连次数
if (reconnectAttempts.value >= config.maxReconnectAttempts) { if (reconnectAttempts.value >= config.maxReconnectAttempts) {
logError(`已达到最大重连次数 (${config.maxReconnectAttempts}),停止重连`); logError(`已达到最大重连次数 (${config.maxReconnectAttempts}),停止重连`)
return; return
} }
reconnectAttempts.value++; reconnectAttempts.value++
connectionState.value = ConnectionState.RECONNECTING; connectionState.value = ConnectionState.RECONNECTING
// 计算重连延迟(支持指数退避) // 计算重连延迟(支持指数退避)
const delay = config.useExponentialBackoff const delay = config.useExponentialBackoff
@@ -276,41 +276,41 @@ export function useStomp(options: UseStompOptions = {}) {
config.reconnectDelay * Math.pow(2, reconnectAttempts.value - 1), config.reconnectDelay * Math.pow(2, reconnectAttempts.value - 1),
config.maxReconnectDelay config.maxReconnectDelay
) )
: config.reconnectDelay; : config.reconnectDelay
log(`准备重连 (${reconnectAttempts.value}/${config.maxReconnectAttempts}),延迟 ${delay}ms`); log(`准备重连 (${reconnectAttempts.value}/${config.maxReconnectAttempts}),延迟 ${delay}ms`)
// 清除之前的重连计时器 // 清除之前的重连计时器
if (reconnectTimer) { if (reconnectTimer) {
clearTimeout(reconnectTimer); clearTimeout(reconnectTimer)
} }
// 设置重连计时器 // 设置重连计时器
reconnectTimer = setTimeout(() => { reconnectTimer = setTimeout(() => {
if (connectionState.value !== ConnectionState.CONNECTED && !isManualDisconnect) { if (connectionState.value !== ConnectionState.CONNECTED && !isManualDisconnect) {
log(`开始第 ${reconnectAttempts.value} 次重连...`); log(`开始第 ${reconnectAttempts.value} 次重连...`)
connect(); connect()
} }
}, delay); }, delay)
}; }
// 监听 brokerURL 的变化,自动重新初始化 // 监听 brokerURL 的变化,自动重新初始化
watch(config.brokerURL, (newURL, oldURL) => { watch(config.brokerURL, (newURL, oldURL) => {
if (newURL !== oldURL) { if (newURL !== oldURL) {
log(`WebSocket 端点已更改: ${oldURL} -> ${newURL}`); log(`WebSocket 端点已更改: ${oldURL} -> ${newURL}`)
// 断开当前连接 // 断开当前连接
if (stompClient.value && stompClient.value.connected) { if (stompClient.value && stompClient.value.connected) {
stompClient.value.deactivate(); stompClient.value.deactivate()
} }
// 重新初始化客户端 // 重新初始化客户端
initializeClient(); initializeClient()
} }
}); })
// 初始化客户端 // 初始化客户端
initializeClient(); initializeClient()
// ==================== 公共接口 ==================== // ==================== 公共接口 ====================
@@ -319,86 +319,86 @@ export function useStomp(options: UseStompOptions = {}) {
*/ */
const connect = () => { const connect = () => {
// 重置手动断开标志 // 重置手动断开标志
isManualDisconnect = false; isManualDisconnect = false
// 检查是否配置了 WebSocket 端点 // 检查是否配置了 WebSocket 端点
if (!config.brokerURL.value) { if (!config.brokerURL.value) {
logError("WebSocket 连接失败: 未配置 WebSocket 端点 URL"); logError('WebSocket 连接失败: 未配置 WebSocket 端点 URL')
return; return
} }
// 防止重复连接 // 防止重复连接
if (connectionState.value === ConnectionState.CONNECTING) { if (connectionState.value === ConnectionState.CONNECTING) {
log("WebSocket 正在连接中,跳过重复连接请求"); log('WebSocket 正在连接中,跳过重复连接请求')
return; return
} }
// 如果客户端不存在,先初始化 // 如果客户端不存在,先初始化
if (!stompClient.value) { if (!stompClient.value) {
initializeClient(); initializeClient()
} }
if (!stompClient.value) { if (!stompClient.value) {
logError("STOMP 客户端初始化失败"); logError('STOMP 客户端初始化失败')
return; return
} }
// 避免重复连接:检查是否已连接 // 避免重复连接:检查是否已连接
if (stompClient.value.connected) { if (stompClient.value.connected) {
log("WebSocket 已连接,跳过重复连接"); log('WebSocket 已连接,跳过重复连接')
connectionState.value = ConnectionState.CONNECTED; connectionState.value = ConnectionState.CONNECTED
return; return
} }
// 设置连接状态 // 设置连接状态
connectionState.value = ConnectionState.CONNECTING; connectionState.value = ConnectionState.CONNECTING
// 设置连接超时 // 设置连接超时
if (connectionTimeoutTimer) { if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer); clearTimeout(connectionTimeoutTimer)
} }
connectionTimeoutTimer = setTimeout(() => { connectionTimeoutTimer = setTimeout(() => {
if (connectionState.value === ConnectionState.CONNECTING) { if (connectionState.value === ConnectionState.CONNECTING) {
logWarn("WebSocket 连接超时"); logWarn('WebSocket 连接超时')
connectionState.value = ConnectionState.DISCONNECTED; connectionState.value = ConnectionState.DISCONNECTED
// 超时后尝试重连 // 超时后尝试重连
if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) { if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) {
scheduleReconnect(); scheduleReconnect()
} }
} }
}, config.connectionTimeout); }, config.connectionTimeout)
try { try {
stompClient.value.activate(); stompClient.value.activate()
log("正在建立 WebSocket 连接..."); log('正在建立 WebSocket 连接...')
} catch (error) { } catch (error) {
logError("激活 WebSocket 连接失败:", error); logError('激活 WebSocket 连接失败:', error)
connectionState.value = ConnectionState.DISCONNECTED; connectionState.value = ConnectionState.DISCONNECTED
} }
}; }
/** /**
* 执行订阅操作(内部方法) * 执行订阅操作(内部方法)
*/ */
const performSubscribe = (destination: string, callback: (message: IMessage) => void): string => { const performSubscribe = (destination: string, callback: (message: IMessage) => void): string => {
if (!stompClient.value || !stompClient.value.connected) { if (!stompClient.value || !stompClient.value.connected) {
logWarn(`尝试订阅 ${destination} 失败: 客户端未连接`); logWarn(`尝试订阅 ${destination} 失败: 客户端未连接`)
return ""; return ''
} }
try { try {
const subscription = stompClient.value.subscribe(destination, callback); const subscription = stompClient.value.subscribe(destination, callback)
const subscriptionId = subscription.id; const subscriptionId = subscription.id
activeSubscriptions.set(subscriptionId, subscription); activeSubscriptions.set(subscriptionId, subscription)
log(`✓ 订阅成功: ${destination} (ID: ${subscriptionId})`); log(`✓ 订阅成功: ${destination} (ID: ${subscriptionId})`)
return subscriptionId; return subscriptionId
} catch (error) { } catch (error) {
logError(`订阅 ${destination} 失败:`, error); logError(`订阅 ${destination} 失败:`, error)
return ""; return ''
} }
}; }
/** /**
* 订阅指定主题 * 订阅指定主题
@@ -409,16 +409,16 @@ export function useStomp(options: UseStompOptions = {}) {
*/ */
const subscribe = (destination: string, callback: (message: IMessage) => void): string => { const subscribe = (destination: string, callback: (message: IMessage) => void): string => {
// 保存订阅配置到注册表,用于断线重连后自动恢复 // 保存订阅配置到注册表,用于断线重连后自动恢复
subscriptionRegistry.set(destination, { destination, callback }); subscriptionRegistry.set(destination, { destination, callback })
// 如果已连接,立即订阅 // 如果已连接,立即订阅
if (stompClient.value?.connected) { if (stompClient.value?.connected) {
return performSubscribe(destination, callback); return performSubscribe(destination, callback)
} }
log(`暂存订阅配置: ${destination},将在连接建立后自动订阅`); log(`暂存订阅配置: ${destination},将在连接建立后自动订阅`)
return ""; return ''
}; }
/** /**
* 取消订阅 * 取消订阅
@@ -426,17 +426,17 @@ export function useStomp(options: UseStompOptions = {}) {
* @param subscriptionId 订阅 ID由 subscribe 方法返回) * @param subscriptionId 订阅 ID由 subscribe 方法返回)
*/ */
const unsubscribe = (subscriptionId: string) => { const unsubscribe = (subscriptionId: string) => {
const subscription = activeSubscriptions.get(subscriptionId); const subscription = activeSubscriptions.get(subscriptionId)
if (subscription) { if (subscription) {
try { try {
subscription.unsubscribe(); subscription.unsubscribe()
activeSubscriptions.delete(subscriptionId); activeSubscriptions.delete(subscriptionId)
log(`✓ 已取消订阅: ${subscriptionId}`); log(`✓ 已取消订阅: ${subscriptionId}`)
} catch (error) { } catch (error) {
logWarn(`取消订阅 ${subscriptionId} 时出错:`, error); logWarn(`取消订阅 ${subscriptionId} 时出错:`, error)
} }
} }
}; }
/** /**
* 取消指定主题的订阅(从注册表中移除) * 取消指定主题的订阅(从注册表中移除)
@@ -445,22 +445,22 @@ export function useStomp(options: UseStompOptions = {}) {
*/ */
const unsubscribeDestination = (destination: string) => { const unsubscribeDestination = (destination: string) => {
// 从注册表中移除 // 从注册表中移除
subscriptionRegistry.delete(destination); subscriptionRegistry.delete(destination)
// 取消所有匹配该主题的活动订阅 // 取消所有匹配该主题的活动订阅
for (const [id, subscription] of activeSubscriptions.entries()) { for (const [id, subscription] of activeSubscriptions.entries()) {
// 注意STOMP 的 subscription 对象没有直接暴露 destination // 注意STOMP 的 subscription 对象没有直接暴露 destination
// 这里简化处理,实际使用时可能需要额外维护 id -> destination 的映射 // 这里简化处理,实际使用时可能需要额外维护 id -> destination 的映射
try { try {
subscription.unsubscribe(); subscription.unsubscribe()
activeSubscriptions.delete(id); activeSubscriptions.delete(id)
} catch (error) { } catch (error) {
logWarn(`取消订阅 ${id} 时出错:`, error); logWarn(`取消订阅 ${id} 时出错:`, error)
} }
} }
log(`✓ 已移除主题订阅配置: ${destination}`); log(`✓ 已移除主题订阅配置: ${destination}`)
}; }
/** /**
* 断开 WebSocket 连接 * 断开 WebSocket 连接
@@ -469,43 +469,43 @@ export function useStomp(options: UseStompOptions = {}) {
*/ */
const disconnect = (clearSubscriptions = true) => { const disconnect = (clearSubscriptions = true) => {
// 设置手动断开标志 // 设置手动断开标志
isManualDisconnect = true; isManualDisconnect = true
// 清除所有定时器 // 清除所有定时器
clearAllTimers(); clearAllTimers()
// 取消所有活动订阅 // 取消所有活动订阅
for (const [id, subscription] of activeSubscriptions.entries()) { for (const [id, subscription] of activeSubscriptions.entries()) {
try { try {
subscription.unsubscribe(); subscription.unsubscribe()
} catch (error) { } catch (error) {
logWarn(`取消订阅 ${id} 时出错:`, error); logWarn(`取消订阅 ${id} 时出错:`, error)
} }
} }
activeSubscriptions.clear(); activeSubscriptions.clear()
// 可选:清除订阅注册表 // 可选:清除订阅注册表
if (clearSubscriptions) { if (clearSubscriptions) {
subscriptionRegistry.clear(); subscriptionRegistry.clear()
log("已清除所有订阅配置"); log('已清除所有订阅配置')
} }
// 断开连接 // 断开连接
if (stompClient.value) { if (stompClient.value) {
try { try {
if (stompClient.value.connected || stompClient.value.active) { if (stompClient.value.connected || stompClient.value.active) {
stompClient.value.deactivate(); stompClient.value.deactivate()
log("✓ WebSocket 连接已主动断开"); log('✓ WebSocket 连接已主动断开')
} }
} catch (error) { } catch (error) {
logError("断开 WebSocket 连接时出错:", error); logError('断开 WebSocket 连接时出错:', error)
} }
stompClient.value = null; stompClient.value = null
} }
connectionState.value = ConnectionState.DISCONNECTED; connectionState.value = ConnectionState.DISCONNECTED
reconnectAttempts.value = 0; reconnectAttempts.value = 0
}; }
// ==================== 返回公共接口 ==================== // ==================== 返回公共接口 ====================
return { return {
@@ -525,6 +525,6 @@ export function useStomp(options: UseStompOptions = {}) {
// 统计信息 // 统计信息
getActiveSubscriptionCount: () => activeSubscriptions.size, getActiveSubscriptionCount: () => activeSubscriptions.size,
getRegisteredSubscriptionCount: () => subscriptionRegistry.size, getRegisteredSubscriptionCount: () => subscriptionRegistry.size
}; }
} }

View File

@@ -3,7 +3,7 @@
* 存储键命名规范:{prefix}:{namespace}:{key} * 存储键命名规范:{prefix}:{namespace}:{key}
*/ */
export const APP_PREFIX = "vea"; export const APP_PREFIX = 'vea'
export const STORAGE_KEYS = { export const STORAGE_KEYS = {
// 用户认证相关 // 用户认证相关
@@ -29,21 +29,21 @@ export const STORAGE_KEYS = {
SIZE: `${APP_PREFIX}:app:size`, // 屏幕尺寸 SIZE: `${APP_PREFIX}:app:size`, // 屏幕尺寸
LANGUAGE: `${APP_PREFIX}:app:language`, // 应用语言 LANGUAGE: `${APP_PREFIX}:app:language`, // 应用语言
SIDEBAR_STATUS: `${APP_PREFIX}:app:sidebar_status`, // 侧边栏状态 SIDEBAR_STATUS: `${APP_PREFIX}:app:sidebar_status`, // 侧边栏状态
ACTIVE_TOP_MENU_PATH: `${APP_PREFIX}:app:active_top_menu_path`, // 当前激活的顶部菜单路径 ACTIVE_TOP_MENU_PATH: `${APP_PREFIX}:app:active_top_menu_path` // 当前激活的顶部菜单路径
} as const; } as const
export const ROLE_ROOT = "ROOT"; // 超级管理员角色 export const ROLE_ROOT = 'ROOT' // 超级管理员角色
// 分组键集合(便于批量操作) // 分组键集合(便于批量操作)
export const AUTH_KEYS = { export const AUTH_KEYS = {
ACCESS_TOKEN: STORAGE_KEYS.ACCESS_TOKEN, ACCESS_TOKEN: STORAGE_KEYS.ACCESS_TOKEN,
REFRESH_TOKEN: STORAGE_KEYS.REFRESH_TOKEN, REFRESH_TOKEN: STORAGE_KEYS.REFRESH_TOKEN,
REMEMBER_ME: STORAGE_KEYS.REMEMBER_ME, REMEMBER_ME: STORAGE_KEYS.REMEMBER_ME
} as const; } as const
export const SYSTEM_KEYS = { export const SYSTEM_KEYS = {
DICT_CACHE: STORAGE_KEYS.DICT_CACHE, DICT_CACHE: STORAGE_KEYS.DICT_CACHE
} as const; } as const
export const SETTINGS_KEYS = { export const SETTINGS_KEYS = {
SHOW_TAGS_VIEW: STORAGE_KEYS.SHOW_TAGS_VIEW, SHOW_TAGS_VIEW: STORAGE_KEYS.SHOW_TAGS_VIEW,
@@ -53,22 +53,22 @@ export const SETTINGS_KEYS = {
SIDEBAR_COLOR_SCHEME: STORAGE_KEYS.SIDEBAR_COLOR_SCHEME, SIDEBAR_COLOR_SCHEME: STORAGE_KEYS.SIDEBAR_COLOR_SCHEME,
LAYOUT: STORAGE_KEYS.LAYOUT, LAYOUT: STORAGE_KEYS.LAYOUT,
THEME_COLOR: STORAGE_KEYS.THEME_COLOR, THEME_COLOR: STORAGE_KEYS.THEME_COLOR,
THEME: STORAGE_KEYS.THEME, THEME: STORAGE_KEYS.THEME
} as const; } as const
export const APP_KEYS = { export const APP_KEYS = {
DEVICE: STORAGE_KEYS.DEVICE, DEVICE: STORAGE_KEYS.DEVICE,
SIZE: STORAGE_KEYS.SIZE, SIZE: STORAGE_KEYS.SIZE,
LANGUAGE: STORAGE_KEYS.LANGUAGE, LANGUAGE: STORAGE_KEYS.LANGUAGE,
SIDEBAR_STATUS: STORAGE_KEYS.SIDEBAR_STATUS, SIDEBAR_STATUS: STORAGE_KEYS.SIDEBAR_STATUS,
ACTIVE_TOP_MENU_PATH: STORAGE_KEYS.ACTIVE_TOP_MENU_PATH, ACTIVE_TOP_MENU_PATH: STORAGE_KEYS.ACTIVE_TOP_MENU_PATH
} as const; } as const
export const ALL_STORAGE_KEYS = { export const ALL_STORAGE_KEYS = {
...AUTH_KEYS, ...AUTH_KEYS,
...SYSTEM_KEYS, ...SYSTEM_KEYS,
...SETTINGS_KEYS, ...SETTINGS_KEYS,
...APP_KEYS, ...APP_KEYS
} as const; } as const
export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS]; export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS]

View File

@@ -1,9 +1,9 @@
import type { App } from "vue"; import type { App } from 'vue'
import { hasPerm, hasRole } from "./permission"; import { hasPerm, hasRole } from './permission'
// 注册指令 // 注册指令
export function setupDirective(app: App) { export function setupDirective(app: App) {
// 权限指令 // 权限指令
app.directive("hasPerm", hasPerm); app.directive('hasPerm', hasPerm)
app.directive("hasRole", hasRole); app.directive('hasRole', hasRole)
} }

View File

@@ -1,6 +1,6 @@
import type { Directive, DirectiveBinding } from "vue"; import type { Directive, DirectiveBinding } from 'vue'
import { useUserStoreHook } from "@/store/modules/user-store"; import { useUserStoreHook } from '@/store/modules/user-store'
import { hasPerm as checkPermission } from "@/utils/auth"; import { hasPerm as checkPermission } from '@/utils/auth'
/** /**
* 按钮权限指令 * 按钮权限指令
@@ -12,15 +12,15 @@ import { hasPerm as checkPermission } from "@/utils/auth";
export const hasPerm: Directive = { export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) { mounted(el: HTMLElement, binding: DirectiveBinding) {
// 获取权限值 // 获取权限值
const { value: requiredPerm } = binding; const { value: requiredPerm } = binding
// 验证权限 // 验证权限
if (!checkPermission(requiredPerm, "button")) { if (!checkPermission(requiredPerm, 'button')) {
// 移除元素 // 移除元素
el.parentNode?.removeChild(el); el.parentNode?.removeChild(el)
} }
}, }
}; }
/** /**
* 角色权限指令 * 角色权限指令
@@ -32,14 +32,14 @@ export const hasPerm: Directive = {
export const hasRole: Directive = { export const hasRole: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) { mounted(el: HTMLElement, binding: DirectiveBinding) {
// 获取角色值 // 获取角色值
const { value: requiredRole } = binding; const { value: requiredRole } = binding
// 验证角色 // 验证角色
if (!checkPermission(requiredRole, "role")) { if (!checkPermission(requiredRole, 'role')) {
// 移除元素 // 移除元素
el.parentNode?.removeChild(el); el.parentNode?.removeChild(el)
} }
}, }
}; }
export default hasPerm; export default hasPerm

View File

@@ -5,19 +5,19 @@ export const enum ApiCodeEnum {
/** /**
* 成功 * 成功
*/ */
SUCCESS = "00000", SUCCESS = '00000',
/** /**
* 错误 * 错误
*/ */
ERROR = "B0001", ERROR = 'B0001',
/** /**
* 访问令牌无效或过期 * 访问令牌无效或过期
*/ */
ACCESS_TOKEN_INVALID = "A0230", ACCESS_TOKEN_INVALID = 'A0230',
/** /**
* 刷新令牌无效或过期 * 刷新令牌无效或过期
*/ */
REFRESH_TOKEN_INVALID = "A0231", REFRESH_TOKEN_INVALID = 'A0231'
} }

View File

@@ -2,14 +2,14 @@
* 表单类型枚举 * 表单类型枚举
*/ */
export const FormTypeEnum: Record<string, OptionType> = { export const FormTypeEnum: Record<string, OptionType> = {
INPUT: { value: 1, label: "输入框" }, INPUT: { value: 1, label: '输入框' },
SELECT: { value: 2, label: "下拉框" }, SELECT: { value: 2, label: '下拉框' },
RADIO: { value: 3, label: "单选框" }, RADIO: { value: 3, label: '单选框' },
CHECK_BOX: { value: 4, label: "复选框" }, CHECK_BOX: { value: 4, label: '复选框' },
INPUT_NUMBER: { value: 5, label: "数字输入框" }, INPUT_NUMBER: { value: 5, label: '数字输入框' },
SWITCH: { value: 6, label: "开关" }, SWITCH: { value: 6, label: '开关' },
TEXT_AREA: { value: 7, label: "文本域" }, TEXT_AREA: { value: 7, label: '文本域' },
DATE: { value: 8, label: "日期框" }, DATE: { value: 8, label: '日期框' },
DATE_TIME: { value: 9, label: "日期时间框" }, DATE_TIME: { value: 9, label: '日期时间框' },
HIDDEN: { value: 10, label: "隐藏域" }, HIDDEN: { value: 10, label: '隐藏域' }
}; }

View File

@@ -3,35 +3,35 @@
*/ */
export const QueryTypeEnum: Record<string, OptionType> = { export const QueryTypeEnum: Record<string, OptionType> = {
/** 等于 */ /** 等于 */
EQ: { value: 1, label: "=" }, EQ: { value: 1, label: '=' },
/** 模糊匹配 */ /** 模糊匹配 */
LIKE: { value: 2, label: "LIKE '%s%'" }, LIKE: { value: 2, label: "LIKE '%s%'" },
/** 包含 */ /** 包含 */
IN: { value: 3, label: "IN" }, IN: { value: 3, label: 'IN' },
/** 范围 */ /** 范围 */
BETWEEN: { value: 4, label: "BETWEEN" }, BETWEEN: { value: 4, label: 'BETWEEN' },
/** 大于 */ /** 大于 */
GT: { value: 5, label: ">" }, GT: { value: 5, label: '>' },
/** 大于等于 */ /** 大于等于 */
GE: { value: 6, label: ">=" }, GE: { value: 6, label: '>=' },
/** 小于 */ /** 小于 */
LT: { value: 7, label: "<" }, LT: { value: 7, label: '<' },
/** 小于等于 */ /** 小于等于 */
LE: { value: 8, label: "<=" }, LE: { value: 8, label: '<=' },
/** 不等于 */ /** 不等于 */
NE: { value: 9, label: "!=" }, NE: { value: 9, label: '!=' },
/** 左模糊匹配 */ /** 左模糊匹配 */
LIKE_LEFT: { value: 10, label: "LIKE '%s'" }, LIKE_LEFT: { value: 10, label: "LIKE '%s'" },
/** 右模糊匹配 */ /** 右模糊匹配 */
LIKE_RIGHT: { value: 11, label: "LIKE 's%'" }, LIKE_RIGHT: { value: 11, label: "LIKE 's%'" }
}; }

View File

@@ -1,11 +1,11 @@
export * from "./api/code-enum"; export * from './api/code-enum'
export * from "./codegen/form-enum"; export * from './codegen/form-enum'
export * from "./codegen/query-enum"; export * from './codegen/query-enum'
export * from "./settings/layout-enum"; export * from './settings/layout-enum'
export * from "./settings/theme-enum"; export * from './settings/theme-enum'
export * from "./settings/locale-enum"; export * from './settings/locale-enum'
export * from "./settings/device-enum"; export * from './settings/device-enum'
export * from "./system/menu-enum"; export * from './system/menu-enum'

View File

@@ -5,10 +5,10 @@ export const enum DeviceEnum {
/** /**
* 宽屏设备 * 宽屏设备
*/ */
DESKTOP = "desktop", DESKTOP = 'desktop',
/** /**
* 窄屏设备 * 窄屏设备
*/ */
MOBILE = "mobile", MOBILE = 'mobile'
} }

View File

@@ -5,16 +5,16 @@ export const enum LayoutMode {
/** /**
* 左侧菜单布局 * 左侧菜单布局
*/ */
LEFT = "left", LEFT = 'left',
/** /**
* 顶部菜单布局 * 顶部菜单布局
*/ */
TOP = "top", TOP = 'top',
/** /**
* 混合菜单布局 * 混合菜单布局
*/ */
MIX = "mix", MIX = 'mix'
} }
/** /**
@@ -24,12 +24,12 @@ export const enum SidebarStatus {
/** /**
* 展开 * 展开
*/ */
OPENED = "opened", OPENED = 'opened',
/** /**
* 关闭 * 关闭
*/ */
CLOSED = "closed", CLOSED = 'closed'
} }
/** /**
@@ -39,15 +39,15 @@ export const enum ComponentSize {
/** /**
* 默认 * 默认
*/ */
DEFAULT = "default", DEFAULT = 'default',
/** /**
* 大型 * 大型
*/ */
LARGE = "large", LARGE = 'large',
/** /**
* 小型 * 小型
*/ */
SMALL = "small", SMALL = 'small'
} }

View File

@@ -5,10 +5,10 @@ export const enum LanguageEnum {
/** /**
* 中文 * 中文
*/ */
ZH_CN = "zh-cn", ZH_CN = 'zh-cn',
/** /**
* 英文 * 英文
*/ */
EN = "en", EN = 'en'
} }

View File

@@ -5,16 +5,16 @@ export const enum ThemeMode {
/** /**
* 明亮主题 * 明亮主题
*/ */
LIGHT = "light", LIGHT = 'light',
/** /**
* 暗黑主题 * 暗黑主题
*/ */
DARK = "dark", DARK = 'dark',
/** /**
* 系统自动 * 系统自动
*/ */
AUTO = "auto", AUTO = 'auto'
} }
/** /**
@@ -24,9 +24,9 @@ export const enum SidebarColor {
/** /**
* 经典蓝 * 经典蓝
*/ */
CLASSIC_BLUE = "classic-blue", CLASSIC_BLUE = 'classic-blue',
/** /**
* 极简白 * 极简白
*/ */
MINIMAL_WHITE = "minimal-white", MINIMAL_WHITE = 'minimal-white'
} }

View File

@@ -3,5 +3,5 @@ export enum MenuTypeEnum {
CATALOG = 2, // 目录 CATALOG = 2, // 目录
MENU = 1, // 菜单 MENU = 1, // 菜单
BUTTON = 4, // 按钮 BUTTON = 4, // 按钮
EXTLINK = 3, // 外链 EXTLINK = 3 // 外链
} }

2
src/env.d.ts vendored
View File

@@ -2,7 +2,7 @@
declare module '*.vue' { declare module '*.vue' {
import { DefineComponent } from 'vue' import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any> const component: DefineComponent<{}, {}, any>
export default component export default component
} }

View File

@@ -1,27 +1,27 @@
import type { App } from "vue"; import type { App } from 'vue'
import { createI18n } from "vue-i18n"; import { createI18n } from 'vue-i18n'
import { useAppStoreHook } from "@/store/modules/app-store"; import { useAppStoreHook } from '@/store/modules/app-store'
// 本地语言包 // 本地语言包
import enLocale from "./package/en.json"; import enLocale from './package/en.json'
import zhCnLocale from "./package/zh-cn.json"; import zhCnLocale from './package/zh-cn.json'
const appStore = useAppStoreHook(); const appStore = useAppStoreHook()
const messages = { const messages = {
"zh-cn": zhCnLocale, 'zh-cn': zhCnLocale,
en: enLocale, en: enLocale
}; }
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
locale: appStore.language, locale: appStore.language,
messages, messages,
globalInjection: true, globalInjection: true
}); })
// 全局注册 i18n // 全局注册 i18n
export function setupI18n(app: App<Element>) { export function setupI18n(app: App<Element>) {
app.use(i18n); app.use(i18n)
} }
export default i18n; export default i18n

View File

@@ -9,15 +9,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import logo from "@/assets/logo.png"; import logo from '@/assets/logo.png'
import { defineProps } from "vue"; import { defineProps } from 'vue'
defineProps({ defineProps({
collapse: { collapse: {
type: Boolean, type: Boolean,
required: true, required: true
}, }
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -18,54 +18,54 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { type RouteLocationNormalized } from "vue-router"; import { type RouteLocationNormalized } from 'vue-router'
import { useSettingsStore, useTagsViewStore } from "@/store"; import { useSettingsStore, useTagsViewStore } from '@/store'
import variables from "@/styles/variables.module.scss"; import variables from '@/styles/variables.module.scss'
import Error404 from "@/views/error/404.vue"; import Error404 from '@/views/error/404.vue'
const { cachedViews } = toRefs(useTagsViewStore()); const { cachedViews } = toRefs(useTagsViewStore())
// 当前组件 // 当前组件
const wrapperMap = new Map<string, Component>(); const wrapperMap = new Map<string, Component>()
const currentComponent = (component: Component, route: RouteLocationNormalized) => { const currentComponent = (component: Component, route: RouteLocationNormalized) => {
if (!component) return; if (!component) return
const { fullPath: componentName } = route; // 使用路由路径作为组件名称 const { fullPath: componentName } = route // 使用路由路径作为组件名称
let wrapper = wrapperMap.get(componentName); let wrapper = wrapperMap.get(componentName)
if (!wrapper) { if (!wrapper) {
wrapper = { wrapper = {
name: componentName, name: componentName,
render: () => { render: () => {
try { try {
return h(component); return h(component)
} catch (error) { } catch (error) {
console.error(`Error rendering component for route: ${componentName}`, error); console.error(`Error rendering component for route: ${componentName}`, error)
return h(Error404); return h(Error404)
} }
}, }
}; }
wrapperMap.set(componentName, wrapper); wrapperMap.set(componentName, wrapper)
} }
// 添加组件数量限制 // 添加组件数量限制
if (wrapperMap.size > 100) { if (wrapperMap.size > 100) {
const firstKey = wrapperMap.keys().next().value; const firstKey = wrapperMap.keys().next().value
if (firstKey) { if (firstKey) {
wrapperMap.delete(firstKey); wrapperMap.delete(firstKey)
} }
} }
return h(wrapper); return h(wrapper)
}; }
const appMainHeight = computed(() => { const appMainHeight = computed(() => {
if (useSettingsStore().showTagsView) { if (useSettingsStore().showTagsView) {
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`; return `calc(100vh - ${variables['navbar-height']} - ${variables['tags-view-height']})`
} else { } else {
return `calc(100vh - ${variables["navbar-height"]})`; return `calc(100vh - ${variables['navbar-height']})`
} }
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -25,71 +25,71 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useRoute } from "vue-router"; import { useRoute } from 'vue-router'
import path from "path-browserify"; import path from 'path-browserify'
import type { MenuInstance } from "element-plus"; import type { MenuInstance } from 'element-plus'
import type { RouteRecordRaw } from "vue-router"; import type { RouteRecordRaw } from 'vue-router'
import { SidebarColor } from "@/enums/settings/theme-enum"; import { SidebarColor } from '@/enums/settings/theme-enum'
import { useSettingsStore, useAppStore } from "@/store"; import { useSettingsStore, useAppStore } from '@/store'
import { isExternal } from "@/utils/index"; import { isExternal } from '@/utils/index'
import MenuItem from "./components/MenuItem.vue"; import MenuItem from './components/MenuItem.vue'
import variables from "@/styles/variables.module.scss"; import variables from '@/styles/variables.module.scss'
const props = defineProps({ const props = defineProps({
data: { data: {
type: Array as PropType<RouteRecordRaw[]>, type: Array as PropType<RouteRecordRaw[]>,
default: () => [], default: () => []
}, },
basePath: { basePath: {
type: String, type: String,
required: true, required: true,
example: "/system", example: '/system'
}, },
menuMode: { menuMode: {
type: String as PropType<"vertical" | "horizontal">, type: String as PropType<'vertical' | 'horizontal'>,
default: "vertical", default: 'vertical',
validator: (value: string) => ["vertical", "horizontal"].includes(value), validator: (value: string) => ['vertical', 'horizontal'].includes(value)
}, }
}); })
const menuRef = ref<MenuInstance>(); const menuRef = ref<MenuInstance>()
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
const appStore = useAppStore(); const appStore = useAppStore()
const currentRoute = useRoute(); const currentRoute = useRoute()
// 存储已展开的菜单项索引 // 存储已展开的菜单项索引
const expandedMenuIndexes = ref<string[]>([]); const expandedMenuIndexes = ref<string[]>([])
// 获取主题 // 获取主题
const theme = computed(() => settingsStore.theme); const theme = computed(() => settingsStore.theme)
// 获取浅色主题下的侧边栏配色方案 // 获取浅色主题下的侧边栏配色方案
const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme); const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme)
// 菜单主题属性 // 菜单主题属性
const menuThemeProps = computed(() => { const menuThemeProps = computed(() => {
const isDarkOrClassicBlue = const isDarkOrClassicBlue =
theme.value === "dark" || sidebarColorScheme.value === SidebarColor.CLASSIC_BLUE; theme.value === 'dark' || sidebarColorScheme.value === SidebarColor.CLASSIC_BLUE
return { return {
backgroundColor: isDarkOrClassicBlue ? variables["menu-background"] : undefined, backgroundColor: isDarkOrClassicBlue ? variables['menu-background'] : undefined,
textColor: isDarkOrClassicBlue ? variables["menu-text"] : undefined, textColor: isDarkOrClassicBlue ? variables['menu-text'] : undefined,
activeTextColor: isDarkOrClassicBlue ? variables["menu-active-text"] : undefined, activeTextColor: isDarkOrClassicBlue ? variables['menu-active-text'] : undefined
}; }
}); })
// 计算当前激活的菜单项 // 计算当前激活的菜单项
const activeMenuPath = computed((): string => { const activeMenuPath = computed((): string => {
const { meta, path } = currentRoute; const { meta, path } = currentRoute
// 如果路由meta中设置了activeMenu则使用它用于处理一些特殊情况如详情页 // 如果路由meta中设置了activeMenu则使用它用于处理一些特殊情况如详情页
if (meta?.activeMenu && typeof meta.activeMenu === "string") { if (meta?.activeMenu && typeof meta.activeMenu === 'string') {
return meta.activeMenu; return meta.activeMenu
} }
// 否则使用当前路由路径 // 否则使用当前路由路径
return path; return path
}); })
/** /**
* 获取完整路径 * 获取完整路径
@@ -99,19 +99,19 @@ const activeMenuPath = computed((): string => {
*/ */
function resolveFullPath(routePath: string) { function resolveFullPath(routePath: string) {
if (isExternal(routePath)) { if (isExternal(routePath)) {
return routePath; return routePath
} }
if (isExternal(props.basePath)) { if (isExternal(props.basePath)) {
return props.basePath; return props.basePath
} }
// 如果 basePath 为空(顶部布局),直接返回 routePath // 如果 basePath 为空(顶部布局),直接返回 routePath
if (!props.basePath || props.basePath === "") { if (!props.basePath || props.basePath === '') {
return routePath; return routePath
} }
// 解析路径,生成完整的绝对路径 // 解析路径,生成完整的绝对路径
return path.resolve(props.basePath, routePath); return path.resolve(props.basePath, routePath)
} }
/** /**
@@ -120,8 +120,8 @@ function resolveFullPath(routePath: string) {
* @param index 当前展开的菜单项索引 * @param index 当前展开的菜单项索引
*/ */
const onMenuOpen = (index: string) => { const onMenuOpen = (index: string) => {
expandedMenuIndexes.value.push(index); expandedMenuIndexes.value.push(index)
}; }
/** /**
* 关闭菜单 * 关闭菜单
@@ -129,8 +129,8 @@ const onMenuOpen = (index: string) => {
* @param index 当前收起的菜单项索引 * @param index 当前收起的菜单项索引
*/ */
const onMenuClose = (index: string) => { const onMenuClose = (index: string) => {
expandedMenuIndexes.value = expandedMenuIndexes.value.filter((item) => item !== index); expandedMenuIndexes.value = expandedMenuIndexes.value.filter((item) => item !== index)
}; }
/** /**
* 监听展开的菜单项变化,更新父菜单样式 * 监听展开的菜单项变化,更新父菜单样式
@@ -138,9 +138,9 @@ const onMenuClose = (index: string) => {
watch( watch(
() => expandedMenuIndexes.value, () => expandedMenuIndexes.value,
() => { () => {
updateParentMenuStyles(); updateParentMenuStyles()
} }
); )
/** /**
* 监听菜单模式变化:当菜单模式切换为水平模式时,关闭所有展开的菜单项, * 监听菜单模式变化:当菜单模式切换为水平模式时,关闭所有展开的菜单项,
@@ -149,11 +149,11 @@ watch(
watch( watch(
() => props.menuMode, () => props.menuMode,
(newMode) => { (newMode) => {
if (newMode === "horizontal" && menuRef.value) { if (newMode === 'horizontal' && menuRef.value) {
expandedMenuIndexes.value.forEach((item) => menuRef.value!.close(item)); expandedMenuIndexes.value.forEach((item) => menuRef.value!.close(item))
} }
} }
); )
/** /**
* 监听激活菜单变化,为包含激活子菜单的父菜单添加样式类 * 监听激活菜单变化,为包含激活子菜单的父菜单添加样式类
@@ -162,11 +162,11 @@ watch(
() => activeMenuPath.value, () => activeMenuPath.value,
() => { () => {
nextTick(() => { nextTick(() => {
updateParentMenuStyles(); updateParentMenuStyles()
}); })
}, },
{ immediate: true } { immediate: true }
); )
/** /**
* 监听路由变化确保菜单能随TagsView切换而正确激活 * 监听路由变化确保菜单能随TagsView切换而正确激活
@@ -175,64 +175,64 @@ watch(
() => currentRoute.path, () => currentRoute.path,
() => { () => {
nextTick(() => { nextTick(() => {
updateParentMenuStyles(); updateParentMenuStyles()
}); })
} }
); )
/** /**
* 更新父菜单样式 - 为包含激活子菜单的父菜单添加 has-active-child 类 * 更新父菜单样式 - 为包含激活子菜单的父菜单添加 has-active-child 类
*/ */
function updateParentMenuStyles() { function updateParentMenuStyles() {
if (!menuRef.value?.$el) return; if (!menuRef.value?.$el) return
nextTick(() => { nextTick(() => {
try { try {
const menuEl = menuRef.value?.$el as HTMLElement; const menuEl = menuRef.value?.$el as HTMLElement
if (!menuEl) return; if (!menuEl) return
// 移除所有现有的 has-active-child 类 // 移除所有现有的 has-active-child 类
const allSubMenus = menuEl.querySelectorAll(".el-sub-menu"); const allSubMenus = menuEl.querySelectorAll('.el-sub-menu')
allSubMenus.forEach((subMenu) => { allSubMenus.forEach((subMenu) => {
subMenu.classList.remove("has-active-child"); subMenu.classList.remove('has-active-child')
}); })
// 查找当前激活的菜单项 // 查找当前激活的菜单项
const activeMenuItem = menuEl.querySelector(".el-menu-item.is-active"); const activeMenuItem = menuEl.querySelector('.el-menu-item.is-active')
if (activeMenuItem) { if (activeMenuItem) {
// 向上查找父级 el-sub-menu 元素 // 向上查找父级 el-sub-menu 元素
let parent = activeMenuItem.parentElement; let parent = activeMenuItem.parentElement
while (parent && parent !== menuEl) { while (parent && parent !== menuEl) {
if (parent.classList.contains("el-sub-menu")) { if (parent.classList.contains('el-sub-menu')) {
parent.classList.add("has-active-child"); parent.classList.add('has-active-child')
} }
parent = parent.parentElement; parent = parent.parentElement
} }
} else { } else {
// 水平模式下可能需要特殊处理 // 水平模式下可能需要特殊处理
if (props.menuMode === "horizontal") { if (props.menuMode === 'horizontal') {
// 对于水平菜单,使用路径匹配来找到父菜单 // 对于水平菜单,使用路径匹配来找到父菜单
const currentPath = activeMenuPath.value; const currentPath = activeMenuPath.value
// 查找所有父菜单项,检查哪个包含当前路径 // 查找所有父菜单项,检查哪个包含当前路径
allSubMenus.forEach((subMenu) => { allSubMenus.forEach((subMenu) => {
const subMenuEl = subMenu as HTMLElement; const subMenuEl = subMenu as HTMLElement
const subMenuPath = const subMenuPath =
subMenuEl.getAttribute("data-path") || subMenuEl.getAttribute('data-path') ||
subMenuEl.querySelector(".el-sub-menu__title")?.getAttribute("data-path"); subMenuEl.querySelector('.el-sub-menu__title')?.getAttribute('data-path')
// 如果找到包含当前路径的父菜单,则添加激活类 // 如果找到包含当前路径的父菜单,则添加激活类
if (subMenuPath && currentPath.startsWith(subMenuPath)) { if (subMenuPath && currentPath.startsWith(subMenuPath)) {
subMenuEl.classList.add("has-active-child"); subMenuEl.classList.add('has-active-child')
} }
}); })
} }
} }
} catch (error) { } catch (error) {
console.error("Error updating parent menu styles:", error); console.error('Error updating parent menu styles:', error)
} }
}); })
} }
/** /**
@@ -240,6 +240,6 @@ function updateParentMenuStyles() {
*/ */
onMounted(() => { onMounted(() => {
// 确保在组件挂载后更新样式,不依赖于异步操作 // 确保在组件挂载后更新样式,不依赖于异步操作
updateParentMenuStyles(); updateParentMenuStyles()
}); })
</script> </script>

View File

@@ -31,67 +31,67 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MenuItemContent from "./components/MenuItemContent.vue"; import MenuItemContent from './components/MenuItemContent.vue'
defineOptions({ defineOptions({
name: "MixTopMenu", name: 'MixTopMenu'
}); })
import { LocationQueryRaw, RouteRecordRaw } from "vue-router"; import { LocationQueryRaw, RouteRecordRaw } from 'vue-router'
import { usePermissionStore, useAppStore, useSettingsStore } from "@/store"; import { usePermissionStore, useAppStore, useSettingsStore } from '@/store'
import variables from "@/styles/variables.module.scss"; import variables from '@/styles/variables.module.scss'
import { SidebarColor } from "@/enums/settings/theme-enum"; import { SidebarColor } from '@/enums/settings/theme-enum'
const router = useRouter(); const router = useRouter()
const appStore = useAppStore(); const appStore = useAppStore()
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore()
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
// 获取主题 // 获取主题
const theme = computed(() => settingsStore.theme); const theme = computed(() => settingsStore.theme)
// 获取浅色主题下的侧边栏配色方案 // 获取浅色主题下的侧边栏配色方案
const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme); const sidebarColorScheme = computed(() => settingsStore.sidebarColorScheme)
// 顶部菜单列表 // 顶部菜单列表
const topMenus = ref<RouteRecordRaw[]>([]); const topMenus = ref<RouteRecordRaw[]>([])
// 处理后的顶部菜单列表 - 智能显示唯一子菜单的标题 // 处理后的顶部菜单列表 - 智能显示唯一子菜单的标题
const processedTopMenus = computed(() => { const processedTopMenus = computed(() => {
return topMenus.value.map((route) => { return topMenus.value.map((route) => {
// 如果路由设置了 alwaysShow=true或者没有子菜单直接返回原路由 // 如果路由设置了 alwaysShow=true或者没有子菜单直接返回原路由
if (route.meta?.alwaysShow || !route.children || route.children.length === 0) { if (route.meta?.alwaysShow || !route.children || route.children.length === 0) {
return route; return route
} }
// 过滤出非隐藏的子菜单 // 过滤出非隐藏的子菜单
const visibleChildren = route.children.filter((child) => !child.meta?.hidden); const visibleChildren = route.children.filter((child) => !child.meta?.hidden)
// 如果只有一个非隐藏的子菜单,显示子菜单的信息 // 如果只有一个非隐藏的子菜单,显示子菜单的信息
if (visibleChildren.length === 1) { if (visibleChildren.length === 1) {
const onlyChild = visibleChildren[0]; const onlyChild = visibleChildren[0]
return { return {
...route, ...route,
meta: { meta: {
...route.meta, ...route.meta,
title: onlyChild.meta?.title || route.meta?.title, title: onlyChild.meta?.title || route.meta?.title,
icon: onlyChild.meta?.icon || route.meta?.icon, icon: onlyChild.meta?.icon || route.meta?.icon
}, }
}; }
} }
// 其他情况返回原路由 // 其他情况返回原路由
return route; return route
}); })
}); })
/** /**
* 处理菜单点击事件,切换顶部菜单并加载对应的左侧菜单 * 处理菜单点击事件,切换顶部菜单并加载对应的左侧菜单
* @param routePath 点击的菜单路径 * @param routePath 点击的菜单路径
*/ */
const handleMenuSelect = (routePath: string) => { const handleMenuSelect = (routePath: string) => {
updateMenuState(routePath); updateMenuState(routePath)
}; }
/** /**
* 更新菜单状态 - 同时处理点击和路由变化情况 * 更新菜单状态 - 同时处理点击和路由变化情况
@@ -101,52 +101,52 @@ const handleMenuSelect = (routePath: string) => {
const updateMenuState = (topMenuPath: string, skipNavigation = false) => { const updateMenuState = (topMenuPath: string, skipNavigation = false) => {
// 不相同才更新,避免重复操作 // 不相同才更新,避免重复操作
if (topMenuPath !== appStore.activeTopMenuPath) { if (topMenuPath !== appStore.activeTopMenuPath) {
appStore.activeTopMenu(topMenuPath); // 设置激活的顶部菜单 appStore.activeTopMenu(topMenuPath) // 设置激活的顶部菜单
permissionStore.setMixLayoutSideMenus(topMenuPath); // 设置混合布局左侧菜单 permissionStore.setMixLayoutSideMenus(topMenuPath) // 设置混合布局左侧菜单
} }
// 如果是点击菜单且状态已变更,才进行导航 // 如果是点击菜单且状态已变更,才进行导航
if (!skipNavigation) { if (!skipNavigation) {
navigateToFirstLeftMenu(permissionStore.mixLayoutSideMenus); // 跳转到左侧第一个菜单 navigateToFirstLeftMenu(permissionStore.mixLayoutSideMenus) // 跳转到左侧第一个菜单
} }
}; }
/** /**
* 跳转到左侧第一个可访问的菜单 * 跳转到左侧第一个可访问的菜单
* @param menus 左侧菜单列表 * @param menus 左侧菜单列表
*/ */
const navigateToFirstLeftMenu = (menus: RouteRecordRaw[]) => { const navigateToFirstLeftMenu = (menus: RouteRecordRaw[]) => {
if (menus.length === 0) return; if (menus.length === 0) return
const [firstMenu] = menus; const [firstMenu] = menus
// 如果第一个菜单有子菜单,递归跳转到第一个子菜单 // 如果第一个菜单有子菜单,递归跳转到第一个子菜单
if (firstMenu.children && firstMenu.children.length > 0) { if (firstMenu.children && firstMenu.children.length > 0) {
navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[]); navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[])
} else if (firstMenu.name) { } else if (firstMenu.name) {
router.push({ router.push({
name: firstMenu.name, name: firstMenu.name,
query: query:
typeof firstMenu.meta?.params === "object" typeof firstMenu.meta?.params === 'object'
? (firstMenu.meta.params as LocationQueryRaw) ? (firstMenu.meta.params as LocationQueryRaw)
: undefined, : undefined
}); })
} }
}; }
// 获取当前路由路径的顶部菜单路径 // 获取当前路由路径的顶部菜单路径
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); const activeTopMenuPath = computed(() => appStore.activeTopMenuPath)
onMounted(() => { onMounted(() => {
topMenus.value = permissionStore.routes.filter((item) => !item.meta || !item.meta.hidden); topMenus.value = permissionStore.routes.filter((item) => !item.meta || !item.meta.hidden)
// 初始化顶部菜单 // 初始化顶部菜单
const currentTopMenuPath = const currentTopMenuPath =
useRoute().path.split("/").filter(Boolean).length > 1 useRoute().path.split('/').filter(Boolean).length > 1
? useRoute().path.match(/^\/[^/]+/)?.[0] || "/" ? useRoute().path.match(/^\/[^/]+/)?.[0] || '/'
: "/"; : '/'
appStore.activeTopMenu(currentTopMenuPath); // 设置激活的顶部菜单 appStore.activeTopMenu(currentTopMenuPath) // 设置激活的顶部菜单
permissionStore.setMixLayoutSideMenus(currentTopMenuPath); // 设置混合布局左侧菜单 permissionStore.setMixLayoutSideMenus(currentTopMenuPath) // 设置混合布局左侧菜单
}); })
// 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态 // 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态
watch( watch(
@@ -155,13 +155,13 @@ watch(
if (newPath) { if (newPath) {
// 提取顶级路径 // 提取顶级路径
const topMenuPath = const topMenuPath =
newPath.split("/").filter(Boolean).length > 1 ? newPath.match(/^\/[^/]+/)?.[0] || "/" : "/"; newPath.split('/').filter(Boolean).length > 1 ? newPath.match(/^\/[^/]+/)?.[0] || '/' : '/'
// 使用公共方法更新菜单状态,但跳过导航(因为路由已经变化) // 使用公共方法更新菜单状态,但跳过导航(因为路由已经变化)
updateMenuState(topMenuPath, true); updateMenuState(topMenuPath, true)
} }
} }
); )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -15,7 +15,7 @@
v-if="onlyOneChild.meta" v-if="onlyOneChild.meta"
:to="{ :to="{
path: resolvePath(onlyOneChild.path), path: resolvePath(onlyOneChild.path),
query: onlyOneChild.meta.params, query: onlyOneChild.meta.params
}" }"
> >
<el-menu-item <el-menu-item
@@ -49,17 +49,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import MenuItemContent from "./MenuItemContent.vue"; import MenuItemContent from './MenuItemContent.vue'
defineOptions({ defineOptions({
name: "MenuItem", name: 'MenuItem',
inheritAttrs: false, inheritAttrs: false
}); })
import path from "path-browserify"; import path from 'path-browserify'
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from 'vue-router'
import { isExternal } from "@/utils"; import { isExternal } from '@/utils'
const props = defineProps({ const props = defineProps({
/** /**
@@ -67,7 +67,7 @@ const props = defineProps({
*/ */
item: { item: {
type: Object as PropType<RouteRecordRaw>, type: Object as PropType<RouteRecordRaw>,
required: true, required: true
}, },
/** /**
@@ -75,7 +75,7 @@ const props = defineProps({
*/ */
basePath: { basePath: {
type: String, type: String,
required: true, required: true
}, },
/** /**
@@ -83,12 +83,12 @@ const props = defineProps({
*/ */
isNest: { isNest: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}); })
// 可见的唯一子节点 // 可见的唯一子节点
const onlyOneChild = ref(); const onlyOneChild = ref()
/** /**
* 检查是否仅有一个可见子节点 * 检查是否仅有一个可见子节点
@@ -101,24 +101,24 @@ function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecord
// 过滤出可见子节点 // 过滤出可见子节点
const showingChildren = children.filter((route: RouteRecordRaw) => { const showingChildren = children.filter((route: RouteRecordRaw) => {
if (!route.meta?.hidden) { if (!route.meta?.hidden) {
onlyOneChild.value = route; onlyOneChild.value = route
return true; return true
} }
return false; return false
}); })
// 仅有一个节点 // 仅有一个节点
if (showingChildren.length === 1) { if (showingChildren.length === 1) {
return true; return true
} }
// 无子节点时 // 无子节点时
if (showingChildren.length === 0) { if (showingChildren.length === 0) {
// 父节点设置为唯一显示节点,并标记为无子节点 // 父节点设置为唯一显示节点,并标记为无子节点
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true }; onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true; return true
} }
return false; return false
} }
/** /**
@@ -128,10 +128,10 @@ function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecord
* @returns 绝对路径 * @returns 绝对路径
*/ */
function resolvePath(routePath: string) { function resolvePath(routePath: string) {
if (isExternal(routePath)) return routePath; if (isExternal(routePath)) return routePath
if (isExternal(props.basePath)) return props.basePath; if (isExternal(props.basePath)) return props.basePath
// 拼接父路径和当前路径 // 拼接父路径和当前路径
return path.resolve(props.basePath, routePath); return path.resolve(props.basePath, routePath)
} }
</script> </script>

View File

@@ -4,15 +4,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { translateRouteTitle } from "@/utils/i18n"; import { translateRouteTitle } from '@/utils/i18n'
const props = defineProps<{ const props = defineProps<{
icon?: string; icon?: string
title?: string; title?: string
}>(); }>()
const isElIcon = computed(() => props.icon?.startsWith("el-icon")); const isElIcon = computed(() => props.icon?.startsWith('el-icon'))
const iconComponent = computed(() => props.icon?.replace("el-icon-", "")); const iconComponent = computed(() => props.icon?.replace('el-icon-', ''))
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -28,10 +28,10 @@
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="handleProfileClick"> <el-dropdown-item @click="handleProfileClick">
{{ t("navbar.profile") }} {{ t('navbar.profile') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item divided @click="logout"> <el-dropdown-item divided @click="logout">
{{ t("navbar.logout") }} {{ t('navbar.logout') }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -50,46 +50,46 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "vue-i18n"; import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from 'vue-router'
import { defaultSettings } from "@/settings"; import { defaultSettings } from '@/settings'
import { DeviceEnum } from "@/enums/settings/device-enum"; import { DeviceEnum } from '@/enums/settings/device-enum'
import { useAppStore, useSettingsStore, useUserStore } from "@/store"; import { useAppStore, useSettingsStore, useUserStore } from '@/store'
import { SidebarColor, ThemeMode } from "@/enums/settings/theme-enum"; import { SidebarColor, ThemeMode } from '@/enums/settings/theme-enum'
import { LayoutMode } from "@/enums"; import { LayoutMode } from '@/enums'
// 导入子组件 // 导入子组件
import MenuSearch from "@/components/MenuSearch/index.vue"; import MenuSearch from '@/components/MenuSearch/index.vue'
import Fullscreen from "@/components/Fullscreen/index.vue"; import Fullscreen from '@/components/Fullscreen/index.vue'
import SizeSelect from "@/components/SizeSelect/index.vue"; import SizeSelect from '@/components/SizeSelect/index.vue'
import LangSelect from "@/components/LangSelect/index.vue"; import LangSelect from '@/components/LangSelect/index.vue'
import Notification from "@/components/Notification/index.vue"; import Notification from '@/components/Notification/index.vue'
const { t } = useI18n(); const { t } = useI18n()
const appStore = useAppStore(); const appStore = useAppStore()
const settingStore = useSettingsStore(); const settingStore = useSettingsStore()
const userStore = useUserStore(); const userStore = useUserStore()
const route = useRoute(); const route = useRoute()
const router = useRouter(); const router = useRouter()
// 是否为桌面设备 // 是否为桌面设备
const isDesktop = computed(() => appStore.device === DeviceEnum.DESKTOP); const isDesktop = computed(() => appStore.device === DeviceEnum.DESKTOP)
/** /**
* 打开个人中心页面 * 打开个人中心页面
*/ */
function handleProfileClick() { function handleProfileClick() {
router.push({ name: "Profile" }); router.push({ name: 'Profile' })
} }
// 根据主题和侧边栏配色方案选择样式类 // 根据主题和侧边栏配色方案选择样式类
const navbarActionsClass = computed(() => { const navbarActionsClass = computed(() => {
const { theme, sidebarColorScheme, layout } = settingStore; const { theme, sidebarColorScheme, layout } = settingStore
// 暗黑主题下,所有布局都使用白色文字 // 暗黑主题下,所有布局都使用白色文字
if (theme === ThemeMode.DARK) { if (theme === ThemeMode.DARK) {
return "navbar-actions--white-text"; return 'navbar-actions--white-text'
} }
// 明亮主题下 // 明亮主题下
@@ -99,37 +99,37 @@ const navbarActionsClass = computed(() => {
// - 如果侧边栏是极简白色,使用深色文字 // - 如果侧边栏是极简白色,使用深色文字
if (layout === LayoutMode.TOP || layout === LayoutMode.MIX) { if (layout === LayoutMode.TOP || layout === LayoutMode.MIX) {
if (sidebarColorScheme === SidebarColor.CLASSIC_BLUE) { if (sidebarColorScheme === SidebarColor.CLASSIC_BLUE) {
return "navbar-actions--white-text"; return 'navbar-actions--white-text'
} else { } else {
return "navbar-actions--dark-text"; return 'navbar-actions--dark-text'
} }
} }
} }
return "navbar-actions--dark-text"; return 'navbar-actions--dark-text'
}); })
/** /**
* 退出登录 * 退出登录
*/ */
function logout() { function logout() {
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", { ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: "确定", confirmButtonText: '确定',
cancelButtonText: "取消", cancelButtonText: '取消',
type: "warning", type: 'warning',
lockScroll: false, lockScroll: false
}).then(() => { }).then(() => {
userStore.logout().then(() => { userStore.logout().then(() => {
router.push(`/login?redirect=${route.fullPath}`); router.push(`/login?redirect=${route.fullPath}`)
}); })
}); })
} }
/** /**
* 打开系统设置页面 * 打开系统设置页面
*/ */
function handleSettingsClick() { function handleSettingsClick() {
settingStore.settingsVisible = true; settingStore.settingsVisible = true
} }
</script> </script>
@@ -170,7 +170,7 @@ function handleSettingsClick() {
} }
// 图标样式 // 图标样式
:deep([class^="i-svg:"]) { :deep([class^='i-svg:']) {
font-size: 18px; font-size: 18px;
line-height: 1; line-height: 1;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
@@ -180,7 +180,7 @@ function handleSettingsClick() {
&:hover { &:hover {
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);
:deep([class^="i-svg:"]) { :deep([class^='i-svg:']) {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
} }
@@ -212,14 +212,14 @@ function handleSettingsClick() {
// 白色文字样式(用于深色背景:暗黑主题、顶部布局、混合布局) // 白色文字样式(用于深色背景:暗黑主题、顶部布局、混合布局)
.navbar-actions--white-text { .navbar-actions--white-text {
.navbar-actions__item { .navbar-actions__item {
:deep([class^="i-svg:"]) { :deep([class^='i-svg:']) {
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.85);
} }
&:hover { &:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
:deep([class^="i-svg:"]) { :deep([class^='i-svg:']) {
color: #fff; color: #fff;
} }
} }
@@ -233,14 +233,14 @@ function handleSettingsClick() {
// 深色文字样式(用于浅色背景:明亮主题下的左侧布局) // 深色文字样式(用于浅色背景:明亮主题下的左侧布局)
.navbar-actions--dark-text { .navbar-actions--dark-text {
.navbar-actions__item { .navbar-actions__item {
:deep([class^="i-svg:"]) { :deep([class^='i-svg:']) {
color: var(--el-text-color-regular) !important; color: var(--el-text-color-regular) !important;
} }
&:hover { &:hover {
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);
:deep([class^="i-svg:"]) { :deep([class^='i-svg:']) {
color: var(--el-color-primary) !important; color: var(--el-color-primary) !important;
} }
} }
@@ -253,7 +253,7 @@ function handleSettingsClick() {
// 确保下拉菜单中的图标不受影响 // 确保下拉菜单中的图标不受影响
:deep(.el-dropdown-menu) { :deep(.el-dropdown-menu) {
[class^="i-svg:"] { [class^='i-svg:'] {
color: var(--el-text-color-regular) !important; color: var(--el-text-color-regular) !important;
&:hover { &:hover {

View File

@@ -14,22 +14,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useAppStore } from "@/store"; import { useAppStore } from '@/store'
import Hamburger from "@/components/Hamburger/index.vue"; import Hamburger from '@/components/Hamburger/index.vue'
import Breadcrumb from "@/components/Breadcrumb/index.vue"; import Breadcrumb from '@/components/Breadcrumb/index.vue'
import NavbarActions from "./components/NavbarActions.vue"; import NavbarActions from './components/NavbarActions.vue'
const appStore = useAppStore(); const appStore = useAppStore()
// 侧边栏展开状态 // 侧边栏展开状态
const isSidebarOpened = computed(() => appStore.sidebar.opened); const isSidebarOpened = computed(() => appStore.sidebar.opened)
// 切换侧边栏展开/折叠状态 // 切换侧边栏展开/折叠状态
function toggleSideBar() { function toggleSideBar() {
console.log("🔄 Hamburger clicked! Current state:", isSidebarOpened.value); console.log('🔄 Hamburger clicked! Current state:', isSidebarOpened.value)
console.log("🔄 Device type:", appStore.device); console.log('🔄 Device type:', appStore.device)
appStore.toggleSidebar(); appStore.toggleSidebar()
console.log("🔄 New state:", appStore.sidebar.opened); console.log('🔄 New state:', appStore.sidebar.opened)
} }
</script> </script>

View File

@@ -8,7 +8,7 @@
> >
<div class="settings-content"> <div class="settings-content">
<section class="config-section"> <section class="config-section">
<el-divider>{{ t("settings.theme") }}</el-divider> <el-divider>{{ t('settings.theme') }}</el-divider>
<div class="flex-center"> <div class="flex-center">
<el-switch <el-switch
@@ -23,10 +23,10 @@
<!-- 界面设置 --> <!-- 界面设置 -->
<section class="config-section"> <section class="config-section">
<el-divider>{{ t("settings.interface") }}</el-divider> <el-divider>{{ t('settings.interface') }}</el-divider>
<div class="config-item flex-x-between"> <div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.themeColor") }}</span> <span class="text-xs">{{ t('settings.themeColor') }}</span>
<el-color-picker <el-color-picker
v-model="selectedThemeColor" v-model="selectedThemeColor"
:predefine="colorPresets" :predefine="colorPresets"
@@ -35,22 +35,22 @@
</div> </div>
<div class="config-item flex-x-between"> <div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.showTagsView") }}</span> <span class="text-xs">{{ t('settings.showTagsView') }}</span>
<el-switch v-model="settingsStore.showTagsView" /> <el-switch v-model="settingsStore.showTagsView" />
</div> </div>
<div class="config-item flex-x-between"> <div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.showAppLogo") }}</span> <span class="text-xs">{{ t('settings.showAppLogo') }}</span>
<el-switch v-model="settingsStore.showAppLogo" /> <el-switch v-model="settingsStore.showAppLogo" />
</div> </div>
<div v-if="!isDark" class="config-item flex-x-between"> <div v-if="!isDark" class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.sidebarColorScheme") }}</span> <span class="text-xs">{{ t('settings.sidebarColorScheme') }}</span>
<el-radio-group v-model="sidebarColor" @change="changeSidebarColor"> <el-radio-group v-model="sidebarColor" @change="changeSidebarColor">
<el-radio :value="SidebarColor.CLASSIC_BLUE"> <el-radio :value="SidebarColor.CLASSIC_BLUE">
{{ t("settings.classicBlue") }} {{ t('settings.classicBlue') }}
</el-radio> </el-radio>
<el-radio :value="SidebarColor.MINIMAL_WHITE"> <el-radio :value="SidebarColor.MINIMAL_WHITE">
{{ t("settings.minimalWhite") }} {{ t('settings.minimalWhite') }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</div> </div>
@@ -58,7 +58,7 @@
<!-- 布局设置 --> <!-- 布局设置 -->
<section class="config-section"> <section class="config-section">
<el-divider>{{ t("settings.navigation") }}</el-divider> <el-divider>{{ t('settings.navigation') }}</el-divider>
<!-- 整合的布局选择器 --> <!-- 整合的布局选择器 -->
<div class="layout-select"> <div class="layout-select">
@@ -76,8 +76,8 @@
'layout-item', 'layout-item',
item.className, item.className,
{ {
'is-active': settingsStore.layout === item.value, 'is-active': settingsStore.layout === item.value
}, }
]" ]"
@click="handleLayoutChange(item.value)" @click="handleLayoutChange(item.value)"
@keydown.enter.space="handleLayoutChange(item.value)" @keydown.enter.space="handleLayoutChange(item.value)"
@@ -112,7 +112,7 @@
:loading="resetLoading" :loading="resetLoading"
@click="handleResetSettings" @click="handleResetSettings"
> >
{{ resetLoading ? "重置中..." : t("settings.resetConfig") }} {{ resetLoading ? '重置中...' : t('settings.resetConfig') }}
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</div> </div>
@@ -121,51 +121,51 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue"; import { DocumentCopy, RefreshLeft, Check } from '@element-plus/icons-vue'
const { t } = useI18n(); const { t } = useI18n()
import { LayoutMode, SidebarColor, ThemeMode } from "@/enums"; import { LayoutMode, SidebarColor, ThemeMode } from '@/enums'
import { useSettingsStore } from "@/store"; import { useSettingsStore } from '@/store'
import { themeColorPresets } from "@/settings"; import { themeColorPresets } from '@/settings'
// 按钮图标 // 按钮图标
const copyIcon = markRaw(DocumentCopy); const copyIcon = markRaw(DocumentCopy)
const resetIcon = markRaw(RefreshLeft); const resetIcon = markRaw(RefreshLeft)
// 加载状态 // 加载状态
const copyLoading = ref(false); const copyLoading = ref(false)
const resetLoading = ref(false); const resetLoading = ref(false)
// 布局选项配置 // 布局选项配置
interface LayoutOption { interface LayoutOption {
value: LayoutMode; value: LayoutMode
label: string; label: string
className: string; className: string
} }
const layoutOptions: LayoutOption[] = [ const layoutOptions: LayoutOption[] = [
{ value: LayoutMode.LEFT, label: t("settings.leftLayout"), className: "left" }, { value: LayoutMode.LEFT, label: t('settings.leftLayout'), className: 'left' },
{ value: LayoutMode.TOP, label: t("settings.topLayout"), className: "top" }, { value: LayoutMode.TOP, label: t('settings.topLayout'), className: 'top' },
{ value: LayoutMode.MIX, label: t("settings.mixLayout"), className: "mix" }, { value: LayoutMode.MIX, label: t('settings.mixLayout'), className: 'mix' }
]; ]
// 使用统一的颜色预设配置 // 使用统一的颜色预设配置
const colorPresets = themeColorPresets; const colorPresets = themeColorPresets
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore()
const isDark = ref<boolean>(settingsStore.theme === ThemeMode.DARK); const isDark = ref<boolean>(settingsStore.theme === ThemeMode.DARK)
const sidebarColor = ref(settingsStore.sidebarColorScheme); const sidebarColor = ref(settingsStore.sidebarColorScheme)
const selectedThemeColor = computed({ const selectedThemeColor = computed({
get: () => settingsStore.themeColor, get: () => settingsStore.themeColor,
set: (value) => settingsStore.updateThemeColor(value), set: (value) => settingsStore.updateThemeColor(value)
}); })
const drawerVisible = computed({ const drawerVisible = computed({
get: () => settingsStore.settingsVisible, get: () => settingsStore.settingsVisible,
set: (value) => (settingsStore.settingsVisible = value), set: (value) => (settingsStore.settingsVisible = value)
}); })
/** /**
* 处理主题切换 * 处理主题切换
@@ -173,8 +173,8 @@ const drawerVisible = computed({
* @param isDark 是否启用暗黑模式 * @param isDark 是否启用暗黑模式
*/ */
const handleThemeChange = (isDark: string | number | boolean) => { const handleThemeChange = (isDark: string | number | boolean) => {
settingsStore.updateTheme(isDark ? ThemeMode.DARK : ThemeMode.LIGHT); settingsStore.updateTheme(isDark ? ThemeMode.DARK : ThemeMode.LIGHT)
}; }
/** /**
* 更改侧边栏颜色 * 更改侧边栏颜色
@@ -182,8 +182,8 @@ const handleThemeChange = (isDark: string | number | boolean) => {
* @param val 颜色方案名称 * @param val 颜色方案名称
*/ */
const changeSidebarColor = (val: any) => { const changeSidebarColor = (val: any) => {
settingsStore.updateSidebarColorScheme(val); settingsStore.updateSidebarColorScheme(val)
}; }
/** /**
* 切换布局 * 切换布局
@@ -191,76 +191,76 @@ const changeSidebarColor = (val: any) => {
* @param layout - 布局模式 * @param layout - 布局模式
*/ */
const handleLayoutChange = (layout: LayoutMode) => { const handleLayoutChange = (layout: LayoutMode) => {
if (settingsStore.layout === layout) return; if (settingsStore.layout === layout) return
settingsStore.updateLayout(layout); settingsStore.updateLayout(layout)
}; }
/** /**
* 复制当前配置 * 复制当前配置
*/ */
const handleCopySettings = async () => { const handleCopySettings = async () => {
try { try {
copyLoading.value = true; copyLoading.value = true
// 生成配置代码 // 生成配置代码
const configCode = generateSettingsCode(); const configCode = generateSettingsCode()
// 复制到剪贴板 // 复制到剪贴板
await navigator.clipboard.writeText(configCode); await navigator.clipboard.writeText(configCode)
// 显示成功消息 // 显示成功消息
ElMessage.success({ ElMessage.success({
message: t("settings.copySuccess"), message: t('settings.copySuccess'),
duration: 3000, duration: 3000
}); })
} catch { } catch {
ElMessage.error("复制配置失败"); ElMessage.error('复制配置失败')
} finally { } finally {
copyLoading.value = false; copyLoading.value = false
} }
}; }
/** /**
* 重置为默认配置 * 重置为默认配置
*/ */
const handleResetSettings = async () => { const handleResetSettings = async () => {
resetLoading.value = true; resetLoading.value = true
try { try {
settingsStore.resetSettings(); settingsStore.resetSettings()
// 同步更新本地状态 // 同步更新本地状态
isDark.value = settingsStore.theme === ThemeMode.DARK; isDark.value = settingsStore.theme === ThemeMode.DARK
sidebarColor.value = settingsStore.sidebarColorScheme; sidebarColor.value = settingsStore.sidebarColorScheme
ElMessage.success(t("settings.resetSuccess")); ElMessage.success(t('settings.resetSuccess'))
} catch { } catch {
ElMessage.error("重置配置失败"); ElMessage.error('重置配置失败')
} finally { } finally {
resetLoading.value = false; resetLoading.value = false
} }
}; }
/** /**
* 生成配置代码字符串 * 生成配置代码字符串
*/ */
const generateSettingsCode = (): string => { const generateSettingsCode = (): string => {
const settings = { const settings = {
title: "pkg.name", title: 'pkg.name',
version: "pkg.version", version: 'pkg.version',
showSettings: true, showSettings: true,
showTagsView: settingsStore.showTagsView, showTagsView: settingsStore.showTagsView,
showAppLogo: settingsStore.showAppLogo, showAppLogo: settingsStore.showAppLogo,
layout: `LayoutMode.${settingsStore.layout.toUpperCase()}`, layout: `LayoutMode.${settingsStore.layout.toUpperCase()}`,
theme: `ThemeMode.${settingsStore.theme.toUpperCase()}`, theme: `ThemeMode.${settingsStore.theme.toUpperCase()}`,
size: "ComponentSize.DEFAULT", size: 'ComponentSize.DEFAULT',
language: "LanguageEnum.ZH_CN", language: 'LanguageEnum.ZH_CN',
themeColor: `"${settingsStore.themeColor}"`, themeColor: `"${settingsStore.themeColor}"`,
showWatermark: settingsStore.showWatermark, showWatermark: settingsStore.showWatermark,
watermarkContent: "pkg.name", watermarkContent: 'pkg.name',
sidebarColorScheme: `SidebarColor.${settingsStore.sidebarColorScheme.toUpperCase().replace("-", "_")}`, sidebarColorScheme: `SidebarColor.${settingsStore.sidebarColorScheme.toUpperCase().replace('-', '_')}`
}; }
return `const defaultSettings: AppSettings = { return `const defaultSettings: AppSettings = {
title: ${settings.title}, title: ${settings.title},
@@ -276,15 +276,15 @@ const generateSettingsCode = (): string => {
showWatermark: ${settings.showWatermark}, showWatermark: ${settings.showWatermark},
watermarkContent: ${settings.watermarkContent}, watermarkContent: ${settings.watermarkContent},
sidebarColorScheme: ${settings.sidebarColorScheme}, sidebarColorScheme: ${settings.sidebarColorScheme},
};`; };`
}; }
/** /**
* 关闭抽屉前的回调 * 关闭抽屉前的回调
*/ */
const handleCloseDrawer = () => { const handleCloseDrawer = () => {
settingsStore.settingsVisible = false; settingsStore.settingsVisible = false
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -22,7 +22,7 @@
@click=" @click="
router.push({ router.push({
path: tag.fullPath, path: tag.fullPath,
query: tag.query, query: tag.query
}) })
" "
> >
@@ -68,114 +68,114 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useRouter, type RouteRecordRaw } from "vue-router"; import { useRoute, useRouter, type RouteRecordRaw } from 'vue-router'
import { resolve } from "path-browserify"; import { resolve } from 'path-browserify'
import { translateRouteTitle } from "@/utils/i18n"; import { translateRouteTitle } from '@/utils/i18n'
import { usePermissionStore, useTagsViewStore } from "@/store"; import { usePermissionStore, useTagsViewStore } from '@/store'
interface ContextMenu { interface ContextMenu {
visible: boolean; visible: boolean
x: number; x: number
y: number; y: number
} }
const router = useRouter(); const router = useRouter()
const route = useRoute(); const route = useRoute()
// 状态管理 // 状态管理
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore()
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore()
const { visitedViews } = storeToRefs(tagsViewStore); const { visitedViews } = storeToRefs(tagsViewStore)
// 当前选中的标签 // 当前选中的标签
const selectedTag = ref<TagView | null>(null); const selectedTag = ref<TagView | null>(null)
// 右键菜单状态 // 右键菜单状态
const contextMenu = reactive<ContextMenu>({ const contextMenu = reactive<ContextMenu>({
visible: false, visible: false,
x: 0, x: 0,
y: 0, y: 0
}); })
// 滚动条引用 // 滚动条引用
const scrollbarRef = ref(); const scrollbarRef = ref()
// 路由映射缓存,提升查找性能 // 路由映射缓存,提升查找性能
const routePathMap = computed(() => { const routePathMap = computed(() => {
const map = new Map<string, TagView>(); const map = new Map<string, TagView>()
visitedViews.value.forEach((tag) => { visitedViews.value.forEach((tag) => {
map.set(tag.path, tag); map.set(tag.path, tag)
}); })
return map; return map
}); })
// 判断是否为第一个标签 // 判断是否为第一个标签
const isFirstView = computed(() => { const isFirstView = computed(() => {
if (!selectedTag.value) return false; if (!selectedTag.value) return false
return ( return (
selectedTag.value.path === "/dashboard" || selectedTag.value.path === '/dashboard' ||
selectedTag.value.fullPath === visitedViews.value[1]?.fullPath selectedTag.value.fullPath === visitedViews.value[1]?.fullPath
); )
}); })
// 判断是否为最后一个标签 // 判断是否为最后一个标签
const isLastView = computed(() => { const isLastView = computed(() => {
if (!selectedTag.value) return false; if (!selectedTag.value) return false
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath; return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath
}); })
/** /**
* 递归提取固定标签 * 递归提取固定标签
*/ */
const extractAffixTags = (routes: RouteRecordRaw[], basePath = "/"): TagView[] => { const extractAffixTags = (routes: RouteRecordRaw[], basePath = '/'): TagView[] => {
const affixTags: TagView[] = []; const affixTags: TagView[] = []
const traverse = (routeList: RouteRecordRaw[], currentBasePath: string) => { const traverse = (routeList: RouteRecordRaw[], currentBasePath: string) => {
routeList.forEach((route) => { routeList.forEach((route) => {
const fullPath = resolve(currentBasePath, route.path); const fullPath = resolve(currentBasePath, route.path)
// 如果是固定标签,添加到列表 // 如果是固定标签,添加到列表
if (route.meta?.affix) { if (route.meta?.affix) {
affixTags.push({ affixTags.push({
path: fullPath, path: fullPath,
fullPath, fullPath,
name: String(route.name || ""), name: String(route.name || ''),
title: route.meta.title || "no-name", title: route.meta.title || 'no-name',
affix: true, affix: true,
keepAlive: route.meta.keepAlive || false, keepAlive: route.meta.keepAlive || false
}); })
} }
// 递归处理子路由 // 递归处理子路由
if (route.children?.length) { if (route.children?.length) {
traverse(route.children, fullPath); traverse(route.children, fullPath)
} }
}); })
}; }
traverse(routes, basePath); traverse(routes, basePath)
return affixTags; return affixTags
}; }
/** /**
* 初始化固定标签 * 初始化固定标签
*/ */
const initAffixTags = () => { const initAffixTags = () => {
const affixTags = extractAffixTags(permissionStore.routes); const affixTags = extractAffixTags(permissionStore.routes)
affixTags.forEach((tag) => { affixTags.forEach((tag) => {
if (tag.name) { if (tag.name) {
tagsViewStore.addVisitedView(tag); tagsViewStore.addVisitedView(tag)
} }
}); })
}; }
/** /**
* 添加当前路由标签 * 添加当前路由标签
*/ */
const addCurrentTag = () => { const addCurrentTag = () => {
if (!route.meta?.title) return; if (!route.meta?.title) return
tagsViewStore.addView({ tagsViewStore.addView({
name: route.name as string, name: route.name as string,
@@ -184,189 +184,189 @@ const addCurrentTag = () => {
fullPath: route.fullPath, fullPath: route.fullPath,
affix: route.meta.affix || false, affix: route.meta.affix || false,
keepAlive: route.meta.keepAlive || false, keepAlive: route.meta.keepAlive || false,
query: route.query, query: route.query
}); })
}; }
/** /**
* 更新当前标签 * 更新当前标签
*/ */
const updateCurrentTag = () => { const updateCurrentTag = () => {
nextTick(() => { nextTick(() => {
const currentTag = routePathMap.value.get(route.path); const currentTag = routePathMap.value.get(route.path)
if (currentTag && currentTag.fullPath !== route.fullPath) { if (currentTag && currentTag.fullPath !== route.fullPath) {
tagsViewStore.updateVisitedView({ tagsViewStore.updateVisitedView({
name: route.name as string, name: route.name as string,
title: route.meta?.title || "", title: route.meta?.title || '',
path: route.path, path: route.path,
fullPath: route.fullPath, fullPath: route.fullPath,
affix: route.meta?.affix || false, affix: route.meta?.affix || false,
keepAlive: route.meta?.keepAlive || false, keepAlive: route.meta?.keepAlive || false,
query: route.query, query: route.query
}); })
} }
}); })
}; }
/** /**
* 处理中键点击 * 处理中键点击
*/ */
const handleMiddleClick = (tag: TagView) => { const handleMiddleClick = (tag: TagView) => {
if (!tag.affix) { if (!tag.affix) {
closeSelectedTag(tag); closeSelectedTag(tag)
} }
}; }
/** /**
* 打开右键菜单 * 打开右键菜单
*/ */
const openContextMenu = (tag: TagView, event: MouseEvent) => { const openContextMenu = (tag: TagView, event: MouseEvent) => {
contextMenu.x = event.clientX; contextMenu.x = event.clientX
contextMenu.y = event.clientY; contextMenu.y = event.clientY
contextMenu.visible = true; contextMenu.visible = true
selectedTag.value = tag; selectedTag.value = tag
}; }
/** /**
* 关闭右键菜单 * 关闭右键菜单
*/ */
const closeContextMenu = () => { const closeContextMenu = () => {
contextMenu.visible = false; contextMenu.visible = false
}; }
/** /**
* 处理滚轮事件 * 处理滚轮事件
*/ */
const handleScroll = (event: WheelEvent) => { const handleScroll = (event: WheelEvent) => {
closeContextMenu(); closeContextMenu()
const scrollWrapper = scrollbarRef.value?.wrapRef; const scrollWrapper = scrollbarRef.value?.wrapRef
if (!scrollWrapper) return; if (!scrollWrapper) return
const hasHorizontalScroll = scrollWrapper.scrollWidth > scrollWrapper.clientWidth; const hasHorizontalScroll = scrollWrapper.scrollWidth > scrollWrapper.clientWidth
if (!hasHorizontalScroll) return; if (!hasHorizontalScroll) return
const deltaY = event.deltaY || -(event as any).wheelDelta || 0; const deltaY = event.deltaY || -(event as any).wheelDelta || 0
const newScrollLeft = scrollWrapper.scrollLeft + deltaY; const newScrollLeft = scrollWrapper.scrollLeft + deltaY
scrollbarRef.value.setScrollLeft(newScrollLeft); scrollbarRef.value.setScrollLeft(newScrollLeft)
}; }
/** /**
* 刷新标签 * 刷新标签
*/ */
const refreshSelectedTag = (tag: TagView | null) => { const refreshSelectedTag = (tag: TagView | null) => {
if (!tag) return; if (!tag) return
tagsViewStore.delCachedView(tag); tagsViewStore.delCachedView(tag)
nextTick(() => { nextTick(() => {
router.replace("/redirect" + tag.fullPath); router.replace('/redirect' + tag.fullPath)
}); })
}; }
/** /**
* 关闭标签 * 关闭标签
*/ */
const closeSelectedTag = (tag: TagView | null) => { const closeSelectedTag = (tag: TagView | null) => {
if (!tag) return; if (!tag) return
tagsViewStore.delView(tag).then((result: any) => { tagsViewStore.delView(tag).then((result: any) => {
if (tagsViewStore.isActive(tag)) { if (tagsViewStore.isActive(tag)) {
tagsViewStore.toLastView(result.visitedViews, tag); tagsViewStore.toLastView(result.visitedViews, tag)
} }
}); })
}; }
/** /**
* 关闭左侧标签 * 关闭左侧标签
*/ */
const closeLeftTags = () => { const closeLeftTags = () => {
if (!selectedTag.value) return; if (!selectedTag.value) return
tagsViewStore.delLeftViews(selectedTag.value).then((result: any) => { tagsViewStore.delLeftViews(selectedTag.value).then((result: any) => {
const hasCurrentRoute = result.visitedViews.some((item: TagView) => item.path === route.path); const hasCurrentRoute = result.visitedViews.some((item: TagView) => item.path === route.path)
if (!hasCurrentRoute) { if (!hasCurrentRoute) {
tagsViewStore.toLastView(result.visitedViews); tagsViewStore.toLastView(result.visitedViews)
} }
}); })
}; }
/** /**
* 关闭右侧标签 * 关闭右侧标签
*/ */
const closeRightTags = () => { const closeRightTags = () => {
if (!selectedTag.value) return; if (!selectedTag.value) return
tagsViewStore.delRightViews(selectedTag.value).then((result: any) => { tagsViewStore.delRightViews(selectedTag.value).then((result: any) => {
const hasCurrentRoute = result.visitedViews.some((item: TagView) => item.path === route.path); const hasCurrentRoute = result.visitedViews.some((item: TagView) => item.path === route.path)
if (!hasCurrentRoute) { if (!hasCurrentRoute) {
tagsViewStore.toLastView(result.visitedViews); tagsViewStore.toLastView(result.visitedViews)
} }
}); })
}; }
/** /**
* 关闭其他标签 * 关闭其他标签
*/ */
const closeOtherTags = () => { const closeOtherTags = () => {
if (!selectedTag.value) return; if (!selectedTag.value) return
router.push(selectedTag.value); router.push(selectedTag.value)
tagsViewStore.delOtherViews(selectedTag.value).then(() => { tagsViewStore.delOtherViews(selectedTag.value).then(() => {
updateCurrentTag(); updateCurrentTag()
}); })
}; }
/** /**
* 关闭所有标签 * 关闭所有标签
*/ */
const closeAllTags = (tag: TagView | null) => { const closeAllTags = (tag: TagView | null) => {
tagsViewStore.delAllViews().then((result: any) => { tagsViewStore.delAllViews().then((result: any) => {
tagsViewStore.toLastView(result.visitedViews, tag || undefined); tagsViewStore.toLastView(result.visitedViews, tag || undefined)
}); })
}; }
// 右键菜单管理 // 右键菜单管理
const useContextMenuManager = () => { const useContextMenuManager = () => {
const handleOutsideClick = () => { const handleOutsideClick = () => {
closeContextMenu(); closeContextMenu()
}; }
watchEffect(() => { watchEffect(() => {
if (contextMenu.visible) { if (contextMenu.visible) {
document.addEventListener("click", handleOutsideClick); document.addEventListener('click', handleOutsideClick)
} else { } else {
document.removeEventListener("click", handleOutsideClick); document.removeEventListener('click', handleOutsideClick)
} }
}); })
// 组件卸载时清理 // 组件卸载时清理
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener("click", handleOutsideClick); document.removeEventListener('click', handleOutsideClick)
}); })
}; }
// 监听路由变化 // 监听路由变化
watch( watch(
route, route,
() => { () => {
addCurrentTag(); addCurrentTag()
updateCurrentTag(); updateCurrentTag()
}, },
{ immediate: true } { immediate: true }
); )
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
initAffixTags(); initAffixTags()
}); })
// 启用右键菜单管理 // 启用右键菜单管理
useContextMenuManager(); useContextMenuManager()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -7,40 +7,40 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from "vue-router"; import { useRoute } from 'vue-router'
import { useLayout } from "@/composables/layout/useLayout"; import { useLayout } from '@/composables/layout/useLayout'
import LeftLayout from "@/layouts/modes/left/index.vue"; import LeftLayout from '@/layouts/modes/left/index.vue'
import TopLayout from "@/layouts/modes/top/index.vue"; import TopLayout from '@/layouts/modes/top/index.vue'
import MixLayout from "@/layouts/modes/mix/index.vue"; import MixLayout from '@/layouts/modes/mix/index.vue'
import Settings from "./components/Settings/index.vue"; import Settings from './components/Settings/index.vue'
import { LayoutMode } from "@/enums/settings/layout-enum"; import { LayoutMode } from '@/enums/settings/layout-enum'
import { defaultSettings } from "@/settings"; import { defaultSettings } from '@/settings'
import { useUserStore } from "@/store/index"; import { useUserStore } from '@/store/index'
const { currentLayout } = useLayout(); const { currentLayout } = useLayout()
const route = useRoute(); const route = useRoute()
const userStore = useUserStore(); const userStore = useUserStore()
/// Select the corresponding component based on the current layout mode /// Select the corresponding component based on the current layout mode
const currentLayoutComponent = computed(() => { const currentLayoutComponent = computed(() => {
const override = route.meta?.layout as LayoutMode | undefined; const override = route.meta?.layout as LayoutMode | undefined
const layoutToUse = override ?? currentLayout.value; const layoutToUse = override ?? currentLayout.value
switch (layoutToUse) { switch (layoutToUse) {
case LayoutMode.TOP: case LayoutMode.TOP:
return TopLayout; return TopLayout
case LayoutMode.MIX: case LayoutMode.MIX:
return MixLayout; return MixLayout
case LayoutMode.LEFT: case LayoutMode.LEFT:
default: default:
return LeftLayout; return LeftLayout
} }
}); })
/// Whether to show the settings panel /// Whether to show the settings panel
const isShowSettings = computed(() => defaultSettings.showSettings); const isShowSettings = computed(() => defaultSettings.showSettings)
onMounted(() => { onMounted(() => {
userStore.getUserInfo(); userStore.getUserInfo()
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -9,13 +9,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useLayout, useDeviceDetection } from "@/composables"; import { useLayout, useDeviceDetection } from '@/composables'
/// Layout-related functionality and state management /// Layout-related functionality and state management
const { layoutClass, isSidebarOpen, closeSidebar } = useLayout(); const { layoutClass, isSidebarOpen, closeSidebar } = useLayout()
/// Device detection for responsive layout /// Device detection for responsive layout
const { isMobile } = useDeviceDetection(); const { isMobile } = useDeviceDetection()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -16,7 +16,7 @@
<div <div
:class="{ :class="{
hasTagsView: isShowTagsView, hasTagsView: isShowTagsView,
'layout__main--collapsed': !isSidebarOpen, 'layout__main--collapsed': !isSidebarOpen
}" }"
class="layout__main" class="layout__main"
> >
@@ -28,20 +28,20 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useLayout } from "@/composables/layout/useLayout"; import { useLayout } from '@/composables/layout/useLayout'
import { useLayoutMenu } from "@/composables/layout/useLayoutMenu"; import { useLayoutMenu } from '@/composables/layout/useLayoutMenu'
import BaseLayout from "../base/index.vue"; import BaseLayout from '../base/index.vue'
import AppLogo from "../../components/AppLogo/index.vue"; import AppLogo from '../../components/AppLogo/index.vue'
import NavBar from "../../components/NavBar/index.vue"; import NavBar from '../../components/NavBar/index.vue'
import TagsView from "../../components/TagsView/index.vue"; import TagsView from '../../components/TagsView/index.vue'
import AppMain from "../../components/AppMain/index.vue"; import AppMain from '../../components/AppMain/index.vue'
import BasicMenu from "../../components/Menu/BasicMenu.vue"; import BasicMenu from '../../components/Menu/BasicMenu.vue'
// 布局相关参数 // 布局相关参数
const { isShowTagsView, isShowLogo, isSidebarOpen } = useLayout(); const { isShowTagsView, isShowLogo, isSidebarOpen } = useLayout()
// 菜单相关 // 菜单相关
const { routes } = useLayoutMenu(); const { routes } = useLayoutMenu()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -58,44 +58,44 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from "vue-router"; import { useRoute } from 'vue-router'
import { useWindowSize } from "@vueuse/core"; import { useWindowSize } from '@vueuse/core'
import { useLayout, useLayoutMenu } from "@/composables"; import { useLayout, useLayoutMenu } from '@/composables'
import BaseLayout from "../base/index.vue"; import BaseLayout from '../base/index.vue'
import AppLogo from "../../components/AppLogo/index.vue"; import AppLogo from '../../components/AppLogo/index.vue'
import MixTopMenu from "../../components/Menu/MixTopMenu.vue"; import MixTopMenu from '../../components/Menu/MixTopMenu.vue'
import NavbarActions from "../../components/NavBar/components/NavbarActions.vue"; import NavbarActions from '../../components/NavBar/components/NavbarActions.vue'
import TagsView from "../../components/TagsView/index.vue"; import TagsView from '../../components/TagsView/index.vue'
import AppMain from "../../components/AppMain/index.vue"; import AppMain from '../../components/AppMain/index.vue'
import MenuItem from "../../components/Menu/components/MenuItem.vue"; import MenuItem from '../../components/Menu/components/MenuItem.vue'
import Hamburger from "@/components/Hamburger/index.vue"; import Hamburger from '@/components/Hamburger/index.vue'
import variables from "@/styles/variables.module.scss"; import variables from '@/styles/variables.module.scss'
import { isExternal } from "@/utils/index"; import { isExternal } from '@/utils/index'
import { useAppStore, usePermissionStore } from "@/store"; import { useAppStore, usePermissionStore } from '@/store'
const route = useRoute(); const route = useRoute()
// 布局相关参数 // 布局相关参数
const { isShowTagsView, isShowLogo, isSidebarOpen, toggleSidebar } = useLayout(); const { isShowTagsView, isShowLogo, isSidebarOpen, toggleSidebar } = useLayout()
// 菜单相关 // 菜单相关
const { sideMenuRoutes, activeTopMenuPath } = useLayoutMenu(); const { sideMenuRoutes, activeTopMenuPath } = useLayoutMenu()
// 响应式窗口尺寸 // 响应式窗口尺寸
const { width } = useWindowSize(); const { width } = useWindowSize()
// 只有在小屏设备移动设备时才折叠Logo只显示图标隐藏文字 // 只有在小屏设备移动设备时才折叠Logo只显示图标隐藏文字
const isLogoCollapsed = computed(() => width.value < 768); const isLogoCollapsed = computed(() => width.value < 768)
// 当前激活的菜单 // 当前激活的菜单
const activeLeftMenuPath = computed(() => { const activeLeftMenuPath = computed(() => {
const { meta, path } = route; const { meta, path } = route
// 如果设置了activeMenu则使用 // 如果设置了activeMenu则使用
if ((meta?.activeMenu as unknown as string) && typeof meta.activeMenu === "string") { if ((meta?.activeMenu as unknown as string) && typeof meta.activeMenu === 'string') {
return meta.activeMenu as unknown as string; return meta.activeMenu as unknown as string
} }
return path; return path
}); })
/** /**
* 解析路径 - 混合模式下,左侧菜单是从顶级菜单下的子菜单开始的 * 解析路径 - 混合模式下,左侧菜单是从顶级菜单下的子菜单开始的
@@ -103,13 +103,13 @@ const activeLeftMenuPath = computed(() => {
*/ */
function resolvePath(routePath: string) { function resolvePath(routePath: string) {
if (isExternal(routePath)) { if (isExternal(routePath)) {
return routePath; return routePath
} }
if (routePath.startsWith("/")) { if (routePath.startsWith('/')) {
return activeTopMenuPath.value + routePath; return activeTopMenuPath.value + routePath
} }
return `${activeTopMenuPath.value}/${routePath}`; return `${activeTopMenuPath.value}/${routePath}`
} }
// 监听路由变化确保左侧菜单能随TagsView切换而正确激活 // 监听路由变化确保左侧菜单能随TagsView切换而正确激活
@@ -118,7 +118,7 @@ watch(
(newPath: string) => { (newPath: string) => {
// 获取顶级路径 // 获取顶级路径
const topMenuPath = const topMenuPath =
newPath.split("/").filter(Boolean).length > 1 ? newPath.match(/^\/[^/]+/)?.[0] || "/" : "/"; newPath.split('/').filter(Boolean).length > 1 ? newPath.match(/^\/[^/]+/)?.[0] || '/' : '/'
// 如果当前路径属于当前激活的顶部菜单 // 如果当前路径属于当前激活的顶部菜单
if (newPath.startsWith(activeTopMenuPath.value)) { if (newPath.startsWith(activeTopMenuPath.value)) {
@@ -126,15 +126,15 @@ watch(
} }
// 如果路径改变了顶级菜单,确保顶部菜单和左侧菜单都更新 // 如果路径改变了顶级菜单,确保顶部菜单和左侧菜单都更新
else if (topMenuPath !== activeTopMenuPath.value) { else if (topMenuPath !== activeTopMenuPath.value) {
const appStore = useAppStore(); const appStore = useAppStore()
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore()
appStore.activeTopMenu(topMenuPath); appStore.activeTopMenu(topMenuPath)
permissionStore.setMixLayoutSideMenus(topMenuPath); permissionStore.setMixLayoutSideMenus(topMenuPath)
} }
}, },
{ immediate: true } { immediate: true }
); )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -23,26 +23,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useLayout } from "@/composables/layout/useLayout"; import { useLayout } from '@/composables/layout/useLayout'
import { useLayoutMenu } from "@/composables/layout/useLayoutMenu"; import { useLayoutMenu } from '@/composables/layout/useLayoutMenu'
import BaseLayout from "../base/index.vue"; import BaseLayout from '../base/index.vue'
import AppLogo from "../../components/AppLogo/index.vue"; import AppLogo from '../../components/AppLogo/index.vue'
import BasicMenu from "../../components/Menu/BasicMenu.vue"; import BasicMenu from '../../components/Menu/BasicMenu.vue'
import NavbarActions from "../../components/NavBar/components/NavbarActions.vue"; import NavbarActions from '../../components/NavBar/components/NavbarActions.vue'
import TagsView from "../../components/TagsView/index.vue"; import TagsView from '../../components/TagsView/index.vue'
import AppMain from "../../components/AppMain/index.vue"; import AppMain from '../../components/AppMain/index.vue'
// 布局相关参数 // 布局相关参数
const { isShowTagsView, isShowLogo } = useLayout(); const { isShowTagsView, isShowLogo } = useLayout()
// 菜单相关 // 菜单相关
const { routes } = useLayoutMenu(); const { routes } = useLayoutMenu()
// 响应式窗口尺寸 // 响应式窗口尺寸
const { width } = useWindowSize(); const { width } = useWindowSize()
// 只有在小屏设备移动设备时才折叠Logo只显示图标隐藏文字 // 只有在小屏设备移动设备时才折叠Logo只显示图标隐藏文字
const isLogoCollapsed = computed(() => width.value < 768); const isLogoCollapsed = computed(() => width.value < 768)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,22 +1,22 @@
import { createApp } from "vue"; import { createApp } from 'vue'
import App from "./App.vue"; import App from './App.vue'
import setupPlugins from "@/plugins"; import setupPlugins from '@/plugins'
// 暗黑主题样式 // 暗黑主题样式
import "element-plus/theme-chalk/dark/css-vars.css"; import 'element-plus/theme-chalk/dark/css-vars.css'
import "vxe-table/lib/style.css"; import 'vxe-table/lib/style.css'
// 暗黑模式自定义变量 // 暗黑模式自定义变量
import "@/styles/dark/css-vars.css"; import '@/styles/dark/css-vars.css'
import "@/styles/index.scss"; import '@/styles/index.scss'
import "uno.css"; import 'uno.css'
// 过渡动画 // 过渡动画
import "animate.css"; import 'animate.css'
// 自动为某些默认事件(如 touchstart、wheel 等)添加 { passive: true },提升滚动性能并消除控制台的非被动事件监听警告 // 自动为某些默认事件(如 touchstart、wheel 等)添加 { passive: true },提升滚动性能并消除控制台的非被动事件监听警告
import "default-passive-events"; import 'default-passive-events'
const app = createApp(App); const app = createApp(App)
// 注册插件 // 注册插件
app.use(setupPlugins); app.use(setupPlugins)
app.mount("#app"); app.mount('#app')

View File

@@ -1,9 +1,9 @@
import type { App } from "vue"; import type { App } from 'vue'
import * as ElementPlusIconsVue from "@element-plus/icons-vue"; import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 注册所有图标 // 注册所有图标
export function setupElIcons(app: App<Element>) { export function setupElIcons(app: App<Element>) {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component); app.component(key, component)
} }
} }

View File

@@ -1,34 +1,34 @@
import type { App } from "vue"; import type { App } from 'vue'
import { setupDirective } from "@/directives"; import { setupDirective } from '@/directives'
import { setupI18n } from "@/lang"; import { setupI18n } from '@/lang'
import { setupRouter } from "@/router"; import { setupRouter } from '@/router'
import { setupStore } from "@/store"; import { setupStore } from '@/store'
import { setupElIcons } from "./icons"; import { setupElIcons } from './icons'
import { setupPermission } from "./permission"; import { setupPermission } from './permission'
import { setupWebSocket } from "./websocket"; import { setupWebSocket } from './websocket'
import { InstallCodeMirror } from "codemirror-editor-vue3"; import { InstallCodeMirror } from 'codemirror-editor-vue3'
import { setupVxeTable } from "./vxeTable"; import { setupVxeTable } from './vxeTable'
export default { export default {
install(app: App<Element>) { install(app: App<Element>) {
// 自定义指令(directive) // 自定义指令(directive)
setupDirective(app); setupDirective(app)
// 路由(router) // 路由(router)
setupRouter(app); setupRouter(app)
// 状态管理(store) // 状态管理(store)
setupStore(app); setupStore(app)
// 国际化 // 国际化
setupI18n(app); setupI18n(app)
// Element-plus图标 // Element-plus图标
setupElIcons(app); setupElIcons(app)
// 路由守卫 // 路由守卫
setupPermission(); setupPermission()
// WebSocket服务 // WebSocket服务
setupWebSocket(); setupWebSocket()
// vxe-table // vxe-table
setupVxeTable(app); setupVxeTable(app)
// 注册 CodeMirror // 注册 CodeMirror
app.use(InstallCodeMirror); app.use(InstallCodeMirror)
}, }
}; }

View File

@@ -1,56 +1,56 @@
import NProgress from "@/utils/nprogress"; import NProgress from '@/utils/nprogress'
import router from "@/router"; import router from '@/router'
import { useUserStore } from "@/store"; import { useUserStore } from '@/store'
export function setupPermission() { export function setupPermission() {
const whiteList = ["/login"]; const whiteList = ['/login']
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start(); NProgress.start()
try { try {
const isLoggedIn = useUserStore().isLoggedIn(); const isLoggedIn = useUserStore().isLoggedIn()
// 未登录处理 // 未登录处理
if (!isLoggedIn) { if (!isLoggedIn) {
if (whiteList.includes(to.path)) { if (whiteList.includes(to.path)) {
next(); next()
} else { } else {
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`); next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
NProgress.done(); NProgress.done()
} }
return; return
} }
// 已登录登录页重定向 // 已登录登录页重定向
if (to.path === "/login") { if (to.path === '/login') {
next({ path: "/" }); next({ path: '/' })
return; return
} }
// 路由404检查 // 路由404检查
if (to.matched.length === 0) { if (to.matched.length === 0) {
next("/404"); next('/404')
return; return
} }
// 动态标题设置 // 动态标题设置
const title = (to.params.title as string) || (to.query.title as string); const title = (to.params.title as string) || (to.query.title as string)
if (title) { if (title) {
to.meta.title = title; to.meta.title = title
} }
next(); next()
} catch (error) { } catch (error) {
// 错误处理:重置状态并跳转登录 // 错误处理:重置状态并跳转登录
console.error("Route guard error:", error); console.error('Route guard error:', error)
await useUserStore().resetAllState(); await useUserStore().resetAllState()
next("/login"); next('/login')
NProgress.done(); NProgress.done()
} }
}); })
router.afterEach(() => { router.afterEach(() => {
NProgress.done(); NProgress.done()
}); })
} }

View File

@@ -1,10 +1,10 @@
import type { App } from "vue"; import type { App } from 'vue'
import VXETable from "vxe-table"; // https://vxetable.cn/v4.6/#/table/start/install import VXETable from 'vxe-table' // https://vxetable.cn/v4.6/#/table/start/install
// 全局默认参数 // 全局默认参数
VXETable.setConfig({ VXETable.setConfig({
// 全局尺寸 // 全局尺寸
size: "medium", size: 'medium',
// 全局 zIndex 起始值,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡 // 全局 zIndex 起始值,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡
zIndex: 9999, zIndex: 9999,
// 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据 // 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据
@@ -13,24 +13,24 @@ VXETable.setConfig({
loadingText: null, loadingText: null,
table: { table: {
showHeader: true, showHeader: true,
showOverflow: "tooltip", showOverflow: 'tooltip',
showHeaderOverflow: "tooltip", showHeaderOverflow: 'tooltip',
autoResize: true, autoResize: true,
// stripe: false, // stripe: false,
border: "inner", border: 'inner',
// round: false, // round: false,
emptyText: "暂无数据", emptyText: '暂无数据',
rowConfig: { rowConfig: {
isHover: true, isHover: true,
isCurrent: true, isCurrent: true,
// 行数据的唯一主键字段名 // 行数据的唯一主键字段名
keyField: "_VXE_ID", keyField: '_VXE_ID'
}, },
columnConfig: { columnConfig: {
resizable: false, resizable: false
}, },
align: "center", align: 'center',
headerAlign: "center", headerAlign: 'center'
}, },
pager: { pager: {
// size: "medium", // size: "medium",
@@ -40,15 +40,15 @@ VXETable.setConfig({
pagerCount: 7, pagerCount: 7,
pageSizes: [10, 20, 50], pageSizes: [10, 20, 50],
layouts: [ layouts: [
"Total", 'Total',
"PrevJump", 'PrevJump',
"PrevPage", 'PrevPage',
"Number", 'Number',
"NextPage", 'NextPage',
"NextJump", 'NextJump',
"Sizes", 'Sizes',
"FullJump", 'FullJump'
], ]
}, },
modal: { modal: {
minWidth: 500, minWidth: 500,
@@ -60,11 +60,11 @@ VXETable.setConfig({
dblclickZoom: false, dblclickZoom: false,
showTitleOverflow: true, showTitleOverflow: true,
transfer: true, transfer: true,
draggable: false, draggable: false
}, }
}); })
export function setupVxeTable(app: App) { export function setupVxeTable(app: App) {
// Vxe Table 组件完整引入 // Vxe Table 组件完整引入
app.use(VXETable); app.use(VXETable)
} }

View File

@@ -1,82 +1,82 @@
import { useDictSync } from "@/composables"; import { useDictSync } from '@/composables'
import { AuthStorage } from "@/utils/auth"; import { AuthStorage } from '@/utils/auth'
// 不直接导入 store 或 userStore // 不直接导入 store 或 userStore
// 全局 WebSocket 实例管理 // 全局 WebSocket 实例管理
const websocketInstances = new Map<string, any>(); const websocketInstances = new Map<string, any>()
// 用于防止重复初始化的状态标记 // 用于防止重复初始化的状态标记
let isInitialized = false; let isInitialized = false
let dictWebSocketInstance: ReturnType<typeof useDictSync> | null = null; let dictWebSocketInstance: ReturnType<typeof useDictSync> | null = null
/** /**
* 注册 WebSocket 实例 * 注册 WebSocket 实例
*/ */
export function registerWebSocketInstance(key: string, instance: any) { export function registerWebSocketInstance(key: string, instance: any) {
websocketInstances.set(key, instance); websocketInstances.set(key, instance)
console.log(`[WebSocketPlugin] Registered WebSocket instance: ${key}`); console.log(`[WebSocketPlugin] Registered WebSocket instance: ${key}`)
} }
/** /**
* 获取 WebSocket 实例 * 获取 WebSocket 实例
*/ */
export function getWebSocketInstance(key: string) { export function getWebSocketInstance(key: string) {
return websocketInstances.get(key); return websocketInstances.get(key)
} }
/** /**
* 初始化WebSocket服务 * 初始化WebSocket服务
*/ */
export function setupWebSocket() { export function setupWebSocket() {
console.log("[WebSocketPlugin] 开始初始化WebSocket服务..."); console.log('[WebSocketPlugin] 开始初始化WebSocket服务...')
// 检查是否已经初始化 // 检查是否已经初始化
if (isInitialized) { if (isInitialized) {
console.log("[WebSocketPlugin] WebSocket服务已经初始化,跳过重复初始化"); console.log('[WebSocketPlugin] WebSocket服务已经初始化,跳过重复初始化')
return; return
} }
// 检查环境变量是否配置 // 检查环境变量是否配置
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT; const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT
if (!wsEndpoint) { if (!wsEndpoint) {
console.log("[WebSocketPlugin] 未配置WebSocket端点,跳过WebSocket初始化"); console.log('[WebSocketPlugin] 未配置WebSocket端点,跳过WebSocket初始化')
return; return
} }
// 检查是否已登录(基于是否存在访问令牌) // 检查是否已登录(基于是否存在访问令牌)
if (!AuthStorage.getAccessToken()) { if (!AuthStorage.getAccessToken()) {
console.warn( console.warn(
"[WebSocketPlugin] 未找到访问令牌WebSocket初始化已跳过。用户登录后将自动重新连接。" '[WebSocketPlugin] 未找到访问令牌WebSocket初始化已跳过。用户登录后将自动重新连接。'
); )
return; return
} }
try { try {
// 延迟初始化,确保应用完全启动 // 延迟初始化,确保应用完全启动
setTimeout(() => { setTimeout(() => {
// 保存实例引用 // 保存实例引用
dictWebSocketInstance = useDictSync(); dictWebSocketInstance = useDictSync()
registerWebSocketInstance("dictSync", dictWebSocketInstance); registerWebSocketInstance('dictSync', dictWebSocketInstance)
// 初始化字典WebSocket服务 // 初始化字典WebSocket服务
dictWebSocketInstance.initWebSocket(); dictWebSocketInstance.initWebSocket()
console.log("[WebSocketPlugin] 字典WebSocket初始化完成"); console.log('[WebSocketPlugin] 字典WebSocket初始化完成')
// 初始化在线用户计数WebSocket // 初始化在线用户计数WebSocket
import("@/composables").then(({ useOnlineCount }) => { import('@/composables').then(({ useOnlineCount }) => {
const onlineCountInstance = useOnlineCount({ autoInit: false }); const onlineCountInstance = useOnlineCount({ autoInit: false })
onlineCountInstance.initWebSocket(); onlineCountInstance.initWebSocket()
console.log("[WebSocketPlugin] 在线用户计数WebSocket初始化完成"); console.log('[WebSocketPlugin] 在线用户计数WebSocket初始化完成')
}); })
// 在窗口关闭前断开WebSocket连接 // 在窗口关闭前断开WebSocket连接
window.addEventListener("beforeunload", handleWindowClose); window.addEventListener('beforeunload', handleWindowClose)
console.log("[WebSocketPlugin] WebSocket服务初始化完成"); console.log('[WebSocketPlugin] WebSocket服务初始化完成')
isInitialized = true; isInitialized = true
}, 1000); // 延迟1秒初始化 }, 1000) // 延迟1秒初始化
} catch (error) { } catch (error) {
console.error("[WebSocketPlugin] 初始化WebSocket服务失败:", error); console.error('[WebSocketPlugin] 初始化WebSocket服务失败:', error)
} }
} }
@@ -84,8 +84,8 @@ export function setupWebSocket() {
* 处理窗口关闭 * 处理窗口关闭
*/ */
function handleWindowClose() { function handleWindowClose() {
console.log("[WebSocketPlugin] 窗口即将关闭断开WebSocket连接"); console.log('[WebSocketPlugin] 窗口即将关闭断开WebSocket连接')
cleanupWebSocket(); cleanupWebSocket()
} }
/** /**
@@ -95,37 +95,37 @@ export function cleanupWebSocket() {
// 清理字典 WebSocket // 清理字典 WebSocket
if (dictWebSocketInstance) { if (dictWebSocketInstance) {
try { try {
dictWebSocketInstance.closeWebSocket(); dictWebSocketInstance.closeWebSocket()
console.log("[WebSocketPlugin] 字典WebSocket连接已断开"); console.log('[WebSocketPlugin] 字典WebSocket连接已断开')
} catch (error) { } catch (error) {
console.error("[WebSocketPlugin] 断开字典WebSocket连接失败:", error); console.error('[WebSocketPlugin] 断开字典WebSocket连接失败:', error)
} }
} }
// 清理所有注册的 WebSocket 实例 // 清理所有注册的 WebSocket 实例
websocketInstances.forEach((instance, key) => { websocketInstances.forEach((instance, key) => {
try { try {
if (instance && typeof instance.disconnect === "function") { if (instance && typeof instance.disconnect === 'function') {
instance.disconnect(); instance.disconnect()
console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`); console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`)
} else if (instance && typeof instance.closeWebSocket === "function") { } else if (instance && typeof instance.closeWebSocket === 'function') {
instance.closeWebSocket(); instance.closeWebSocket()
console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`); console.log(`[WebSocketPlugin] ${key} WebSocket连接已断开`)
} }
} catch (error) { } catch (error) {
console.error(`[WebSocketPlugin] 断开 ${key} WebSocket连接失败:`, error); console.error(`[WebSocketPlugin] 断开 ${key} WebSocket连接失败:`, error)
} }
}); })
// 清空实例映射 // 清空实例映射
websocketInstances.clear(); websocketInstances.clear()
// 移除事件监听器 // 移除事件监听器
window.removeEventListener("beforeunload", handleWindowClose); window.removeEventListener('beforeunload', handleWindowClose)
// 重置状态 // 重置状态
dictWebSocketInstance = null; dictWebSocketInstance = null
isInitialized = false; isInitialized = false
} }
/** /**
@@ -133,10 +133,10 @@ export function cleanupWebSocket() {
*/ */
export function reinitializeWebSocket() { export function reinitializeWebSocket() {
// 先清理现有连接 // 先清理现有连接
cleanupWebSocket(); cleanupWebSocket()
// 延迟后重新初始化 // 延迟后重新初始化
setTimeout(() => { setTimeout(() => {
setupWebSocket(); setupWebSocket()
}, 500); }, 500)
} }

View File

@@ -1,323 +1,323 @@
import type { App } from "vue"; import type { App } from 'vue'
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router"; import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router'
export const Layout = () => import("@/layouts/index.vue"); export const Layout = () => import('@/layouts/index.vue')
// 静态路由 // 静态路由
export const constantRoutes: RouteRecordRaw[] = [ export const constantRoutes: RouteRecordRaw[] = [
{ {
path: "/login", path: '/login',
component: () => import("@/views/login/index.vue"), component: () => import('@/views/login/index.vue'),
meta: { hidden: true }, meta: { hidden: true }
}, },
{ {
path: "/", path: '/',
name: "/", name: '/',
component: Layout, component: Layout,
redirect: "/dashboard", redirect: '/dashboard',
children: [ children: [
{ {
path: "dashboard", path: 'dashboard',
component: () => import("@/views/dashboard/index.vue"), component: () => import('@/views/dashboard/index.vue'),
// 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致 // 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致
// 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude // 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude
name: "Dashboard", name: 'Dashboard',
meta: { meta: {
title: "dashboard", title: 'dashboard',
icon: "homepage", icon: 'homepage',
affix: true, affix: true,
keepAlive: true, keepAlive: true
}, }
}, },
{ {
path: "401", path: '401',
component: () => import("@/views/error/401.vue"), component: () => import('@/views/error/401.vue'),
meta: { hidden: true }, meta: { hidden: true }
}, },
{ {
path: "404", path: '404',
component: () => import("@/views/error/404.vue"), component: () => import('@/views/error/404.vue'),
meta: { hidden: true }, meta: { hidden: true }
}, },
{ {
path: "profile", path: 'profile',
name: "Profile", name: 'Profile',
component: () => import("@/views/profile/index.vue"), component: () => import('@/views/profile/index.vue'),
meta: { title: "个人中心", icon: "user", hidden: true }, meta: { title: '个人中心', icon: 'user', hidden: true }
}, },
{ {
path: "my-notice", path: 'my-notice',
name: "MyNotice", name: 'MyNotice',
component: () => import("@/views/system/notice/components/MyNotice.vue"), component: () => import('@/views/system/notice/components/MyNotice.vue'),
meta: { title: "我的通知", icon: "user", hidden: true }, meta: { title: '我的通知', icon: 'user', hidden: true }
}, },
{ {
path: "/detail/:id(\\d+)", path: '/detail/:id(\\d+)',
name: "DemoDetail", name: 'DemoDetail',
component: () => import("@/views/demo/detail.vue"), component: () => import('@/views/demo/detail.vue'),
meta: { title: "详情页缓存", icon: "user", hidden: true, keepAlive: true }, meta: { title: '详情页缓存', icon: 'user', hidden: true, keepAlive: true }
}, }
], ]
}, },
// 人员管理模块 // 人员管理模块
{ {
path: "/personnel", path: '/personnel',
component: Layout, component: Layout,
name: "Personnel", name: 'Personnel',
meta: { meta: {
title: "人员管理", title: '人员管理',
icon: "setting", icon: 'setting'
}, },
children: [ children: [
{ {
path: "user", path: 'user',
name: "PersonnelUser", name: 'PersonnelUser',
component: () => import("@/views/calibration/personnelManagement/index.vue"), component: () => import('@/views/calibration/personnelManagement/index.vue'),
meta: { meta: {
title: "人事管理", title: '人事管理'
}, }
}, },
{ {
path: "role", path: 'role',
name: "PersonnelRole", name: 'PersonnelRole',
component: () => import("@/views/calibration/department/index.vue"), component: () => import('@/views/calibration/department/index.vue'),
meta: { meta: {
title: "角色管理", title: '角色管理'
}, }
}, }
], ]
}, },
// 财务管理模块 // 财务管理模块
{ {
path: "/finance", path: '/finance',
component: Layout, component: Layout,
name: "Finance", name: 'Finance',
meta: { meta: {
title: "财务管理", title: '财务管理',
icon: "setting", icon: 'setting'
}, },
children: [ children: [
{ {
path: "onboardingRegistration", path: 'onboardingRegistration',
name: "OnboardingRegistration", name: 'OnboardingRegistration',
component: () => import("@/views/calibration/onboardingRegistration/index.vue"), component: () => import('@/views/calibration/onboardingRegistration/index.vue'),
// component: () => import("@/views/system/user/index.vue"), // component: () => import("@/views/system/user/index.vue"),
meta: { meta: {
title: "入职财务登记", title: '入职财务登记'
}, }
}, },
{ {
path: "departure ", path: 'departure ',
name: "Departure", name: 'Departure',
component: () => import("@/views/calibration/departureFinancialRegistration/index.vue"), component: () => import('@/views/calibration/departureFinancialRegistration/index.vue'),
meta: { meta: {
title: "离职财务登记", title: '离职财务登记'
}, }
}, },
{ {
path: "invoiceApplication", path: 'invoiceApplication',
name: "InvoiceApplication", name: 'InvoiceApplication',
component: () => import("@/views/calibration/invoiceApplication/index.vue"), component: () => import('@/views/calibration/invoiceApplication/index.vue'),
meta: { meta: {
title: "开票申请", title: '开票申请'
}, }
}, },
{ {
path: "revenueRecognition", path: 'revenueRecognition',
name: "RevenueRecognition", name: 'RevenueRecognition',
component: () => import("@/views/calibration/revenueRecognition/index.vue"), component: () => import('@/views/calibration/revenueRecognition/index.vue'),
meta: { meta: {
title: "收入确认", title: '收入确认'
}, }
}, },
{ {
path: "accountAdjustmentApplication", path: 'accountAdjustmentApplication',
name: "AccountAdjustmentApplication", name: 'AccountAdjustmentApplication',
component: () => import("@/views/calibration/accountAdjustmentApplication/index.vue"), component: () => import('@/views/calibration/accountAdjustmentApplication/index.vue'),
meta: { meta: {
title: "调账申请", title: '调账申请'
}, }
}, },
{ {
path: "paymentApplicationForm", path: 'paymentApplicationForm',
name: "PaymentApplicationForm", name: 'PaymentApplicationForm',
component: () => import("@/views/calibration/paymentApplicationForm/index.vue"), component: () => import('@/views/calibration/paymentApplicationForm/index.vue'),
meta: { meta: {
title: "付款申请单", title: '付款申请单'
}, }
}, },
{ {
path: "reimbursement", path: 'reimbursement',
name: "Reimbursement", name: 'Reimbursement',
component: () => import("@/views/calibration/reimbursement/index.vue"), component: () => import('@/views/calibration/reimbursement/index.vue'),
meta: { meta: {
title: "报销", title: '报销'
}, }
}, },
{ {
path: "salaryBonusAdjustment", path: 'salaryBonusAdjustment',
name: "SalaryBonusAdjustment", name: 'SalaryBonusAdjustment',
component: () => import("@/views/calibration/salaryBonusAdjustment/index.vue"), component: () => import('@/views/calibration/salaryBonusAdjustment/index.vue'),
meta: { meta: {
title: "工资/奖金变更", title: '工资/奖金变更'
}, }
}, }
], ]
}, },
// 业务管理模块 // 业务管理模块
{ {
path: "/business", path: '/business',
component: Layout, component: Layout,
name: "Business", name: 'Business',
meta: { meta: {
title: "业务管理", title: '业务管理',
icon: "setting", icon: 'setting'
}, },
children: [ children: [
{ {
path: "user", path: 'user',
name: "BusinessConflict", name: 'BusinessConflict',
component: () => import("@/views/business/conflict/index.vue"), component: () => import('@/views/business/conflict/index.vue'),
meta: { meta: {
title: "利益冲突检索", title: '利益冲突检索'
}, }
}, },
{ {
path: "role", path: 'role',
name: "BusinessPreRegistration", name: 'BusinessPreRegistration',
component: () => import("@/views/business/preRegistration/index.vue"), component: () => import('@/views/business/preRegistration/index.vue'),
meta: { meta: {
title: "预立案登记", title: '预立案登记'
}, }
}, }
], ]
}, },
// 案件管理模块 // 案件管理模块
{ {
path: "/case", path: '/case',
component: Layout, component: Layout,
name: "Case", name: 'Case',
meta: { meta: {
title: "案件管理", title: '案件管理',
icon: "setting", icon: 'setting'
}, },
children: [ children: [
{ {
path: "user", path: 'user',
name: "CaseUser", name: 'CaseUser',
component: () => import("@/views/case/index.vue"), component: () => import('@/views/case/index.vue'),
meta: { meta: {
title: "案件管理", title: '案件管理'
}, }
}, }
], ]
}, },
// 申请用印 // 申请用印
{ {
path: "/stamp", path: '/stamp',
name: "StampApplication", name: 'StampApplication',
component: Layout, component: Layout,
meta: { meta: {
title: "申请用印", title: '申请用印',
icon: "setting", icon: 'setting'
}, },
redirect: "/stamp/index", redirect: '/stamp/index',
children: [ children: [
{ {
path: "index", path: 'index',
name: "StampApplicationIndex", name: 'StampApplicationIndex',
component: () => import("@/views/stamp-application/index.vue"), component: () => import('@/views/stamp-application/index.vue'),
meta: { meta: {
title: "申请用印", title: '申请用印'
}, }
}, }
], ]
}, },
// 业绩 // 业绩
{ {
path: "/performance", path: '/performance',
name: "StampPerformance", name: 'StampPerformance',
component: Layout, component: Layout,
meta: { meta: {
title: "业绩展示", title: '业绩展示',
icon: "setting", icon: 'setting'
}, },
redirect: "/performance/index", redirect: '/performance/index',
children: [ children: [
{ {
path: "index", path: 'index',
name: "StampPerformanceIndex", name: 'StampPerformanceIndex',
component: () => import("@/views/performance/list/index.vue"), component: () => import('@/views/performance/list/index.vue'),
meta: { meta: {
title: "业绩展示", title: '业绩展示'
}, }
}, }
], ]
}, },
// 入库登记 // 入库登记
{ {
path: "/registration", path: '/registration',
name: "Registration", name: 'Registration',
component: Layout, component: Layout,
meta: { meta: {
title: "入库登记", title: '入库登记',
icon: "setting", icon: 'setting'
}, },
redirect: "/registration/index", redirect: '/registration/index',
children: [ children: [
{ {
path: "index", path: 'index',
name: "RegistrationIndex", name: 'RegistrationIndex',
component: () => import("@/views/registration/index.vue"), component: () => import('@/views/registration/index.vue'),
meta: { meta: {
title: "入库登记", title: '入库登记'
}, }
}, }
], ]
}, },
// 公告 // 公告
{ {
path: "/notice", path: '/notice',
name: "Notice", name: 'Notice',
component: Layout, component: Layout,
meta: { meta: {
title: "公告管理", title: '公告管理',
icon: "setting", icon: 'setting'
}, },
redirect: "/notice/index", redirect: '/notice/index',
children: [ children: [
{ {
path: "index", path: 'index',
name: "NoticeIndex", name: 'NoticeIndex',
component: () => import("@/views/notice/index.vue"), component: () => import('@/views/notice/index.vue'),
meta: { meta: {
title: "公告管理", title: '公告管理'
}, }
}, }
], ]
}, },
// 律所标准文件 // 律所标准文件
{ {
path: "/lawyer-file", path: '/lawyer-file',
name: "LawyerFile", name: 'LawyerFile',
component: Layout, component: Layout,
meta: { meta: {
title: "律所标准文件", title: '律所标准文件',
icon: "setting", icon: 'setting'
}, },
redirect: "/lawyer-file/index", redirect: '/lawyer-file/index',
children: [ children: [
{ {
path: "index", path: 'index',
name: "LawyerFileIndex", name: 'LawyerFileIndex',
component: () => import("@/views/lawyer/index.vue"), component: () => import('@/views/lawyer/index.vue'),
meta: { meta: {
title: "律所标准文件", title: '律所标准文件'
}, }
}, }
], ]
}, }
]; ]
/** /**
* 创建路由 * 创建路由
@@ -326,12 +326,12 @@ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: constantRoutes, routes: constantRoutes,
// 刷新时,滚动条位置还原 // 刷新时,滚动条位置还原
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 })
}); })
// 全局注册 router // 全局注册 router
export function setupRouter(app: App<Element>) { export function setupRouter(app: App<Element>) {
app.use(router); app.use(router)
} }
export default router; export default router

View File

@@ -1,9 +1,9 @@
import { LayoutMode, ComponentSize, SidebarColor, ThemeMode, LanguageEnum } from "./enums"; import { LayoutMode, ComponentSize, SidebarColor, ThemeMode, LanguageEnum } from './enums'
const { pkg } = __APP_INFO__; const { pkg } = __APP_INFO__
// 检查用户的操作系统是否使用深色模式 // 检查用户的操作系统是否使用深色模式
const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')
export const defaultSettings: AppSettings = { export const defaultSettings: AppSettings = {
// 系统Title // 系统Title
@@ -25,7 +25,7 @@ export const defaultSettings: AppSettings = {
// 语言 // 语言
language: LanguageEnum.ZH_CN, language: LanguageEnum.ZH_CN,
// 主题颜色 - 修改此值时需同步修改 src/styles/variables.scss // 主题颜色 - 修改此值时需同步修改 src/styles/variables.scss
themeColor: "#4080FF", themeColor: '#4080FF',
// 是否显示水印 // 是否显示水印
showWatermark: false, showWatermark: false,
// 水印内容 // 水印内容
@@ -33,8 +33,8 @@ export const defaultSettings: AppSettings = {
// 侧边栏配色方案 // 侧边栏配色方案
sidebarColorScheme: SidebarColor.CLASSIC_BLUE, sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
// 是否启用 AI 助手 // 是否启用 AI 助手
enableAiAssistant: false, enableAiAssistant: false
}; }
/** /**
* 认证功能配置 * 认证功能配置
@@ -48,20 +48,20 @@ export const authConfig = {
* *
* 适用场景后端没有刷新接口或不需要自动刷新的项目可设为false * 适用场景后端没有刷新接口或不需要自动刷新的项目可设为false
*/ */
enableTokenRefresh: true, enableTokenRefresh: true
} as const; } as const
// 主题色预设 - 经典配色方案 // 主题色预设 - 经典配色方案
// 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值 // 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值
export const themeColorPresets = [ export const themeColorPresets = [
"#4080FF", // Arco Design 蓝 - 现代感强 '#4080FF', // Arco Design 蓝 - 现代感强
"#1890FF", // Ant Design 蓝 - 经典商务 '#1890FF', // Ant Design 蓝 - 经典商务
"#409EFF", // Element Plus 蓝 - 清新自然 '#409EFF', // Element Plus 蓝 - 清新自然
"#FA8C16", // 活力橙 - 温暖友好 '#FA8C16', // 活力橙 - 温暖友好
"#722ED1", // 优雅紫 - 高端大气 '#722ED1', // 优雅紫 - 高端大气
"#13C2C2", // 青色 - 科技感 '#13C2C2', // 青色 - 科技感
"#52C41A", // 成功绿 - 活力清新 '#52C41A', // 成功绿 - 活力清新
"#F5222D", // 警示红 - 醒目强烈 '#F5222D', // 警示红 - 醒目强烈
"#2F54EB", // 深蓝 - 稳重专业 '#2F54EB', // 深蓝 - 稳重专业
"#EB2F96", // 品红 - 时尚个性 '#EB2F96' // 品红 - 时尚个性
]; ]

Some files were not shown because too many files have changed in this diff Show More