修改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 注释
requirePragma: false
# 在语句末尾添加分号
semi: true
semi: false
# 使用双引号而不是单引号
singleQuote: false
singleQuote: true
# 缩进使用 2 个空格
tabWidth: 2
# 在多行元素的末尾添加逗号ES5 支持的对象、数组等)
trailingComma: "es5"
trailingComma: "none"
# 使用空格而不是制表符缩进
useTabs: false
# Vue 文件中的 <script> 和 <style> 不增加额外的缩进

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
import request from "@/utils/request";
import request from '@/utils/request'
/**
* AI 命令请求参数
*/
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 {
/** 函数名称 */
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 {
/** 解析日志ID用于关联执行记录 */
parseLogId?: string;
parseLogId?: string
/** 是否成功解析 */
success: boolean;
success: boolean
/** 解析后的函数调用列表 */
functionCalls: FunctionCall[];
functionCalls: FunctionCall[]
/** AI 的理解和说明 */
explanation?: string;
explanation?: string
/** 置信度 (0-1) */
confidence?: number;
confidence?: number
/** 错误信息 */
error?: string;
error?: string
/** 原始 LLM 响应(用于调试) */
rawResponse?: string;
rawResponse?: string
}
/**
@@ -51,19 +51,19 @@ export interface AiCommandResponse {
*/
export interface AiExecuteRequest {
/** 关联的解析日志ID */
parseLogId?: string;
parseLogId?: string
/** 原始命令(用于审计) */
originalCommand?: string;
originalCommand?: string
/** 要执行的函数调用 */
functionCall: FunctionCall;
functionCall: FunctionCall
/** 确认模式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 {
/** 是否执行成功 */
success: boolean;
success: boolean
/** 执行结果数据 */
data?: any;
data?: any
/** 执行结果说明 */
message?: string;
message?: string
/** 影响的记录数 */
affectedRows?: number;
affectedRows?: number
/** 错误信息 */
error?: string;
error?: string
/** 记录ID用于追踪 */
recordId?: string;
recordId?: string
/** 需要用户确认 */
requiresConfirmation?: boolean;
requiresConfirmation?: boolean
/** 确认提示信息 */
confirmationPrompt?: string;
confirmationPrompt?: string
}
export interface AiCommandRecordPageQuery extends PageQuery {
keywords?: string;
executeStatus?: string;
parseSuccess?: boolean;
userId?: number;
isDangerous?: boolean;
provider?: string;
model?: string;
functionName?: string;
createTime?: [string, string];
keywords?: string
executeStatus?: string
parseSuccess?: boolean
userId?: number
isDangerous?: boolean
provider?: string
model?: string
functionName?: string
createTime?: [string, string]
}
export interface AiCommandRecordVO {
id: string;
userId: number;
username: string;
originalCommand: string;
provider?: string;
model?: string;
parseSuccess?: boolean;
functionCalls?: string;
explanation?: string;
confidence?: number;
parseErrorMessage?: string;
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
parseTime?: number;
functionName?: string;
functionArguments?: string;
executeStatus?: string;
executeResult?: string;
executeErrorMessage?: string;
affectedRows?: number;
isDangerous?: boolean;
requiresConfirmation?: boolean;
userConfirmed?: boolean;
executionTime?: number;
ipAddress?: string;
userAgent?: string;
currentRoute?: string;
createTime?: string;
updateTime?: string;
remark?: string;
id: string
userId: number
username: string
originalCommand: string
provider?: string
model?: string
parseSuccess?: boolean
functionCalls?: string
explanation?: string
confidence?: number
parseErrorMessage?: string
inputTokens?: number
outputTokens?: number
totalTokens?: number
parseTime?: number
functionName?: string
functionArguments?: string
executeStatus?: string
executeResult?: string
executeErrorMessage?: string
affectedRows?: number
isDangerous?: boolean
requiresConfirmation?: boolean
userConfirmed?: boolean
executionTime?: number
ipAddress?: string
userAgent?: string
currentRoute?: string
createTime?: string
updateTime?: string
remark?: string
}
/**
@@ -146,10 +146,10 @@ class AiCommandApi {
*/
static parseCommand(data: AiCommandRequest): Promise<AiCommandResponse> {
return request<any, AiCommandResponse>({
url: "/api/v1/ai/command/parse",
method: "post",
data,
});
url: '/api/v1/ai/command/parse',
method: 'post',
data
})
}
/**
@@ -160,10 +160,10 @@ class AiCommandApi {
*/
static executeCommand(data: AiExecuteRequest): Promise<any> {
return request<any, any>({
url: "/api/v1/ai/command/execute",
method: "post",
data,
});
url: '/api/v1/ai/command/execute',
method: 'post',
data
})
}
/**
@@ -171,10 +171,10 @@ class AiCommandApi {
*/
static getCommandRecordPage(queryParams: AiCommandRecordPageQuery) {
return request<any, PageResult<AiCommandRecordVO[]>>({
url: "/api/v1/ai/command/records",
method: "get",
params: queryParams,
});
url: '/api/v1/ai/command/records',
method: 'get',
params: queryParams
})
}
/**
@@ -183,9 +183,9 @@ class AiCommandApi {
static rollbackCommand(recordId: string) {
return request({
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 = {
/** 登录接口*/
login(data: LoginFormData) {
const formData = new FormData();
formData.append("username", "admin");
formData.append("password", data.password);
formData.append("captchaKey", data.captchaKey);
formData.append("captchaCode", data.captchaCode);
const formData = new FormData()
formData.append('username', 'admin')
formData.append('password', data.password)
formData.append('captchaKey', data.captchaKey)
formData.append('captchaCode', data.captchaCode)
return request<any, LoginResult>({
url: `${AUTH_BASE_URL}/login`,
method: "post",
method: 'post',
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
'Content-Type': 'multipart/form-data'
}
})
},
/** 刷新 token 接口*/
refreshToken(refreshToken: string) {
return request<any, LoginResult>({
url: `${AUTH_BASE_URL}/refresh-token`,
method: "post",
method: 'post',
params: { refreshToken },
headers: {
Authorization: "no-auth",
},
});
Authorization: 'no-auth'
}
})
},
/** 退出登录接口 */
logout() {
return request({
url: `${AUTH_BASE_URL}/logout`,
method: "delete",
});
method: 'delete'
})
},
/** 获取验证码接口*/
getCaptcha() {
return request<any, CaptchaInfo>({
url: `${AUTH_BASE_URL}/captcha`,
method: "get",
});
},
};
method: 'get'
})
}
}
export default AuthAPI;
export default AuthAPI
/** 登录表单数据 */
export interface LoginFormData {
/** 用户名 */
username: string;
username: string
/** 密码 */
password: string;
password: string
/** 验证码缓存key */
captchaKey: string;
captchaKey: string
/** 验证码 */
captchaCode: string;
captchaCode: string
/** 记住我 */
rememberMe: boolean;
rememberMe: boolean
}
/** 登录响应 */
export interface LoginResult {
/** 访问令牌 */
accessToken: string;
accessToken: string
/** 刷新令牌 */
refreshToken: string;
refreshToken: string
/** 令牌类型 */
tokenType: string;
tokenType: string
/** 过期时间(秒) */
expiresIn: number;
expiresIn: number
}
/** 验证码信息 */
export interface CaptchaInfo {
/** 验证码缓存key */
captchaKey: string;
captchaKey: string
/** 验证码图片Base64字符串 */
captchaBase64: string;
captchaBase64: string
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import request from "@/utils/request";
const AUTH_BASE_URL = "/api2";
import request from '@/utils/request'
const AUTH_BASE_URL = '/api2'
/*
* 收入确认
@@ -7,19 +7,19 @@ const AUTH_BASE_URL = "/api2";
// 新增收入确认
export const FinanceConfirm = (data: any) => {
const formData = new FormData();
formData.append("times", data.times);
formData.append("ContractNo", data.ContractNo);
formData.append("CustomerID", data.CustomerID);
formData.append("amount", data.amount);
formData.append("allocate", data.allocate);
formData.append("personincharge", data.personincharge);
const formData = new FormData()
formData.append('times', data.times)
formData.append('ContractNo', data.ContractNo)
formData.append('CustomerID', data.CustomerID)
formData.append('amount', data.amount)
formData.append('allocate', data.allocate)
formData.append('personincharge', data.personincharge)
return request({
url: `${AUTH_BASE_URL}/finance/confirm`,
method: "post",
method: 'post',
data: formData,
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 = {
/** 获取数据表分页列表 */
getTablePage(params: TablePageQuery) {
return request<any, PageResult<TablePageVO[]>>({
url: `${GENERATOR_BASE_URL}/table/page`,
method: "get",
params,
});
method: 'get',
params
})
},
/** 获取代码生成配置 */
getGenConfig(tableName: string) {
return request<any, GenConfigForm>({
url: `${GENERATOR_BASE_URL}/${tableName}/config`,
method: "get",
});
method: 'get'
})
},
/** 获取代码生成配置 */
saveGenConfig(tableName: string, data: GenConfigForm) {
return request({
url: `${GENERATOR_BASE_URL}/${tableName}/config`,
method: "post",
data,
});
method: 'post',
data
})
},
/** 获取代码生成预览数据 */
getPreviewData(tableName: string, pageType?: "classic" | "curd") {
getPreviewData(tableName: string, pageType?: 'classic' | 'curd') {
return request<any, GeneratorPreviewVO[]>({
url: `${GENERATOR_BASE_URL}/${tableName}/preview`,
method: "get",
params: pageType ? { pageType } : undefined,
});
method: 'get',
params: pageType ? { pageType } : undefined
})
},
/** 重置代码生成配置 */
resetGenConfig(tableName: string) {
return request({
url: `${GENERATOR_BASE_URL}/${tableName}/config`,
method: "delete",
});
method: 'delete'
})
},
/**
@@ -51,149 +51,149 @@ const GeneratorAPI = {
* @param url
* @param fileName
*/
download(tableName: string, pageType?: "classic" | "curd") {
download(tableName: string, pageType?: 'classic' | 'curd') {
return request({
url: `${GENERATOR_BASE_URL}/${tableName}/download`,
method: "get",
method: 'get',
params: pageType ? { pageType } : undefined,
responseType: "blob",
responseType: 'blob'
}).then((response) => {
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 a = document.createElement("a");
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
});
},
};
const blob = new Blob([response.data], { type: 'application/zip' })
const a = document.createElement('a')
const url = window.URL.createObjectURL(blob)
a.href = url
a.download = fileName
a.click()
window.URL.revokeObjectURL(url)
})
}
}
export default GeneratorAPI;
export default GeneratorAPI
/** 代码生成预览对象 */
export interface GeneratorPreviewVO {
/** 文件生成路径 */
path: string;
path: string
/** 文件名称 */
fileName: string;
fileName: string
/** 文件内容 */
content: string;
content: string
}
/** 数据表分页查询参数 */
export interface TablePageQuery extends PageQuery {
/** 关键字(表名) */
keywords?: string;
keywords?: string
}
/** 数据表分页对象 */
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 {
/** 主键 */
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 */
pageType?: "classic" | "curd";
pageType?: 'classic' | 'curd'
/** 要移除的表前缀,如 sys_ */
removeTablePrefix?: string;
removeTablePrefix?: string
}
/** 字段配置 */
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 = {
/** 上传文件 (传入 FormData上传进度回调 */
upload(formData: FormData, onProgress?: (percent: number) => void) {
return request<any, FileInfo>({
url: "/api/v1/files",
method: "post",
url: '/api/v1/files',
method: 'post',
data: formData,
headers: { "Content-Type": "multipart/form-data" },
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress?.(percent);
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
onProgress?.(percent)
}
},
});
}
})
},
/** 上传文件(传入 File */
uploadFile(file: File) {
const formData = new FormData();
formData.append("file", file);
const formData = new FormData()
formData.append('file', file)
return request<any, FileInfo>({
url: "/api/v1/files",
method: "post",
url: '/api/v1/files',
method: 'post',
data: formData,
headers: { "Content-Type": "multipart/form-data" },
});
headers: { 'Content-Type': 'multipart/form-data' }
})
},
/** 删除文件 */
delete(filePath?: string) {
return request({
url: "/api/v1/files",
method: "delete",
params: { filePath },
});
url: '/api/v1/files',
method: 'delete',
params: { filePath }
})
},
/** 下载文件 */
download(url: string, fileName?: string) {
return request({
url,
method: "get",
responseType: "blob",
method: 'get',
responseType: 'blob'
}).then((res) => {
const blob = new Blob([res.data]);
const a = document.createElement("a");
const urlObject = window.URL.createObjectURL(blob);
a.href = urlObject;
a.download = fileName || "下载文件";
a.click();
window.URL.revokeObjectURL(urlObject);
});
},
};
const blob = new Blob([res.data])
const a = document.createElement('a')
const urlObject = window.URL.createObjectURL(blob)
a.href = urlObject
a.download = fileName || '下载文件'
a.click()
window.URL.revokeObjectURL(urlObject)
})
}
}
export default FileAPI;
export default FileAPI
export interface FileInfo {
name: string;
url: string;
name: 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 = {
/** 获取配置分页数据 */
getPage(queryParams?: ConfigPageQuery) {
return request<any, PageResult<ConfigPageVO[]>>({
url: `${CONFIG_BASE_URL}/page`,
method: "get",
params: queryParams,
});
method: 'get',
params: queryParams
})
},
/** 获取配置表单数据 */
getFormData(id: string) {
return request<any, ConfigForm>({
url: `${CONFIG_BASE_URL}/${id}/form`,
method: "get",
});
method: 'get'
})
},
/** 新增配置 */
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) {
return request({ url: `${CONFIG_BASE_URL}/${id}`, method: "put", data });
return request({ url: `${CONFIG_BASE_URL}/${id}`, method: 'put', data })
},
/** 删除配置 */
deleteById(id: string) {
return request({ url: `${CONFIG_BASE_URL}/${id}`, method: "delete" });
return request({ url: `${CONFIG_BASE_URL}/${id}`, method: 'delete' })
},
/** 刷新配置缓存 */
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 {
/** 搜索关键字 */
keywords?: string;
keywords?: string
}
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 {
/** 主键 */
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 = {
/** 获取部门树形列表 */
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() {
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) {
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) {
return request({ url: `${DEPT_BASE_URL}`, method: "post", data });
return request({ url: `${DEPT_BASE_URL}`, method: 'post', data })
},
/** 修改部门 */
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) {
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 {
/** 搜索关键字 */
keywords?: string;
keywords?: string
/** 状态 */
status?: number;
status?: number
}
export interface DeptVO {
/** 子部门 */
children?: DeptVO[];
children?: DeptVO[]
/** 创建时间 */
createTime?: Date;
createTime?: Date
/** 部门ID */
id?: string;
id?: string
/** 部门名称 */
name?: string;
name?: string
/** 部门编号 */
code?: string;
code?: string
/** 父部门ID */
parentid?: string;
parentid?: string
/** 排序 */
sort?: number;
sort?: number
/** 状态(1:启用0:禁用) */
status?: number;
status?: number
/** 修改时间 */
updateTime?: Date;
updateTime?: Date
}
export interface DeptForm {
/** 部门ID(新增不填) */
id?: string;
id?: string
/** 部门名称 */
name?: string;
name?: string
/** 部门编号 */
code?: string;
code?: string
/** 父部门ID */
parentId: string;
parentId: string
/** 排序 */
sort?: number;
sort?: number
/** 状态(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 = {
/** 字典分页列表 */
getPage(queryParams: DictPageQuery) {
return request<any, PageResult<DictPageVO[]>>({
url: `${DICT_BASE_URL}/page`,
method: "get",
params: queryParams,
});
method: 'get',
params: queryParams
})
},
/** 字典列表 */
getList() {
return request<any, OptionType[]>({ url: `${DICT_BASE_URL}`, method: "get" });
return request<any, OptionType[]>({ url: `${DICT_BASE_URL}`, method: 'get' })
},
/** 字典表单数据 */
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) {
return request({ url: `${DICT_BASE_URL}`, method: "post", data });
return request({ url: `${DICT_BASE_URL}`, method: 'post', data })
},
/** 修改字典 */
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) {
return request({ url: `${DICT_BASE_URL}/${ids}`, method: "delete" });
return request({ url: `${DICT_BASE_URL}/${ids}`, method: 'delete' })
},
/** 获取字典项分页列表 */
getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) {
return request<any, PageResult<DictItemPageVO[]>>({
url: `${DICT_BASE_URL}/${dictCode}/items/page`,
method: "get",
params: queryParams,
});
method: 'get',
params: queryParams
})
},
/** 获取字典项列表 */
getDictItems(dictCode: string) {
return request<any, DictItemOption[]>({
url: `${DICT_BASE_URL}/${dictCode}/items`,
method: "get",
});
method: 'get'
})
},
/** 新增字典项 */
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) {
return request<any, DictItemForm>({
url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`,
method: "get",
});
method: 'get'
})
},
/** 修改字典项 */
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) {
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 {
/** 搜索关键字 */
keywords?: string;
keywords?: string
/** 状态(1:启用;0:禁用) */
status?: number;
status?: number
}
export interface DictPageVO {
/** 字典ID */
id: string;
id: string
/** 字典名称 */
name: string;
name: string
/** 字典编码 */
dictCode: string;
dictCode: string
/** 状态(1:启用;0:禁用) */
status: number;
status: number
}
export interface DictForm {
/** 字典ID(新增不填) */
id?: string;
id?: string
/** 字典名称 */
name?: string;
name?: string
/** 字典编码 */
dictCode?: string;
dictCode?: string
/** 状态(1:启用;0:禁用) */
status?: number;
status?: number
/** 备注 */
remark?: string;
remark?: string
}
export interface DictItemPageQuery extends PageQuery {
/** 搜索关键字 */
keywords?: string;
keywords?: string
/** 字典编码 */
dictCode?: string;
dictCode?: string
}
export interface DictItemPageVO {
/** 字典项ID */
id: string;
id: string
/** 字典编码 */
dictCode: string;
dictCode: string
/** 字典项值 */
value: string;
value: string
/** 字典项标签 */
label: string;
label: string
/** 状态(1:启用;0:禁用) */
status: number;
status: number
/** 排序 */
sort?: number;
sort?: number
}
export interface DictItemForm {
/** 字典项ID(新增不填) */
id?: string;
id?: string
/** 字典编码 */
dictCode?: string;
dictCode?: string
/** 字典项值 */
value?: string;
value?: string
/** 字典项标签 */
label?: string;
label?: string
/** 状态(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 {
/** 值 */
value: number | string;
value: number | string
/** 标签 */
label: string;
label: string
/** 标签类型 */
tagType?: "" | "success" | "info" | "warning" | "danger";
[key: string]: any;
tagType?: '' | 'success' | 'info' | 'warning' | 'danger'
[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 = {
/** 获取日志分页列表 */
getPage(queryParams: LogPageQuery) {
return request<any, PageResult<LogPageVO[]>>({
url: `${LOG_BASE_URL}/page`,
method: "get",
params: queryParams,
});
method: 'get',
params: queryParams
})
},
/** 获取访问趋势 */
getVisitTrend(queryParams: VisitTrendQuery) {
return request<any, VisitTrendVO>({
url: `${LOG_BASE_URL}/visit-trend`,
method: "get",
params: queryParams,
});
method: 'get',
params: queryParams
})
},
/** 获取访问统计 */
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 {
/** 搜索关键字 */
keywords?: string;
keywords?: string
/** 操作时间 */
createTime?: [string, string];
createTime?: [string, string]
}
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: 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 {
/** 日期列表 */
dates: string[];
dates: string[]
/** 浏览量(PV) */
pvList: number[];
pvList: number[]
/** 访客数(UV) */
uvList: number[];
uvList: number[]
/** IP数 */
ipList: number[];
ipList: number[]
}
export interface VisitTrendQuery {
/** 开始日期 */
startDate: string;
startDate: string
/** 结束日期 */
endDate: string;
endDate: string
}
export interface VisitStatsVO {
/** 今日访客数(UV) */
todayUvCount: number;
todayUvCount: number
/** 总访客数 */
totalUvCount: number;
totalUvCount: number
/** 访客数同比增长率(相对于昨天同一时间段的增长率) */
uvGrowthRate: number;
uvGrowthRate: number
/** 今日浏览量(PV) */
todayPvCount: number;
todayPvCount: number
/** 总浏览量 */
totalPvCount: number;
totalPvCount: number
/** 同比增长率(相对于昨天同一时间段的增长率) */
pvGrowthRate: number;
pvGrowthRate: number
}

View File

@@ -1,135 +1,135 @@
import request from "@/utils/request";
const MENU_BASE_URL = "/api/v1/menus";
import request from '@/utils/request'
const MENU_BASE_URL = '/api/v1/menus'
const MenuAPI = {
/** 获取当前用户的路由列表 */
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) {
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) {
return request<any, OptionType[]>({
url: `${MENU_BASE_URL}/options`,
method: "get",
params: { onlyParent },
});
method: 'get',
params: { onlyParent }
})
},
/** 获取菜单表单数据 */
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) {
return request({ url: `${MENU_BASE_URL}`, method: "post", data });
return request({ url: `${MENU_BASE_URL}`, method: 'post', data })
},
/** 修改菜单 */
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) {
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 {
/** 搜索关键字 */
keywords?: string;
keywords?: string
}
import type { MenuTypeEnum } from "@/enums/system/menu-enum";
import type { MenuTypeEnum } from '@/enums/system/menu-enum'
export interface MenuVO {
/** 子菜单 */
children?: MenuVO[];
children?: MenuVO[]
/** 组件路径 */
component?: string;
component?: string
/** ICON */
icon?: string;
icon?: string
/** 菜单ID */
id?: string;
id?: string
/** 菜单名称 */
name?: string;
name?: string
/** 父菜单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:隐藏) */
visible?: number;
visible?: number
}
export interface MenuForm {
/** 菜单ID */
id?: string;
id?: string
/** 父菜单ID */
parentId?: string;
parentId?: string
/** 菜单名称 */
name?: string;
name?: string
/** 是否可见(1-是 0-否) */
visible: number;
visible: number
/** 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 {
key: string;
value: string;
key: string
value: string
}
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 {
/** 【目录】只有一个子路由是否始终显示 */
alwaysShow?: boolean;
alwaysShow?: boolean
/** 是否隐藏(true-是 false-否) */
hidden?: boolean;
hidden?: boolean
/** ICON */
icon?: string;
icon?: string
/** 【菜单】是否开启页面缓存 */
keepAlive?: boolean;
keepAlive?: boolean
/** 路由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 = {
/** 获取通知公告分页数据 */
getPage(queryParams?: NoticePageQuery) {
return request<any, PageResult<NoticePageVO[]>>({
url: `${NOTICE_BASE_URL}/page`,
method: "get",
params: queryParams,
});
method: 'get',
params: queryParams
})
},
/** 获取通知公告表单数据 */
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) {
return request({ url: `${NOTICE_BASE_URL}`, method: "post", data });
return request({ url: `${NOTICE_BASE_URL}`, method: 'post', data })
},
/** 更新通知公告 */
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) {
return request({ url: `${NOTICE_BASE_URL}/${ids}`, method: "delete" });
return request({ url: `${NOTICE_BASE_URL}/${ids}`, method: 'delete' })
},
/** 发布通知 */
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) {
return request({ url: `${NOTICE_BASE_URL}/${id}/revoke`, method: "put" });
return request({ url: `${NOTICE_BASE_URL}/${id}/revoke`, method: 'put' })
},
/** 查看通知 */
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() {
return request({ url: `${NOTICE_BASE_URL}/read-all`, method: "put" });
return request({ url: `${NOTICE_BASE_URL}/read-all`, method: 'put' })
},
/** 获取我的通知分页列表 */
getMyNoticePage(queryParams?: NoticePageQuery) {
return request<any, PageResult<NoticePageVO[]>>({
url: `${NOTICE_BASE_URL}/my-page`,
method: "get",
params: queryParams,
});
},
};
method: 'get',
params: queryParams
})
}
}
export default NoticeAPI;
export default NoticeAPI
export interface NoticePageQuery extends PageQuery {
/** 标题 */
title?: string;
title?: string
/** 发布状态(0:草稿;1:已发布;2:已撤回) */
publishStatus?: number;
publishStatus?: number
/** 是否已读(1:是;0:否) */
isRead?: number;
isRead?: number
}
export interface NoticeForm {
/** 通知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(多个以英文逗号(,)分割) */
targetUserIds?: string;
targetUserIds?: string
}
export interface NoticePageVO {
/** 通知ID */
id: string;
id: string
/** 标题 */
title?: string;
title?: string
/** 内容 */
content?: string;
content?: string
/** 类型 */
type?: number;
type?: number
/** 发布人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 {
/** 通知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 = {
/** 获取角色分页数据 */
getPage(queryParams?: RolePageQuery) {
return request<any, PageResult<RolePageVO[]>>({
url: `${ROLE_BASE_URL}/page`,
method: "get",
params: queryParams,
});
method: 'get',
params: queryParams
})
},
/** 获取角色下拉数据源 */
getOptions() {
return request<any, OptionType[]>({ url: `${ROLE_BASE_URL}/options`, method: "get" });
return request<any, OptionType[]>({ url: `${ROLE_BASE_URL}/options`, method: 'get' })
},
/** 获取角色的菜单ID集合 */
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[]) {
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) {
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) {
return request({ url: `${ROLE_BASE_URL}`, method: "post", data });
return request({ url: `${ROLE_BASE_URL}`, method: 'post', data })
},
/** 更新角色 */
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) {
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 {
/** 搜索关键字 */
keywords?: string;
keywords?: string
}
export interface RolePageVO {
/** 角色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 {
/** 角色ID */
id?: string;
id?: string
/** 角色编码 */
code?: string;
code?: string
/** 数据权限 */
dataScope?: number;
dataScope?: number
/** 角色名称 */
name?: string;
name?: string
/** 排序 */
sort?: number;
sort?: number
/** 角色状态(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 = {
/**
@@ -11,8 +11,8 @@ const UserAPI = {
getInfo() {
return request<any, UserInfo>({
url: `${USER_BASE_URL}/me`,
method: "get",
});
method: 'get'
})
},
/**
@@ -23,9 +23,9 @@ const UserAPI = {
getPage(queryParams: UserPageQuery) {
return request<any, PageResult<UserPageVO[]>>({
url: `${USER_BASE_URL}/page`,
method: "get",
params: queryParams,
});
method: 'get',
params: queryParams
})
},
/**
@@ -37,8 +37,8 @@ const UserAPI = {
getFormData(userId: string) {
return request<any, UserForm>({
url: `${USER_BASE_URL}/${userId}/form`,
method: "get",
});
method: 'get'
})
},
/**
@@ -49,9 +49,9 @@ const UserAPI = {
create(data: UserForm) {
return request({
url: `${USER_BASE_URL}`,
method: "post",
data,
});
method: 'post',
data
})
},
/**
@@ -63,9 +63,9 @@ const UserAPI = {
update(id: string, data: UserForm) {
return request({
url: `${USER_BASE_URL}/${id}`,
method: "put",
data,
});
method: 'put',
data
})
},
/**
@@ -77,9 +77,9 @@ const UserAPI = {
resetPassword(id: string, password: string) {
return request({
url: `${USER_BASE_URL}/${id}/password/reset`,
method: "put",
params: { password },
});
method: 'put',
params: { password }
})
},
/**
@@ -90,17 +90,17 @@ const UserAPI = {
deleteByIds(ids: string | number) {
return request({
url: `${USER_BASE_URL}/${ids}`,
method: "delete",
});
method: 'delete'
})
},
/** 下载用户导入模板 */
downloadTemplate() {
return request({
url: `${USER_BASE_URL}/template`,
method: "get",
responseType: "blob",
});
method: 'get',
responseType: 'blob'
})
},
/**
@@ -111,10 +111,10 @@ const UserAPI = {
export(queryParams: UserPageQuery) {
return request({
url: `${USER_BASE_URL}/export`,
method: "get",
method: 'get',
params: queryParams,
responseType: "blob",
});
responseType: 'blob'
})
},
/**
@@ -124,79 +124,79 @@ const UserAPI = {
* @param file 导入文件
*/
import(deptId: string, file: File) {
const formData = new FormData();
formData.append("file", file);
const formData = new FormData()
formData.append('file', file)
return request<any, ExcelResult>({
url: `${USER_BASE_URL}/import`,
method: "post",
method: 'post',
params: { deptId },
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
'Content-Type': 'multipart/form-data'
}
})
},
/** 获取个人中心用户信息 */
getProfile() {
return request<any, UserProfileVO>({
url: `${USER_BASE_URL}/profile`,
method: "get",
});
method: 'get'
})
},
/** 修改个人中心用户信息 */
updateProfile(data: UserProfileForm) {
return request({
url: `${USER_BASE_URL}/profile`,
method: "put",
data,
});
method: 'put',
data
})
},
/** 修改个人中心用户密码 */
changePassword(data: PasswordChangeForm) {
return request({
url: `${USER_BASE_URL}/password`,
method: "put",
data,
});
method: 'put',
data
})
},
/** 发送短信验证码(绑定或更换手机号)*/
sendMobileCode(mobile: string) {
return request({
url: `${USER_BASE_URL}/mobile/code`,
method: "post",
params: { mobile },
});
method: 'post',
params: { mobile }
})
},
/** 绑定或更换手机号 */
bindOrChangeMobile(data: MobileUpdateForm) {
return request({
url: `${USER_BASE_URL}/mobile`,
method: "put",
data,
});
method: 'put',
data
})
},
/** 发送邮箱验证码(绑定或更换邮箱)*/
sendEmailCode(email: string) {
return request({
url: `${USER_BASE_URL}/email/code`,
method: "post",
params: { email },
});
method: 'post',
params: { email }
})
},
/** 绑定或更换邮箱 */
bindOrChangeEmail(data: EmailUpdateForm) {
return request({
url: `${USER_BASE_URL}/email`,
method: "put",
data,
});
method: 'put',
data
})
},
/**
@@ -205,32 +205,32 @@ const UserAPI = {
getOptions() {
return request<any, OptionType[]>({
url: `${USER_BASE_URL}/options`,
method: "get",
});
},
};
method: 'get'
})
}
}
export default UserAPI;
export default UserAPI
/** 登录用户信息 */
export interface UserInfo {
/** 用户ID */
userId?: string;
userId?: string
/** 用户名 */
username?: string;
username?: string
/** 昵称 */
nickname?: string;
nickname?: string
/** 头像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 {
/** 搜索关键字 */
keywords?: string;
keywords?: string
/** 用户状态 */
status?: number;
status?: number
/** 部门ID */
deptId?: string;
deptId?: string
/** 开始时间 */
createTime?: [string, string];
createTime?: [string, string]
}
/** 用户分页对象 */
export interface UserPageVO {
/** 用户ID */
id: string;
id: string
/** 用户头像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:禁用) */
status?: number;
status?: number
/** 用户名 */
username?: string;
username?: string
}
/** 用户表单类型 */
export interface UserForm {
/** 用户ID */
id?: string;
id?: string
/** 用户头像 */
avatar?: string;
avatar?: string
/** 部门ID */
deptId?: string;
deptId?: string
/** 邮箱 */
email?: string;
email?: string
/** 性别 */
gender?: number;
gender?: number
/** 手机号 */
mobile?: string;
mobile?: string
/** 昵称 */
nickname?: string;
nickname?: string
/** 角色ID集合 */
roleIds?: number[];
roleIds?: number[]
/** 用户状态(1:正常;0:禁用) */
status?: number;
status?: number
/** 用户名 */
username?: string;
username?: string
}
/** 个人中心用户信息 */
export interface UserProfileVO {
/** 用户ID */
id?: string;
id?: string
/** 用户名 */
username?: string;
username?: string
/** 昵称 */
nickname?: string;
nickname?: string
/** 头像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 {
/** 用户ID */
id?: string;
id?: string
/** 用户名 */
username?: string;
username?: string
/** 昵称 */
nickname?: string;
nickname?: string
/** 头像URL */
avatar?: string;
avatar?: string
/** 性别 */
gender?: number;
gender?: number
/** 手机号 */
mobile?: string;
mobile?: string
/** 邮箱 */
email?: string;
email?: string
}
/** 修改密码表单 */
export interface PasswordChangeForm {
/** 原密码 */
oldPassword?: string;
oldPassword?: string
/** 新密码 */
newPassword?: string;
newPassword?: string
/** 确认新密码 */
confirmPassword?: string;
confirmPassword?: string
}
/** 修改手机表单 */
export interface MobileUpdateForm {
/** 手机号 */
mobile?: string;
mobile?: string
/** 验证码 */
code?: string;
code?: string
}
/** 修改邮箱表单 */
export interface EmailUpdateForm {
/** 邮箱 */
email?: string;
email?: string
/** 验证码 */
code?: string;
code?: string
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
<!-- Label -->
<template #label>
<span class="flex-y-center">
{{ item?.label || "" }}
{{ item?.label || '' }}
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
@@ -55,7 +55,7 @@
<!-- 展开/收起 -->
<template v-if="isExpandable && formItems.length > showNumber">
<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" />
</el-link>
</template>
@@ -66,96 +66,96 @@
</template>
<script setup lang="ts">
import type { IObject, IForm, ISearchConfig, ISearchComponent } from "./types";
import { ArrowUp, ArrowDown } from "@element-plus/icons-vue";
import type { FormInstance } from "element-plus";
import InputTag from "@/components/InputTag/index.vue";
import type { IObject, IForm, ISearchConfig, ISearchComponent } from './types'
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import InputTag from '@/components/InputTag/index.vue'
// 定义接收的属性
const props = defineProps<{ searchConfig: ISearchConfig }>();
const props = defineProps<{ searchConfig: ISearchConfig }>()
// 自定义事件
const emit = defineEmits<{
queryClick: [queryParams: IObject];
resetClick: [queryParams: IObject];
}>();
queryClick: [queryParams: IObject]
resetClick: [queryParams: IObject]
}>()
// 组件映射表
const componentMap = new Map<ISearchComponent, any>([
// @ts-ignore
["input", markRaw(ElInput)], // @ts-ignore
["select", markRaw(ElSelect)], // @ts-ignore
["cascader", markRaw(ElCascader)], // @ts-ignore
["input-number", markRaw(ElInputNumber)], // @ts-ignore
["date-picker", markRaw(ElDatePicker)], // @ts-ignore
["time-picker", markRaw(ElTimePicker)], // @ts-ignore
["time-select", markRaw(ElTimeSelect)], // @ts-ignore
["tree-select", markRaw(ElTreeSelect)], // @ts-ignore
["input-tag", markRaw(ElInputTag)], // @ts-ignore
["custom-tag", markRaw(InputTag)],
]);
['input', markRaw(ElInput)], // @ts-ignore
['select', markRaw(ElSelect)], // @ts-ignore
['cascader', markRaw(ElCascader)], // @ts-ignore
['input-number', markRaw(ElInputNumber)], // @ts-ignore
['date-picker', markRaw(ElDatePicker)], // @ts-ignore
['time-picker', markRaw(ElTimePicker)], // @ts-ignore
['time-select', markRaw(ElTimeSelect)], // @ts-ignore
['tree-select', markRaw(ElTreeSelect)], // @ts-ignore
['input-tag', markRaw(ElInputTag)], // @ts-ignore
['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
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(() =>
isExpandable.value ? (props.searchConfig?.showNumber ?? 3) : formItems.length
);
)
// 卡片组件自定义属性(阴影、自定义边距样式等)
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位置、宽度、对齐方式等
const formAttrs = computed<IForm>(() => {
return { inline: true, ...props.searchConfig?.form };
});
return { inline: true, ...props.searchConfig?.form }
})
// 是否使用自适应网格布局
const isGrid = computed(() =>
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"
: "flex flex-wrap gap-x-8 gap-y-4"
);
? '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'
)
// 获取tooltip提示框属性
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 = () => {
queryFormRef.value?.resetFields();
emit("resetClick", queryParams);
};
queryFormRef.value?.resetFields()
emit('resetClick', queryParams)
}
onMounted(() => {
formItems.forEach((item) => {
if (item?.initFn) {
item.initFn(item);
item.initFn(item)
}
if (["input-tag", "custom-tag", "cascader"].includes(item?.type ?? "")) {
queryParams[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : [];
} else if (item.type === "input-number") {
queryParams[item.prop] = item.initialValue ?? null;
if (['input-tag', 'custom-tag', 'cascader'].includes(item?.type ?? '')) {
queryParams[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : []
} else if (item.type === 'input-number') {
queryParams[item.prop] = item.initialValue ?? null
} else {
queryParams[item.prop] = item.initialValue ?? "";
queryParams[item.prop] = item.initialValue ?? ''
}
});
});
})
})
// 暴露的属性和方法
defineExpose({
// 获取分页数据
getQueryParams: () => queryParams,
// 显示/隐藏 SearchForm
toggleVisible: () => (visible.value = !visible.value),
});
toggleVisible: () => (visible.value = !visible.value)
})
</script>
<style lang="scss" scoped>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,15 +15,15 @@
<script setup lang="ts">
// 引入 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 是必须的一步
import { CanvasRenderer } from "echarts/renderers";
import { CanvasRenderer } from 'echarts/renderers'
import { useResizeObserver } from "@vueuse/core";
import { useResizeObserver } from '@vueuse/core'
// 按需注册组件
echarts.use([
@@ -33,49 +33,49 @@ echarts.use([
PieChart,
GridComponent,
TooltipComponent,
LegendComponent,
]);
LegendComponent
])
const props = defineProps<{
options: echarts.EChartsCoreOption;
width?: string;
height?: string;
}>();
options: echarts.EChartsCoreOption
width?: string
height?: string
}>()
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const chartRef = ref<HTMLDivElement | null>(null)
let chartInstance: echarts.ECharts | null = null
// 初始化图表
const initChart = () => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value);
chartInstance = echarts.init(chartRef.value)
if (props.options) {
chartInstance.setOption(props.options);
chartInstance.setOption(props.options)
}
}
};
}
// 监听尺寸变化,自动调整
useResizeObserver(chartRef, () => {
chartInstance?.resize();
});
chartInstance?.resize()
})
// 监听 options 变化,更新图表
watch(
() => props.options,
(newOptions) => {
if (chartInstance && newOptions) {
chartInstance.setOption(newOptions);
chartInstance.setOption(newOptions)
}
},
{ deep: true }
);
)
onMounted(() => {
nextTick(() => initChart());
});
nextTick(() => initChart())
})
onBeforeUnmount(() => {
chartInstance?.dispose();
});
chartInstance?.dispose()
})
</script>

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
<template>
<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>
<script lang="ts">
import {defineComponent, ref} from 'vue'
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
@@ -14,15 +13,14 @@ export default defineComponent({
console.log(value)
}
return {
input:ref(''),
input: ref(''),
num,
handleChange,
handleChange
}
},
}
})
</script>
<style scoped>
a {
color: #42b983;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,55 +41,55 @@
</template>
<script setup lang="ts">
import { useElementHover } from "@vueuse/core";
import { useElementHover } from '@vueuse/core'
const emit = defineEmits(["close"]);
const emit = defineEmits(['close'])
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>(), {
speed: 70,
direction: "left",
type: "default",
direction: 'left',
type: 'default',
showClose: false,
typewriter: false,
typewriterSpeed: 100,
});
typewriterSpeed: 100
})
// 容器元素引用
const containerRef = ref<HTMLElement | null>(null);
const containerRef = ref<HTMLElement | null>(null)
// 使用 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(() => {
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 风险
*/
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 变量传递给样式
*/
const scrollStyle = computed(() => ({
"--animation-duration": `${animationDuration.value}s`,
"--animation-play-state": shouldScroll.value ? "running" : "paused",
"--animation-direction": props.direction === "left" ? "normal" : "reverse",
}));
'--animation-duration': `${animationDuration.value}s`,
'--animation-play-state': shouldScroll.value ? 'running' : 'paused',
'--animation-direction': props.direction === 'left' ? 'normal' : 'reverse'
}))
/**
* 计算动画持续时间
@@ -130,71 +130,71 @@ const scrollStyle = computed(() => ({
*/
const calculateDuration = () => {
if (scrollContent.value) {
const contentWidth = scrollContent.value.scrollWidth / 2;
animationDuration.value = contentWidth / props.speed;
const contentWidth = scrollContent.value.scrollWidth / 2
animationDuration.value = contentWidth / props.speed
}
};
}
/**
* 处理关闭按钮点击事件
* 触发 close 事件,并直接销毁当前组件
*/
const handleRightIconClick = () => {
emit("close");
emit('close')
// 获取当前组件的DOM元素
if (containerRef.value) {
// 从DOM中移除元素
containerRef.value.remove();
containerRef.value.remove()
}
};
}
/**
* 启动打字机效果
* 逐字显示文本内容,完成后设置状态以开始滚动
*/
const startTypewriter = () => {
let index = 0;
currentText.value = "";
isTypewriterComplete.value = false; // 重置状态
let index = 0
currentText.value = ''
isTypewriterComplete.value = false // 重置状态
// 递归函数,逐字添加文本
const type = () => {
if (index < props.text.length) {
// 添加一个字符
currentText.value += props.text[index];
index++;
currentText.value += props.text[index]
index++
// 设置下一个字符的延迟
typewriterTimer = setTimeout(type, props.typewriterSpeed);
typewriterTimer = setTimeout(type, props.typewriterSpeed)
} else {
// 所有字符都已添加,设置完成状态
isTypewriterComplete.value = true;
isTypewriterComplete.value = true
}
};
}
// 开始打字过程
type();
};
type()
}
onMounted(() => {
// 计算初始动画持续时间
calculateDuration();
calculateDuration()
// 监听窗口大小变化,重新计算动画持续时间
window.addEventListener("resize", calculateDuration);
window.addEventListener('resize', calculateDuration)
// 如果启用了打字机效果,开始打字
if (props.typewriter) {
startTypewriter();
startTypewriter()
}
});
})
onUnmounted(() => {
// 移除事件监听
window.removeEventListener("resize", calculateDuration);
window.removeEventListener('resize', calculateDuration)
// 清除打字机定时器
if (typewriterTimer) {
clearTimeout(typewriterTimer);
clearTimeout(typewriterTimer)
}
});
})
/**
* 监听文本内容变化
@@ -206,13 +206,13 @@ watch(
if (props.typewriter) {
// 清除现有定时器
if (typewriterTimer) {
clearTimeout(typewriterTimer);
clearTimeout(typewriterTimer)
}
// 重新开始打字效果
startTypewriter();
startTypewriter()
}
}
);
)
</script>
<style scoped lang="scss">
@@ -386,7 +386,7 @@ watch(
// 添加打字机效果的光标样式
.text-scroll-content .scroll-item {
&::after {
content: "";
content: '';
opacity: 0;
animation: none;
}
@@ -394,7 +394,7 @@ watch(
// 仅在启用打字机效果时显示光标
.text-scroll-container[typewriter] .text-scroll-content .scroll-item::after {
content: "|";
content: '|';
opacity: 0;
animation: cursor 1s infinite;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
export { useStomp } from "./websocket/useStomp";
export { useDictSync } from "./websocket/useDictSync";
export type { DictMessage } from "./websocket/useDictSync";
export { useOnlineCount } from "./websocket/useOnlineCount";
export { useTokenRefresh } from "./auth/useTokenRefresh";
export { useStomp } from './websocket/useStomp'
export { useDictSync } from './websocket/useDictSync'
export type { DictMessage } from './websocket/useDictSync'
export { useOnlineCount } from './websocket/useOnlineCount'
export { useTokenRefresh } from './auth/useTokenRefresh'
export { useLayout } from "./layout/useLayout";
export { useLayoutMenu } from "./layout/useLayoutMenu";
export { useDeviceDetection } from "./layout/useDeviceDetection";
export { useLayout } from './layout/useLayout'
export { useLayoutMenu } from './layout/useLayoutMenu'
export { useDeviceDetection } from './layout/useDeviceDetection'
export { useAiAction } from "./useAiAction";
export type { UseAiActionOptions, AiActionHandler } from "./useAiAction";
export { useAiAction } 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 { useWindowSize } from "@vueuse/core";
import { useAppStore } from "@/store";
import { DeviceEnum } from "@/enums/settings/device-enum";
import { watchEffect, computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
import { useAppStore } from '@/store'
import { DeviceEnum } from '@/enums/settings/device-enum'
/**
* 设备检测和响应式处理
* 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态
*/
export function useDeviceDetection() {
const appStore = useAppStore();
const { width } = useWindowSize();
const appStore = useAppStore()
const { width } = useWindowSize()
// 桌面设备断点
const DESKTOP_BREAKPOINT = 992;
const DESKTOP_BREAKPOINT = 992
// 计算设备类型
const isDesktop = computed(() => width.value >= DESKTOP_BREAKPOINT);
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE);
const isDesktop = computed(() => width.value >= DESKTOP_BREAKPOINT)
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE)
// 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态
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) {
appStore.openSideBar();
appStore.openSideBar()
} else {
appStore.closeSideBar();
appStore.closeSideBar()
}
});
})
return {
isDesktop,
isMobile,
};
isMobile
}
}

View File

@@ -1,51 +1,51 @@
import { useAppStore, useSettingsStore } from "@/store";
import { defaultSettings } from "@/settings";
import { useAppStore, useSettingsStore } from '@/store'
import { defaultSettings } from '@/settings'
/**
* 布局相关的通用逻辑
*/
export function useLayout() {
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const appStore = useAppStore()
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
const isShowLogo = computed(() => settingsStore.showAppLogo);
const isShowLogo = computed(() => settingsStore.showAppLogo)
// 是否移动设备
const isMobile = computed(() => appStore.device === "mobile");
const isMobile = computed(() => appStore.device === 'mobile')
// 布局CSS类
const layoutClass = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
mobile: appStore.device === "mobile",
[`layout-${settingsStore.layout}`]: true,
}));
mobile: appStore.device === 'mobile',
[`layout-${settingsStore.layout}`]: true
}))
/**
* 处理切换侧边栏的展开/收起状态
*/
function toggleSidebar() {
appStore.toggleSidebar();
appStore.toggleSidebar()
}
/**
* 关闭侧边栏(移动端)
*/
function closeSidebar() {
appStore.closeSideBar();
appStore.closeSideBar()
}
return {
@@ -57,6 +57,6 @@ export function useLayout() {
isMobile,
layoutClass,
toggleSidebar,
closeSidebar,
};
closeSidebar
}
}

View File

@@ -1,39 +1,39 @@
import { useRoute } from "vue-router";
import { useAppStore, usePermissionStore } from "@/store";
import { useRoute } from 'vue-router'
import { useAppStore, usePermissionStore } from '@/store'
/**
* 布局菜单处理逻辑
*/
export function useLayoutMenu() {
const route = useRoute();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const route = useRoute()
const appStore = useAppStore()
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 { meta, path } = route;
const { meta, path } = route
// 如果设置了activeMenu则使用
if (meta?.activeMenu) {
return meta.activeMenu;
return meta.activeMenu
}
return path;
});
return path
})
return {
routes,
sideMenuRoutes,
activeMenu,
activeTopMenuPath,
};
activeTopMenuPath
}
}

View File

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

View File

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

View File

@@ -1,37 +1,37 @@
import { useDictStoreHook } from "@/store/modules/dict-store";
import { useStomp } from "./useStomp";
import type { IMessage } from "@stomp/stompjs";
import { useDictStoreHook } from '@/store/modules/dict-store'
import { useStomp } from './useStomp'
import type { IMessage } from '@stomp/stompjs'
/**
* 字典变更消息结构
*/
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() {
const dictStore = useDictStoreHook();
const dictStore = useDictStoreHook()
// 使用优化后的 useStomp
const stomp = useStomp({
@@ -40,100 +40,100 @@ function createDictSyncComposable() {
useExponentialBackoff: false,
maxReconnectAttempts: 3,
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用于取消订阅
let subscriptionId: string | null = null;
let subscriptionId: string | null = null
/**
* 处理字典变更事件
*/
const handleDictChangeMessage = (message: IMessage) => {
if (!message.body) {
return;
return
}
try {
const data = JSON.parse(message.body) as DictChangeMessage;
const { dictCode } = data;
const data = JSON.parse(message.body) as DictChangeMessage
const { dictCode } = data
if (!dictCode) {
console.warn("[DictSync] 收到无效的字典变更消息:缺少 dictCode");
return;
console.warn('[DictSync] 收到无效的字典变更消息:缺少 dictCode')
return
}
console.log(`[DictSync] 字典 "${dictCode}" 已更新,清除本地缓存`);
console.log(`[DictSync] 字典 "${dictCode}" 已更新,清除本地缓存`)
// 清除缓存,等待按需加载
dictStore.removeDictItem(dictCode);
dictStore.removeDictItem(dictCode)
// 执行所有注册的回调函数
messageCallbacks.value.forEach((callback) => {
try {
callback(data);
callback(data)
} catch (error) {
console.error("[DictSync] 回调函数执行失败:", error);
console.error('[DictSync] 回调函数执行失败:', error)
}
});
})
} catch (error) {
console.error("[DictSync] 解析字典变更消息失败:", error);
console.error('[DictSync] 解析字典变更消息失败:', error)
}
};
}
/**
* 初始化 WebSocket 连接并订阅字典主题
*/
const initialize = () => {
// 检查是否配置了 WebSocket 端点
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT
if (!wsEndpoint) {
console.log("[DictSync] 未配置 WebSocket 端点,跳过字典同步功能");
return;
console.log('[DictSync] 未配置 WebSocket 端点,跳过字典同步功能')
return
}
console.log("[DictSync] 初始化字典同步服务...");
console.log('[DictSync] 初始化字典同步服务...')
// 建立 WebSocket 连接
stomp.connect();
stomp.connect()
// 订阅字典主题useStomp 会自动处理重连后的订阅恢复)
subscriptionId = stomp.subscribe(DICT_TOPIC, handleDictChangeMessage);
subscriptionId = stomp.subscribe(DICT_TOPIC, handleDictChangeMessage)
if (subscriptionId) {
console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`);
console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`)
} else {
console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`);
console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`)
}
};
}
/**
* 关闭 WebSocket 连接并清理资源
*/
const cleanup = () => {
console.log("[DictSync] 清理字典同步服务...");
console.log('[DictSync] 清理字典同步服务...')
// 取消订阅(如果有的话)
if (subscriptionId) {
stomp.unsubscribe(subscriptionId);
subscriptionId = null;
stomp.unsubscribe(subscriptionId)
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 返回一个取消注册的函数
*/
const onDictChange = (callback: DictChangeCallback) => {
messageCallbacks.value.push(callback);
messageCallbacks.value.push(callback)
// 返回取消注册的函数
return () => {
const index = messageCallbacks.value.indexOf(callback);
const index = messageCallbacks.value.indexOf(callback)
if (index !== -1) {
messageCallbacks.value.splice(index, 1);
messageCallbacks.value.splice(index, 1)
}
};
};
}
}
return {
// 状态
@@ -169,8 +169,8 @@ function createDictSyncComposable() {
onDictMessage: onDictChange,
// 用于测试和调试
handleDictChangeMessage,
};
handleDictChangeMessage
}
}
/**
@@ -199,7 +199,7 @@ function createDictSyncComposable() {
*/
export function useDictSync() {
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 { useStomp } from "./useStomp";
import { registerWebSocketInstance } from "@/plugins/websocket";
import { AuthStorage } from "@/utils/auth";
import { ref, watch, onMounted, onUnmounted, getCurrentInstance } from 'vue'
import { useStomp } from './useStomp'
import { registerWebSocketInstance } from '@/plugins/websocket'
import { AuthStorage } from '@/utils/auth'
/**
* 在线用户数量消息结构
*/
interface OnlineCountMessage {
count?: number;
timestamp?: number;
count?: number
timestamp?: number
}
/**
* 全局单例实例
*/
let globalInstance: ReturnType<typeof createOnlineCountComposable> | null = null;
let globalInstance: ReturnType<typeof createOnlineCountComposable> | null = null
/**
* 创建在线用户计数组合式函数(内部工厂函数)
*/
function createOnlineCountComposable() {
// ==================== 状态管理 ====================
const onlineUserCount = ref(0);
const lastUpdateTime = ref(0);
const onlineUserCount = ref(0)
const lastUpdateTime = ref(0)
// ==================== WebSocket 客户端 ====================
const stomp = useStomp({
@@ -31,124 +31,124 @@ function createOnlineCountComposable() {
connectionTimeout: 10000,
useExponentialBackoff: true,
autoRestoreSubscriptions: true, // 自动恢复订阅
debug: false,
});
debug: false
})
// 在线用户计数主题
const ONLINE_COUNT_TOPIC = "/topic/online-count";
const ONLINE_COUNT_TOPIC = '/topic/online-count'
// 订阅 ID
let subscriptionId: string | null = null;
let subscriptionId: string | null = null
// 注册到全局实例管理器
registerWebSocketInstance("onlineCount", stomp);
registerWebSocketInstance('onlineCount', stomp)
/**
* 处理在线用户数量消息
*/
const handleOnlineCountMessage = (message: any) => {
try {
const data = message.body;
const jsonData = JSON.parse(data) as OnlineCountMessage;
const data = message.body
const jsonData = JSON.parse(data) as OnlineCountMessage
// 支持两种消息格式
// 1. 直接是数字: 42
// 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)) {
onlineUserCount.value = count;
lastUpdateTime.value = Date.now();
console.log(`[useOnlineCount] 在线用户数更新: ${count}`);
onlineUserCount.value = count
lastUpdateTime.value = Date.now()
console.log(`[useOnlineCount] 在线用户数更新: ${count}`)
} else {
console.warn("[useOnlineCount] 收到无效的在线用户数:", data);
console.warn('[useOnlineCount] 收到无效的在线用户数:', data)
}
} catch (error) {
console.error("[useOnlineCount] 解析在线用户数失败:", error);
console.error('[useOnlineCount] 解析在线用户数失败:', error)
}
};
}
/**
* 订阅在线用户计数主题
*/
const subscribeToOnlineCount = () => {
if (subscriptionId) {
console.log("[useOnlineCount] 已存在订阅,跳过");
return;
console.log('[useOnlineCount] 已存在订阅,跳过')
return
}
// 订阅在线用户计数主题useStomp 会处理重连后的订阅恢复)
subscriptionId = stomp.subscribe(ONLINE_COUNT_TOPIC, handleOnlineCountMessage);
subscriptionId = stomp.subscribe(ONLINE_COUNT_TOPIC, handleOnlineCountMessage)
if (subscriptionId) {
console.log(`[useOnlineCount] 已订阅主题: ${ONLINE_COUNT_TOPIC}`);
console.log(`[useOnlineCount] 已订阅主题: ${ONLINE_COUNT_TOPIC}`)
} else {
console.log(`[useOnlineCount] 暂存订阅配置,等待连接建立后自动订阅`);
console.log(`[useOnlineCount] 暂存订阅配置,等待连接建立后自动订阅`)
}
};
}
/**
* 初始化 WebSocket 连接并订阅在线用户主题
*/
const initialize = () => {
// 检查 WebSocket 端点是否配置
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT
if (!wsEndpoint) {
console.log("[useOnlineCount] 未配置 WebSocket 端点,跳过初始化");
return;
console.log('[useOnlineCount] 未配置 WebSocket 端点,跳过初始化')
return
}
// 检查令牌有效性
const accessToken = AuthStorage.getAccessToken();
const accessToken = AuthStorage.getAccessToken()
if (!accessToken) {
console.log("[useOnlineCount] 未检测到有效令牌,跳过初始化");
return;
console.log('[useOnlineCount] 未检测到有效令牌,跳过初始化')
return
}
console.log("[useOnlineCount] 初始化在线用户计数服务...");
console.log('[useOnlineCount] 初始化在线用户计数服务...')
// 建立 WebSocket 连接
stomp.connect();
stomp.connect()
// 订阅主题
subscribeToOnlineCount();
};
subscribeToOnlineCount()
}
/**
* 关闭 WebSocket 连接并清理资源
*/
const cleanup = () => {
console.log("[useOnlineCount] 清理在线用户计数服务...");
console.log('[useOnlineCount] 清理在线用户计数服务...')
// 取消订阅
if (subscriptionId) {
stomp.unsubscribe(subscriptionId);
subscriptionId = null;
stomp.unsubscribe(subscriptionId)
subscriptionId = null
}
// 也可以通过主题地址取消订阅
stomp.unsubscribeDestination(ONLINE_COUNT_TOPIC);
stomp.unsubscribeDestination(ONLINE_COUNT_TOPIC)
// 断开连接
stomp.disconnect();
stomp.disconnect()
// 重置状态
onlineUserCount.value = 0;
lastUpdateTime.value = 0;
};
onlineUserCount.value = 0
lastUpdateTime.value = 0
}
// 监听连接状态变化
watch(
stomp.isConnected,
(connected) => {
if (connected) {
console.log("[useOnlineCount] WebSocket 已连接");
console.log('[useOnlineCount] WebSocket 已连接')
} else {
console.log("[useOnlineCount] WebSocket 已断开");
console.log('[useOnlineCount] WebSocket 已断开')
}
},
{ immediate: false }
);
)
return {
// 状态
@@ -163,8 +163,8 @@ function createOnlineCountComposable() {
// 别名方法(向后兼容)
initWebSocket: initialize,
closeWebSocket: cleanup,
};
closeWebSocket: cleanup
}
}
/**
@@ -187,31 +187,31 @@ function createOnlineCountComposable() {
* ```
*/
export function useOnlineCount(options: { autoInit?: boolean } = {}) {
const { autoInit = true } = options;
const { autoInit = true } = options
// 获取或创建单例实例
if (!globalInstance) {
globalInstance = createOnlineCountComposable();
globalInstance = createOnlineCountComposable()
}
// 只在组件上下文中且 autoInit 为 true 时使用生命周期钩子
const instance = getCurrentInstance();
const instance = getCurrentInstance()
if (autoInit && instance) {
onMounted(() => {
// 只有在未连接时才尝试初始化
if (!globalInstance!.isConnected.value) {
console.log("[useOnlineCount] 组件挂载,初始化 WebSocket 连接");
globalInstance!.initialize();
console.log('[useOnlineCount] 组件挂载,初始化 WebSocket 连接')
globalInstance!.initialize()
} else {
console.log("[useOnlineCount] WebSocket 已连接,跳过初始化");
console.log('[useOnlineCount] WebSocket 已连接,跳过初始化')
}
});
})
// 注意:不在卸载时关闭连接,保持全局连接
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 { AuthStorage } from "@/utils/auth";
import { Client, type IMessage, type StompSubscription } from '@stomp/stompjs'
import { AuthStorage } from '@/utils/auth'
export interface UseStompOptions {
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
brokerURL?: string;
brokerURL?: string
/** 用于鉴权的 token不传时使用 getAccessToken() 的返回值 */
token?: string;
token?: string
/** 重连延迟,单位毫秒,默认为 15000 */
reconnectDelay?: number;
reconnectDelay?: number
/** 连接超时时间,单位毫秒,默认为 10000 */
connectionTimeout?: number;
connectionTimeout?: number
/** 是否开启指数退避重连策略 */
useExponentialBackoff?: boolean;
useExponentialBackoff?: boolean
/** 最大重连次数,默认为 3 */
maxReconnectAttempts?: number;
maxReconnectAttempts?: number
/** 最大重连延迟,单位毫秒,默认为 60000 */
maxReconnectDelay?: number;
maxReconnectDelay?: number
/** 是否开启调试日志 */
debug?: boolean;
debug?: boolean
/** 是否在重连时自动恢复订阅,默认为 true */
autoRestoreSubscriptions?: boolean;
autoRestoreSubscriptions?: boolean
}
/**
* 订阅配置信息
*/
interface SubscriptionConfig {
destination: string;
callback: (message: IMessage) => void;
destination: string
callback: (message: IMessage) => void
}
/**
* 连接状态枚举
*/
enum ConnectionState {
DISCONNECTED = "DISCONNECTED",
CONNECTING = "CONNECTING",
CONNECTED = "CONNECTED",
RECONNECTING = "RECONNECTING",
DISCONNECTED = 'DISCONNECTED',
CONNECTING = 'CONNECTING',
CONNECTED = 'CONNECTED',
RECONNECTING = 'RECONNECTING'
}
/**
@@ -54,7 +54,7 @@ enum ConnectionState {
*/
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 = {
brokerURL: ref(options.brokerURL ?? defaultBrokerURL),
@@ -64,27 +64,27 @@ export function useStomp(options: UseStompOptions = {}) {
maxReconnectAttempts: options.maxReconnectAttempts ?? 3,
maxReconnectDelay: options.maxReconnectDelay ?? 60000,
autoRestoreSubscriptions: options.autoRestoreSubscriptions ?? true,
debug: options.debug ?? false,
};
debug: options.debug ?? false
}
// ==================== 状态管理 ====================
const connectionState = ref<ConnectionState>(ConnectionState.DISCONNECTED);
const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED);
const reconnectAttempts = ref(0);
const connectionState = ref<ConnectionState>(ConnectionState.DISCONNECTED)
const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED)
const reconnectAttempts = ref(0)
// ==================== 定时器管理 ====================
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let connectionTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let connectionTimeoutTimer: ReturnType<typeof setTimeout> | null = null
// ==================== 订阅管理 ====================
// 活动订阅:存储当前 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);
let isManualDisconnect = false;
const stompClient = ref<Client | null>(null)
let isManualDisconnect = false
// ==================== 工具函数 ====================
@@ -93,50 +93,50 @@ export function useStomp(options: UseStompOptions = {}) {
*/
const clearAllTimers = () => {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
clearTimeout(reconnectTimer)
reconnectTimer = null
}
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = null;
clearTimeout(connectionTimeoutTimer)
connectionTimeoutTimer = null
}
};
}
/**
* 日志输出(支持调试模式控制)
*/
const log = (...args: any[]) => {
if (config.debug) {
console.log("[useStomp]", ...args);
console.log('[useStomp]', ...args)
}
};
}
const logWarn = (...args: any[]) => {
console.warn("[useStomp]", ...args);
};
console.warn('[useStomp]', ...args)
}
const logError = (...args: any[]) => {
console.error("[useStomp]", ...args);
};
console.error('[useStomp]', ...args)
}
/**
* 恢复所有订阅
*/
const restoreSubscriptions = () => {
if (!config.autoRestoreSubscriptions || subscriptionRegistry.size === 0) {
return;
return
}
log(`开始恢复 ${subscriptionRegistry.size} 个订阅...`);
log(`开始恢复 ${subscriptionRegistry.size} 个订阅...`)
for (const [destination, subscriptionConfig] of subscriptionRegistry.entries()) {
try {
performSubscribe(destination, subscriptionConfig.callback);
performSubscribe(destination, subscriptionConfig.callback)
} catch (error) {
logError(`恢复订阅 ${destination} 失败:`, error);
logError(`恢复订阅 ${destination} 失败:`, error)
}
}
};
}
/**
* 初始化 STOMP 客户端
@@ -144,82 +144,82 @@ export function useStomp(options: UseStompOptions = {}) {
const initializeClient = () => {
// 如果客户端已存在且处于活动状态,直接返回
if (stompClient.value && (stompClient.value.active || stompClient.value.connected)) {
log("STOMP 客户端已存在且处于活动状态,跳过初始化");
return;
log('STOMP 客户端已存在且处于活动状态,跳过初始化')
return
}
// 检查 WebSocket 端点是否配置
if (!config.brokerURL.value) {
logWarn("WebSocket 连接失败: 未配置 WebSocket 端点 URL");
return;
logWarn('WebSocket 连接失败: 未配置 WebSocket 端点 URL')
return
}
// 每次连接前重新获取最新令牌
const accessToken = AuthStorage.getAccessToken();
const accessToken = AuthStorage.getAccessToken()
if (!accessToken) {
logWarn("WebSocket 连接失败:授权令牌为空,请先登录");
return;
logWarn('WebSocket 连接失败:授权令牌为空,请先登录')
return
}
// 清理旧客户端
if (stompClient.value) {
try {
stompClient.value.deactivate();
stompClient.value.deactivate()
} catch (error) {
logWarn("清理旧客户端时出错:", error);
logWarn('清理旧客户端时出错:', error)
}
stompClient.value = null;
stompClient.value = null
}
// 创建 STOMP 客户端
stompClient.value = new Client({
brokerURL: config.brokerURL.value,
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, // 禁用内置重连,使用自定义重连逻辑
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
heartbeatOutgoing: 4000
})
// ==================== 事件监听器 ====================
// 连接成功
stompClient.value.onConnect = () => {
connectionState.value = ConnectionState.CONNECTED;
reconnectAttempts.value = 0;
clearAllTimers();
connectionState.value = ConnectionState.CONNECTED
reconnectAttempts.value = 0
clearAllTimers()
log("✅ WebSocket 连接已建立");
log('✅ WebSocket 连接已建立')
// 自动恢复订阅
restoreSubscriptions();
};
restoreSubscriptions()
}
// 连接断开
stompClient.value.onDisconnect = () => {
connectionState.value = ConnectionState.DISCONNECTED;
log("❌ WebSocket 连接已断开");
connectionState.value = ConnectionState.DISCONNECTED
log('❌ WebSocket 连接已断开')
// 清空活动订阅(但保留订阅配置用于恢复)
activeSubscriptions.clear();
activeSubscriptions.clear()
// 如果不是手动断开且未达到最大重连次数,则尝试重连
if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) {
scheduleReconnect();
scheduleReconnect()
}
};
}
// WebSocket 关闭
stompClient.value.onWebSocketClose = (event) => {
connectionState.value = ConnectionState.DISCONNECTED;
log(`WebSocket 已关闭: code=${event?.code}, reason=${event?.reason}`);
connectionState.value = ConnectionState.DISCONNECTED
log(`WebSocket 已关闭: code=${event?.code}, reason=${event?.reason}`)
// 如果是手动断开,不重连
if (isManualDisconnect) {
log("手动断开连接,不进行重连");
return;
log('手动断开连接,不进行重连')
return
}
// 对于异常关闭,尝试重连
@@ -228,29 +228,29 @@ export function useStomp(options: UseStompOptions = {}) {
[1000, 1006, 1008, 1011].includes(event.code) &&
reconnectAttempts.value < config.maxReconnectAttempts
) {
log("检测到连接异常关闭,将尝试重连");
scheduleReconnect();
log('检测到连接异常关闭,将尝试重连')
scheduleReconnect()
}
};
}
// STOMP 错误
stompClient.value.onStompError = (frame) => {
logError("STOMP 错误:", frame.headers, frame.body);
connectionState.value = ConnectionState.DISCONNECTED;
logError('STOMP 错误:', frame.headers, frame.body)
connectionState.value = ConnectionState.DISCONNECTED
// 检查是否是授权错误
const isAuthError =
frame.headers?.message?.includes("Unauthorized") ||
frame.body?.includes("Unauthorized") ||
frame.body?.includes("Token") ||
frame.body?.includes("401");
frame.headers?.message?.includes('Unauthorized') ||
frame.body?.includes('Unauthorized') ||
frame.body?.includes('Token') ||
frame.body?.includes('401')
if (isAuthError) {
logWarn("WebSocket 授权错误,停止重连");
isManualDisconnect = true; // 授权错误不进行重连
logWarn('WebSocket 授权错误,停止重连')
isManualDisconnect = true // 授权错误不进行重连
}
};
};
}
}
/**
* 调度重连任务
@@ -258,17 +258,17 @@ export function useStomp(options: UseStompOptions = {}) {
const scheduleReconnect = () => {
// 如果正在连接或手动断开,不重连
if (connectionState.value === ConnectionState.CONNECTING || isManualDisconnect) {
return;
return
}
// 检查是否达到最大重连次数
if (reconnectAttempts.value >= config.maxReconnectAttempts) {
logError(`已达到最大重连次数 (${config.maxReconnectAttempts}),停止重连`);
return;
logError(`已达到最大重连次数 (${config.maxReconnectAttempts}),停止重连`)
return
}
reconnectAttempts.value++;
connectionState.value = ConnectionState.RECONNECTING;
reconnectAttempts.value++
connectionState.value = ConnectionState.RECONNECTING
// 计算重连延迟(支持指数退避)
const delay = config.useExponentialBackoff
@@ -276,41 +276,41 @@ export function useStomp(options: UseStompOptions = {}) {
config.reconnectDelay * Math.pow(2, reconnectAttempts.value - 1),
config.maxReconnectDelay
)
: config.reconnectDelay;
: config.reconnectDelay
log(`准备重连 (${reconnectAttempts.value}/${config.maxReconnectAttempts}),延迟 ${delay}ms`);
log(`准备重连 (${reconnectAttempts.value}/${config.maxReconnectAttempts}),延迟 ${delay}ms`)
// 清除之前的重连计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
clearTimeout(reconnectTimer)
}
// 设置重连计时器
reconnectTimer = setTimeout(() => {
if (connectionState.value !== ConnectionState.CONNECTED && !isManualDisconnect) {
log(`开始第 ${reconnectAttempts.value} 次重连...`);
connect();
log(`开始第 ${reconnectAttempts.value} 次重连...`)
connect()
}
}, delay);
};
}, delay)
}
// 监听 brokerURL 的变化,自动重新初始化
watch(config.brokerURL, (newURL, oldURL) => {
if (newURL !== oldURL) {
log(`WebSocket 端点已更改: ${oldURL} -> ${newURL}`);
log(`WebSocket 端点已更改: ${oldURL} -> ${newURL}`)
// 断开当前连接
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 = () => {
// 重置手动断开标志
isManualDisconnect = false;
isManualDisconnect = false
// 检查是否配置了 WebSocket 端点
if (!config.brokerURL.value) {
logError("WebSocket 连接失败: 未配置 WebSocket 端点 URL");
return;
logError('WebSocket 连接失败: 未配置 WebSocket 端点 URL')
return
}
// 防止重复连接
if (connectionState.value === ConnectionState.CONNECTING) {
log("WebSocket 正在连接中,跳过重复连接请求");
return;
log('WebSocket 正在连接中,跳过重复连接请求')
return
}
// 如果客户端不存在,先初始化
if (!stompClient.value) {
initializeClient();
initializeClient()
}
if (!stompClient.value) {
logError("STOMP 客户端初始化失败");
return;
logError('STOMP 客户端初始化失败')
return
}
// 避免重复连接:检查是否已连接
if (stompClient.value.connected) {
log("WebSocket 已连接,跳过重复连接");
connectionState.value = ConnectionState.CONNECTED;
return;
log('WebSocket 已连接,跳过重复连接')
connectionState.value = ConnectionState.CONNECTED
return
}
// 设置连接状态
connectionState.value = ConnectionState.CONNECTING;
connectionState.value = ConnectionState.CONNECTING
// 设置连接超时
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
clearTimeout(connectionTimeoutTimer)
}
connectionTimeoutTimer = setTimeout(() => {
if (connectionState.value === ConnectionState.CONNECTING) {
logWarn("WebSocket 连接超时");
connectionState.value = ConnectionState.DISCONNECTED;
logWarn('WebSocket 连接超时')
connectionState.value = ConnectionState.DISCONNECTED
// 超时后尝试重连
if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) {
scheduleReconnect();
scheduleReconnect()
}
}
}, config.connectionTimeout);
}, config.connectionTimeout)
try {
stompClient.value.activate();
log("正在建立 WebSocket 连接...");
stompClient.value.activate()
log('正在建立 WebSocket 连接...')
} catch (error) {
logError("激活 WebSocket 连接失败:", error);
connectionState.value = ConnectionState.DISCONNECTED;
logError('激活 WebSocket 连接失败:', error)
connectionState.value = ConnectionState.DISCONNECTED
}
};
}
/**
* 执行订阅操作(内部方法)
*/
const performSubscribe = (destination: string, callback: (message: IMessage) => void): string => {
if (!stompClient.value || !stompClient.value.connected) {
logWarn(`尝试订阅 ${destination} 失败: 客户端未连接`);
return "";
logWarn(`尝试订阅 ${destination} 失败: 客户端未连接`)
return ''
}
try {
const subscription = stompClient.value.subscribe(destination, callback);
const subscriptionId = subscription.id;
activeSubscriptions.set(subscriptionId, subscription);
log(`✓ 订阅成功: ${destination} (ID: ${subscriptionId})`);
return subscriptionId;
const subscription = stompClient.value.subscribe(destination, callback)
const subscriptionId = subscription.id
activeSubscriptions.set(subscriptionId, subscription)
log(`✓ 订阅成功: ${destination} (ID: ${subscriptionId})`)
return subscriptionId
} catch (error) {
logError(`订阅 ${destination} 失败:`, error);
return "";
logError(`订阅 ${destination} 失败:`, error)
return ''
}
};
}
/**
* 订阅指定主题
@@ -409,16 +409,16 @@ export function useStomp(options: UseStompOptions = {}) {
*/
const subscribe = (destination: string, callback: (message: IMessage) => void): string => {
// 保存订阅配置到注册表,用于断线重连后自动恢复
subscriptionRegistry.set(destination, { destination, callback });
subscriptionRegistry.set(destination, { destination, callback })
// 如果已连接,立即订阅
if (stompClient.value?.connected) {
return performSubscribe(destination, callback);
return performSubscribe(destination, callback)
}
log(`暂存订阅配置: ${destination},将在连接建立后自动订阅`);
return "";
};
log(`暂存订阅配置: ${destination},将在连接建立后自动订阅`)
return ''
}
/**
* 取消订阅
@@ -426,17 +426,17 @@ export function useStomp(options: UseStompOptions = {}) {
* @param subscriptionId 订阅 ID由 subscribe 方法返回)
*/
const unsubscribe = (subscriptionId: string) => {
const subscription = activeSubscriptions.get(subscriptionId);
const subscription = activeSubscriptions.get(subscriptionId)
if (subscription) {
try {
subscription.unsubscribe();
activeSubscriptions.delete(subscriptionId);
log(`✓ 已取消订阅: ${subscriptionId}`);
subscription.unsubscribe()
activeSubscriptions.delete(subscriptionId)
log(`✓ 已取消订阅: ${subscriptionId}`)
} catch (error) {
logWarn(`取消订阅 ${subscriptionId} 时出错:`, error);
logWarn(`取消订阅 ${subscriptionId} 时出错:`, error)
}
}
};
}
/**
* 取消指定主题的订阅(从注册表中移除)
@@ -445,22 +445,22 @@ export function useStomp(options: UseStompOptions = {}) {
*/
const unsubscribeDestination = (destination: string) => {
// 从注册表中移除
subscriptionRegistry.delete(destination);
subscriptionRegistry.delete(destination)
// 取消所有匹配该主题的活动订阅
for (const [id, subscription] of activeSubscriptions.entries()) {
// 注意STOMP 的 subscription 对象没有直接暴露 destination
// 这里简化处理,实际使用时可能需要额外维护 id -> destination 的映射
try {
subscription.unsubscribe();
activeSubscriptions.delete(id);
subscription.unsubscribe()
activeSubscriptions.delete(id)
} catch (error) {
logWarn(`取消订阅 ${id} 时出错:`, error);
logWarn(`取消订阅 ${id} 时出错:`, error)
}
}
log(`✓ 已移除主题订阅配置: ${destination}`);
};
log(`✓ 已移除主题订阅配置: ${destination}`)
}
/**
* 断开 WebSocket 连接
@@ -469,43 +469,43 @@ export function useStomp(options: UseStompOptions = {}) {
*/
const disconnect = (clearSubscriptions = true) => {
// 设置手动断开标志
isManualDisconnect = true;
isManualDisconnect = true
// 清除所有定时器
clearAllTimers();
clearAllTimers()
// 取消所有活动订阅
for (const [id, subscription] of activeSubscriptions.entries()) {
try {
subscription.unsubscribe();
subscription.unsubscribe()
} catch (error) {
logWarn(`取消订阅 ${id} 时出错:`, error);
logWarn(`取消订阅 ${id} 时出错:`, error)
}
}
activeSubscriptions.clear();
activeSubscriptions.clear()
// 可选:清除订阅注册表
if (clearSubscriptions) {
subscriptionRegistry.clear();
log("已清除所有订阅配置");
subscriptionRegistry.clear()
log('已清除所有订阅配置')
}
// 断开连接
if (stompClient.value) {
try {
if (stompClient.value.connected || stompClient.value.active) {
stompClient.value.deactivate();
log("✓ WebSocket 连接已主动断开");
stompClient.value.deactivate()
log('✓ WebSocket 连接已主动断开')
}
} catch (error) {
logError("断开 WebSocket 连接时出错:", error);
logError('断开 WebSocket 连接时出错:', error)
}
stompClient.value = null;
stompClient.value = null
}
connectionState.value = ConnectionState.DISCONNECTED;
reconnectAttempts.value = 0;
};
connectionState.value = ConnectionState.DISCONNECTED
reconnectAttempts.value = 0
}
// ==================== 返回公共接口 ====================
return {
@@ -525,6 +525,6 @@ export function useStomp(options: UseStompOptions = {}) {
// 统计信息
getActiveSubscriptionCount: () => activeSubscriptions.size,
getRegisteredSubscriptionCount: () => subscriptionRegistry.size,
};
getRegisteredSubscriptionCount: () => subscriptionRegistry.size
}
}

