diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs
index e6b1be15..215713f3 100644
--- a/.stylelintrc.cjs
+++ b/.stylelintrc.cjs
@@ -1,13 +1,15 @@
module.exports = {
- // 继承推荐规范配置
extends: [
- "stylelint-config-standard",
+ "stylelint-config-recommended",
"stylelint-config-recommended-scss",
"stylelint-config-recommended-vue/scss",
"stylelint-config-html/vue",
"stylelint-config-recess-order",
],
- // 指定不同文件对应的解析器
+
+ plugins: [
+ "stylelint-prettier", // 统一代码风格,格式冲突时以 Prettier 规则为准
+ ],
overrides: [
{
files: ["**/*.{vue,html}"],
@@ -18,29 +20,18 @@ module.exports = {
customSyntax: "postcss-scss",
},
],
- // 自定义规则
rules: {
- "import-notation": "string", // 指定导入CSS文件的方式("string"|"url")
- "selector-class-pattern": null, // 选择器类名命名规则
- "custom-property-pattern": null, // 自定义属性命名规则
- "keyframes-name-pattern": null, // 动画帧节点样式命名规则
- "no-descending-specificity": null, // 允许无降序特异性
- "no-empty-source": null, // 允许空样式
- // 允许 global 、export 、deep伪类
+ "prettier/prettier": true, // 强制执行 Prettier 格式化规则(需配合 .prettierrc 配置文件)
+ "no-empty-source": null, // 允许空的样式文件
+ "declaration-property-value-no-unknown": null, // 允许非常规数值格式 ,如 height: calc(100% - 50)
+ // 允许使用未知伪类
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["global", "export", "deep"],
},
],
- // 允许未知属性
- "property-no-unknown": [
- true,
- {
- ignoreProperties: [],
- },
- ],
- // 允许未知规则
+ // 允许使用未知伪元素
"at-rule-no-unknown": [
true,
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd2cef30..aef3f6da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@
## ✨ feat
-- 支持后端文件导入([#142](https://github.com/youlaitech/vue3-element-admin/pull/142)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 支持后端文件导入([#142](https://github.com/youlaitech/vue3-element-admin/pull/142)) [@cshaptx4869](https://github.com/cshaptx4869)
## 🐛 fix
@@ -15,7 +15,7 @@
## ✨ feat
-- 操作栏增加render配置参数([#138](https://github.com/youlaitech/vue3-element-admin/pull/140)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 操作栏增加render配置参数([#138](https://github.com/youlaitech/vue3-element-admin/pull/140)) [@cshaptx4869](https://github.com/cshaptx4869)
- 左侧工具栏增加type配置参数([#141](https://github.com/youlaitech/vue3-element-admin/pull/141)) [@diamont1001](https://github.com/diamont1001)
## ♻️ refactor
@@ -27,7 +27,7 @@
## ✨ feat
-- 支持默认工具栏的导入([#138](https://github.com/youlaitech/vue3-element-admin/pull/138)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 支持默认工具栏的导入([#138](https://github.com/youlaitech/vue3-element-admin/pull/138)) [@cshaptx4869](https://github.com/cshaptx4869)
- 添加CURD导入示例([19e7bb](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869)
## ♻️ refactor
@@ -40,13 +40,13 @@
## ✨ feat
-- 支持表格远程筛选([#131](https://github.com/youlaitech/vue3-element-admin/pull/131)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 支持表格远程筛选([#131](https://github.com/youlaitech/vue3-element-admin/pull/131)) [@cshaptx4869](https://github.com/cshaptx4869)
- 支持标签输入框([#132](https://github.com/youlaitech/vue3-element-admin/pull/132)) [@cshaptx4869](https://github.com/cshaptx4869)
-- 表单项支持tips配置([#133](https://github.com/youlaitech/vue3-element-admin/pull/133)) [@cshaptx4869](https://github.com/cshaptx4869)
-- 前端导出支持全量数据([#134](https://github.com/youlaitech/vue3-element-admin/pull/134)) [@cshaptx4869](https://github.com/cshaptx4869)
-- 支持选中数据导出([#135](https://github.com/youlaitech/vue3-element-admin/pull/135)) [@cshaptx4869](https://github.com/cshaptx4869)
-- 表格默认工具栏的导出、搜索按钮增加权限点控制([883128](https://github.com/youlaitech/vue3-element-admin/commit/8831289b655f2cc086ecdababaa89f8d8a087c42)) [@cshaptx4869](https://github.com/cshaptx4869)
-- 页签title支持动态设置([23876a](https://github.com/youlaitech/vue3-element-admin/commit/23876aa396143bf77cb5c86af8d6023d9ff6555a)) [@haoxianrui](https://github.com/haoxianrui)
+- 表单项支持tips配置([#133](https://github.com/youlaitech/vue3-element-admin/pull/133)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 前端导出支持全量数据([#134](https://github.com/youlaitech/vue3-element-admin/pull/134)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 支持选中数据导出([#135](https://github.com/youlaitech/vue3-element-admin/pull/135)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 表格默认工具栏的导出、搜索按钮增加权限点控制([883128](https://github.com/youlaitech/vue3-element-admin/commit/8831289b655f2cc086ecdababaa89f8d8a087c42)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 页签title支持动态设置([23876a](https://github.com/youlaitech/vue3-element-admin/commit/23876aa396143bf77cb5c86af8d6023d9ff6555a)) [@haoxianrui](https://github.com/haoxianrui)
## ♻️ refactor
- 默认工具栏支持自定义([#136](https://github.com/youlaitech/vue3-element-admin/pull/136)) [@cshaptx4869](https://github.com/cshaptx4869)
@@ -59,9 +59,9 @@
## ✨ feat
-- 增加pagination、request、parseData配置参数([#119](https://github.com/youlaitech/vue3-element-admin/pull/119)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 增加pagination、request、parseData配置参数([#119](https://github.com/youlaitech/vue3-element-admin/pull/119)) [@cshaptx4869](https://github.com/cshaptx4869)
- 增加返回顶部功能([#120](https://github.com/youlaitech/vue3-element-admin/pull/120)) [@cshaptx4869](https://github.com/cshaptx4869)
-- 支持前端导出([#126](https://github.com/youlaitech/vue3-element-admin/pull/126)) [@cshaptx4869](https://github.com/cshaptx4869)
+- 支持前端导出([#126](https://github.com/youlaitech/vue3-element-admin/pull/126)) [@cshaptx4869](https://github.com/cshaptx4869)
## ♻️ refactor
- 重构布局样式(解决页面抖动问题)([#116](https://github.com/youlaitech/vue3-element-admin/pull/116)) [@cshaptx4869](https://github.com/cshaptx4869)
@@ -174,7 +174,7 @@
- 本地缓存的 token 变量重命名(author by [haoxianrui](https://github.com/haoxianrui))
- 完善 Vite 环境变量类型声明(author by [haoxianrui](https://github.com/haoxianrui))
-## 🐛 fix
+## 🐛 fix
- 修复构建时提示iconComponent.name可能为undefined的报错 (author by [wangji1042](https://github.com/wangji1042))
- 修复浏览器密码自动填充时可能存在的报错 (author by [cshaptx4869](https://github.com/cshaptx4869))
- 修复eslint报错(author by [cshaptx4869](https://github.com/cshaptx4869))
@@ -211,7 +211,7 @@
## ♻️ refactor
- 项目配置按钮移入navbar(author by [cshaptx4869](https://github.com/cshaptx4869))
- 优化user数据定义(author by [cshaptx4869](https://github.com/cshaptx4869))
-- 统一设置栏的 SVG 图标风格
+- 统一设置栏的 SVG 图标风格
## 🐛 fix
- 规整一些开发依赖(author by [cshaptx4869](https://github.com/cshaptx4869))
@@ -241,7 +241,7 @@
# 2.8.1 (2024/01/10)
## ✨ feat
-- 替换 Mock 解决方案 vite-plugin-mock 为 vite-plugin-mock-dev-server 适配 Vite5
+- 替换 Mock 解决方案 vite-plugin-mock 为 vite-plugin-mock-dev-server 适配 Vite5
# 2.8.0 (2023/12/27)
@@ -280,7 +280,7 @@
## ✨ feat
- 菜单管理新增目录只有一级子路由是否始终显示(alwaysShow)和路由页面是否缓存(keepAlive)的配置
- 接口文档新增 swagger、knife4j
-- 引入和支持 tsx
+- 引入和支持 tsx
## ♻️ refactor
- 代码瘦身,整理并删除未使用的 svg
@@ -329,7 +329,7 @@
- 字典组件封装(author by [haoxr](https://juejin.cn/user/4187394044331261/posts))
## 🐛 fix
-- 分页组件hidden无效
+- 分页组件hidden无效
- 签名无法保存至后端
- Git 提交 stylelint 校验部分机器报错
diff --git a/README.md b/README.md
index a97ac1ae..5b0e94ee 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@
vue3-element-admin
-
-
+
+
@@ -12,7 +12,7 @@
-
+
@@ -26,37 +26,32 @@
-
## 项目简介
-[vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) 是基于 Vue3 + Vite5+ TypeScript5 + Element-Plus + Pinia 等主流技术栈构建的免费开源的中后台管理的前端模板(配套[Java 后端源码](https://gitee.com/youlaiorg/youlai-boot))。
+[vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) 基于 Vue3、Vite、TypeScript 和 Element-Plus 搭建的极简开箱即用企业级后台管理前端模板。 (配套 Java 后端 [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) 和 Node 后端 [youlai-nest](https://gitee.com/youlaiorg/youlai-nest))。
## 项目特色
- **简洁易用**:基于 [vue-element-admin](https://gitee.com/panjiachen/vue-element-admin) 升级的 Vue3 版本,无过渡封装 ,易上手。
+- **数据交互**: 支持 `Mock` 数据和[线上接口文档](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5),并提供配套的 [Java](https://gitee.com/youlaiorg/youlai-boot) 和 [Node](https://gitee.com/youlaiorg/youlai-nest) 后端源码。
-- **数据交互**:同时支持本地 `Mock` 和线上接口,配套 [Java 后端源码](https://gitee.com/youlaiorg/youlai-boot)和[在线接口文档](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5)。
-
-- **权限管理**:用户、角色、菜单、字典、部门等完善的权限系统功能。
-
-- **基础设施**:动态路由、按钮权限、国际化、代码规范、Git 提交规范、常用组件封装。
+- **系统功能:** 提供用户管理、角色管理、菜单管理、部门管理、字典管理等功能模块。
+- **权限管理:** 支持动态路由、按钮权限、角色权限和数据权限等多种权限管理方式。
+- **基础设施:** 提供国际化、多布局、暗黑模式、全屏、水印、接口文档和代码生成器等功能。
- **持续更新**:项目持续开源更新,实时更新工具和依赖。
+## 项目截图
-## 项目预览
+
-
-
-
-
-
+
## 项目源码
@@ -100,29 +95,34 @@ pnpm run dev
## 项目部署
+执行 `pnpm run build` 命令后,项目将被打包并生成 `dist` 目录。接下来,将 `dist` 目录下的文件上传到服务器 `/usr/share/nginx/html` 目录下,并配置 Nginx 进行反向代理。
+
```bash
-# 项目打包
pnpm run build
+```
-# 上传文件至远程服务器
-将本地打包生成的 dist 目录下的所有文件拷贝至服务器的 /usr/share/nginx/html 目录。
+以下是 Nginx 的配置示例:
-# nginx.cofig 配置
+```nginx
server {
- listen 80;
- server_name localhost;
- location / {
- root /usr/share/nginx/html;
- index index.html index.htm;
- }
- # 反向代理配置
- location /prod-api/ {
- # api.youlai.tech 替换后端API地址,注意保留后面的斜杠 /
- proxy_pass http://api.youlai.tech/;
- }
+ listen 80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ }
+
+ # 反向代理配置
+ location /prod-api/ {
+ # 请将 api.youlai.tech 替换为您的后端 API 地址,并注意保留后面的斜杠 /
+ proxy_pass http://api.youlai.tech/;
+ }
}
```
+更多详细信息,请参考这篇文章:[Nginx 安装和配置](https://blog.csdn.net/u013737132/article/details/145667694)。
+
## 本地Mock
项目同时支持在线和本地 Mock 接口,默认使用线上接口,如需替换为 Mock 接口,修改文件 `.env.development` 的 `VITE_MOCK_DEV_SERVER` 为 `true` **即可**。
@@ -166,8 +166,8 @@ server {
- [基于 Vue3 + Vite + TypeScript + Element-Plus 从0到1搭建后台管理系统](https://blog.csdn.net/u013737132/article/details/130191394)
-- [ESLint+Prettier+Stylelint+EditorConfig 约束和统一前端代码规范](https://blog.csdn.net/u013737132/article/details/130190788)
-- [Husky + Lint-staged + Commitlint + Commitizen + cz-git 配置 Git 提交规范](https://blog.csdn.net/u013737132/article/details/130191363)
+- [ESLint+Prettier+Stylelint+EditorConfig 约束和统一前端代码规范](https://youlai.blog.csdn.net/article/details/145608723)
+- [Husky + Lint-staged + Commitlint + Commitizen + cz-git 配置 Git 提交规范](https://youlai.blog.csdn.net/article/details/145615236)
## 提交规范
@@ -190,15 +190,13 @@ Thanks to all the contributors!

-## 交流群🚀
+## 加群交流
-> **关注「有来技术」公众号,获取交流群二维码。**
+> **关注「有来技术」公众号,点击菜单“交流群”获取加群二维码。**
>
-> 如果交流群的二维码过期,请加微信(haoxianrui)并备注「前端」、「后端」或「全栈」以获取最新二维码。
+> 如果二维码过期,请加微信(haoxianrui)备注「前端」、「后端」或「全栈」拉你进群。
>
-> 为确保交流群质量,防止营销广告人群混入,我们采取了此措施。望各位理解!
+> 交流群仅限技术交流,为过滤广告营销暂设此门槛,感谢理解与配合
-| 公众号 | 交流群 |
-|:----:|:----:|
-|  |  |
+
diff --git a/commitlint.config.cjs b/commitlint.config.cjs
index 4ecb995f..5f556e2c 100644
--- a/commitlint.config.cjs
+++ b/commitlint.config.cjs
@@ -74,9 +74,7 @@ module.exports = {
breaklineNumber: 100,
breaklineChar: "|",
skipQuestions: [],
- issuePrefixes: [
- { value: "closed", name: "closed: ISSUES has been processed" },
- ],
+ issuePrefixes: [{ value: "closed", name: "closed: ISSUES has been processed" }],
customIssuePrefixAlign: "top",
emptyIssuePrefixAlias: "skip",
customIssuePrefixAlias: "custom",
diff --git a/eslint.config.js b/eslint.config.js
index f5997a7f..d952d1e7 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,115 +1,99 @@
+// https://eslint.nodejs.cn/docs/latest/use/configure/configuration-files
+
import globals from "globals";
-import js from "@eslint/js";
+import pluginJs from "@eslint/js"; // JavaScript 规则
+import pluginVue from "eslint-plugin-vue"; // Vue 规则
+import pluginTypeScript from "@typescript-eslint/eslint-plugin"; // TypeScript 规则
-// ESLint 核心插件
-import pluginVue from "eslint-plugin-vue";
-import pluginTypeScript from "@typescript-eslint/eslint-plugin";
+import parserVue from "vue-eslint-parser"; // Vue 解析器
+import parserTypeScript from "@typescript-eslint/parser"; // TypeScript 解析器
-// Prettier 插件及配置
-import configPrettier from "eslint-config-prettier";
-import pluginPrettier from "eslint-plugin-prettier";
+import configPrettier from "eslint-config-prettier"; // 禁用与 Prettier 冲突的规则
+import pluginPrettier from "eslint-plugin-prettier"; // 运行 Prettier 规则
-// 解析器
-import * as parserVue from "vue-eslint-parser";
-import * as parserTypeScript from "@typescript-eslint/parser";
+// 解析自动导入配置
+import fs from "fs";
+const autoImportConfig = JSON.parse(fs.readFileSync(".eslintrc-auto-import.json", "utf-8"));
-// 定义 ESLint 配置
+/** @type {import('eslint').Linter.Config[]} */
export default [
- // 通用 JavaScript 配置
+ // 指定检查文件和忽略文件
+ {
+ files: ["**/*.{js,mjs,cjs,ts,vue}"],
+ ignores: ["**/*.d.ts"],
+ },
+ // 全局配置
{
- ...js.configs.recommended,
- ignores: ["**/.*", "dist/*", "*.d.ts", "public/*", "src/assets/**"],
languageOptions: {
globals: {
- ...globals.browser, // 浏览器变量 (window, document 等)
- ...globals.node, // Node.js 变量 (process, require 等)
+ ...globals.browser,
+ ...globals.node,
+ ...autoImportConfig.globals,
+ ...{
+ PageQuery: "readonly",
+ PageResult: "readonly",
+ OptionType: "readonly",
+ ResponseData: "readonly",
+ ExcelResult: "readonly",
+ TagView: "readonly",
+ AppSettings: "readonly",
+ __APP_INFO__: "readonly",
+ },
},
},
- plugins: {
- prettier: pluginPrettier,
- },
+ plugins: { prettier: pluginPrettier },
rules: {
- ...configPrettier.rules,
- ...pluginPrettier.configs.recommended.rules,
- "no-debug": "off", // 禁止 debugger
- "prettier/prettier": [
+ ...configPrettier.rules, // 关闭与 Prettier 冲突的规则
+ ...pluginPrettier.configs.recommended.rules, // 启用 Prettier 规则
+ "prettier/prettier": "error", // 强制 Prettier 格式化
+ "no-unused-vars": [
"error",
{
- endOfLine: "auto", // 自动识别换行符
+ argsIgnorePattern: "^_", // 忽略参数名以 _ 开头的参数未使用警告
+ varsIgnorePattern: "^[A-Z0-9_]+$", // 忽略变量名为大写字母、数字或下划线组合的未使用警告(枚举定义未使用场景)
+ ignoreRestSiblings: true, // 忽略解构赋值中同级未使用变量的警告
},
],
},
},
+ // JavaScript 配置
+ pluginJs.configs.recommended,
// TypeScript 配置
{
- files: ["**/*.?([cm])ts"],
+ files: ["**/*.ts"],
+ ignores: ["**/*.d.ts"], // 排除d.ts文件
languageOptions: {
parser: parserTypeScript,
parserOptions: {
sourceType: "module",
},
},
- plugins: {
- "@typescript-eslint": pluginTypeScript,
- },
+ plugins: { "@typescript-eslint": pluginTypeScript },
rules: {
- ...pluginTypeScript.configs.strict.rules,
+ ...pluginTypeScript.configs.strict.rules, // TypeScript 严格规则
"@typescript-eslint/no-explicit-any": "off", // 允许使用 any
"@typescript-eslint/no-empty-function": "off", // 允许空函数
"@typescript-eslint/no-empty-object-type": "off", // 允许空对象类型
- "@typescript-eslint/consistent-type-imports": [
- "error",
- { disallowTypeAnnotations: false, fixStyle: "inline-type-imports" },
- ], // 统一类型导入风格
},
},
- // TypeScript 声明文件的特殊配置
- {
- files: ["**/*.d.ts"],
- rules: {
- "eslint-comments/no-unlimited-disable": "off",
- "unused-imports/no-unused-vars": "off",
- "@typescript-eslint/ban-ts-comment": "off", // 允许使用 @ts-nocheck 注释
- },
- },
-
- // JavaScript (commonjs) 配置
- {
- files: ["**/*.?([cm])js"],
- rules: {
- "@typescript-eslint/no-var-requires": "off", // 允许 require
- },
- },
-
- // Vue 文件配置
+ // Vue 配置
{
files: ["**/*.vue"],
languageOptions: {
parser: parserVue,
parserOptions: {
- parser: "@typescript-eslint/parser",
+ parser: parserTypeScript,
sourceType: "module",
},
},
- plugins: {
- vue: pluginVue,
- },
+ plugins: { vue: pluginVue, "@typescript-eslint": pluginTypeScript },
processor: pluginVue.processors[".vue"],
rules: {
- ...pluginVue.configs["vue3-recommended"].rules,
+ ...pluginVue.configs["vue3-recommended"].rules, // Vue 3 推荐规则
"vue/no-v-html": "off", // 允许 v-html
- "vue/require-default-prop": "off", // 允许没有默认值的 prop
- "vue/multi-word-component-names": "off", // 关闭组件名称多词要求
- "vue/html-self-closing": [
- "error",
- {
- html: { void: "always", normal: "always", component: "always" },
- svg: "always",
- math: "always",
- },
- ], // 自闭合标签
+ "vue/multi-word-component-names": "off", // 允许单个单词组件名
},
},
];
diff --git a/package.json b/package.json
index ff1502d3..6fead508 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "vue3-element-admin",
- "version": "2.20.4",
+ "version": "2.23.0",
"private": true,
"type": "module",
"scripts": {
@@ -9,9 +9,9 @@
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
- "lint:eslint": "eslint --fix ./src",
- "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
- "lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix",
+ "lint:eslint": "eslint --cache \"src/**/*.{vue,ts}\" --fix",
+ "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\"",
+ "lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix",
"lint:lint-staged": "lint-staged",
"preinstall": "npx only-allow pnpm",
"prepare": "husky",
@@ -46,70 +46,73 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@stomp/stompjs": "^7.0.0",
- "@vueuse/core": "^10.11.1",
- "@wangeditor/editor": "^5.1.23",
- "@wangeditor/editor-for-vue": "5.1.10",
+ "@vueuse/core": "^12.6.1",
+ "@wangeditor-next/editor": "^5.6.31",
+ "@wangeditor-next/editor-for-vue": "^5.1.14",
+ "animate.css": "^4.1.1",
"axios": "^1.7.9",
"codemirror": "^5.65.18",
"codemirror-editor-vue3": "^2.8.0",
+ "default-passive-events": "^2.0.0",
"echarts": "^5.6.0",
- "element-plus": "^2.9.3",
+ "element-plus": "^2.9.4",
"exceljs": "^4.4.0",
"lodash-es": "^4.17.21",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
- "path-to-regexp": "^6.3.0",
- "pinia": "^2.3.0",
+ "path-to-regexp": "^8.2.0",
+ "pinia": "^3.0.1",
"qs": "^6.14.0",
"sortablejs": "^1.15.6",
"vue": "^3.5.13",
- "vue-i18n": "9.9.1",
+ "vue-i18n": "^11.1.1",
"vue-router": "^4.5.0"
},
"devDependencies": {
- "@commitlint/cli": "^19.6.1",
- "@commitlint/config-conventional": "^19.6.0",
- "@eslint/js": "^9.18.0",
+ "@commitlint/cli": "^19.7.1",
+ "@commitlint/config-conventional": "^19.7.1",
+ "@eslint/js": "^9.20.0",
+ "@iconify/utils": "^2.3.0",
"@types/codemirror": "^5.60.15",
- "@types/lodash": "^4.17.14",
- "@types/node": "^22.10.7",
+ "@types/lodash-es": "^4.17.12",
+ "@types/node": "^22.13.4",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.18",
"@types/sortablejs": "^1.15.8",
- "@typescript-eslint/eslint-plugin": "^8.20.0",
- "@typescript-eslint/parser": "^8.20.0",
+ "@typescript-eslint/eslint-plugin": "^8.24.0",
+ "@typescript-eslint/parser": "^8.24.0",
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",
"commitizen": "^4.3.1",
- "cz-git": "1.9.4",
- "eslint": "^9.18.0",
- "eslint-config-prettier": "^9.1.0",
+ "cz-git": "^1.11.0",
+ "eslint": "^9.20.1",
+ "eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.32.0",
- "globals": "^15.14.0",
+ "globals": "^15.15.0",
"husky": "^9.1.7",
- "lint-staged": "^15.4.1",
- "postcss": "^8.5.1",
+ "lint-staged": "^15.4.3",
+ "postcss": "^8.5.2",
"postcss-html": "^1.8.0",
"postcss-scss": "^4.0.9",
- "prettier": "^3.4.2",
- "sass": "^1.83.4",
- "stylelint": "^16.13.2",
+ "prettier": "^3.5.1",
+ "sass": "^1.85.0",
+ "stylelint": "^16.14.1",
"stylelint-config-html": "^1.1.0",
- "stylelint-config-recess-order": "^5.1.1",
+ "stylelint-config-recess-order": "^6.0.0",
+ "stylelint-config-recommended": "^15.0.0",
"stylelint-config-recommended-scss": "^14.1.0",
- "stylelint-config-recommended-vue": "^1.5.0",
- "stylelint-config-standard": "^36.0.1",
- "terser": "^5.37.0",
- "typescript": "5.5.4",
- "typescript-eslint": "^8.20.0",
- "unocss": "0.65.3",
- "unplugin-auto-import": "^0.18.6",
- "unplugin-vue-components": "^0.27.5",
- "vite": "^6.0.7",
- "vite-plugin-mock-dev-server": "^1.8.3",
- "vite-plugin-svg-icons": "^2.0.1",
+ "stylelint-config-recommended-vue": "^1.6.0",
+ "stylelint-prettier": "^5.0.3",
+ "terser": "^5.39.0",
+ "typescript": "^5.7.3",
+ "typescript-eslint": "^8.24.0",
+ "unocss": "65.4.3",
+ "unplugin-auto-import": "^19.0.0",
+ "unplugin-vue-components": "^28.0.0",
+ "vite": "^6.1.0",
+ "vite-plugin-mock-dev-server": "^1.8.4",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.2.0"
},
diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts
index ec95620c..64fb97b2 100644
--- a/src/api/auth/index.ts
+++ b/src/api/auth/index.ts
@@ -22,19 +22,17 @@ const AuthAPI = {
/** 刷新 token 接口*/
refreshToken(refreshToken: string) {
- const formData = new FormData();
- formData.append("refreshToken", refreshToken);
return request({
url: `${AUTH_BASE_URL}/refresh-token`,
method: "post",
- data: formData,
+ params: { refreshToken: refreshToken },
headers: {
Authorization: "no-auth",
},
});
},
- /** 注销接口 */
+ /** 注销登录接口 */
logout() {
return request({
url: `${AUTH_BASE_URL}/logout`,
diff --git a/src/api/file/index.ts b/src/api/file/index.ts
index 509e6955..0b109772 100644
--- a/src/api/file/index.ts
+++ b/src/api/file/index.ts
@@ -2,16 +2,25 @@ import request from "@/utils/request";
const FileAPI = {
/**
- * 文件上传地址
+ * 上传文件
+ *
+ * @param formData
*/
- uploadUrl: import.meta.env.VITE_APP_BASE_API + "/api/v1/files",
+ upload(formData: FormData) {
+ return request({
+ url: "/api/v1/files",
+ method: "post",
+ data: formData,
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ });
+ },
/**
* 上传文件
- *
- * @param file
*/
- upload(file: File) {
+ uploadFile(file: File) {
const formData = new FormData();
formData.append("file", file);
return request({
@@ -29,7 +38,7 @@ const FileAPI = {
*
* @param filePath 文件完整路径
*/
- deleteByPath(filePath?: string) {
+ delete(filePath?: string) {
return request({
url: "/api/v1/files",
method: "delete",
@@ -42,7 +51,7 @@ const FileAPI = {
* @param url
* @param fileName
*/
- downloadFile(url: string, fileName?: string) {
+ download(url: string, fileName?: string) {
return request({
url: url,
method: "get",
diff --git a/src/components/CURD/PageContent.vue b/src/components/CURD/PageContent.vue
index e8a408f7..f079d934 100644
--- a/src/components/CURD/PageContent.vue
+++ b/src/components/CURD/PageContent.vue
@@ -240,7 +240,7 @@
-
+
@@ -453,7 +453,6 @@
diff --git a/src/components/Fullscreen/index.vue b/src/components/Fullscreen/index.vue
index bd85b4b4..bd888fe3 100644
--- a/src/components/Fullscreen/index.vue
+++ b/src/components/Fullscreen/index.vue
@@ -1,6 +1,6 @@
diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue
index 1c0642a4..4b624c96 100644
--- a/src/components/Hamburger/index.vue
+++ b/src/components/Hamburger/index.vue
@@ -1,10 +1,6 @@
-
-
-
+
@@ -25,13 +21,25 @@ function toggleClick() {
diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue
index 006158a1..b96e93e4 100644
--- a/src/components/IconSelect/index.vue
+++ b/src/components/IconSelect/index.vue
@@ -11,7 +11,7 @@
-
+
@@ -52,7 +52,7 @@
@click="selectIcon(icon)"
>
-
+
diff --git a/src/components/LangSelect/index.vue b/src/components/LangSelect/index.vue
index 2f35582e..ef7f0cb5 100644
--- a/src/components/LangSelect/index.vue
+++ b/src/components/LangSelect/index.vue
@@ -1,8 +1,6 @@
-
-
-
+
-
-
diff --git a/src/components/TableSelect/index.vue b/src/components/TableSelect/index.vue
index 54fc7630..95fb808b 100644
--- a/src/components/TableSelect/index.vue
+++ b/src/components/TableSelect/index.vue
@@ -143,7 +143,7 @@
@@ -298,8 +227,8 @@ function downloadFile(file: UploadUserFile) {
color: var(--el-text-color-regular);
cursor: pointer;
opacity: 0.75;
- transition: opacity var(--el-transition-duration);
transform: translateY(-50%);
+ transition: opacity var(--el-transition-duration);
}
:deep(.el-upload-list) {
@@ -309,16 +238,4 @@ function downloadFile(file: UploadUserFile) {
:deep(.el-upload-list__item) {
margin: 0;
}
-
-.show-upload-btn {
- :deep(.el-upload) {
- display: inline-flex;
- }
-}
-
-.hide-upload-btn {
- :deep(.el-upload) {
- display: none;
- }
-}
diff --git a/src/components/Upload/ImageUpload.vue b/src/components/Upload/ImageUpload.vue
deleted file mode 100644
index 5b259677..00000000
--- a/src/components/Upload/ImageUpload.vue
+++ /dev/null
@@ -1,406 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
![]()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/Upload/MultiImageUpload.vue b/src/components/Upload/MultiImageUpload.vue
new file mode 100644
index 00000000..a66b94c0
--- /dev/null
+++ b/src/components/Upload/MultiImageUpload.vue
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Upload/SingleImageUpload.vue b/src/components/Upload/SingleImageUpload.vue
new file mode 100644
index 00000000..3ce77c7f
--- /dev/null
+++ b/src/components/Upload/SingleImageUpload.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/WangEditor/index.vue b/src/components/WangEditor/index.vue
index f79062de..bfb6577b 100644
--- a/src/components/WangEditor/index.vue
+++ b/src/components/WangEditor/index.vue
@@ -1,80 +1,87 @@
+
+
-
+
-
-
diff --git a/src/hooks/useStomp.ts b/src/hooks/useStomp.ts
new file mode 100644
index 00000000..302482dc
--- /dev/null
+++ b/src/hooks/useStomp.ts
@@ -0,0 +1,150 @@
+import { Client, IMessage, StompSubscription } from "@stomp/stompjs";
+import { getAccessToken } from "@/utils/auth";
+
+export interface UseStompOptions {
+ /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
+ brokerURL?: string;
+ /** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */
+ token?: string;
+ /** 重连延迟,单位毫秒,默认为 5000 */
+ reconnectDelay?: number;
+ /** 是否开启调试日志 */
+ debug?: boolean;
+}
+
+export function useStomp(options: UseStompOptions = {}) {
+ // 默认值:brokerURL 从环境变量中获取,token 从 getAccessToken() 获取
+ const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
+ const defaultToken = getAccessToken();
+
+ const brokerURL = ref(options.brokerURL ?? defaultBrokerURL);
+ const token = options.token ?? defaultToken;
+
+ // 连接状态标记
+ const isConnected = ref(false);
+ // 存储所有订阅
+ const subscriptions = new Map
();
+
+ // 用于保存 STOMP 客户端的实例
+ let client = ref(null);
+
+ /**
+ * 初始化 STOMP 客户端
+ * 只有在 brokerURL 非空时才会初始化客户端
+ */
+ const initializeClient = () => {
+ if (!brokerURL.value) {
+ console.warn(
+ "brokerURL is required. Please set the WebSocket URL in your .env file (VITE_APP_WS_ENDPOINT)."
+ );
+ return;
+ }
+
+ if (!client.value) {
+ client.value = new Client({
+ brokerURL: brokerURL.value,
+ reconnectDelay: options.reconnectDelay ?? 5000,
+ debug: options.debug ? (msg) => console.log("[STOMP]", msg) : () => {},
+ connectHeaders: {
+ Authorization: `Bearer ${token}`,
+ },
+ heartbeatIncoming: 4000,
+ heartbeatOutgoing: 4000,
+ });
+
+ client.value.onConnect = (frame) => {
+ isConnected.value = true;
+ console.log("STOMP connected", frame);
+ };
+
+ client.value.onStompError = (frame) => {
+ console.error("Broker reported error: " + frame.headers["message"]);
+ console.error("Additional details: " + frame.body);
+ };
+
+ client.value.onWebSocketClose = (evt) => {
+ isConnected.value = false;
+ console.warn("WebSocket closed", evt);
+ };
+ }
+ };
+
+ // 监听 brokerURL 的变化,若地址改变则重新初始化
+ watch(brokerURL, (newURL, oldURL) => {
+ if (newURL !== oldURL) {
+ console.log(`brokerURL changed from ${oldURL} to ${newURL}`);
+ // 断开当前连接,重新激活客户端
+ if (client.value && client.value.connected) {
+ client.value.deactivate();
+ }
+ brokerURL.value = newURL;
+ initializeClient(); // 重新初始化客户端
+ }
+ });
+
+ // 在组件挂载时检查并初始化客户端
+ onMounted(() => {
+ console.log("useStomp onMounted initializeClient");
+ initializeClient();
+ });
+
+ /**
+ * 激活连接(如果已经连接或正在激活则直接返回)
+ */
+ const connect = () => {
+ if (client.value && (client.value.connected || client.value.active)) {
+ console.log("Already connected or connecting, skipping connect() call.");
+ return;
+ }
+ client.value?.activate();
+ };
+
+ /**
+ * 订阅指定主题
+ * @param destination 目标主题地址
+ * @param callback 接收到消息时的回调函数
+ * @returns 返回订阅 id,用于后续取消订阅
+ */
+ const subscribe = (destination: string, callback: (message: IMessage) => void): string => {
+ if (client.value) {
+ const subscription = client.value.subscribe(destination, callback);
+ subscriptions.set(subscription.id, subscription);
+ return subscription.id;
+ }
+ return "";
+ };
+
+ /**
+ * 取消指定订阅
+ * @param subscriptionId 要取消的订阅 id
+ */
+ const unsubscribe = (subscriptionId: string) => {
+ const subscription = subscriptions.get(subscriptionId);
+ if (subscription) {
+ subscription.unsubscribe();
+ subscriptions.delete(subscriptionId);
+ }
+ };
+
+ /**
+ * 主动断开连接(如果未连接则不执行)
+ */
+ const disconnect = () => {
+ if (client.value && !(client.value.connected || client.value.active)) {
+ console.log("Already disconnected, skipping disconnect() call.");
+ return;
+ }
+ client.value?.deactivate();
+ isConnected.value = false;
+ };
+
+ return {
+ client,
+ isConnected,
+ connect,
+ subscribe,
+ unsubscribe,
+ disconnect,
+ brokerURL,
+ };
+}
diff --git a/src/layout/components/AppMain/index.vue b/src/layout/components/AppMain/index.vue
index 8f33f3c4..398ee216 100644
--- a/src/layout/components/AppMain/index.vue
+++ b/src/layout/components/AppMain/index.vue
@@ -2,7 +2,7 @@
-
+
diff --git a/src/layout/components/NavBar/components/NavbarRight.vue b/src/layout/components/NavBar/components/NavbarRight.vue
index 959573f4..99c6dc05 100644
--- a/src/layout/components/NavBar/components/NavbarRight.vue
+++ b/src/layout/components/NavBar/components/NavbarRight.vue
@@ -1,7 +1,7 @@
-
-
+
+
@@ -19,28 +19,52 @@
-
+
+
+
![]()
+
{{ userStore.userInfo.username }}
+
+
+
+
+ {{ $t("navbar.profile") }}
+
+
+ {{ $t("navbar.logout") }}
+
+
+
+
diff --git a/src/layout/components/NavBar/components/Notification.vue b/src/layout/components/NavBar/components/Notification.vue
index bc6069b0..70bc8308 100644
--- a/src/layout/components/NavBar/components/Notification.vue
+++ b/src/layout/components/NavBar/components/Notification.vue
@@ -1,162 +1,61 @@
-
-
+
+
-
+
+
-
+
-
-
-
-
-
-
-
-
- {{ item.title }}
-
+
+
+
+
+
+
+ {{ item.title }}
+
-
- {{ item.publishTime }}
-
-
+
+ {{ item.publishTime }}
-
-
-
-
-
-
-
-
-
- 查看更多
-
-
-
-
-
- 全部已读
-
-
-
-
-
-
-
-
- {{ item.title }}
-
-
-
- {{ item.publishTime }}
-
-
-
-
-
-
-
-
-
-
-
-
- 查看更多
-
-
-
-
-
- 全部已读
-
-
-
-
-
-
-
-
-
-
- {{ item.title }}
-
-
- {{ item.publishTime }}
-
-
-
-
-
-
-
-
-
-
-
-
- 查看更多
-
-
-
-
-
- 全部已读
-
-
-
-
+
+
+
+
+ 查看更多
+
+
+
+
+
+ 全部已读
+
+
+
+
+
+
+
+
@@ -167,45 +66,53 @@
diff --git a/src/layout/components/NavBar/components/UserProfile.vue b/src/layout/components/NavBar/components/UserProfile.vue
deleted file mode 100644
index 32c3e706..00000000
--- a/src/layout/components/NavBar/components/UserProfile.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
![]()
-
{{ userStore.userInfo.username }}
-
-
-
-
- {{ $t("navbar.profile") }}
-
-
- {{ $t("navbar.logout") }}
-
-
-
-
-
-
-
-
-
diff --git a/src/layout/components/Settings/components/LayoutSelect.vue b/src/layout/components/Settings/components/LayoutSelect.vue
index ff8fb1a3..f259baeb 100644
--- a/src/layout/components/Settings/components/LayoutSelect.vue
+++ b/src/layout/components/Settings/components/LayoutSelect.vue
@@ -1,35 +1,20 @@
-
-
+
@@ -38,71 +23,118 @@
-
diff --git a/src/layout/components/Sidebar/components/SidebarLogo.vue b/src/layout/components/Sidebar/components/SidebarLogo.vue
index 420afc1e..5fa80e9c 100644
--- a/src/layout/components/Sidebar/components/SidebarLogo.vue
+++ b/src/layout/components/Sidebar/components/SidebarLogo.vue
@@ -1,6 +1,6 @@
-
+
diff --git a/src/layout/components/Sidebar/components/SidebarMenu.vue b/src/layout/components/Sidebar/components/SidebarMenu.vue
index 2f52d063..bc8bf94b 100644
--- a/src/layout/components/Sidebar/components/SidebarMenu.vue
+++ b/src/layout/components/Sidebar/components/SidebarMenu.vue
@@ -28,7 +28,7 @@
>
import path from "path-browserify";
import type { MenuInstance } from "element-plus";
+import type { RouteRecordRaw } from "vue-router";
import { LayoutEnum } from "@/enums/LayoutEnum";
import { SidebarLightThemeEnum } from "@/enums/ThemeEnum";
@@ -48,8 +49,8 @@ import { isExternal } from "@/utils/index";
import variables from "@/styles/variables.module.scss";
const props = defineProps({
- menuList: {
- type: Array,
+ data: {
+ type: Array,
required: true,
default: () => [],
},
diff --git a/src/layout/components/Sidebar/components/SidebarMenuItem.vue b/src/layout/components/Sidebar/components/SidebarMenuItem.vue
index c89142b6..2139c07d 100644
--- a/src/layout/components/Sidebar/components/SidebarMenuItem.vue
+++ b/src/layout/components/Sidebar/components/SidebarMenuItem.vue
@@ -149,10 +149,10 @@ function resolvePath(routePath: string) {
& > span {
display: inline-block;
+ visibility: hidden;
width: 0;
height: 0;
overflow: hidden;
- visibility: hidden;
}
}
@@ -178,10 +178,10 @@ function resolvePath(routePath: string) {
.el-sub-menu {
& > .el-sub-menu__title > span {
display: inline-block;
+ visibility: hidden;
width: 0;
height: 0;
overflow: hidden;
- visibility: hidden;
}
}
}
diff --git a/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue b/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue
index 06bfef1a..67bf325c 100644
--- a/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue
+++ b/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue
@@ -1,10 +1,14 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
{{ translateRouteTitle(title) }}
@@ -12,32 +16,38 @@
diff --git a/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue b/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue
index 36d81158..870abf7b 100644
--- a/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue
+++ b/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue
@@ -27,7 +27,7 @@
-
+
首页
@@ -40,18 +40,12 @@
diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue
index 0e1ce48f..38947e3c 100644
--- a/src/layout/components/TagsView/index.vue
+++ b/src/layout/components/TagsView/index.vue
@@ -28,27 +28,27 @@
:style="{ left: left + 'px', top: top + 'px' }"
>
-
+
刷新
-
+
关闭
-
+
关闭其它
-
+
关闭左侧
-
+
关闭右侧
-
+
关闭所有
@@ -187,25 +187,17 @@ function isAffix(tag: TagView) {
}
function isFirstView() {
- try {
- return (
- selectedTag.value.path === "/dashboard" ||
- selectedTag.value.fullPath === tagsViewStore.visitedViews[1].fullPath
- );
- } catch (err) {
- return false;
- }
+ return (
+ selectedTag.value.path === "/dashboard" ||
+ selectedTag.value.fullPath === tagsViewStore.visitedViews[1]?.fullPath
+ );
}
function isLastView() {
- try {
- return (
- selectedTag.value.fullPath ===
- tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
- );
- } catch (err) {
- return false;
- }
+ return (
+ selectedTag.value.fullPath ===
+ tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1]?.fullPath
+ );
}
function refreshSelectedTag(view: TagView) {
diff --git a/src/layout/index.vue b/src/layout/index.vue
index d13cd30d..1f709c5a 100644
--- a/src/layout/index.vue
+++ b/src/layout/index.vue
@@ -14,33 +14,33 @@
-
-
@@ -62,15 +62,15 @@ const width = useWindowSize().width;
const WIDTH_DESKTOP = 992; // 响应式布局容器固定宽度 大屏(>=1200px) 中屏(>=992px) 小屏(>=768px)
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE);
const isOpenSidebar = computed(() => appStore.sidebar.opened);
-const showTagsView = computed(() => settingsStore.tagsView); // 是否显示tagsView
+const isShowTagsView = computed(() => settingsStore.tagsView); // 是否显示tagsView
const layout = computed(() => settingsStore.layout); // 布局模式 left top mix
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath); // 顶部菜单激活path
-const mixLeftMenus = computed(() => permissionStore.mixLeftMenus); // 混合布局左侧菜单
+const mixedLayoutLeftRoutes = computed(() => permissionStore.mixedLayoutLeftRoutes); // 混合布局左侧菜单
watch(
() => activeTopMenuPath.value,
(newVal: string) => {
- permissionStore.setMixLeftMenus(newVal);
+ permissionStore.setMixedLayoutLeftRoutes(newVal);
},
{
deep: true,
@@ -245,8 +245,10 @@ watch(route, () => {
}
.hideSidebar {
- .main-container {
- margin-left: $sidebar-width-collapsed;
+ &.layout-left {
+ .main-container {
+ margin-left: $sidebar-width-collapsed;
+ }
}
&.layout-top {
@@ -280,8 +282,8 @@ watch(route, () => {
&.mobile {
.sidebar-container {
pointer-events: none;
+ transform: translate3d(-$sidebar-width, 0, 0);
transition-duration: 0.3s;
- transform: translate3d(-210px, 0, 0);
}
.main-container {
@@ -291,13 +293,10 @@ watch(route, () => {
}
.mobile {
- .main-container {
+ .layout-mix,
+ .layout-top,
+ .layout-left {
margin-left: 0;
}
-
- &.layout-top {
- // 顶部模式全局变量修改
- --el-menu-item-height: $navbar-height;
- }
}
diff --git a/src/main.ts b/src/main.ts
index c1c1a4e4..0167bf8c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -2,8 +2,6 @@ import { createApp } from "vue";
import App from "./App.vue";
import setupPlugins from "@/plugins";
-// 本地SVG图标
-import "virtual:svg-icons-register";
// 暗黑主题样式
import "element-plus/theme-chalk/dark/css-vars.css";
// 暗黑模式自定义变量
@@ -11,6 +9,12 @@ import "@/styles/dark/css-vars.css";
import "@/styles/index.scss";
import "uno.css";
+// 全局引入 animate.css
+import "animate.css";
+
+// 自动为某些默认事件(如 touchstart、wheel 等)添加 { passive: true },提升滚动性能并消除控制台的非被动事件监听警告
+import "default-passive-events";
+
const app = createApp(App);
// 注册插件
app.use(setupPlugins);
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index df853d88..e5262519 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -6,7 +6,6 @@ import { setupRouter } from "@/router";
import { setupStore } from "@/store";
import { setupElIcons } from "./icons";
import { setupPermission } from "./permission";
-import webSocketManager from "@/utils/websocket";
import { InstallCodeMirror } from "codemirror-editor-vue3";
export default {
@@ -23,8 +22,6 @@ export default {
setupElIcons(app);
// 路由守卫
setupPermission();
- // 初始化 WebSocket
- webSocketManager.setupWebSocket();
// 注册 CodeMirror
app.use(InstallCodeMirror);
},
diff --git a/src/plugins/permission.ts b/src/plugins/permission.ts
index 09096cef..04604e92 100644
--- a/src/plugins/permission.ts
+++ b/src/plugins/permission.ts
@@ -1,6 +1,6 @@
import type { NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router";
import NProgress from "@/utils/nprogress";
-import { getToken } from "@/utils/auth";
+import { getAccessToken } from "@/utils/auth";
import router from "@/router";
import { usePermissionStore, useUserStore } from "@/store";
@@ -11,7 +11,7 @@ export function setupPermission() {
router.beforeEach(async (to, from, next) => {
NProgress.start();
- const isLogin = !!getToken(); // 判断是否登录
+ const isLogin = !!getAccessToken(); // 判断是否登录
if (isLogin) {
if (to.path === "/login") {
// 已登录,访问登录页,跳转到首页
diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts
index de3a0c3f..6a8e532b 100644
--- a/src/store/modules/permission.ts
+++ b/src/store/modules/permission.ts
@@ -8,22 +8,24 @@ const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");
export const usePermissionStore = defineStore("permission", () => {
- // 所有路由,包括静态和动态路由
+ // 储所有路由,包括静态路由和动态路由
const routes = ref
([]);
- // 混合模式左侧菜单
- const mixLeftMenus = ref([]);
- // 路由是否已加载
+ // 混合模式左侧菜单路由
+ const mixedLayoutLeftRoutes = ref([]);
+ // 路由是否加载完成
const isRoutesLoaded = ref(false);
/**
- * 生成动态路由
+ * 获取后台动态路由数据,解析并注册到全局路由
+ *
+ * @returns Promise 解析后的动态路由列表
*/
function generateRoutes() {
return new Promise((resolve, reject) => {
MenuAPI.getRoutes()
.then((data) => {
- const dynamicRoutes = transformRoutes(data);
- routes.value = constantRoutes.concat(dynamicRoutes);
+ const dynamicRoutes = parseDynamicRoutes(data);
+ routes.value = [...constantRoutes, ...dynamicRoutes];
isRoutesLoaded.value = true;
resolve(dynamicRoutes);
})
@@ -34,14 +36,14 @@ export const usePermissionStore = defineStore("permission", () => {
}
/**
- * 混合模式菜单下根据顶部菜单路径设置左侧菜单
+ * 根据父菜单路径设置混合模式左侧菜单
*
- * @param topMenuPath - 顶部菜单路径
+ * @param parentPath 父菜单的路径,用于查找对应的菜单项
*/
- const setMixLeftMenus = (topMenuPath: string) => {
- const matchedItem = routes.value.find((item) => item.path === topMenuPath);
+ const setMixedLayoutLeftRoutes = (parentPath: string) => {
+ const matchedItem = routes.value.find((item) => item.path === parentPath);
if (matchedItem && matchedItem.children) {
- mixLeftMenus.value = matchedItem.children;
+ mixedLayoutLeftRoutes.value = matchedItem.children;
}
};
@@ -49,59 +51,63 @@ export const usePermissionStore = defineStore("permission", () => {
* 重置路由
*/
const resetRouter = () => {
- // 删除动态路由,保留静态路由
+ // 从 router 实例中移除动态路由
routes.value.forEach((route) => {
if (route.name && !constantRoutes.find((r) => r.name === route.name)) {
- // 从 router 实例中移除动态路由
router.removeRoute(route.name);
}
});
+ // 清空本地存储的路由和菜单数据
routes.value = [];
- mixLeftMenus.value = [];
+ mixedLayoutLeftRoutes.value = [];
isRoutesLoaded.value = false;
};
return {
routes,
- generateRoutes,
- mixLeftMenus,
- setMixLeftMenus,
+ mixedLayoutLeftRoutes,
isRoutesLoaded,
+ generateRoutes,
+ setMixedLayoutLeftRoutes,
resetRouter,
};
});
/**
- * 转换路由数据为组件
+ * 解析后端返回的路由数据并转换为 Vue Router 兼容的路由配置
+ *
+ * 1. 遍历 `rawRoutes` 并转换为 `RouteRecordRaw` 格式。
+ * 2. 若 `component` 为 `"Layout"`,则替换为 `Layout` 组件。
+ * 3. 若 `component` 为字符串路径,则动态加载对应的 Vue 组件,找不到则默认 `404.vue`。
+ * 4. 递归解析 `children`,确保子路由也被正确转换。
+ *
+ * @param rawRoutes 后端返回的原始路由数据
+ * @returns 解析后的路由配置数组
*/
-const transformRoutes = (routes: RouteVO[]) => {
- const asyncRoutes: RouteRecordRaw[] = [];
- routes.forEach((route) => {
- const tmpRoute = { ...route } as RouteRecordRaw;
- // 顶级目录,替换为 Layout 组件
- if (tmpRoute.component?.toString() == "Layout") {
- tmpRoute.component = Layout;
- } else {
- // 其他菜单,根据组件路径动态加载组件
- const component = modules[`../../views/${tmpRoute.component}.vue`];
- if (component) {
- tmpRoute.component = component;
- } else {
- tmpRoute.component = modules["../../views/error-page/404.vue"];
- }
+const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
+ const parsedRoutes: RouteRecordRaw[] = [];
+
+ rawRoutes.forEach((route) => {
+ const normalizedRoute = { ...route } as RouteRecordRaw;
+
+ // 处理组件路径
+ normalizedRoute.component =
+ normalizedRoute.component?.toString() === "Layout"
+ ? Layout
+ : modules[`../../views/${normalizedRoute.component}.vue`] ||
+ modules["../../views/error-page/404.vue"];
+
+ // 递归解析子路由
+ if (normalizedRoute.children) {
+ normalizedRoute.children = parseDynamicRoutes(route.children);
}
- if (tmpRoute.children) {
- tmpRoute.children = transformRoutes(route.children);
- }
-
- asyncRoutes.push(tmpRoute);
+ parsedRoutes.push(normalizedRoute);
});
- return asyncRoutes;
+ return parsedRoutes;
};
-
/**
* 在组件外使用 Pinia store 实例 @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html
*/
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
index 5812f7a5..9bd019fa 100644
--- a/src/store/modules/user.ts
+++ b/src/store/modules/user.ts
@@ -5,7 +5,7 @@ import { useDictStoreHook } from "@/store/modules/dict";
import AuthAPI, { type LoginFormData } from "@/api/auth";
import UserAPI, { type UserInfo } from "@/api/system/user";
-import { setToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth";
+import { setAccessToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth";
export const useUserStore = defineStore("user", () => {
const userInfo = useStorage("userInfo", {} as UserInfo);
@@ -20,8 +20,8 @@ export const useUserStore = defineStore("user", () => {
return new Promise((resolve, reject) => {
AuthAPI.login(LoginFormData)
.then((data) => {
- const { tokenType, accessToken, refreshToken } = data;
- setToken(tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
+ const { accessToken, refreshToken } = data;
+ setAccessToken(accessToken); // eyJhbGciOiJIUzI1NiJ9.xxx.xxx
setRefreshToken(refreshToken);
resolve();
})
@@ -77,8 +77,8 @@ export const useUserStore = defineStore("user", () => {
return new Promise((resolve, reject) => {
AuthAPI.refreshToken(refreshToken)
.then((data) => {
- const { tokenType, accessToken, refreshToken } = data;
- setToken(tokenType + " " + accessToken);
+ const { accessToken, refreshToken } = data;
+ setAccessToken(accessToken);
setRefreshToken(refreshToken);
resolve();
})
diff --git a/src/styles/reset.scss b/src/styles/reset.scss
index a20f04a5..56689def 100644
--- a/src/styles/reset.scss
+++ b/src/styles/reset.scss
@@ -25,8 +25,8 @@ body {
width: 100%;
height: 100%;
margin: 0;
- font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
- "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
+ font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
+ "微软雅黑", Arial, sans-serif;
line-height: inherit;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
diff --git a/src/types/components.d.ts b/src/types/components.d.ts
index fcc072db..d9ef9c47 100644
--- a/src/types/components.d.ts
+++ b/src/types/components.d.ts
@@ -13,6 +13,7 @@ declare module "vue" {
CURD: (typeof import("./../components/CURD/index.vue"))["default"];
Dict: (typeof import("./../components/Dict/index.vue"))["default"];
DictLabel: (typeof import("./../components/Dict/DictLabel.vue"))["default"];
+ ECharts: (typeof import("./../components/ECharts/index.vue"))["default"];
ElBacktop: (typeof import("element-plus/es"))["ElBacktop"];
ElBreadcrumb: (typeof import("element-plus/es"))["ElBreadcrumb"];
ElBreadcrumbItem: (typeof import("element-plus/es"))["ElBreadcrumbItem"];
@@ -84,12 +85,11 @@ declare module "vue" {
SidebarMenuItem: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItem.vue"))["default"];
SidebarMenuItemTitle: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItemTitle.vue"))["default"];
SidebarMixTopMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMixTopMenu.vue"))["default"];
+ SingleImageUpload: (typeof import("./../components/Upload/SingleImageUpload.vue"))["default"];
SizeSelect: (typeof import("./../components/SizeSelect/index.vue"))["default"];
- SvgIcon: (typeof import("./../components/SvgIcon/index.vue"))["default"];
TableSelect: (typeof import("./../components/TableSelect/index.vue"))["default"];
TagsView: (typeof import("./../layout/components/TagsView/index.vue"))["default"];
ThemeColorPicker: (typeof import("./../layout/components/Settings/components/ThemeColorPicker.vue"))["default"];
- SingleImageUpload: (typeof import("./../components/Upload/SingleImageUpload.vue"))["default"];
WangEditor: (typeof import("./../components/WangEditor/index.vue"))["default"];
}
export interface ComponentCustomProperties {
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
index bd3e9b3e..cfbedf84 100644
--- a/src/utils/auth.ts
+++ b/src/utils/auth.ts
@@ -3,11 +3,11 @@ const ACCESS_TOKEN_KEY = "access_token";
// 刷新 token 缓存的 key
const REFRESH_TOKEN_KEY = "refresh_token";
-function getToken(): string {
+function getAccessToken(): string {
return localStorage.getItem(ACCESS_TOKEN_KEY) || "";
}
-function setToken(token: string) {
+function setAccessToken(token: string) {
localStorage.setItem(ACCESS_TOKEN_KEY, token);
}
@@ -24,4 +24,4 @@ function clearToken() {
localStorage.removeItem(REFRESH_TOKEN_KEY);
}
-export { getToken, setToken, clearToken, getRefreshToken, setRefreshToken };
+export { getAccessToken, setAccessToken, clearToken, getRefreshToken, setRefreshToken };
diff --git a/src/utils/request.ts b/src/utils/request.ts
index 1a8211dc..86959e88 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -2,7 +2,7 @@ import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axio
import qs from "qs";
import { useUserStoreHook } from "@/store/modules/user";
import { ResultEnum } from "@/enums/ResultEnum";
-import { getToken } from "@/utils/auth";
+import { getAccessToken } from "@/utils/auth";
import router from "@/router";
// 创建 axios 实例
@@ -16,10 +16,10 @@ const service = axios.create({
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
- const accessToken = getToken();
+ const accessToken = getAccessToken();
// 如果 Authorization 设置为 no-auth,则不携带 Token,用于登录、刷新 Token 等接口
if (config.headers.Authorization !== "no-auth" && accessToken) {
- config.headers.Authorization = accessToken;
+ config.headers.Authorization = `Bearer ${accessToken}`;
} else {
delete config.headers.Authorization;
}
@@ -44,7 +44,8 @@ service.interceptors.response.use(
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Error"));
},
- async (error: any) => {
+ async (error) => {
+ console.error("request error", error); // for debug
// 非 2xx 状态码处理 401、403、500 等
const { config, response } = error;
if (response) {
@@ -64,20 +65,21 @@ service.interceptors.response.use(
export default service;
-// 刷新 Token 的锁
+// 是否正在刷新标识,避免重复刷新
let isRefreshing = false;
-// 因 Token 过期导致失败的请求队列
-let requestsQueue: Array<() => void> = [];
+// 因 Token 过期导致的请求等待队列
+const waitingQueue: Array<() => void> = [];
// 刷新 Token 处理
async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
return new Promise((resolve) => {
- const requestCallback = () => {
- config.headers.Authorization = getToken();
+ // 封装需要重试的请求
+ const retryRequest = () => {
+ config.headers.Authorization = getAccessToken();
resolve(service(config));
};
- requestsQueue.push(requestCallback);
+ waitingQueue.push(retryRequest);
if (!isRefreshing) {
isRefreshing = true;
@@ -86,13 +88,13 @@ async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
useUserStoreHook()
.refreshToken()
.then(() => {
- // Token 刷新成功,执行请求队列
- requestsQueue.forEach((callback) => callback());
- requestsQueue = [];
+ // 依次重试队列中所有请求, 重试后清空队列
+ waitingQueue.forEach((callback) => callback());
+ waitingQueue.length = 0;
})
- .catch((error) => {
+ .catch((error: any) => {
console.log("handleTokenRefresh error", error);
- // Token 刷新失败,清除用户数据并跳转到登录
+ // 刷新 Token 失败,跳转登录页
ElNotification({
title: "提示",
message: "您的会话已过期,请重新登录",
diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts
deleted file mode 100644
index bd8fadb6..00000000
--- a/src/utils/websocket.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { Client } from "@stomp/stompjs";
-import { getToken } from "@/utils/auth";
-
-class WebSocketManager {
- private client: Client | null = null;
- private messageHandlers: Map void)[]> = new Map();
- private reconnectAttempts = 0;
- private maxReconnectAttempts = 3; // 自定义最大重试次数
- private reconnectDelay = 5000; // 重试延迟(单位:毫秒)
-
- // 初始化 WebSocket 客户端
- setupWebSocket() {
- const endpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
-
- // 如果没有配置 WebSocket 端点或显式关闭,直接返回
- if (!endpoint) {
- console.log("WebSocket 已被禁用,如需打开请在配置文件中配置 VITE_APP_WS_ENDPOINT");
- return;
- }
-
- if (this.client && this.client.connected) {
- console.log("客户端已存在并且连接正常");
- return this.client;
- }
-
- this.client = new Client({
- brokerURL: endpoint,
- connectHeaders: {
- Authorization: getToken(),
- },
- heartbeatIncoming: 30000,
- heartbeatOutgoing: 30000,
- reconnectDelay: 0, // 设置为 0 禁用重连
- onConnect: () => {
- console.log(`连接到 WebSocket 服务器: ${endpoint}`);
- this.reconnectAttempts = 0; // 重置重连计数
- this.messageHandlers.forEach((handlers, topic) => {
- handlers.forEach((handler) => {
- this.subscribeToTopic(topic, handler);
- });
- });
- },
- onStompError: (frame) => {
- console.error(`连接错误: ${frame.headers["message"]}`);
- console.error(`错误详情: ${frame.body}`);
- },
- onDisconnect: () => {
- console.log(`WebSocket 连接已断开: ${endpoint}`);
- this.reconnectAttempts++;
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
- console.log(`正在尝试重连... 尝试次数: ${this.reconnectAttempts}`);
- } else {
- console.log("重连次数已达上限,停止重连");
- this.client?.deactivate();
- }
- },
- });
-
- this.client.activate();
- }
-
- // 订阅主题
- public subscribeToTopic(topic: string, onMessage: (message: string) => void) {
- console.log(`正在订阅主题: ${topic}`);
- if (!this.client || !this.client.connected) {
- this.setupWebSocket();
- }
-
- if (this.messageHandlers.has(topic)) {
- this.messageHandlers.get(topic)?.push(onMessage);
- } else {
- this.messageHandlers.set(topic, [onMessage]);
- }
-
- if (this.client?.connected) {
- this.client.subscribe(topic, (message) => {
- const handlers = this.messageHandlers.get(topic);
- handlers?.forEach((handler) => handler(message.body));
- });
- }
- }
-
- // 断开 WebSocket 连接
- public disconnect() {
- if (this.client) {
- console.log("断开 WebSocket 连接");
- this.client.deactivate();
- this.client = null;
- }
- }
-}
-
-export default new WebSocketManager();
diff --git a/src/views/codegen/index.vue b/src/views/codegen/index.vue
index 76bd29a1..66809295 100644
--- a/src/views/codegen/index.vue
+++ b/src/views/codegen/index.vue
@@ -354,7 +354,7 @@
@node-click="handleFileTreeNodeClick"
>
-
+
{{ data.label }}
@@ -437,7 +437,7 @@ interface TreeNode {
}
const treeData = ref([]);
-const queryFormRef = ref(ElForm);
+const queryFormRef = ref();
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
@@ -535,7 +535,6 @@ const initSort = () => {
ghostClass: "sortable-ghost", //拖拽样式
handle: ".sortable-handle", //拖拽区域
easing: "cubic-bezier(1, 0, 0, 1)",
- onStart: (item: any) => {},
// 结束拖动事件
onEnd: (item: any) => {
diff --git a/src/views/dashboard/components/visit-trend.vue b/src/views/dashboard/components/visit-trend.vue
deleted file mode 100644
index 24fef6ad..00000000
--- a/src/views/dashboard/components/visit-trend.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-
-
-
-
-
-
- 访问趋势
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue
index 6177dcf8..f784e3e2 100644
--- a/src/views/dashboard/index.vue
+++ b/src/views/dashboard/index.vue
@@ -8,7 +8,7 @@
@@ -27,15 +27,15 @@
@@ -47,18 +47,18 @@
@@ -70,7 +70,7 @@
@@ -116,7 +116,11 @@
{{ visitStatsData.todayUvCount }}
@@ -125,7 +129,7 @@
{{ formatGrowthRate(visitStatsData.uvGrowthRate) }}
-
+
@@ -172,7 +176,11 @@
{{ visitStatsData.todayPvCount }}
@@ -181,7 +189,7 @@
{{ formatGrowthRate(visitStatsData.pvGrowthRate) }}
-
+
@@ -197,7 +205,18 @@
-
+
+
+
+ 访问趋势
+
+
+
+
+
+
+
+
@@ -206,7 +225,7 @@
@@ -218,7 +237,7 @@
{{ item.title }}
-
+
@@ -237,38 +256,43 @@ defineOptions({
inheritAttrs: false,
});
-import VisitTrend from "./components/visit-trend.vue";
-
+import { dayjs } from "element-plus";
import router from "@/router";
-
-import LogAPI, { VisitStatsVO } from "@/api/system/log";
+import LogAPI, { VisitStatsVO, VisitTrendVO } from "@/api/system/log";
import NoticeAPI, { NoticePageVO } from "@/api/system/notice";
-
import { useUserStore } from "@/store/modules/user";
import { formatGrowthRate } from "@/utils";
+const userStore = useUserStore();
+
const noticeDetailRef = ref();
+// 当前通知公告列表
const notices = ref([]);
-const userStore = useUserStore();
-const date: Date = new Date();
+// 当前时间(用于计算问候语)
+const currentDate = new Date();
+
+// 问候语:根据当前小时返回不同问候语
const greetings = computed(() => {
- const hours = date.getHours();
+ const hours = currentDate.getHours();
+ const nickname = userStore.userInfo.nickname;
if (hours >= 6 && hours < 8) {
return "晨起披衣出草堂,轩窗已自喜微凉🌅!";
} else if (hours >= 8 && hours < 12) {
- return "上午好," + userStore.userInfo.nickname + "!";
+ return `上午好,${nickname}!`;
} else if (hours >= 12 && hours < 18) {
- return "下午好," + userStore.userInfo.nickname + "!";
+ return `下午好,${nickname}!`;
} else if (hours >= 18 && hours < 24) {
- return "晚上好," + userStore.userInfo.nickname + "!";
+ return `晚上好,${nickname}!`;
} else {
return "偷偷向银河要了一把碎星,只等你闭上眼睛撒入你的梦中,晚安🌛!";
}
});
+// 访客统计数据加载状态
const visitStatsLoading = ref(true);
+// 访客统计数据
const visitStatsData = ref({
todayUvCount: 0,
uvGrowthRate: 0,
@@ -278,8 +302,15 @@ const visitStatsData = ref({
totalPvCount: 0,
});
-// 加载访问统计数据
-const loadVisitStatsData = async () => {
+// 访问趋势日期范围(单位:天)
+const visitTrendDateRange = ref(7);
+// 访问趋势图表配置
+const visitTrendChartOptions = ref();
+
+/**
+ * 获取访客统计数据
+ */
+const fetchVisitStatsData = () => {
LogAPI.getVisitStats()
.then((data) => {
visitStatsData.value = data;
@@ -289,12 +320,102 @@ const loadVisitStatsData = async () => {
});
};
-// 根据增长率获取样式
-const getGrowthRateClass = (growthRate?: number): string => {
+/**
+ * 获取访问趋势数据,并更新图表配置
+ */
+const fetchVisitTrendData = () => {
+ const startDate = dayjs()
+ .subtract(visitTrendDateRange.value - 1, "day")
+ .toDate();
+ const endDate = new Date();
+
+ LogAPI.getVisitTrend({
+ startDate: dayjs(startDate).format("YYYY-MM-DD"),
+ endDate: dayjs(endDate).format("YYYY-MM-DD"),
+ }).then((data) => {
+ updateVisitTrendChartOptions(data);
+ });
+};
+
+/**
+ * 更新访问趋势图表的配置项
+ *
+ * @param data - 访问趋势数据
+ */
+const updateVisitTrendChartOptions = (data: VisitTrendVO) => {
+ console.log("Updating visit trend chart options");
+
+ visitTrendChartOptions.value = {
+ tooltip: {
+ trigger: "axis",
+ },
+ legend: {
+ data: ["浏览量(PV)", "访客数(UV)"],
+ bottom: 0,
+ },
+ grid: {
+ left: "1%",
+ right: "5%",
+ bottom: "10%",
+ containLabel: true,
+ },
+ xAxis: {
+ type: "category",
+ data: data.dates,
+ },
+ yAxis: {
+ type: "value",
+ splitLine: {
+ show: true,
+ lineStyle: {
+ type: "dashed",
+ },
+ },
+ },
+ series: [
+ {
+ name: "浏览量(PV)",
+ type: "line",
+ data: data.pvList,
+ areaStyle: {
+ color: "rgba(64, 158, 255, 0.1)",
+ },
+ smooth: true,
+ itemStyle: {
+ color: "#4080FF",
+ },
+ lineStyle: {
+ color: "#4080FF",
+ },
+ },
+ {
+ name: "访客数(UV)",
+ type: "line",
+ data: data.ipList,
+ areaStyle: {
+ color: "rgba(103, 194, 58, 0.1)",
+ },
+ smooth: true,
+ itemStyle: {
+ color: "#67C23A",
+ },
+ lineStyle: {
+ color: "#67C23A",
+ },
+ },
+ ],
+ };
+};
+
+/**
+ * 根据增长率计算对应的 CSS 类名
+ *
+ * @param growthRate - 增长率数值
+ */
+const computeGrowthRateClass = (growthRate?: number): string => {
if (!growthRate) {
return "color-[--el-color-info]";
}
-
if (growthRate > 0) {
return "color-[--el-color-danger]";
} else if (growthRate < 0) {
@@ -304,25 +425,45 @@ const getGrowthRateClass = (growthRate?: number): string => {
}
};
-const loadMyNotice = () => {
+/**
+ * 获取当前用户的通知公告数据
+ */
+const fetchMyNotices = () => {
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 10 }).then((data) => {
notices.value = data.list;
});
};
-// 查看更多
-function handleViewMoreNotice() {
+/**
+ * 跳转至通知公告详情页面(查看更多通知)
+ */
+function navigateToNoticePage() {
router.push({ path: "/myNotice" });
}
-// 打开通知公告
-function handleOpenNoticeDetail(id: string) {
+/**
+ * 打开指定通知详情
+ *
+ * @param id - 通知 ID
+ */
+function openNoticeDetail(id: string) {
noticeDetailRef.value.openNotice(id);
}
+// 监听访问趋势日期范围的变化,重新获取趋势数据
+watch(
+ () => visitTrendDateRange.value,
+ (newVal) => {
+ console.log("Visit trend date range changed:", newVal);
+ fetchVisitTrendData();
+ },
+ { immediate: true }
+);
+
+// 组件挂载后加载访客统计数据和通知公告数据
onMounted(() => {
- loadVisitStatsData();
- loadMyNotice();
+ fetchVisitStatsData();
+ fetchMyNotices();
});
diff --git a/src/views/demo/icons.vue b/src/views/demo/icons.vue
index 73a7aef4..d6b8c242 100644
--- a/src/views/demo/icons.vue
+++ b/src/views/demo/icons.vue
@@ -7,7 +7,7 @@
@@ -36,7 +36,6 @@
@@ -42,6 +46,9 @@ const text = computed(() => {
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
+
+
+
diff --git a/src/views/demo/upload.vue b/src/views/demo/upload.vue
index 901a3edd..c590dc58 100644
--- a/src/views/demo/upload.vue
+++ b/src/views/demo/upload.vue
@@ -11,216 +11,30 @@
-
- {{ picUrl }}
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
diff --git a/src/views/demo/wang-editor.vue b/src/views/demo/wang-editor.vue
index 67db92d0..560f0ecb 100644
--- a/src/views/demo/wang-editor.vue
+++ b/src/views/demo/wang-editor.vue
@@ -1,8 +1,8 @@
@@ -15,6 +15,10 @@ const value = ref("初始内容");
>
示例源码 请点击>>>>
-
+
+
+
+
+
diff --git a/src/views/demo/websocket.vue b/src/views/demo/websocket.vue
index 1550a169..7ca89b1b 100644
--- a/src/views/demo/websocket.vue
+++ b/src/views/demo/websocket.vue
@@ -12,8 +12,8 @@
-
-
+
+
-
+
连接状态:
- 已连接
- 已断开
+ 已连接
+ 已断开
@@ -62,28 +62,31 @@
-
+
-
-
- {{ message.sender }}
+
+
+
+ {{ message.sender }}
+
+
{{ message.content }}
- {{ message.content }}
-
+
{{ message.content }}
@@ -94,98 +97,77 @@
-
diff --git a/src/views/error/404.vue b/src/views/error/404.vue
index 005c09eb..efd7e393 100644
--- a/src/views/error/404.vue
+++ b/src/views/error/404.vue
@@ -89,8 +89,8 @@ function back() {
}
&__return-home {
- display: block;
float: left;
+ display: block;
width: 110px;
height: 36px;
font-size: 14px;
diff --git a/src/views/login/index.vue b/src/views/login/index.vue
index 84e7784a..edf30fde 100644
--- a/src/views/login/index.vue
+++ b/src/views/login/index.vue
@@ -86,7 +86,8 @@
diff --git a/src/views/profile/index.vue b/src/views/profile/index.vue
index e36f06b3..68b4e7c5 100644
--- a/src/views/profile/index.vue
+++ b/src/views/profile/index.vue
@@ -59,14 +59,14 @@
-
+
部门
{{ userProfile.deptName }}
-
+
角色
{{ userProfile.roleNames }}
@@ -268,7 +268,7 @@ import { Camera } from "@element-plus/icons-vue";
const userProfile = ref({});
-enum DialogType {
+const enum DialogType {
ACCOUNT = "account",
PASSWORD = "password",
MOBILE = "mobile",
@@ -287,10 +287,10 @@ const mobileUpdateForm = reactive({});
const emailUpdateForm = reactive({});
const mobileCountdown = ref(0);
-const mobileTimer = ref(null);
+const mobileTimer = ref();
const emailCountdown = ref(0);
-const emailTimer = ref(null);
+const emailTimer = ref();
// 修改密码校验规则
const passwordChangeRules = {
@@ -458,7 +458,7 @@ const handleFileChange = async (event: Event) => {
if (file) {
// 调用文件上传API
try {
- const data = await FileAPI.upload(file);
+ const data = await FileAPI.uploadFile(file);
// 更新用户头像
userProfile.value.avatar = data.url;
// 更新用户信息
@@ -466,6 +466,7 @@ const handleFileChange = async (event: Event) => {
avatar: data.url,
});
} catch (error) {
+ console.error("头像上传失败:" + error);
ElMessage.error("头像上传失败");
}
}
diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue
index 0955806f..034efa23 100644
--- a/src/views/system/config/index.vue
+++ b/src/views/system/config/index.vue
@@ -137,8 +137,8 @@ defineOptions({
import ConfigAPI, { ConfigPageVO, ConfigForm, ConfigPageQuery } from "@/api/system/config";
-const queryFormRef = ref(ElForm);
-const dataFormRef = ref(ElForm);
+const queryFormRef = ref();
+const dataFormRef = ref();
const loading = ref(false);
const selectIds = ref([]);
diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue
index 9ecc354a..45a39a2d 100644
--- a/src/views/system/dept/index.vue
+++ b/src/views/system/dept/index.vue
@@ -160,8 +160,8 @@ defineOptions({
import DeptAPI, { DeptVO, DeptForm, DeptQuery } from "@/api/system/dept";
-const queryFormRef = ref(ElForm);
-const deptFormRef = ref(ElForm);
+const queryFormRef = ref();
+const deptFormRef = ref();
const loading = ref(false);
const selectIds = ref([]);
diff --git a/src/views/system/dict/data.vue b/src/views/system/dict/data.vue
index 2f7a3c65..e8cf6253 100644
--- a/src/views/system/dict/data.vue
+++ b/src/views/system/dict/data.vue
@@ -144,8 +144,8 @@ const route = useRoute();
const dictCode = ref(route.query.dictCode as string);
-const queryFormRef = ref(ElForm);
-const dataFormRef = ref(ElForm);
+const queryFormRef = ref();
+const dataFormRef = ref();
const loading = ref(false);
const ids = ref([]);
diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue
index 32517f01..39260418 100644
--- a/src/views/system/dict/index.vue
+++ b/src/views/system/dict/index.vue
@@ -57,7 +57,7 @@
link
size="small"
icon="edit"
- @click.stop="handleEditClick(scope.row.id, scope.row.name)"
+ @click.stop="handleEditClick(scope.row.id)"
>
编辑
@@ -133,8 +133,8 @@ import DictAPI, { DictPageQuery, DictPageVO, DictForm } from "@/api/system/dict"
import router from "@/router";
-const queryFormRef = ref(ElForm);
-const dataFormRef = ref(ElForm);
+const queryFormRef = ref();
+const dataFormRef = ref();
const loading = ref(false);
const ids = ref([]);
@@ -198,7 +198,7 @@ function handleAddClick() {
*
* @param id 字典ID
*/
-function handleEditClick(id: number, name: string) {
+function handleEditClick(id: number) {
dialog.visible = true;
dialog.title = "修改字典";
DictAPI.getFormData(id).then((data) => {
diff --git a/src/views/system/log/index.vue b/src/views/system/log/index.vue
index 13cca4ef..f0b8367c 100644
--- a/src/views/system/log/index.vue
+++ b/src/views/system/log/index.vue
@@ -62,7 +62,7 @@ defineOptions({
import LogAPI, { LogPageVO, LogPageQuery } from "@/api/system/log";
-const queryFormRef = ref(ElForm);
+const queryFormRef = ref();
const loading = ref(false);
const total = ref(0);
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
index 8622309a..23bab330 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -48,7 +48,7 @@
-
+
{{ scope.row.name }}
@@ -338,8 +338,8 @@ defineOptions({
import MenuAPI, { MenuQuery, MenuForm, MenuVO } from "@/api/system/menu";
import { MenuTypeEnum } from "@/enums/MenuTypeEnum";
-const queryFormRef = ref(ElForm);
-const menuFormRef = ref(ElForm);
+const queryFormRef = ref();
+const menuFormRef = ref();
const loading = ref(false);
const dialog = reactive({
diff --git a/src/views/system/notice/components/MyNotice.vue b/src/views/system/notice/components/MyNotice.vue
index 9aa09bec..e8900b30 100644
--- a/src/views/system/notice/components/MyNotice.vue
+++ b/src/views/system/notice/components/MyNotice.vue
@@ -87,7 +87,7 @@ defineOptions({
import NoticeAPI, { NoticePageVO, NoticePageQuery } from "@/api/system/notice";
-const queryFormRef = ref(ElForm);
+const queryFormRef = ref();
const noticeDetailRef = ref();
const pageData = ref([]);
diff --git a/src/views/system/notice/components/NoticeDetail.vue b/src/views/system/notice/components/NoticeDetail.vue
index dd20a4c5..d573c3e9 100644
--- a/src/views/system/notice/components/NoticeDetail.vue
+++ b/src/views/system/notice/components/NoticeDetail.vue
@@ -13,8 +13,7 @@