修改eslint规则配置
This commit is contained in:
@@ -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> 不增加额外的缩进
|
||||
|
||||
274
eslint.config.ts
274
eslint.config.ts
@@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
12
src/App.vue
12
src/App.vue
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
defineProps({
|
||||
padding: {
|
||||
type: String,
|
||||
default: "p-2",
|
||||
},
|
||||
});
|
||||
default: 'p-2'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.el {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
}); */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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: '隐藏域' }
|
||||
}
|
||||
|
||||
@@ -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%'" }
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -5,10 +5,10 @@ export const enum DeviceEnum {
|
||||
/**
|
||||
* 宽屏设备
|
||||
*/
|
||||
DESKTOP = "desktop",
|
||||
DESKTOP = 'desktop',
|
||||
|
||||
/**
|
||||
* 窄屏设备
|
||||
*/
|
||||
MOBILE = "mobile",
|
||||
MOBILE = 'mobile'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ export const enum LanguageEnum {
|
||||
/**
|
||||
* 中文
|
||||
*/
|
||||
ZH_CN = "zh-cn",
|
||||
ZH_CN = 'zh-cn',
|
||||
|
||||
/**
|
||||
* 英文
|
||||
*/
|
||||
EN = "en",
|
||||
EN = 'en'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ export enum MenuTypeEnum {
|
||||
CATALOG = 2, // 目录
|
||||
MENU = 1, // 菜单
|
||||
BUTTON = 4, // 按钮
|
||||
EXTLINK = 3, // 外链
|
||||
EXTLINK = 3 // 外链
|
||||
}
|
||||
|
||||
2
src/env.d.ts
vendored
2
src/env.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
26
src/main.ts
26
src/main.ts
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user