View File

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

View File

@@ -1,6 +1,6 @@
import type { Directive, DirectiveBinding } from "vue";
import { useUserStoreHook } from "@/store/modules/user-store";
import { hasPerm as checkPermission } from "@/utils/auth";
import type { Directive, DirectiveBinding } from 'vue'
import { useUserStoreHook } from '@/store/modules/user-store'
import { hasPerm as checkPermission } from '@/utils/auth'
/**
* 按钮权限指令
@@ -12,15 +12,15 @@ import { hasPerm as checkPermission } from "@/utils/auth";
export const hasPerm: Directive = {
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 = {
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> = {
INPUT: { value: 1, label: "输入框" },
SELECT: { value: 2, label: "下拉框" },
RADIO: { value: 3, label: "单选框" },
CHECK_BOX: { value: 4, label: "复选框" },
INPUT_NUMBER: { value: 5, label: "数字输入框" },
SWITCH: { value: 6, label: "开关" },
TEXT_AREA: { value: 7, label: "文本域" },
DATE: { value: 8, label: "日期框" },
DATE_TIME: { value: 9, label: "日期时间框" },
HIDDEN: { value: 10, label: "隐藏域" },
};
INPUT: { value: 1, label: '输入框' },
SELECT: { value: 2, label: '下拉框' },
RADIO: { value: 3, label: '单选框' },
CHECK_BOX: { value: 4, label: '复选框' },
INPUT_NUMBER: { value: 5, label: '数字输入框' },
SWITCH: { value: 6, label: '开关' },
TEXT_AREA: { value: 7, label: '文本域' },
DATE: { value: 8, label: '日期框' },
DATE_TIME: { value: 9, label: '日期时间框' },
HIDDEN: { value: 10, label: '隐藏域' }
}

View File

@@ -3,35 +3,35 @@
*/
export const QueryTypeEnum: Record<string, OptionType> = {
/** 等于 */
EQ: { value: 1, label: "=" },
EQ: { value: 1, label: '=' },
/** 模糊匹配 */
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_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/query-enum";
export * from './codegen/form-enum'
export * from './codegen/query-enum'
export * from "./settings/layout-enum";
export * from "./settings/theme-enum";
export * from "./settings/locale-enum";
export * from "./settings/device-enum";
export * from './settings/layout-enum'
export * from './settings/theme-enum'
export * from './settings/locale-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, // 目录
MENU = 1, // 菜单
BUTTON = 4, // 按钮
EXTLINK = 3, // 外链
EXTLINK = 3 // 外链
}

2
src/env.d.ts vendored
View File

@@ -2,7 +2,7 @@
declare module '*.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>
export default component
}

View File

@@ -1,27 +1,27 @@
import type { App } from "vue";
import { createI18n } from "vue-i18n";
import { useAppStoreHook } from "@/store/modules/app-store";
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import { useAppStoreHook } from '@/store/modules/app-store'
// 本地语言包
import enLocale from "./package/en.json";
import zhCnLocale from "./package/zh-cn.json";
import enLocale from './package/en.json'
import zhCnLocale from './package/zh-cn.json'
const appStore = useAppStoreHook();
const appStore = useAppStoreHook()
const messages = {
"zh-cn": zhCnLocale,
en: enLocale,
};
'zh-cn': zhCnLocale,
en: enLocale
}
const i18n = createI18n({
legacy: false,
locale: appStore.language,
messages,
globalInjection: true,
});
globalInjection: true
})
// 全局注册 i18n
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>
<script lang="ts" setup>
import logo from "@/assets/logo.png";
import { defineProps } from "vue";
import logo from '@/assets/logo.png'
import { defineProps } from 'vue'
defineProps({
collapse: {
type: Boolean,
required: true,
},
});
required: true
}
})
</script>
<style lang="scss" scoped>

View File

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

View File

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

View File

@@ -31,67 +31,67 @@
</template>
<script lang="ts" setup>
import MenuItemContent from "./components/MenuItemContent.vue";
import MenuItemContent from './components/MenuItemContent.vue'
defineOptions({
name: "MixTopMenu",
});
name: 'MixTopMenu'
})
import { LocationQueryRaw, RouteRecordRaw } from "vue-router";
import { usePermissionStore, useAppStore, useSettingsStore } from "@/store";
import variables from "@/styles/variables.module.scss";
import { SidebarColor } from "@/enums/settings/theme-enum";
import { LocationQueryRaw, RouteRecordRaw } from 'vue-router'
import { usePermissionStore, useAppStore, useSettingsStore } from '@/store'
import variables from '@/styles/variables.module.scss'
import { SidebarColor } from '@/enums/settings/theme-enum'
const router = useRouter();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const settingsStore = useSettingsStore();
const router = useRouter()
const appStore = useAppStore()
const permissionStore = usePermissionStore()
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(() => {
return topMenus.value.map((route) => {
// 如果路由设置了 alwaysShow=true或者没有子菜单直接返回原路由
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) {
const onlyChild = visibleChildren[0];
const onlyChild = visibleChildren[0]
return {
...route,
meta: {
...route.meta,
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 点击的菜单路径
*/
const handleMenuSelect = (routePath: string) => {
updateMenuState(routePath);
};
updateMenuState(routePath)
}
/**
* 更新菜单状态 - 同时处理点击和路由变化情况
@@ -101,52 +101,52 @@ const handleMenuSelect = (routePath: string) => {
const updateMenuState = (topMenuPath: string, skipNavigation = false) => {
// 不相同才更新,避免重复操作
if (topMenuPath !== appStore.activeTopMenuPath) {
appStore.activeTopMenu(topMenuPath); // 设置激活的顶部菜单
permissionStore.setMixLayoutSideMenus(topMenuPath); // 设置混合布局左侧菜单
appStore.activeTopMenu(topMenuPath) // 设置激活的顶部菜单
permissionStore.setMixLayoutSideMenus(topMenuPath) // 设置混合布局左侧菜单
}
// 如果是点击菜单且状态已变更,才进行导航
if (!skipNavigation) {
navigateToFirstLeftMenu(permissionStore.mixLayoutSideMenus); // 跳转到左侧第一个菜单
navigateToFirstLeftMenu(permissionStore.mixLayoutSideMenus) // 跳转到左侧第一个菜单
}
};
}
/**
* 跳转到左侧第一个可访问的菜单
* @param menus 左侧菜单列表
*/
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) {
navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[]);
navigateToFirstLeftMenu(firstMenu.children as RouteRecordRaw[])
} else if (firstMenu.name) {
router.push({
name: firstMenu.name,
query:
typeof firstMenu.meta?.params === "object"
typeof firstMenu.meta?.params === 'object'
? (firstMenu.meta.params as LocationQueryRaw)
: undefined,
});
: undefined
})
}
};
}
// 获取当前路由路径的顶部菜单路径
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath)
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 =
useRoute().path.split("/").filter(Boolean).length > 1
? useRoute().path.match(/^\/[^/]+/)?.[0] || "/"
: "/";
appStore.activeTopMenu(currentTopMenuPath); // 设置激活的顶部菜单
permissionStore.setMixLayoutSideMenus(currentTopMenuPath); // 设置混合布局左侧菜单
});
useRoute().path.split('/').filter(Boolean).length > 1
? useRoute().path.match(/^\/[^/]+/)?.[0] || '/'
: '/'
appStore.activeTopMenu(currentTopMenuPath) // 设置激活的顶部菜单
permissionStore.setMixLayoutSideMenus(currentTopMenuPath) // 设置混合布局左侧菜单
})
// 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态
watch(
@@ -155,13 +155,13 @@ watch(
if (newPath) {
// 提取顶级路径
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>
<style lang="scss" scoped>

View File

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

View File

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

View File

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

View File

@@ -14,22 +14,22 @@
</template>
<script setup lang="ts">
import { useAppStore } from "@/store";
import Hamburger from "@/components/Hamburger/index.vue";
import Breadcrumb from "@/components/Breadcrumb/index.vue";
import NavbarActions from "./components/NavbarActions.vue";
import { useAppStore } from '@/store'
import Hamburger from '@/components/Hamburger/index.vue'
import Breadcrumb from '@/components/Breadcrumb/index.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() {
console.log("🔄 Hamburger clicked! Current state:", isSidebarOpened.value);
console.log("🔄 Device type:", appStore.device);
appStore.toggleSidebar();
console.log("🔄 New state:", appStore.sidebar.opened);
console.log('🔄 Hamburger clicked! Current state:', isSidebarOpened.value)
console.log('🔄 Device type:', appStore.device)
appStore.toggleSidebar()
console.log('🔄 New state:', appStore.sidebar.opened)
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
<div
:class="{
hasTagsView: isShowTagsView,
'layout__main--collapsed': !isSidebarOpen,
'layout__main--collapsed': !isSidebarOpen
}"
class="layout__main"
>
@@ -28,20 +28,20 @@
</template>
<script setup lang="ts">
import { useLayout } from "@/composables/layout/useLayout";
import { useLayoutMenu } from "@/composables/layout/useLayoutMenu";
import BaseLayout from "../base/index.vue";
import AppLogo from "../../components/AppLogo/index.vue";
import NavBar from "../../components/NavBar/index.vue";
import TagsView from "../../components/TagsView/index.vue";
import AppMain from "../../components/AppMain/index.vue";
import BasicMenu from "../../components/Menu/BasicMenu.vue";
import { useLayout } from '@/composables/layout/useLayout'
import { useLayoutMenu } from '@/composables/layout/useLayoutMenu'
import BaseLayout from '../base/index.vue'
import AppLogo from '../../components/AppLogo/index.vue'
import NavBar from '../../components/NavBar/index.vue'
import TagsView from '../../components/TagsView/index.vue'
import AppMain from '../../components/AppMain/index.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>
<style lang="scss" scoped>

View File

@@ -58,44 +58,44 @@
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
import { useWindowSize } from "@vueuse/core";
import { useLayout, useLayoutMenu } from "@/composables";
import BaseLayout from "../base/index.vue";
import AppLogo from "../../components/AppLogo/index.vue";
import MixTopMenu from "../../components/Menu/MixTopMenu.vue";
import NavbarActions from "../../components/NavBar/components/NavbarActions.vue";
import TagsView from "../../components/TagsView/index.vue";
import AppMain from "../../components/AppMain/index.vue";
import MenuItem from "../../components/Menu/components/MenuItem.vue";
import Hamburger from "@/components/Hamburger/index.vue";
import variables from "@/styles/variables.module.scss";
import { isExternal } from "@/utils/index";
import { useAppStore, usePermissionStore } from "@/store";
import { useRoute } from 'vue-router'
import { useWindowSize } from '@vueuse/core'
import { useLayout, useLayoutMenu } from '@/composables'
import BaseLayout from '../base/index.vue'
import AppLogo from '../../components/AppLogo/index.vue'
import MixTopMenu from '../../components/Menu/MixTopMenu.vue'
import NavbarActions from '../../components/NavBar/components/NavbarActions.vue'
import TagsView from '../../components/TagsView/index.vue'
import AppMain from '../../components/AppMain/index.vue'
import MenuItem from '../../components/Menu/components/MenuItem.vue'
import Hamburger from '@/components/Hamburger/index.vue'
import variables from '@/styles/variables.module.scss'
import { isExternal } from '@/utils/index'
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只显示图标隐藏文字
const isLogoCollapsed = computed(() => width.value < 768);
const isLogoCollapsed = computed(() => width.value < 768)
// 当前激活的菜单
const activeLeftMenuPath = computed(() => {
const { meta, path } = route;
const { meta, path } = route
// 如果设置了activeMenu则使用
if ((meta?.activeMenu as unknown as string) && typeof meta.activeMenu === "string") {
return meta.activeMenu as unknown as string;
if ((meta?.activeMenu as unknown as string) && typeof meta.activeMenu === 'string') {
return meta.activeMenu as unknown as string
}
return path;
});
return path
})
/**
* 解析路径 - 混合模式下,左侧菜单是从顶级菜单下的子菜单开始的
@@ -103,13 +103,13 @@ const activeLeftMenuPath = computed(() => {
*/
function resolvePath(routePath: string) {
if (isExternal(routePath)) {
return routePath;
return routePath
}
if (routePath.startsWith("/")) {
return activeTopMenuPath.value + routePath;
if (routePath.startsWith('/')) {
return activeTopMenuPath.value + routePath
}
return `${activeTopMenuPath.value}/${routePath}`;
return `${activeTopMenuPath.value}/${routePath}`
}
// 监听路由变化确保左侧菜单能随TagsView切换而正确激活
@@ -118,7 +118,7 @@ watch(
(newPath: string) => {
// 获取顶级路径
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)) {
@@ -126,15 +126,15 @@ watch(
}
// 如果路径改变了顶级菜单,确保顶部菜单和左侧菜单都更新
else if (topMenuPath !== activeTopMenuPath.value) {
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const appStore = useAppStore()
const permissionStore = usePermissionStore()
appStore.activeTopMenu(topMenuPath);
permissionStore.setMixLayoutSideMenus(topMenuPath);
appStore.activeTopMenu(topMenuPath)
permissionStore.setMixLayoutSideMenus(topMenuPath)
}
},
{ immediate: true }
);
)
</script>
<style lang="scss" scoped>

View File

@@ -23,26 +23,26 @@
</template>
<script setup lang="ts">
import { useLayout } from "@/composables/layout/useLayout";
import { useLayoutMenu } from "@/composables/layout/useLayoutMenu";
import BaseLayout from "../base/index.vue";
import AppLogo from "../../components/AppLogo/index.vue";
import BasicMenu from "../../components/Menu/BasicMenu.vue";
import NavbarActions from "../../components/NavBar/components/NavbarActions.vue";
import TagsView from "../../components/TagsView/index.vue";
import AppMain from "../../components/AppMain/index.vue";
import { useLayout } from '@/composables/layout/useLayout'
import { useLayoutMenu } from '@/composables/layout/useLayoutMenu'
import BaseLayout from '../base/index.vue'
import AppLogo from '../../components/AppLogo/index.vue'
import BasicMenu from '../../components/Menu/BasicMenu.vue'
import NavbarActions from '../../components/NavBar/components/NavbarActions.vue'
import TagsView from '../../components/TagsView/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只显示图标隐藏文字
const isLogoCollapsed = computed(() => width.value < 768);
const isLogoCollapsed = computed(() => width.value < 768)
</script>
<style lang="scss" scoped>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,323 +1,323 @@
import type { App } from "vue";
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
import type { App } from 'vue'
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[] = [
{
path: "/login",
component: () => import("@/views/login/index.vue"),
meta: { hidden: true },
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true }
},
{
path: "/",
name: "/",
path: '/',
name: '/',
component: Layout,
redirect: "/dashboard",
redirect: '/dashboard',
children: [
{
path: "dashboard",
component: () => import("@/views/dashboard/index.vue"),
path: 'dashboard',
component: () => import('@/views/dashboard/index.vue'),
// 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致
// 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude
name: "Dashboard",
name: 'Dashboard',
meta: {
title: "dashboard",
icon: "homepage",
title: 'dashboard',
icon: 'homepage',
affix: true,
keepAlive: true,
},
keepAlive: true
}
},
{
path: "401",
component: () => import("@/views/error/401.vue"),
meta: { hidden: true },
path: '401',
component: () => import('@/views/error/401.vue'),
meta: { hidden: true }
},
{
path: "404",
component: () => import("@/views/error/404.vue"),
meta: { hidden: true },
path: '404',
component: () => import('@/views/error/404.vue'),
meta: { hidden: true }
},
{
path: "profile",
name: "Profile",
component: () => import("@/views/profile/index.vue"),
meta: { title: "个人中心", icon: "user", hidden: true },
path: 'profile',
name: 'Profile',
component: () => import('@/views/profile/index.vue'),
meta: { title: '个人中心', icon: 'user', hidden: true }
},
{
path: "my-notice",
name: "MyNotice",
component: () => import("@/views/system/notice/components/MyNotice.vue"),
meta: { title: "我的通知", icon: "user", hidden: true },
path: 'my-notice',
name: 'MyNotice',
component: () => import('@/views/system/notice/components/MyNotice.vue'),
meta: { title: '我的通知', icon: 'user', hidden: true }
},
{
path: "/detail/:id(\\d+)",
name: "DemoDetail",
component: () => import("@/views/demo/detail.vue"),
meta: { title: "详情页缓存", icon: "user", hidden: true, keepAlive: true },
},
],
path: '/detail/:id(\\d+)',
name: 'DemoDetail',
component: () => import('@/views/demo/detail.vue'),
meta: { title: '详情页缓存', icon: 'user', hidden: true, keepAlive: true }
}
]
},
// 人员管理模块
{
path: "/personnel",
path: '/personnel',
component: Layout,
name: "Personnel",
name: 'Personnel',
meta: {
title: "人员管理",
icon: "setting",
title: '人员管理',
icon: 'setting'
},
children: [
{
path: "user",
name: "PersonnelUser",
component: () => import("@/views/calibration/personnelManagement/index.vue"),
path: 'user',
name: 'PersonnelUser',
component: () => import('@/views/calibration/personnelManagement/index.vue'),
meta: {
title: "人事管理",
},
title: '人事管理'
}
},
{
path: "role",
name: "PersonnelRole",
component: () => import("@/views/calibration/department/index.vue"),
path: 'role',
name: 'PersonnelRole',
component: () => import('@/views/calibration/department/index.vue'),
meta: {
title: "角色管理",
},
},
],
title: '角色管理'
}
}
]
},
// 财务管理模块
{
path: "/finance",
path: '/finance',
component: Layout,
name: "Finance",
name: 'Finance',
meta: {
title: "财务管理",
icon: "setting",
title: '财务管理',
icon: 'setting'
},
children: [
{
path: "onboardingRegistration",
name: "OnboardingRegistration",
component: () => import("@/views/calibration/onboardingRegistration/index.vue"),
path: 'onboardingRegistration',
name: 'OnboardingRegistration',
component: () => import('@/views/calibration/onboardingRegistration/index.vue'),
// component: () => import("@/views/system/user/index.vue"),
meta: {
title: "入职财务登记",
},
title: '入职财务登记'
}
},
{
path: "departure ",
name: "Departure",
component: () => import("@/views/calibration/departureFinancialRegistration/index.vue"),
path: 'departure ',
name: 'Departure',
component: () => import('@/views/calibration/departureFinancialRegistration/index.vue'),
meta: {
title: "离职财务登记",
},
title: '离职财务登记'
}
},
{
path: "invoiceApplication",
name: "InvoiceApplication",
component: () => import("@/views/calibration/invoiceApplication/index.vue"),
path: 'invoiceApplication',
name: 'InvoiceApplication',
component: () => import('@/views/calibration/invoiceApplication/index.vue'),
meta: {
title: "开票申请",
},
title: '开票申请'
}
},
{
path: "revenueRecognition",
name: "RevenueRecognition",
component: () => import("@/views/calibration/revenueRecognition/index.vue"),
path: 'revenueRecognition',
name: 'RevenueRecognition',
component: () => import('@/views/calibration/revenueRecognition/index.vue'),
meta: {
title: "收入确认",
},
title: '收入确认'
}
},
{
path: "accountAdjustmentApplication",
name: "AccountAdjustmentApplication",
component: () => import("@/views/calibration/accountAdjustmentApplication/index.vue"),
path: 'accountAdjustmentApplication',
name: 'AccountAdjustmentApplication',
component: () => import('@/views/calibration/accountAdjustmentApplication/index.vue'),
meta: {
title: "调账申请",
},
title: '调账申请'
}
},
{
path: "paymentApplicationForm",
name: "PaymentApplicationForm",
component: () => import("@/views/calibration/paymentApplicationForm/index.vue"),
path: 'paymentApplicationForm',
name: 'PaymentApplicationForm',
component: () => import('@/views/calibration/paymentApplicationForm/index.vue'),
meta: {
title: "付款申请单",
},
title: '付款申请单'
}
},
{
path: "reimbursement",
name: "Reimbursement",
component: () => import("@/views/calibration/reimbursement/index.vue"),
path: 'reimbursement',
name: 'Reimbursement',
component: () => import('@/views/calibration/reimbursement/index.vue'),
meta: {
title: "报销",
},
title: '报销'
}
},
{
path: "salaryBonusAdjustment",
name: "SalaryBonusAdjustment",
component: () => import("@/views/calibration/salaryBonusAdjustment/index.vue"),
path: 'salaryBonusAdjustment',
name: 'SalaryBonusAdjustment',
component: () => import('@/views/calibration/salaryBonusAdjustment/index.vue'),
meta: {
title: "工资/奖金变更",
},
},
],
title: '工资/奖金变更'
}
}
]
},
// 业务管理模块
{
path: "/business",
path: '/business',
component: Layout,
name: "Business",
name: 'Business',
meta: {
title: "业务管理",
icon: "setting",
title: '业务管理',
icon: 'setting'
},
children: [
{
path: "user",
name: "BusinessConflict",
component: () => import("@/views/business/conflict/index.vue"),
path: 'user',
name: 'BusinessConflict',
component: () => import('@/views/business/conflict/index.vue'),
meta: {
title: "利益冲突检索",
},
title: '利益冲突检索'
}
},
{
path: "role",
name: "BusinessPreRegistration",
component: () => import("@/views/business/preRegistration/index.vue"),
path: 'role',
name: 'BusinessPreRegistration',
component: () => import('@/views/business/preRegistration/index.vue'),
meta: {
title: "预立案登记",
},
},
],
title: '预立案登记'
}
}
]
},
// 案件管理模块
{
path: "/case",
path: '/case',
component: Layout,
name: "Case",
name: 'Case',
meta: {
title: "案件管理",
icon: "setting",
title: '案件管理',
icon: 'setting'
},
children: [
{
path: "user",
name: "CaseUser",
component: () => import("@/views/case/index.vue"),
path: 'user',
name: 'CaseUser',
component: () => import('@/views/case/index.vue'),
meta: {
title: "案件管理",
},
},
],
title: '案件管理'
}
}
]
},
// 申请用印
{
path: "/stamp",
name: "StampApplication",
path: '/stamp',
name: 'StampApplication',
component: Layout,
meta: {
title: "申请用印",
icon: "setting",
title: '申请用印',
icon: 'setting'
},
redirect: "/stamp/index",
redirect: '/stamp/index',
children: [
{
path: "index",
name: "StampApplicationIndex",
component: () => import("@/views/stamp-application/index.vue"),
path: 'index',
name: 'StampApplicationIndex',
component: () => import('@/views/stamp-application/index.vue'),
meta: {
title: "申请用印",
},
},
],
title: '申请用印'
}
}
]
},
// 业绩
{
path: "/performance",
name: "StampPerformance",
path: '/performance',
name: 'StampPerformance',
component: Layout,
meta: {
title: "业绩展示",
icon: "setting",
title: '业绩展示',
icon: 'setting'
},
redirect: "/performance/index",
redirect: '/performance/index',
children: [
{
path: "index",
name: "StampPerformanceIndex",
component: () => import("@/views/performance/list/index.vue"),
path: 'index',
name: 'StampPerformanceIndex',
component: () => import('@/views/performance/list/index.vue'),
meta: {
title: "业绩展示",
},
},
],
title: '业绩展示'
}
}
]
},
// 入库登记
{
path: "/registration",
name: "Registration",
path: '/registration',
name: 'Registration',
component: Layout,
meta: {
title: "入库登记",
icon: "setting",
title: '入库登记',
icon: 'setting'
},
redirect: "/registration/index",
redirect: '/registration/index',
children: [
{
path: "index",
name: "RegistrationIndex",
component: () => import("@/views/registration/index.vue"),
path: 'index',
name: 'RegistrationIndex',
component: () => import('@/views/registration/index.vue'),
meta: {
title: "入库登记",
},
},
],
title: '入库登记'
}
}
]
},
// 公告
{
path: "/notice",
name: "Notice",
path: '/notice',
name: 'Notice',
component: Layout,
meta: {
title: "公告管理",
icon: "setting",
title: '公告管理',
icon: 'setting'
},
redirect: "/notice/index",
redirect: '/notice/index',
children: [
{
path: "index",
name: "NoticeIndex",
component: () => import("@/views/notice/index.vue"),
path: 'index',
name: 'NoticeIndex',
component: () => import('@/views/notice/index.vue'),
meta: {
title: "公告管理",
},
},
],
title: '公告管理'
}
}
]
},
// 律所标准文件
{
path: "/lawyer-file",
name: "LawyerFile",
path: '/lawyer-file',
name: 'LawyerFile',
component: Layout,
meta: {
title: "律所标准文件",
icon: "setting",
title: '律所标准文件',
icon: 'setting'
},
redirect: "/lawyer-file/index",
redirect: '/lawyer-file/index',
children: [
{
path: "index",
name: "LawyerFileIndex",
component: () => import("@/views/lawyer/index.vue"),
path: 'index',
name: 'LawyerFileIndex',
component: () => import('@/views/lawyer/index.vue'),
meta: {
title: "律所标准文件",
},
},
],
},
];
title: '律所标准文件'
}
}
]
}
]
/**
* 创建路由
@@ -326,12 +326,12 @@ const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes,
// 刷新时,滚动条位置还原
scrollBehavior: () => ({ left: 0, top: 0 }),
});
scrollBehavior: () => ({ left: 0, top: 0 })
})
// 全局注册 router
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 = {
// 系统Title
@@ -25,7 +25,7 @@ export const defaultSettings: AppSettings = {
// 语言
language: LanguageEnum.ZH_CN,
// 主题颜色 - 修改此值时需同步修改 src/styles/variables.scss
themeColor: "#4080FF",
themeColor: '#4080FF',
// 是否显示水印
showWatermark: false,
// 水印内容
@@ -33,8 +33,8 @@ export const defaultSettings: AppSettings = {
// 侧边栏配色方案
sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
// 是否启用 AI 助手
enableAiAssistant: false,
};
enableAiAssistant: false
}
/**
* 认证功能配置
@@ -48,20 +48,20 @@ export const authConfig = {
*
* 适用场景后端没有刷新接口或不需要自动刷新的项目可设为false
*/
enableTokenRefresh: true,
} as const;
enableTokenRefresh: true
} as const
// 主题色预设 - 经典配色方案
// 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值
export const themeColorPresets = [
"#4080FF", // Arco Design 蓝 - 现代感强
"#1890FF", // Ant Design 蓝 - 经典商务
"#409EFF", // Element Plus 蓝 - 清新自然
"#FA8C16", // 活力橙 - 温暖友好
"#722ED1", // 优雅紫 - 高端大气
"#13C2C2", // 青色 - 科技感
"#52C41A", // 成功绿 - 活力清新
"#F5222D", // 警示红 - 醒目强烈
"#2F54EB", // 深蓝 - 稳重专业
"#EB2F96", // 品红 - 时尚个性
];
'#4080FF', // Arco Design 蓝 - 现代感强
'#1890FF', // Ant Design 蓝 - 经典商务
'#409EFF', // Element Plus 蓝 - 清新自然
'#FA8C16', // 活力橙 - 温暖友好
'#722ED1', // 优雅紫 - 高端大气
'#13C2C2', // 青色 - 科技感
'#52C41A', // 成功绿 - 活力清新
'#F5222D', // 警示红 - 醒目强烈
'#2F54EB', // 深蓝 - 稳重专业
'#EB2F96' // 品红 - 时尚个性
]

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