├── server ├── resource │ └── upload │ │ └── empty ├── configs │ ├── file.go │ ├── router.go │ ├── redis.go │ ├── captcha.go │ ├── system.go │ ├── rotatelogs.go │ ├── jwt.go │ ├── crontab.go │ ├── cors.go │ ├── config.go │ └── mysql.go ├── internal │ ├── model │ │ ├── entity │ │ │ ├── authority │ │ │ │ ├── response │ │ │ │ │ ├── menu.go │ │ │ │ │ ├── api.go │ │ │ │ │ └── user.go │ │ │ │ ├── request │ │ │ │ │ ├── role.go │ │ │ │ │ ├── api.go │ │ │ │ │ ├── menu.go │ │ │ │ │ └── user.go │ │ │ │ ├── role.go │ │ │ │ ├── user.go │ │ │ │ ├── api.go │ │ │ │ └── menu.go │ │ │ ├── base │ │ │ │ ├── response │ │ │ │ │ └── logReg.go │ │ │ │ └── request │ │ │ │ │ ├── jwt.go │ │ │ │ │ ├── logReg.go │ │ │ │ │ └── casbin.go │ │ │ ├── sysSet │ │ │ │ ├── request │ │ │ │ │ └── dict.go │ │ │ │ ├── dict.go │ │ │ │ └── dictDetail.go │ │ │ ├── fileM │ │ │ │ ├── request │ │ │ │ │ └── file.go │ │ │ │ └── file.go │ │ │ ├── monitor │ │ │ │ ├── request │ │ │ │ │ └── operationLog.go │ │ │ │ └── operationLog.go │ │ │ └── sysTool │ │ │ │ └── request │ │ │ │ └── cron.go │ │ └── common │ │ │ ├── response │ │ │ ├── page.go │ │ │ └── response.go │ │ │ └── request │ │ │ └── request.go │ ├── service │ │ ├── authority │ │ │ └── enter.go │ │ ├── base │ │ │ ├── jwt.go │ │ │ └── logReg.go │ │ ├── sysSet │ │ │ └── dict.go │ │ └── monitor │ │ │ └── operationLog.go │ ├── pkg │ │ ├── md5.go │ │ ├── slice.go │ │ ├── file.go │ │ ├── directory.go │ │ ├── cron.go │ │ ├── claims.go │ │ └── jwt.go │ ├── router │ │ ├── sysTool │ │ │ ├── enter.go │ │ │ └── cron.go │ │ ├── fileM │ │ │ ├── enter.go │ │ │ └── file.go │ │ ├── monitor │ │ │ ├── enter.go │ │ │ └── operationLog.go │ │ ├── base │ │ │ ├── enter.go │ │ │ ├── logReg.go │ │ │ └── casbin.go │ │ ├── sysSet │ │ │ ├── enter.go │ │ │ ├── dict.go │ │ │ └── dictDetail.go │ │ ├── authority │ │ │ ├── enter.go │ │ │ ├── menu.go │ │ │ ├── role.go │ │ │ ├── api.go │ │ │ └── user.go │ │ └── enter.go │ ├── global │ │ ├── model.go │ │ └── global.go │ ├── core │ │ ├── zap │ │ │ ├── core.go │ │ │ └── rotatelogs.go │ │ ├── viper.go │ │ └── zap.go │ ├── middleware │ │ ├── log │ │ │ ├── access.go │ │ │ └── error.go │ │ └── casbin.go │ ├── initialize │ │ ├── redis.go │ │ ├── server.go │ │ ├── router.go │ │ └── cron.go │ └── api │ │ ├── base │ │ └── casbin.go │ │ └── sysSet │ │ └── dict.go ├── scripts │ └── wait-for-it.sh ├── Dockerfile └── cmd │ └── server │ └── main.go ├── web ├── src │ ├── pinia │ │ ├── index.ts │ │ └── stores │ │ │ ├── settings.ts │ │ │ ├── app.ts │ │ │ ├── permission_n.ts │ │ │ └── dictionary.ts │ ├── common │ │ ├── assets │ │ │ ├── styles │ │ │ │ ├── theme │ │ │ │ │ ├── dark │ │ │ │ │ │ ├── index.scss │ │ │ │ │ │ └── variables.scss │ │ │ │ │ ├── dark-blue │ │ │ │ │ │ ├── index.scss │ │ │ │ │ │ └── variables.scss │ │ │ │ │ ├── register.scss │ │ │ │ │ └── core │ │ │ │ │ │ ├── index.scss │ │ │ │ │ │ ├── element-plus.scss │ │ │ │ │ │ └── layouts.scss │ │ │ │ ├── td27.scss │ │ │ │ ├── element-plus.scss │ │ │ │ ├── view-transition.scss │ │ │ │ ├── transition.scss │ │ │ │ ├── vxe-table.scss │ │ │ │ ├── mixins.scss │ │ │ │ └── index.scss │ │ │ ├── images │ │ │ │ ├── github.png │ │ │ │ └── layouts │ │ │ │ │ ├── logo.png │ │ │ │ │ ├── logo-text-1.png │ │ │ │ │ └── logo-text-2.png │ │ │ └── icons │ │ │ │ ├── keyboard-down.svg │ │ │ │ ├── keyboard-up.svg │ │ │ │ ├── keyboard-enter.svg │ │ │ │ ├── preserve-color │ │ │ │ └── README.md │ │ │ │ ├── plus.svg │ │ │ │ ├── menu.svg │ │ │ │ ├── lock.svg │ │ │ │ ├── keyboard-esc.svg │ │ │ │ ├── fullscreen.svg │ │ │ │ ├── fullscreen-exit.svg │ │ │ │ ├── monitor.svg │ │ │ │ ├── link.svg │ │ │ │ ├── config.svg │ │ │ │ ├── file.svg │ │ │ │ ├── access.svg │ │ │ │ ├── radar.svg │ │ │ │ ├── search.svg │ │ │ │ ├── bug.svg │ │ │ │ ├── dashboard.svg │ │ │ │ └── setting.svg │ │ ├── components │ │ │ ├── Notify │ │ │ │ ├── data.ts │ │ │ │ ├── type.ts │ │ │ │ └── List.vue │ │ │ ├── SearchMenu │ │ │ │ ├── index.vue │ │ │ │ └── Footer.vue │ │ │ ├── ThemeSwitch │ │ │ │ └── index.vue │ │ │ └── WarningBar │ │ │ │ └── warningBar.vue │ │ ├── utils │ │ │ ├── datetime.ts │ │ │ ├── validate.ts │ │ │ ├── css.ts │ │ │ ├── useValidate.ts │ │ │ ├── router_m.ts │ │ │ └── cache │ │ │ │ └── local-storage.ts │ │ ├── composables │ │ │ ├── useDevice.ts │ │ │ ├── useTitle.ts │ │ │ ├── useGreyAndColorWeakness.ts │ │ │ ├── useLayoutMode.ts │ │ │ ├── usePagination_n.ts │ │ │ ├── useFetchSelect.ts │ │ │ ├── useFullscreenLoading.ts │ │ │ ├── useRouteListener.ts │ │ │ └── useTheme.ts │ │ └── constants │ │ │ ├── app-key.ts │ │ │ └── cache-key_n.ts │ ├── pages │ │ ├── cenu │ │ │ └── cenu1 │ │ │ │ ├── index.vue │ │ │ │ ├── cenu1-1 │ │ │ │ └── index.vue │ │ │ │ └── cenu1-2 │ │ │ │ └── index.vue │ │ ├── sysTool │ │ │ └── cron │ │ │ │ └── filter.ts │ │ ├── redirect │ │ │ └── index.vue │ │ ├── error │ │ │ ├── 403.vue │ │ │ ├── 404.vue │ │ │ └── components │ │ │ │ └── Layout.vue │ │ └── dashboard │ │ │ ├── index.vue │ │ │ └── components │ │ │ ├── Admin.vue │ │ │ └── Editor.vue │ ├── plugins │ │ ├── svg-icon.ts │ │ ├── element-plus-icons.ts │ │ ├── index_n.ts │ │ └── vxe-table.ts │ ├── api │ │ ├── base │ │ │ ├── casbin.ts │ │ │ └── login.ts │ │ ├── sysSet │ │ │ ├── dict.ts │ │ │ └── dictDetail.ts │ │ ├── fileM │ │ │ └── file.ts │ │ ├── monitor │ │ │ └── operationLog.ts │ │ ├── authority │ │ │ ├── role.ts │ │ │ ├── menu.ts │ │ │ ├── user.ts │ │ │ └── api.ts │ │ └── sysTool │ │ │ └── cron.ts │ ├── layouts │ │ ├── components │ │ │ ├── Sidebar │ │ │ │ └── Link.vue │ │ │ ├── Footer │ │ │ │ └── index.vue │ │ │ ├── index.ts │ │ │ ├── Hamburger │ │ │ │ └── index.vue │ │ │ ├── RightPanel │ │ │ │ └── index.vue │ │ │ ├── AppMain │ │ │ │ └── index.vue │ │ │ ├── Logo │ │ │ │ └── index.vue │ │ │ └── Breadcrumb │ │ │ │ └── index.vue │ │ ├── config.ts │ │ ├── composables │ │ │ └── useResize.ts │ │ ├── index.vue │ │ └── modes │ │ │ └── TopMode.vue │ ├── router │ │ ├── whitelist.ts │ │ ├── config.ts │ │ ├── guard_n.ts │ │ ├── helper.ts │ │ └── index_n.ts │ ├── App.vue │ └── main.ts ├── .github │ ├── FUNDING.yml │ └── workflows │ │ ├── release.yml │ │ └── deploy.yml ├── public │ ├── favicon.ico │ ├── detect-ie.js │ └── app-loading.css ├── .npmrc ├── .husky │ └── pre-commit ├── .cursor │ ├── mcp.json │ └── rules │ │ ├── index.mdc │ │ ├── git.mdc │ │ ├── ts.mdc │ │ └── vue.mdc ├── .env ├── .env.production ├── .vscode │ ├── extensions.json │ ├── vue.code-snippets │ ├── hook.code-snippets │ └── settings.json ├── .env.staging ├── .gitignore ├── types │ ├── directives.d.ts │ ├── env.d.ts │ ├── api.d.ts │ ├── auto │ │ ├── svg-component-global.d.ts │ │ └── svg-component.d.ts │ └── vue-router.d.ts ├── .env.development ├── .editorconfig ├── index.html ├── uno.config.ts ├── eslint.config.js ├── tsconfig.json └── package.json ├── img ├── p0.png ├── p1.png ├── p2.png ├── api.png ├── menu.png ├── oplog.png ├── multi-menu.png └── personal.png ├── docker-compose ├── redis │ └── redis.conf └── docker-compose.yml ├── .gitignore └── LICENSE /server/resource/upload/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/pinia/index.ts: -------------------------------------------------------------------------------- 1 | export const pinia = createPinia() 2 | -------------------------------------------------------------------------------- /img/p0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/img/p0.png -------------------------------------------------------------------------------- /img/p1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/img/p1.png -------------------------------------------------------------------------------- /img/p2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/img/p2.png -------------------------------------------------------------------------------- /img/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/img/api.png -------------------------------------------------------------------------------- /img/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/img/menu.png -------------------------------------------------------------------------------- /img/oplog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/img/oplog.png -------------------------------------------------------------------------------- /img/multi-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/img/multi-menu.png -------------------------------------------------------------------------------- /img/personal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/img/personal.png -------------------------------------------------------------------------------- /web/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://github.com/un-pany/v3-admin-vite/issues/69 2 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/dark/index.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | @import "../core/index.scss"; 3 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/dark-blue/index.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | @import "../core/index.scss"; 3 | -------------------------------------------------------------------------------- /web/.npmrc: -------------------------------------------------------------------------------- 1 | # China mirror of npm 2 | registry = https://registry.npmmirror.com 3 | 4 | # 安装依赖时锁定版本号 5 | save-exact = true 6 | -------------------------------------------------------------------------------- /web/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # 全局 ts 类型检查(此操作会增加 git commit 时长) 2 | npx vue-tsc 3 | 4 | # 执行 lint-staged 中配置的任务 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /web/src/common/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/web/src/common/assets/images/github.png -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/register.scss: -------------------------------------------------------------------------------- 1 | // 注册多主题 2 | @import "./dark/index.scss"; 3 | @import "./dark-blue/index.scss"; 4 | -------------------------------------------------------------------------------- /docker-compose/redis/redis.conf: -------------------------------------------------------------------------------- 1 | save 900 1 2 | save 300 10 3 | save 60 10000 4 | rdbcompression yes 5 | dbfilename dump.rdb 6 | dir /data 7 | -------------------------------------------------------------------------------- /web/src/common/assets/images/layouts/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/web/src/common/assets/images/layouts/logo.png -------------------------------------------------------------------------------- /server/configs/file.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type File struct { 4 | Upload string `mapstructure:"upload" json:"upload" yaml:"upload"` 5 | } 6 | -------------------------------------------------------------------------------- /web/.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "vue-mcp": { 4 | "url": "http://localhost:3333/__mcp/sse" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/core/index.scss: -------------------------------------------------------------------------------- 1 | .#{$theme-name} { 2 | @import "./layouts.scss"; 3 | @import "./element-plus.scss"; 4 | } 5 | -------------------------------------------------------------------------------- /server/configs/router.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type Router struct { 4 | Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` 5 | } 6 | -------------------------------------------------------------------------------- /web/.env: -------------------------------------------------------------------------------- 1 | # 所有环境的环境变量(命名必须以 VITE_ 开头) 2 | 3 | ## 项目标题 4 | VITE_APP_TITLE = TD27 ADMIN 5 | 6 | ## 路由模式 hash 或 html5 7 | VITE_ROUTER_HISTORY = hash 8 | -------------------------------------------------------------------------------- /web/src/common/assets/images/layouts/logo-text-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/web/src/common/assets/images/layouts/logo-text-1.png -------------------------------------------------------------------------------- /web/src/common/assets/images/layouts/logo-text-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pddzl/td27-admin/HEAD/web/src/common/assets/images/layouts/logo-text-2.png -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/dark/variables.scss: -------------------------------------------------------------------------------- 1 | // dark 主题下的变量 2 | 3 | // 主题名称 4 | $theme-name: "dark"; 5 | // 主题背景颜色 6 | $theme-bg-color: #141414; 7 | -------------------------------------------------------------------------------- /web/.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境的环境变量(命名必须以 VITE_ 开头) 2 | 3 | ## 后端接口公共路径(如果解决跨域问题采用 CORS 就需要写全路径) 4 | VITE_BASE_URL = '/api' 5 | 6 | ## 打包路径 7 | VITE_PUBLIC_PATH = '/' 8 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/dark-blue/variables.scss: -------------------------------------------------------------------------------- 1 | // dark-blue 主题下的变量 2 | 3 | // 主题名称 4 | $theme-name: "dark-blue"; 5 | // 主题背景颜色 6 | $theme-bg-color: #001b44; 7 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/response/menu.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type Menu struct { 4 | List interface{} `json:"list"` 5 | MenuIds []uint `json:"menuIds"` 6 | } 7 | -------------------------------------------------------------------------------- /web/src/pages/cenu/cenu1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /server/internal/service/authority/enter.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import serviceBase "server/internal/service/base" 4 | 5 | var ( 6 | casbinService = serviceBase.NewCasbinService() 7 | ) 8 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/response/api.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type ApiTree struct { 4 | List interface{} `json:"list"` 5 | CheckedKey []string `json:"checkedKey"` 6 | } 7 | -------------------------------------------------------------------------------- /server/scripts/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | until nc -w2 mysql 3306 && nc -w2 redis 6379 6 | do 7 | echo "mysql or redis is unavailable - sleeping" >&2 8 | sleep 1 9 | done 10 | 11 | exec "$@" 12 | -------------------------------------------------------------------------------- /web/src/plugins/svg-icon.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import SvgIcon from "~virtual/svg-component" 3 | 4 | export function installSvgIcon(app: App) { 5 | // 注册 SvgIcon 组件 6 | app.component("SvgIcon", SvgIcon) 7 | } 8 | -------------------------------------------------------------------------------- /web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "editorconfig.editorconfig", 5 | "dbaeumer.vscode-eslint", 6 | "antfu.unocss", 7 | "wiensss.region-highlighter" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /web/src/common/components/Notify/data.ts: -------------------------------------------------------------------------------- 1 | import type { NotifyItem } from "./type" 2 | 3 | export const notifyData: NotifyItem[] = [] 4 | 5 | export const messageData: NotifyItem[] = [] 6 | 7 | export const todoData: NotifyItem[] = [] 8 | -------------------------------------------------------------------------------- /web/src/pages/sysTool/cron/filter.ts: -------------------------------------------------------------------------------- 1 | const strategyMap: Record = { 2 | always: "重复执行", 3 | once: "执行一次" 4 | } 5 | 6 | export function strategyFilter(strategy: string) { 7 | return strategyMap[strategy] || "未知" 8 | } 9 | -------------------------------------------------------------------------------- /server/internal/pkg/md5.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | ) 7 | 8 | func MD5V(str []byte) string { 9 | h := md5.New() 10 | h.Write(str) 11 | return hex.EncodeToString(h.Sum(nil)) 12 | } 13 | -------------------------------------------------------------------------------- /web/src/common/components/Notify/type.ts: -------------------------------------------------------------------------------- 1 | export interface NotifyItem { 2 | avatar?: string 3 | title: string 4 | datetime?: string 5 | description?: string 6 | status?: "primary" | "success" | "info" | "warning" | "danger" 7 | extra?: string 8 | } 9 | -------------------------------------------------------------------------------- /server/internal/model/common/response/page.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type Page struct { 4 | List interface{} `json:"list"` 5 | Total int64 `json:"total"` 6 | Page int `json:"page"` 7 | PageSize int `json:"pageSize"` 8 | } 9 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/response/user.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "server/internal/model/entity/authority" 5 | ) 6 | 7 | type UserResult struct { 8 | authority.UserModel 9 | RoleName string `json:"roleName"` // 角色名 10 | } 11 | -------------------------------------------------------------------------------- /server/internal/pkg/slice.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | // IsContain slice contain 4 | func IsContain[T comparable](list []T, item T) bool { 5 | for _, value := range list { 6 | if value == item { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /web/.env.staging: -------------------------------------------------------------------------------- 1 | # 预发布环境的环境变量(命名必须以 VITE_ 开头) 2 | 3 | ## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径) 4 | VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1 5 | 6 | ## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 /) 7 | VITE_PUBLIC_PATH = / 8 | -------------------------------------------------------------------------------- /web/src/pages/cenu/cenu1/cenu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /web/src/pages/cenu/cenu1/cenu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/keyboard-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/keyboard-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Common 2 | dist 3 | node_modules 4 | .eslintcache 5 | vite.config.*.timestamp* 6 | 7 | # MacOS 8 | .DS_Store 9 | 10 | # Local env files 11 | *.local 12 | 13 | # Logs 14 | *.log 15 | 16 | # Use the pnpm 17 | package-lock.json 18 | yarn.lock 19 | -------------------------------------------------------------------------------- /web/src/pages/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /web/.cursor/rules/index.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | --- 4 | 5 | # 规则 6 | 7 | ## 模型回复 8 | 9 | - 请简明扼要地回答,避免不必要的重复或填充语言 10 | - 始终以简体中文回复 11 | 12 | ## 细分规则文件 13 | 14 | - project.mdc: 项目开发规范 15 | - vue.mdc: Vue 开发规范 16 | - ts.mdc: TypeScript 开发规范 17 | - git.mdc: Git 提交规范 18 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/keyboard-enter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/pages/error/403.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /web/src/pages/error/404.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /web/types/directives.d.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from "vue" 2 | 3 | export {} 4 | 5 | // 由 app.directive 全局注册的自定义指令需要在这里声明 TS 类型才能获得类型提示 6 | declare module "vue" { 7 | export interface ComponentCustomProperties { 8 | vPermission: Directive 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web/public/detect-ie.js: -------------------------------------------------------------------------------- 1 | // Tip: Simple judgments may not fully cover 2 | if (/MSIE\s|Trident\//.test(navigator.userAgent)) { 3 | document.body.innerHTML = "Sorry, this browser is currently not supported. We recommend using the latest version of a modern browser. For example, Chrome/Firefox/Edge." 4 | } 5 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/td27.scss: -------------------------------------------------------------------------------- 1 | // 组件通用样式 2 | 3 | .toolbar-wrapper { 4 | display: flex; 5 | justify-content: space-between; 6 | margin-bottom: 20px; 7 | } 8 | 9 | .table-wrapper { 10 | margin-bottom: 20px; 11 | } 12 | 13 | .pager-wrapper { 14 | display: flex; 15 | justify-content: flex-end; 16 | } 17 | -------------------------------------------------------------------------------- /web/.env.development: -------------------------------------------------------------------------------- 1 | # 开发环境的环境变量(命名必须以 VITE_ 开头) 2 | 3 | ## 后端接口公共路径(如果解决跨域问题采用反向代理就只需写公共路径) 4 | VITE_BASE_URL = '/api' 5 | 6 | ## 开发环境地址前缀(一般 '/','./' 都可以) 7 | VITE_PUBLIC_PATH = '/' 8 | 9 | ## 前端端口 10 | VITE_CLI_PORT = 8080 11 | 12 | ## 后端地址 13 | VITE_BASE_PATH = http://127.0.0.1 14 | 15 | ## 后端端口 16 | VITE_SERVER_PORT = 8888 17 | -------------------------------------------------------------------------------- /web/types/env.d.ts: -------------------------------------------------------------------------------- 1 | /** 声明 vite 环境变量的类型(如果未声明则默认是 any) */ 2 | interface ImportMetaEnv { 3 | readonly VITE_APP_TITLE: string 4 | readonly VITE_BASE_URL: string 5 | readonly VITE_ROUTER_HISTORY: "hash" | "html5" 6 | readonly VITE_PUBLIC_PATH: string 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } 12 | -------------------------------------------------------------------------------- /server/configs/redis.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type Redis struct { 4 | DB int `mapstructure:"db" json:"db" yaml:"db"` 5 | Host string `mapstructure:"host" json:"host" yaml:"host"` 6 | Port int `mapstructure:"port" json:"port" yaml:"port"` 7 | Password string `mapstructure:"password" json:"password" yaml:"password"` 8 | } 9 | -------------------------------------------------------------------------------- /server/configs/captcha.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type Captcha struct { 4 | KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度 5 | ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度 6 | ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度 7 | } 8 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/request/role.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type Role struct { 4 | RoleName string `json:"roleName" binding:"required"` // 角色名称 5 | } 6 | 7 | type EditRole struct { 8 | ID uint `json:"id" binding:"required"` // 角色ID 9 | RoleName string `json:"roleName" binding:"required"` // 角色名称 10 | } 11 | -------------------------------------------------------------------------------- /web/src/common/utils/datetime.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs" 2 | 3 | const INVALID_DATE = "N/A" 4 | 5 | /** 格式化日期时间 */ 6 | export function formatDateTime(datetime: string | number | Date = "", template: string = "YYYY-MM-DD HH:mm:ss") { 7 | const day = dayjs(datetime) 8 | return day.isValid() ? day.format(template) : INVALID_DATE 9 | } 10 | -------------------------------------------------------------------------------- /server/internal/model/entity/base/response/logReg.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "server/internal/model/entity/authority" 5 | ) 6 | 7 | type LoginResponse struct { 8 | User authority.UserModel `json:"user"` // 用户 9 | Token string `json:"token"` 10 | ExpiresAt int64 `json:"expiresAt"` // 过期时间 11 | } 12 | -------------------------------------------------------------------------------- /web/src/plugins/element-plus-icons.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import * as ElementPlusIconsVue from "@element-plus/icons-vue" 3 | 4 | export function installElementPlusIcons(app: App) { 5 | // 注册所有 Element Plus Icons 6 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 7 | app.component(key, component) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/internal/pkg/file.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "path/filepath" 4 | 5 | // GetFileAndExt 获取文件名、文件后缀 6 | // "/path/to/your/file.txt" -> file, txt 7 | func GetFileAndExt(filePath string) (string, string) { 8 | fileName := filepath.Base(filePath) // 获取完整文件名,包括后缀 9 | ext := filepath.Ext(fileName) 10 | return fileName[:len(fileName)-len(ext)], ext 11 | } 12 | -------------------------------------------------------------------------------- /server/configs/system.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type System struct { 4 | Env string `mapstructure:"env" json:"env" yaml:"env"` // 环境值 5 | Host string `mapstructure:"host" json:"host" yaml:"host"` // IP地址 6 | Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口号 7 | Stack bool `mapstructure:"stack" json:"stack" yaml:"stack"` // 是否开启日志栈 8 | } 9 | -------------------------------------------------------------------------------- /server/internal/model/entity/sysSet/request/dict.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "server/internal/model/common/request" 5 | ) 6 | 7 | type DictDetailSearchParams struct { 8 | request.PageInfo 9 | DictID uint `json:"dictID" binding:"required"` 10 | } 11 | 12 | type DictDetailFlatReq struct { 13 | DictID uint `json:"dictID" binding:"required"` 14 | } 15 | -------------------------------------------------------------------------------- /web/src/plugins/index_n.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import { installElementPlusIcons } from "./element-plus-icons" 3 | import { installSvgIcon } from "./svg-icon" 4 | import { installVxeTable } from "./vxe-table" 5 | 6 | export function installPlugins(app: App) { 7 | installElementPlusIcons(app) 8 | installSvgIcon(app) 9 | installVxeTable(app) 10 | } 11 | -------------------------------------------------------------------------------- /server/internal/model/entity/fileM/request/file.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "server/internal/model/common/request" 5 | ) 6 | 7 | // FileSearchParams file分页条件查询 8 | type FileSearchParams struct { 9 | request.PageInfo 10 | Name string `json:"name"` 11 | OrderKey string `json:"orderKey"` // 排序 12 | Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true 13 | } 14 | -------------------------------------------------------------------------------- /server/configs/rotatelogs.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type RotateLogs struct { 4 | MaxSize int `mapstructure:"max-size" json:"max-size" yaml:"max-size"` 5 | MaxBackups int `mapstructure:"max-backups" json:"max-backups" yaml:"max-backups"` 6 | MaxAge int `mapstructure:"max-age" json:"max-age" yaml:"max-age"` 7 | Compress bool `mapstructure:"compress" json:"compress" yaml:"compress"` 8 | } 9 | -------------------------------------------------------------------------------- /server/internal/model/common/request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | // PageInfo 分页 4 | type PageInfo struct { 5 | Page int `json:"page"` // 页码 6 | PageSize int `json:"pageSize"` // 每页大小 7 | } 8 | 9 | // CId 主键ID 10 | type CId struct { 11 | ID uint `json:"id" binding:"required"` // 主键ID 12 | } 13 | 14 | type CIds struct { 15 | IDs []uint `json:"ids" binding:"required"` 16 | } 17 | -------------------------------------------------------------------------------- /server/internal/pkg/directory.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | ) 7 | 8 | func PathExists(path string) (bool, error) { 9 | fi, err := os.Stat(path) 10 | if err == nil { 11 | if fi.IsDir() { 12 | return true, nil 13 | } 14 | return false, errors.New("存在同名文件") 15 | } 16 | if os.IsNotExist(err) { 17 | return false, nil 18 | } 19 | return false, err 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /web/node_modules 3 | /web/dist 4 | 5 | .DS_Store 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | /server/log/ 25 | /server/resource/upload 26 | 27 | *.iml 28 | 29 | -------------------------------------------------------------------------------- /server/internal/model/entity/base/request/jwt.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "github.com/golang-jwt/jwt/v4" 5 | ) 6 | 7 | // CustomClaims Custom claims structure 8 | type CustomClaims struct { 9 | ID uint `json:"ID"` 10 | Username string `json:"username"` 11 | RoleId uint `json:"roleId"` // 角色Id 12 | BufferTime int64 `json:"bufferTime"` 13 | jwt.RegisteredClaims 14 | } 15 | -------------------------------------------------------------------------------- /web/src/pages/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /web/.vscode/vue.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3 SFC 代码结构一键生成": { 3 | "prefix": "Vue3 SFC", 4 | "body": [ 5 | "\n", 6 | "\n", 11 | "", 12 | "$1" 13 | ], 14 | "description": "Vue3 SFC" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/internal/model/entity/fileM/file.go: -------------------------------------------------------------------------------- 1 | package fileM 2 | 3 | import ( 4 | "server/internal/global" 5 | ) 6 | 7 | type FileModel struct { 8 | global.TD27_MODEL 9 | FileName string `json:"fileName" gorm:"comment:文件名"` 10 | FullPath string `json:"fullPath" gorm:"comment:文件完整路径"` 11 | Mime string `json:"mime" gorm:"comment:文件类型"` 12 | } 13 | 14 | func (FileModel) TableName() string { 15 | return "fileM_file" 16 | } 17 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/preserve-color/README.md: -------------------------------------------------------------------------------- 1 | ## 目录说明 2 | 3 | - `common/assets/icons/preserve-color` 目录下存放带颜色的 svg icon 4 | 5 | - `common/assets/icons` 目录存放的 svg icon 会被插件重写 `fill` 和 `stroke` 属性,使得图片自带的颜色丢失,从而继承父元素的颜色 6 | 7 | ## 使用说明 8 | 9 | `common/assets/icons/preserve-color` 目录下需要添加 `preserve-color/` 前缀,像这样: `` 10 | 11 | `common/assets/icons` 目录下则不需要,像这样: `` 12 | -------------------------------------------------------------------------------- /web/src/api/base/casbin.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@/http/axios_n" 2 | 3 | export interface CasbinInfo { 4 | path: string 5 | method: string 6 | } 7 | 8 | interface reqCasbin { 9 | roleId: number 10 | casbinInfos: CasbinInfo[] 11 | } 12 | 13 | export function editCasbinApi(data: reqCasbin) { 14 | return request>({ 15 | url: "/casbin/editCasbin", 16 | method: "post", 17 | data 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /server/internal/model/entity/monitor/request/operationLog.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "server/internal/model/common/request" 5 | ) 6 | 7 | type orStruct struct { 8 | Method string // 请求方法 9 | Path string // 请求路径 10 | Status int // http code 11 | } 12 | 13 | // OrSearchParams api分页条件查询及排序结构体 14 | type OrSearchParams struct { 15 | orStruct 16 | request.PageInfo 17 | Asc bool `json:"asc"` // 排序方式:升序true|降序true(默认) 18 | } 19 | -------------------------------------------------------------------------------- /server/internal/router/sysTool/enter.go: -------------------------------------------------------------------------------- 1 | package sysTool 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type RouterGroup struct { 6 | *CronRouter 7 | } 8 | 9 | func NewRouterGroup() *RouterGroup { 10 | return &RouterGroup{CronRouter: NewCronRouter()} 11 | } 12 | 13 | func (rg *RouterGroup) InitPublic(group *gin.RouterGroup) {} 14 | 15 | func (rg *RouterGroup) InitPrivate(group *gin.RouterGroup) { 16 | rg.InitCronRouter(group) 17 | } 18 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/role.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "server/internal/global" 5 | ) 6 | 7 | type RoleModel struct { 8 | global.TD27_MODEL 9 | RoleName string `json:"roleName" gorm:"unique" binding:"required"` 10 | //Users []*UserModel `json:"users"` 11 | Menus []*MenuModel `json:"menus" gorm:"many2many:role_menus;"` 12 | } 13 | 14 | func (RoleModel) TableName() string { 15 | return "authority_role" 16 | } 17 | -------------------------------------------------------------------------------- /server/internal/router/fileM/enter.go: -------------------------------------------------------------------------------- 1 | package fileM 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type RouterGroup struct { 6 | *FileRouter 7 | } 8 | 9 | func NewRouterGroup() *RouterGroup { 10 | return &RouterGroup{ 11 | FileRouter: NewFileRouter(), 12 | } 13 | } 14 | 15 | func (rg *RouterGroup) InitPublic(group *gin.RouterGroup) {} 16 | 17 | func (rg *RouterGroup) InitPrivate(group *gin.RouterGroup) { 18 | rg.InitFileRouter(group) 19 | } 20 | -------------------------------------------------------------------------------- /web/src/common/composables/useDevice.ts: -------------------------------------------------------------------------------- 1 | import { DeviceEnum } from "@@/constants/app-key" 2 | import { useAppStore } from "@/pinia/stores/app" 3 | 4 | const appStore = useAppStore() 5 | 6 | const isMobile = computed(() => appStore.device === DeviceEnum.Mobile) 7 | 8 | const isDesktop = computed(() => appStore.device === DeviceEnum.Desktop) 9 | 10 | /** 设备类型 Composable */ 11 | export function useDevice() { 12 | return { isMobile, isDesktop } 13 | } 14 | -------------------------------------------------------------------------------- /server/configs/jwt.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type JWT struct { 4 | SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名 5 | ExpiresTime int64 `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间 6 | BufferTime int64 `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间 7 | Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者 8 | } 9 | -------------------------------------------------------------------------------- /web/src/layouts/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/element-plus.scss: -------------------------------------------------------------------------------- 1 | // 自定义 Element Plus 样式 2 | 3 | // 卡片 4 | .el-card { 5 | background-color: var(--el-bg-color) !important; 6 | } 7 | 8 | // 分页 9 | .el-pagination { 10 | // 参考 Bootstrap 的响应式设计 WIDTH = 768 11 | @media screen and (max-width: 768px) { 12 | .el-pagination__total, 13 | .el-pagination__sizes, 14 | .el-pagination__jump, 15 | .btn-prev, 16 | .btn-next { 17 | display: none; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/.vscode/hook.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3 Composable 代码结构一键生成": { 3 | "prefix": "Vue3 Composable", 4 | "body": [ 5 | "const refName1 = ref(\"这是一个响应式变量\")\n", 6 | "export function useName() {", 7 | "\tconst refName2 = ref(\"这是一个响应式变量\")\n", 8 | "\tconst fnName = () => {}\n", 9 | "\treturn { refName1, refName2, fnName }", 10 | "}", 11 | "$1" 12 | ], 13 | "description": "Vue3 Composable" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/internal/router/monitor/enter.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type RouterGroup struct { 6 | *OperationLogRouter 7 | } 8 | 9 | func NewRouterGroup() *RouterGroup { 10 | return &RouterGroup{ 11 | OperationLogRouter: NewOperationLogRouter(), 12 | } 13 | } 14 | 15 | func (rg *RouterGroup) InitPublic(group *gin.RouterGroup) {} 16 | 17 | func (rg *RouterGroup) InitPrivate(group *gin.RouterGroup) { 18 | rg.InitOperationLogRouter(group) 19 | } 20 | -------------------------------------------------------------------------------- /web/src/common/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /** 判断是否为数组 */ 2 | export function isArray(arg: T) { 3 | return Array.isArray ? Array.isArray(arg) : Object.prototype.toString.call(arg) === "[object Array]" 4 | } 5 | 6 | /** 判断是否为字符串 */ 7 | export function isString(str: unknown) { 8 | return typeof str === "string" || str instanceof String 9 | } 10 | 11 | /** 判断是否为外链 */ 12 | export function isExternal(path: string) { 13 | const reg = /^(https?:|mailto:|tel:)/ 14 | return reg.test(path) 15 | } 16 | -------------------------------------------------------------------------------- /web/src/common/constants/app-key.ts: -------------------------------------------------------------------------------- 1 | /** 设备类型 */ 2 | export enum DeviceEnum { 3 | Mobile, 4 | Desktop 5 | } 6 | 7 | /** 布局模式 */ 8 | export enum LayoutModeEnum { 9 | Left = "left", 10 | Top = "top", 11 | LeftTop = "left-top" 12 | } 13 | 14 | /** 侧边栏打开状态常量 */ 15 | export const SIDEBAR_OPENED = "opened" 16 | 17 | /** 侧边栏关闭状态常量 */ 18 | export const SIDEBAR_CLOSED = "closed" 19 | 20 | export type SidebarOpened = typeof SIDEBAR_OPENED 21 | 22 | export type SidebarClosed = typeof SIDEBAR_CLOSED 23 | -------------------------------------------------------------------------------- /web/src/router/whitelist.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalizedGeneric, RouteRecordNameGeneric } from "vue-router" 2 | 3 | /** 免登录白名单(匹配路由 path) */ 4 | const whiteListByPath: string[] = ["/login"] 5 | 6 | /** 免登录白名单(匹配路由 name) */ 7 | const whiteListByName: RouteRecordNameGeneric[] = [] 8 | 9 | /** 判断是否在白名单 */ 10 | export function isWhiteList(to: RouteLocationNormalizedGeneric) { 11 | // path 和 name 任意一个匹配上即可 12 | return whiteListByPath.includes(to.path) || whiteListByName.includes(to.name) 13 | } 14 | -------------------------------------------------------------------------------- /server/internal/model/entity/sysSet/dict.go: -------------------------------------------------------------------------------- 1 | package sysSet 2 | 3 | import ( 4 | "server/internal/global" 5 | ) 6 | 7 | type DictModel struct { 8 | global.TD27_MODEL 9 | CHName string `json:"chName" gorm:"column:ch_name;unique" binding:"required"` 10 | ENName string `json:"enName" gorm:"column:en_name;unique" binding:"required"` 11 | DictDetails []DictDetailModel `json:"dictDetails"` 12 | } 13 | 14 | func (dm *DictModel) TableName() string { 15 | return "sysSet_dict" 16 | } 17 | -------------------------------------------------------------------------------- /server/internal/model/entity/sysTool/request/cron.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type ExtraParams struct { 4 | TableInfo []ClearTable `json:"tableInfo"` // for clearTable 5 | Command string `json:"command"` // for shell 6 | } 7 | 8 | type ClearTable struct { 9 | TableName string `json:"tableName"` 10 | CompareField string `json:"compareField"` 11 | Interval string `json:"interval"` 12 | } 13 | 14 | type SwitchReq struct { 15 | ID uint `json:"id" binding:"required"` 16 | Open bool `json:"open"` 17 | } 18 | -------------------------------------------------------------------------------- /web/src/layouts/components/Footer/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /server/internal/model/entity/base/request/logReg.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type Login struct { 4 | Username string `json:"username" binding:"required"` // 用户名 5 | Password string `json:"password" binding:"required"` // 密码 6 | Captcha string `json:"captcha" binding:"required"` // 验证码 7 | CaptchaId string `json:"captchaId" binding:"required"` // 验证码ID 8 | } 9 | 10 | type CaptchaResponse struct { 11 | CaptchaId string `json:"captchaId"` 12 | PicPath string `json:"picPath"` 13 | CaptchaLength int `json:"captchaLength"` 14 | } 15 | -------------------------------------------------------------------------------- /server/internal/router/base/enter.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type RouterGroup struct { 6 | *CasbinRouter 7 | *LogRegRouter 8 | } 9 | 10 | func NewRouterGroup() *RouterGroup { 11 | return &RouterGroup{ 12 | CasbinRouter: NewCasbinRouter(), 13 | LogRegRouter: NewLogRegRouter(), 14 | } 15 | } 16 | 17 | func (rg *RouterGroup) InitPublic(group *gin.RouterGroup) { 18 | rg.InitLogRegRouter(group) 19 | } 20 | 21 | func (rg *RouterGroup) InitPrivate(group *gin.RouterGroup) { 22 | rg.InitCasbinRouter(group) 23 | } 24 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | # 配置项文档:https://editorconfig.org(修改配置后重启编辑器) 2 | 3 | ## 告知 EditorConfig 插件,当前即是根文件 4 | root = true 5 | 6 | ## 适用全部文件 7 | [*] 8 | ### 设置字符集 9 | charset = utf-8 10 | ### 缩进风格 space | tab,建议 space 11 | indent_style = space 12 | ### 缩进的空格数 13 | indent_size = 2 14 | ### 换行符类型 lf | cr | crlf,一般都是设置为 lf 15 | end_of_line = lf 16 | ### 是否在文件末尾插入空白行 17 | insert_final_newline = true 18 | ### 是否删除一行中的前后空格 19 | trim_trailing_whitespace = true 20 | 21 | ## 适用 .md 文件 22 | [*.md] 23 | insert_final_newline = false 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/view-transition.scss: -------------------------------------------------------------------------------- 1 | // 控制切换主题时的动画效果(只在较新的浏览器上生效,例如 Chrome 111+) 2 | 3 | ::view-transition-old(root) { 4 | animation: none; 5 | mix-blend-mode: normal; 6 | } 7 | 8 | ::view-transition-new(root) { 9 | animation: 0.5s ease-in clip-animation; 10 | mix-blend-mode: normal; 11 | } 12 | 13 | @keyframes clip-animation { 14 | from { 15 | clip-path: circle(0px at var(--v3-theme-x) var(--v3-theme-y)); 16 | } 17 | to { 18 | clip-path: circle(var(--v3-theme-r) at var(--v3-theme-x) var(--v3-theme-y)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/constants/cache-key_n.ts: -------------------------------------------------------------------------------- 1 | const SYSTEM_NAME = "td27-admin" 2 | 3 | /** 缓存数据时用到的 Key */ 4 | export class CacheKey { 5 | static readonly TOKEN = `${SYSTEM_NAME}-token-key` 6 | static readonly CONFIG_LAYOUT = `${SYSTEM_NAME}-config-layout-key` 7 | static readonly SIDEBAR_STATUS = `${SYSTEM_NAME}-sidebar-status-key` 8 | static readonly ACTIVE_THEME_NAME = `${SYSTEM_NAME}-active-theme-name-key` 9 | static readonly VISITED_VIEWS = `${SYSTEM_NAME}-visited-views-key` 10 | static readonly CACHED_VIEWS = `${SYSTEM_NAME}-cached-views-key` 11 | } 12 | -------------------------------------------------------------------------------- /server/internal/router/sysSet/enter.go: -------------------------------------------------------------------------------- 1 | package sysSet 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type RouterGroup struct { 6 | *DictRouter 7 | *DictDetailRouter 8 | } 9 | 10 | func NewRouterGroup() *RouterGroup { 11 | return &RouterGroup{ 12 | DictRouter: NewDictRouter(), 13 | DictDetailRouter: NewDictDetailRouter(), 14 | } 15 | } 16 | 17 | func (rg *RouterGroup) InitPublic(group *gin.RouterGroup) {} 18 | 19 | func (rg *RouterGroup) InitPrivate(group *gin.RouterGroup) { 20 | rg.InitDictRouter(group) 21 | rg.InitDictDetailRouter(group) 22 | } 23 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %VITE_APP_TITLE% 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server/internal/global/model.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type TD27_MODEL struct { 10 | ID uint `json:"id" gorm:"primarykey"` // 主键ID 11 | CreatedAt time.Time `json:"createdAt" gorm:"column:created_at;type:datetime;default:null"` // 创建时间 12 | UpdatedAt time.Time `json:"updatedAt" gorm:"column:updated_at;type:datetime;default:null"` // 更新时间 13 | DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 删除时间 14 | } 15 | -------------------------------------------------------------------------------- /web/src/pages/error/components/Layout.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /web/src/pages/dashboard/components/Admin.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /web/src/pages/dashboard/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /server/internal/router/base/logReg.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "server/internal/api/base" 6 | ) 7 | 8 | type LogRegRouter struct { 9 | logRegApi *base.LogRegApi 10 | } 11 | 12 | func NewLogRegRouter() *LogRegRouter { 13 | return &LogRegRouter{ 14 | logRegApi: base.NewLogRegApi(), 15 | } 16 | } 17 | 18 | func (br *LogRegRouter) InitLogRegRouter(rg *gin.RouterGroup) { 19 | baseG := rg.Group("logReg") 20 | baseG.POST("captcha", br.logRegApi.Captcha) 21 | baseG.POST("login", br.logRegApi.Login) 22 | baseG.POST("logout", br.logRegApi.LogOut) 23 | } 24 | -------------------------------------------------------------------------------- /web/src/common/composables/useTitle.ts: -------------------------------------------------------------------------------- 1 | /** 项目标题 */ 2 | const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "V3 Admin Vite" 3 | 4 | /** 动态标题 */ 5 | const dynamicTitle = ref("") 6 | 7 | /** 设置标题 */ 8 | function setTitle(title?: string) { 9 | dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE 10 | } 11 | 12 | // 监听标题变化 13 | watch(dynamicTitle, (value, oldValue) => { 14 | if (document && value !== oldValue) { 15 | document.title = value 16 | } 17 | }) 18 | 19 | /** 标题 Composable */ 20 | export function useTitle() { 21 | return { setTitle } 22 | } 23 | -------------------------------------------------------------------------------- /server/internal/router/base/casbin.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/base" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type CasbinRouter struct { 11 | casbinApi *base.CasbinApi 12 | } 13 | 14 | func NewCasbinRouter() *CasbinRouter { 15 | return &CasbinRouter{ 16 | casbinApi: base.NewCasbinApi(), 17 | } 18 | } 19 | 20 | func (cr *CasbinRouter) InitCasbinRouter(rg *gin.RouterGroup) { 21 | baseG := rg.Group("casbin") 22 | record := baseG.Use(middleware.OperationRecord()) 23 | record.POST("editCasbin", cr.casbinApi.EditCasbin) 24 | } 25 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /server/internal/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "github.com/redis/go-redis/v9" 5 | "github.com/robfig/cron/v3" 6 | "github.com/spf13/viper" 7 | "go.uber.org/zap" 8 | "golang.org/x/sync/singleflight" 9 | "gorm.io/gorm" 10 | 11 | "server/configs" 12 | ) 13 | 14 | var ( 15 | TD27_VP *viper.Viper 16 | TD27_CONFIG configs.Server 17 | TD27_LOG *zap.Logger 18 | TD27_DB *gorm.DB 19 | TD27_REDIS *redis.Client 20 | TD27_Concurrency_Control = &singleflight.Group{} 21 | TD27_CRON *cron.Cron 22 | ) 23 | -------------------------------------------------------------------------------- /web/.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v4 21 | with: 22 | registry-url: https://registry.npmjs.org/ 23 | node-version: lts/* 24 | 25 | - run: npx changelogithub 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.V3_ADMIN_VITE }} 28 | -------------------------------------------------------------------------------- /server/internal/core/zap/core.go: -------------------------------------------------------------------------------- 1 | package zap 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | 7 | "server/internal/global" 8 | ) 9 | 10 | type ZapCore struct { 11 | level zapcore.Level 12 | zapcore.Core 13 | } 14 | 15 | func NewZapCore(level zapcore.Level) *ZapCore { 16 | entity := &ZapCore{level: level} 17 | syncer := LumberjackLogs.GetWriteSyncer(level.String()) 18 | levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool { 19 | return l == level 20 | }) 21 | entity.Core = zapcore.NewCore(global.TD27_CONFIG.Zap.Encoder(), syncer, levelEnabler) 22 | return entity 23 | } 24 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // https://cn.vuejs.org/guide/built-ins/transition 2 | 3 | // fade-transform 4 | .fade-transform-leave-active, 5 | .fade-transform-enter-active { 6 | transition: all 0.5s; 7 | } 8 | .fade-transform-enter { 9 | opacity: 0; 10 | transform: translateX(-30px); 11 | } 12 | .fade-transform-leave-to { 13 | opacity: 0; 14 | transform: translateX(30px); 15 | } 16 | 17 | // layout-logo-fade 18 | .layout-logo-fade-enter-active, 19 | .layout-logo-fade-leave-active { 20 | transition: opacity 1.5s; 21 | } 22 | .layout-logo-fade-enter-from, 23 | .layout-logo-fade-leave-to { 24 | opacity: 0; 25 | } 26 | -------------------------------------------------------------------------------- /web/src/layouts/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from "./AppMain/index.vue" 2 | export { default as Breadcrumb } from "./Breadcrumb/index.vue" 3 | export { default as Footer } from "./Footer/index.vue" 4 | export { default as Hamburger } from "./Hamburger/index.vue" 5 | export { default as Logo } from "./Logo/index.vue" 6 | export { default as NavigationBar } from "./NavigationBar/index.vue" 7 | export { default as RightPanel } from "./RightPanel/index.vue" 8 | export { default as Settings } from "./Settings/index.vue" 9 | export { default as Sidebar } from "./Sidebar/index.vue" 10 | export { default as TagsView } from "./TagsView/index.vue" 11 | -------------------------------------------------------------------------------- /web/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetAttributify, presetWind3 } from "unocss" 2 | 3 | export default defineConfig({ 4 | // 预设 5 | presets: [ 6 | // 属性化模式 & 无值的属性模式 7 | presetAttributify({ 8 | prefix: "un-", 9 | prefixedOnly: false 10 | }), 11 | // 默认预设 12 | presetWind3({ 13 | important: "#app" 14 | }) 15 | ], 16 | // 自定义规则 17 | rules: [] 18 | // 自定义快捷方式 19 | // shortcuts: { 20 | // "wh-full": "w-full h-full", 21 | // "flex-center": "flex justify-center items-center", 22 | // "flex-x-center": "flex justify-center", 23 | // "flex-y-center": "flex items-center" 24 | // } 25 | }) 26 | -------------------------------------------------------------------------------- /web/src/common/utils/css.ts: -------------------------------------------------------------------------------- 1 | /** 获取指定元素(默认全局)上的 CSS 变量的值 */ 2 | export function getCssVar(varName: string, element: HTMLElement = document.documentElement) { 3 | if (!varName?.startsWith("--")) { 4 | console.error("CSS 变量名应以 '--' 开头") 5 | return "" 6 | } 7 | // 没有拿到值时,会返回空串 8 | return getComputedStyle(element).getPropertyValue(varName) 9 | } 10 | 11 | /** 设置指定元素(默认全局)上的 CSS 变量的值 */ 12 | export function setCssVar(varName: string, value: string, element: HTMLElement = document.documentElement) { 13 | if (!varName?.startsWith("--")) { 14 | console.error("CSS 变量名应以 '--' 开头") 15 | return 16 | } 17 | element.style.setProperty(varName, value) 18 | } 19 | -------------------------------------------------------------------------------- /web/src/common/components/SearchMenu/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /web/src/common/composables/useGreyAndColorWeakness.ts: -------------------------------------------------------------------------------- 1 | import { useSettingsStore } from "@/pinia/stores/settings" 2 | 3 | const GREY_MODE = "grey-mode" 4 | 5 | const COLOR_WEAKNESS = "color-weakness" 6 | 7 | const classList = document.documentElement.classList 8 | 9 | /** 初始化 */ 10 | function initGreyAndColorWeakness() { 11 | const settingsStore = useSettingsStore() 12 | watchEffect(() => { 13 | classList.toggle(GREY_MODE, settingsStore.showGreyMode) 14 | classList.toggle(COLOR_WEAKNESS, settingsStore.showColorWeakness) 15 | }) 16 | } 17 | 18 | /** 灰色模式和色弱模式 Composable */ 19 | export function useGreyAndColorWeakness() { 20 | return { initGreyAndColorWeakness } 21 | } 22 | -------------------------------------------------------------------------------- /server/internal/router/authority/enter.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type RouterGroup struct { 6 | *UserRouter 7 | *RoleRouter 8 | *MenuRouter 9 | *ApiRouter 10 | } 11 | 12 | func NewRouterGroup() *RouterGroup { 13 | return &RouterGroup{ 14 | UserRouter: NewUserRouter(), 15 | RoleRouter: NewRoleRouter(), 16 | MenuRouter: NewMenuRouter(), 17 | ApiRouter: NewApiRouter(), 18 | } 19 | } 20 | 21 | func (rg *RouterGroup) InitPublic(group *gin.RouterGroup) {} 22 | 23 | func (rg *RouterGroup) InitPrivate(group *gin.RouterGroup) { 24 | rg.InitApiRouter(group) 25 | rg.InitMenuRouter(group) 26 | rg.InitRoleRouter(group) 27 | rg.InitUserRouter(group) 28 | } 29 | -------------------------------------------------------------------------------- /web/src/common/composables/useLayoutMode.ts: -------------------------------------------------------------------------------- 1 | import { LayoutModeEnum } from "@@/constants/app-key" 2 | import { useSettingsStore } from "@/pinia/stores/settings" 3 | 4 | const settingsStore = useSettingsStore() 5 | 6 | const isLeft = computed(() => settingsStore.layoutMode === LayoutModeEnum.Left) 7 | 8 | const isTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.Top) 9 | 10 | const isLeftTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.LeftTop) 11 | 12 | function setLayoutMode(mode: LayoutModeEnum) { 13 | settingsStore.layoutMode = mode 14 | } 15 | 16 | /** 布局模式 Composable */ 17 | export function useLayoutMode() { 18 | return { isLeft, isTop, isLeftTop, setLayoutMode } 19 | } 20 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/keyboard-esc.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/.cursor/rules/git.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 辅助生成 Git 提交信息 3 | alwaysApply: false 4 | --- 5 | 6 | # Git 提交规范 7 | 8 | - 你是一位前端开发专家,精通 Git 操作 9 | 10 | ## Commit 规范 11 | 12 | 提交模板 `type: message`,具体要求如下: 13 | 14 | 1. 注意英文冒号后有一个空格 15 | 2. `type` 的枚举值有: 16 | 17 | - `feat` 新功能 18 | - `fix` 修复错误 19 | - `perf` 性能优化 20 | - `refactor` 重构代码 21 | - `docs` 文档和注释 22 | - `types` 类型相关 23 | - `test` 单测相关 24 | - `ci` 持续集成、工作流 25 | - `revert` 撤销更改 26 | - `chore` 琐事(更新依赖、修改配置等) 27 | 28 | 3. 保持 `message` 简洁明了,描述清楚变更内容 29 | 30 | ## 分支说明 31 | 32 | - `main / master`: 主分支 33 | - `4.x`: 已停止维护的 4.x 版本代码 34 | - `gh-pages`: GitHub Pages 构建分支 35 | 36 | ## 其他 37 | 38 | - 禁止自动提交,除非有明确的指示 39 | - 提交前确保代码通过代码校验和单元测试 40 | - 避免大型提交,尽量将变更分解为小的、相关的提交 41 | -------------------------------------------------------------------------------- /web/src/common/utils/useValidate.ts: -------------------------------------------------------------------------------- 1 | /** 表单电话号码校验 */ 2 | export function useValidatePhone(rule: any, value: any, callback: any) { 3 | if (value === "") { 4 | callback() 5 | } else { 6 | const phoneReg = /^1\d{10}$/ 7 | if (!phoneReg.test(value)) { 8 | callback(new Error("手机号码不合规")) 9 | } else { 10 | callback() 11 | } 12 | } 13 | } 14 | 15 | /** 邮箱校验 */ 16 | export function useValidateEmail(rule: any, value: any, callback: any) { 17 | if (value === "") { 18 | callback() 19 | } else { 20 | const emailReg = /^[\w-]+@[\w-]+(\.[\w-]+)+$/ 21 | if (!emailReg.test(value)) { 22 | callback(new Error("邮箱不合规")) 23 | } else { 24 | callback() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/core/element-plus.scss: -------------------------------------------------------------------------------- 1 | // Element Plus 相关 2 | 3 | // 侧边栏的 item 的 popper 4 | .el-popper { 5 | .el-menu { 6 | background-color: var(--el-bg-color); 7 | .el-menu-item { 8 | background-color: var(--el-bg-color); 9 | &.is-active, 10 | &:hover { 11 | background-color: var(--el-bg-color-overlay); 12 | color: #ffffff; 13 | } 14 | } 15 | .el-sub-menu__title { 16 | background-color: var(--el-bg-color); 17 | } 18 | .el-sub-menu { 19 | &.is-active { 20 | > .el-sub-menu__title { 21 | color: #ffffff; 22 | } 23 | } 24 | } 25 | } 26 | .el-menu--horizontal { 27 | border: none; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | WORKDIR /server 4 | COPY . . 5 | 6 | RUN go env -w GO111MODULE=on \ 7 | && go env -w GOPROXY=https://goproxy.cn,direct \ 8 | && go env -w CGO_ENABLED=0 \ 9 | && go env \ 10 | && go mod tidy \ 11 | && go build -o td27Server ./cmd/server/ 12 | 13 | FROM alpine:latest 14 | 15 | LABEL MAINTAINER="pddzl5@foxmail.com" 16 | 17 | WORKDIR /server 18 | 19 | RUN mkdir -p ./configs 20 | 21 | COPY --from=0 /server/td27Server ./ 22 | COPY --from=0 /server/configs/config.yaml ./configs/ 23 | COPY --from=0 /server/scripts/wait-for-it.sh ./ 24 | 25 | RUN mkdir -p ./resource/upload && chmod +x ./wait-for-it.sh 26 | 27 | EXPOSE 8888 28 | 29 | CMD ["./wait-for-it.sh", "./td27Server"] 30 | -------------------------------------------------------------------------------- /server/configs/crontab.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type Crontab struct { 4 | Open bool `mapstructure:"open" json:"open" yaml:"open"` // 是否启用 5 | Spec string `mapstructure:"spec" json:"spec" yaml:"spec"` // CRON表达式 6 | //WithSeconds bool `mapstructure:"with_seconds" json:"with_seconds" yaml:"with_seconds"` // 是否精确到秒 7 | Objects []Object `mapstructure:"objects" json:"objects" yaml:"objects"` 8 | } 9 | 10 | type Object struct { 11 | TableName string `mapstructure:"tableName" json:"tableName" yaml:"tableName"` // 需要清理的表名 12 | CompareField string `mapstructure:"compareField" json:"compareField" yaml:"compareField"` // 需要比较时间的字段 13 | Interval string `mapstructure:"interval" json:"interval" yaml:"interval"` // 时间间隔 14 | } 15 | -------------------------------------------------------------------------------- /web/src/layouts/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable perfectionist/sort-imports */ 2 | 3 | // core 4 | import { pinia } from "@/pinia" 5 | import { router } from "@/router/index_n" 6 | import { installPlugins } from "@/plugins/index_n" 7 | import App from "@/App.vue" 8 | // css 9 | import "normalize.css" 10 | import "nprogress/nprogress.css" 11 | import "element-plus/theme-chalk/dark/css-vars.css" 12 | import "vxe-table/lib/style.css" 13 | import "@@/assets/styles/index.scss" 14 | import "virtual:uno.css" 15 | 16 | // 创建应用实例 17 | const app = createApp(App) 18 | 19 | // 安装插件(全局组件、自定义指令等) 20 | installPlugins(app) 21 | 22 | // 安装 pinia 和 router 23 | app.use(pinia).use(router) 24 | 25 | // router 准备就绪后挂载应用 26 | router.isReady().then(() => { 27 | app.mount("#app") 28 | }) 29 | -------------------------------------------------------------------------------- /server/internal/router/sysSet/dict.go: -------------------------------------------------------------------------------- 1 | package sysSet 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/sysSet" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type DictRouter struct { 11 | dictApi *sysSet.DictApi 12 | } 13 | 14 | func NewDictRouter() *DictRouter { 15 | return &DictRouter{ 16 | dictApi: sysSet.NewDictApi(), 17 | } 18 | } 19 | 20 | func (dr *DictRouter) InitDictRouter(rg *gin.RouterGroup) { 21 | base := rg.Group("dict") 22 | record := base.Use(middleware.OperationRecord()) 23 | // record 24 | record.POST("delDict", dr.dictApi.DelDict) 25 | record.POST("addDict", dr.dictApi.AddDict) 26 | record.POST("editDict", dr.dictApi.EditDict) 27 | // not record 28 | base.GET("getDict", dr.dictApi.GetDict) 29 | } 30 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/.cursor/rules/ts.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | globs: *.ts,*.tsx,*.d.ts 3 | alwaysApply: false 4 | --- 5 | 6 | # TypeScript 开发规范 7 | 8 | - 你是一位前端开发专家,精通 TypeScript、JavaScript 等前端技术 9 | 10 | ## 类型 11 | 12 | - 对于对象定义,优先使用接口而非类型 13 | - 对于联合类型、交叉类型和映射类型,使用 `type` 14 | - 避免使用 `any`,对于未知类型优先使用 `unknown` 15 | - 使用泛型实现可复用的类型模式 16 | - 不可变属性使用 `readonly` 17 | 18 | ## 命名 19 | 20 | - 类型名称和接口使用 PascalCase 21 | - 变量和函数使用 camelCase 22 | - 常量使用 UPPER_CASE 23 | - 使用带有辅助动词的描述性名称(例如 isLoading、hasError) 24 | 25 | ## 代码组织 26 | 27 | - 类型定义应靠近使用它们的地方 28 | - 共享的类型和接口从公共类型文件导出 29 | - 将 `*.d.ts` 文件放在 `types` 目录中 30 | 31 | ## 错误处理 32 | 33 | - 捕获可能的异常,并对其进行处理 34 | 35 | ## 其他 36 | 37 | - 实现适当的空值检查 38 | - 避免不必要的类型断言 39 | - 为公共函数使用显式返回类型 40 | - 回调使用箭头函数 41 | - 启用 TypeScript 严格模式 42 | - 禁止不必要的类型体操,以可读性为主 43 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/fullscreen-exit.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/internal/model/entity/base/request/casbin.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type CasbinInfo struct { 4 | Path string `json:"path"` // 路径 5 | Method string `json:"method"` // 方法 6 | } 7 | 8 | type ReqCasbin struct { 9 | RoleId uint `json:"roleId" binding:"required"` // 角色ID 10 | CasbinInfos []CasbinInfo `json:"casbinInfos"` 11 | } 12 | 13 | func DefaultCasbin() []CasbinInfo { 14 | return []CasbinInfo{ 15 | {Path: "/logReg/captcha", Method: "POST"}, 16 | {Path: "/logReg/login", Method: "POST"}, 17 | {Path: "/logReg/logout", Method: "POST"}, 18 | {Path: "/user/getUserInfo", Method: "GET"}, 19 | {Path: "/user/editUser", Method: "POST"}, 20 | {Path: "/user/modifyPass", Method: "POST"}, 21 | {Path: "/menu/getMenus", Method: "GET"}, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/request/api.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "server/internal/model/common/request" 5 | ) 6 | 7 | type apiStruct struct { 8 | Path string `json:"path"` // 路径 9 | ApiGroup string `json:"apiGroup"` // API分组 10 | Method string `json:"method" binding:"omitempty,oneof=GET POST DELETE PUT"` // 请求方法 11 | Description string `json:"description"` // 描述 12 | } 13 | 14 | // ApiSearchParams api分页条件查询及排序结构体 15 | type ApiSearchParams struct { 16 | apiStruct 17 | request.PageInfo 18 | OrderKey string `json:"orderKey"` // 排序 19 | Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true 20 | } 21 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/user.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "server/internal/global" 5 | ) 6 | 7 | type UserModel struct { 8 | global.TD27_MODEL 9 | Username string `json:"username" gorm:"unique;comment:用户名" binding:"required"` // 用户名 10 | Password string `json:"-" gorm:"not null;comment:密码"` 11 | Phone string `json:"phone" gorm:"comment:手机号"` // 手机号 12 | Email string `json:"email" gorm:"comment:邮箱" binding:"omitempty,email"` // 邮箱 13 | Active bool `json:"active"` // 是否活跃 14 | RoleModelID uint `json:"roleId" gorm:"not null" binding:"required"` // 角色ID 15 | } 16 | 17 | func (UserModel) TableName() string { 18 | return "authority_user" 19 | } 20 | -------------------------------------------------------------------------------- /server/configs/cors.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type CORS struct { 4 | Mode string `mapstructure:"mode" json:"mode" yaml:"mode"` 5 | Whitelist []CORSWhitelist `mapstructure:"whitelist" json:"whitelist" yaml:"whitelist"` 6 | } 7 | 8 | type CORSWhitelist struct { 9 | AllowOrigin string `mapstructure:"allow-origin" json:"allow-origin" yaml:"allow-origin"` 10 | AllowMethods string `mapstructure:"allow-methods" json:"allow-methods" yaml:"allow-methods"` 11 | AllowHeaders string `mapstructure:"allow-headers" json:"allow-headers" yaml:"allow-headers"` 12 | ExposeHeaders string `mapstructure:"expose-headers" json:"expose-headers" yaml:"expose-headers"` 13 | AllowCredentials bool `mapstructure:"allow-credentials" json:"allow-credentials" yaml:"allow-credentials"` 14 | } 15 | -------------------------------------------------------------------------------- /server/internal/router/fileM/file.go: -------------------------------------------------------------------------------- 1 | package fileM 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/fileM" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type FileRouter struct { 11 | fileApi *fileM.FileApi 12 | } 13 | 14 | func NewFileRouter() *FileRouter { 15 | return &FileRouter{ 16 | fileApi: fileM.NewFileApi(), 17 | } 18 | } 19 | 20 | func (fr *FileRouter) InitFileRouter(rg *gin.RouterGroup) { 21 | base := rg.Group("file") 22 | record := base.Use(middleware.OperationRecord()) 23 | // record 24 | record.POST("upload", fr.fileApi.Upload) // 文件上传 25 | record.GET("download", fr.fileApi.Download) // 下载文件 26 | record.GET("delete", fr.fileApi.Delete) // 删除文件 27 | // without record 28 | base.POST("getFileList", fr.fileApi.GetFileList) // 分页获取文件信息 29 | } 30 | -------------------------------------------------------------------------------- /server/internal/service/base/jwt.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "server/internal/global" 8 | ) 9 | 10 | type JwtService struct{} 11 | 12 | func NewJwtService() *JwtService { 13 | return &JwtService{} 14 | } 15 | 16 | // GetRedisJWT 获取jwt 17 | func (jwtService *JwtService) GetRedisJWT(username string) (redisJWT string, err error) { 18 | redisJWT, err = global.TD27_REDIS.Get(context.Background(), username).Result() 19 | return redisJWT, err 20 | } 21 | 22 | // SetRedisJWT jwt存入redis并设置过期时间 23 | func (jwtService *JwtService) SetRedisJWT(username string, jwt string) (err error) { 24 | // 此处过期时间等于jwt过期时间 25 | err = global.TD27_REDIS.Set(context.Background(), username, jwt, time.Duration(global.TD27_CONFIG.JWT.ExpiresTime)*time.Second).Err() 26 | return err 27 | } 28 | -------------------------------------------------------------------------------- /server/internal/router/authority/menu.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/authority" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type MenuRouter struct { 11 | menuApi *authority.MenuApi 12 | } 13 | 14 | func NewMenuRouter() *MenuRouter { 15 | return &MenuRouter{menuApi: authority.NewMenuApi()} 16 | } 17 | 18 | func (mr *MenuRouter) InitMenuRouter(rg *gin.RouterGroup) { 19 | base := rg.Group("menu") 20 | record := base.Use(middleware.OperationRecord()) 21 | // record 22 | record.POST("create", mr.menuApi.Create) 23 | record.POST("update", mr.menuApi.Update) 24 | record.POST("delete", mr.menuApi.Delete) 25 | record.POST("getElTreeMenus", mr.menuApi.GetElTreeMenus) 26 | // without record 27 | base.GET("list", mr.menuApi.List) 28 | } 29 | -------------------------------------------------------------------------------- /server/internal/router/authority/role.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/authority" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type RoleRouter struct { 11 | roleApi *authority.RoleApi 12 | } 13 | 14 | func NewRoleRouter() *RoleRouter { 15 | return &RoleRouter{roleApi: authority.NewRoleApi()} 16 | } 17 | 18 | func (rr *RoleRouter) InitRoleRouter(rg *gin.RouterGroup) { 19 | base := rg.Group("role") 20 | record := base.Use(middleware.OperationRecord()) 21 | // record 22 | record.POST("create", rr.roleApi.Create) 23 | record.POST("delete", rr.roleApi.Delete) 24 | record.POST("update", rr.roleApi.Update) 25 | record.POST("editRoleMenu", rr.roleApi.EditRoleMenu) 26 | // without record 27 | base.POST("list", rr.roleApi.List) 28 | } 29 | -------------------------------------------------------------------------------- /server/internal/pkg/cron.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "gorm.io/gorm" 7 | "time" 8 | 9 | "server/internal/global" 10 | ) 11 | 12 | func ClearTable(db *gorm.DB, tableName string, compareField string, interval string) error { 13 | if db == nil { 14 | return errors.New("db Cannot be empty") 15 | } 16 | duration, err := time.ParseDuration(interval) 17 | if err != nil { 18 | return err 19 | } 20 | if duration < 0 { 21 | return errors.New("parse duration < 0") 22 | } 23 | return db.Exec(fmt.Sprintf("DELETE FROM %s WHERE %s < ?", tableName, compareField), time.Now().Add(-duration)).Error 24 | } 25 | 26 | func GetEntries() (entrySlice []int) { 27 | for _, v := range global.TD27_CRON.Entries() { 28 | entrySlice = append(entrySlice, int(v.ID)) 29 | } 30 | 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /server/internal/core/viper.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fsnotify/fsnotify" 7 | "github.com/spf13/viper" 8 | 9 | "server/internal/global" 10 | ) 11 | 12 | func Viper() *viper.Viper { 13 | config := "configs/config.yaml" 14 | 15 | v := viper.New() 16 | v.SetConfigFile(config) 17 | v.SetConfigType("yaml") 18 | err := v.ReadInConfig() 19 | if err != nil { 20 | panic(fmt.Errorf("Fatal error configs file: %s \n", err)) 21 | } 22 | v.WatchConfig() 23 | 24 | v.OnConfigChange(func(e fsnotify.Event) { 25 | fmt.Println("configs file changed:", e.Name) 26 | if err = v.Unmarshal(&global.TD27_CONFIG); err != nil { 27 | fmt.Println(err) 28 | } 29 | }) 30 | 31 | if err = v.Unmarshal(&global.TD27_CONFIG); err != nil { 32 | fmt.Println(err) 33 | } 34 | 35 | return v 36 | } 37 | -------------------------------------------------------------------------------- /server/internal/router/authority/api.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/authority" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type ApiRouter struct { 11 | apiApi *authority.ApiApi 12 | } 13 | 14 | func NewApiRouter() *ApiRouter { 15 | return &ApiRouter{apiApi: authority.NewApiApi()} 16 | } 17 | 18 | func (ur *ApiRouter) InitApiRouter(rg *gin.RouterGroup) { 19 | base := rg.Group("api") 20 | record := base.Use(middleware.OperationRecord()) 21 | // record 22 | record.POST("create", ur.apiApi.Create) 23 | record.POST("delete", ur.apiApi.Delete) 24 | record.POST("deleteByIds", ur.apiApi.DeleteByIds) 25 | record.POST("update", ur.apiApi.Update) 26 | record.POST("getElTree", ur.apiApi.GetElTree) 27 | // without record 28 | base.POST("list", ur.apiApi.List) 29 | } 30 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/vxe-table.scss: -------------------------------------------------------------------------------- 1 | // 自定义 Vxe Table 样式 2 | 3 | .vxe-grid { 4 | // 表单 5 | &--form-wrapper { 6 | .vxe-form { 7 | padding: 10px 20px; 8 | margin-bottom: 20px; 9 | } 10 | } 11 | 12 | // 工具栏 13 | &--toolbar-wrapper { 14 | .vxe-toolbar { 15 | padding: 20px; 16 | } 17 | } 18 | 19 | // 分页 20 | &--pager-wrapper { 21 | .vxe-pager { 22 | height: 70px; 23 | padding: 0 20px; 24 | &--wrapper { 25 | // 参考 Bootstrap 的响应式设计 WIDTH = 768 26 | @media screen and (max-width: 768px) { 27 | .vxe-pager--total, 28 | .vxe-pager--sizes, 29 | .vxe-pager--jump, 30 | .vxe-pager--jump-prev, 31 | .vxe-pager--jump-next { 32 | display: none; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/internal/model/entity/sysSet/dictDetail.go: -------------------------------------------------------------------------------- 1 | package sysSet 2 | 3 | import ( 4 | "server/internal/global" 5 | ) 6 | 7 | type DictDetailModel struct { 8 | global.TD27_MODEL 9 | Label string `json:"label" gorm:"column:label" binding:"required"` 10 | Value string `json:"value" gorm:"column:value" binding:"required"` 11 | Sort int `json:"sort" gorm:"column:sort"` 12 | DictModelID int `json:"dictId" gorm:"column:dict_id" binding:"required"` 13 | ParentID *int `json:"parentId" gorm:"column:parent_id"` // new 14 | Children []*DictDetailModel `json:"children" gorm:"-"` 15 | Description string `json:"description" gorm:"column:description"` 16 | } 17 | 18 | func (ddm *DictDetailModel) TableName() string { 19 | return "sysSet_dictDetail" 20 | } 21 | -------------------------------------------------------------------------------- /server/internal/router/monitor/operationLog.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/monitor" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type OperationLogRouter struct { 11 | operationLogApi *monitor.OperationLogApi 12 | } 13 | 14 | func NewOperationLogRouter() *OperationLogRouter { 15 | return &OperationLogRouter{operationLogApi: monitor.NewOperationLogApi()} 16 | } 17 | 18 | func (or *OperationLogRouter) InitOperationLogRouter(rg *gin.RouterGroup) { 19 | base := rg.Group("opl") 20 | record := base.Use(middleware.OperationRecord()) 21 | // record 22 | record.POST("deleteOpl", or.operationLogApi.DeleteOperationLog) 23 | record.POST("deleteOplByIds", or.operationLogApi.DeleteOperationLogByIds) 24 | // not record 25 | base.POST("getOplList", or.operationLogApi.GetOperationLogList) 26 | } 27 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/core/layouts.scss: -------------------------------------------------------------------------------- 1 | // Layout 相关 2 | 3 | .app-wrapper { 4 | // 侧边栏 5 | .sidebar-container { 6 | background-color: var(--el-bg-color); 7 | .el-menu { 8 | background-color: var(--el-bg-color); 9 | .el-menu-item { 10 | background-color: var(--el-bg-color); 11 | &.is-active, 12 | &:hover { 13 | background-color: var(--el-bg-color-overlay); 14 | color: #ffffff; 15 | } 16 | } 17 | } 18 | .el-sub-menu__title { 19 | background-color: var(--el-bg-color); 20 | } 21 | .el-sub-menu { 22 | &.is-active { 23 | > .el-sub-menu__title { 24 | color: #ffffff; 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | // 右侧设置面板 32 | .handle-button { 33 | background-color: lighten($theme-bg-color, 20%) !important; 34 | } 35 | -------------------------------------------------------------------------------- /web/public/app-loading.css: -------------------------------------------------------------------------------- 1 | /* 白屏阶段会执行的 CSS 加载动画 */ 2 | 3 | #app-loading { 4 | position: relative; 5 | top: 45vh; 6 | margin: 0 auto; 7 | color: #409eff; 8 | font-size: 12px; 9 | } 10 | 11 | #app-loading, 12 | #app-loading::before, 13 | #app-loading::after { 14 | width: 2em; 15 | height: 2em; 16 | border-radius: 50%; 17 | animation: 2s ease-in-out infinite app-loading-animation; 18 | } 19 | 20 | #app-loading::before, 21 | #app-loading::after { 22 | content: ""; 23 | position: absolute; 24 | } 25 | 26 | #app-loading::before { 27 | left: -4em; 28 | animation-delay: -0.2s; 29 | } 30 | 31 | #app-loading::after { 32 | left: 4em; 33 | animation-delay: 0.2s; 34 | } 35 | 36 | @keyframes app-loading-animation { 37 | 0%, 38 | 80%, 39 | 100% { 40 | box-shadow: 0 2em 0 -2em; 41 | } 42 | 40% { 43 | box-shadow: 0 2em 0 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/internal/middleware/log/access.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | 9 | "server/internal/global" 10 | ) 11 | 12 | // GinLogger 接收gin框架默认的日志 13 | func GinLogger() gin.HandlerFunc { 14 | return func(c *gin.Context) { 15 | start := time.Now() 16 | path := c.Request.URL.Path 17 | query := c.Request.URL.RawQuery 18 | c.Next() 19 | 20 | cost := time.Since(start) 21 | global.TD27_LOG.Info( 22 | path, 23 | zap.Int("status", c.Writer.Status()), 24 | zap.String("method", c.Request.Method), 25 | //zap.String("path", path), 26 | zap.String("query", query), 27 | zap.String("ip", c.ClientIP()), 28 | zap.String("user-agent", c.Request.UserAgent()), 29 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), 30 | zap.Duration("cost", cost), 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/monitor.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/internal/router/sysTool/cron.go: -------------------------------------------------------------------------------- 1 | package sysTool 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/sysTool" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type CronRouter struct { 11 | cronApi *sysTool.CronApi 12 | } 13 | 14 | func NewCronRouter() *CronRouter { 15 | return &CronRouter{ 16 | cronApi: sysTool.NewCronApi(), 17 | } 18 | } 19 | 20 | func (cr *CronRouter) InitCronRouter(rg *gin.RouterGroup) { 21 | base := rg.Group("cron") 22 | record := base.Use(middleware.OperationRecord()) 23 | // record 24 | record.POST("addCron", cr.cronApi.AddCron) 25 | record.POST("deleteCron", cr.cronApi.DeleteCron) 26 | record.POST("deleteCronByIds", cr.cronApi.DeleteCronByIds) 27 | record.POST("editCron", cr.cronApi.EditCron) 28 | record.POST("switchOpen", cr.cronApi.SwitchOpen) 29 | // not record 30 | base.POST("getCronList", cr.cronApi.GetCronList) 31 | } 32 | -------------------------------------------------------------------------------- /web/src/layouts/components/RightPanel/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /web/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build And Deploy v3-admin-vite 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@master 19 | with: 20 | node-version: 22.16.0 21 | 22 | - name: Setup pnpm 23 | uses: pnpm/action-setup@v2 24 | with: 25 | version: 10.12.1 26 | 27 | - name: Build 28 | run: pnpm install && pnpm build 29 | 30 | - name: Deploy 31 | uses: JamesIves/github-pages-deploy-action@releases/v3 32 | with: 33 | ACCESS_TOKEN: ${{ secrets.V3_ADMIN_VITE }} 34 | BRANCH: gh-pages 35 | FOLDER: dist 36 | -------------------------------------------------------------------------------- /server/internal/router/authority/user.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/authority" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type UserRouter struct { 11 | userApi *authority.UserApi 12 | } 13 | 14 | func NewUserRouter() *UserRouter { 15 | return &UserRouter{userApi: authority.NewUserApi()} 16 | } 17 | 18 | func (ur *UserRouter) InitUserRouter(rg *gin.RouterGroup) { 19 | base := rg.Group("user") 20 | record := base.Use(middleware.OperationRecord()) 21 | // record 22 | record.POST("delete", ur.userApi.Delete) 23 | record.POST("create", ur.userApi.Create) 24 | record.POST("update", ur.userApi.Update) 25 | record.POST("modifyPass", ur.userApi.ModifyPass) 26 | record.POST("switchActive", ur.userApi.SwitchActive) 27 | // without record 28 | base.GET("getUserInfo", ur.userApi.GetUserInfo) 29 | base.POST("list", ur.userApi.List) 30 | } 31 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | // 清除浮动 2 | %clearfix { 3 | &::after { 4 | content: ""; 5 | display: table; 6 | clear: both; 7 | } 8 | } 9 | 10 | // 美化原生滚动条 11 | %scrollbar { 12 | // 整个滚动条 13 | &::-webkit-scrollbar { 14 | width: 8px; 15 | height: 8px; 16 | } 17 | // 滚动条上的滚动滑块 18 | &::-webkit-scrollbar-thumb { 19 | border-radius: 4px; 20 | background-color: #90939955; 21 | } 22 | &::-webkit-scrollbar-thumb:hover { 23 | background-color: #90939977; 24 | } 25 | &::-webkit-scrollbar-thumb:active { 26 | background-color: #90939999; 27 | } 28 | // 当同时有垂直滚动条和水平滚动条时交汇的部分 29 | &::-webkit-scrollbar-corner { 30 | background-color: transparent; 31 | } 32 | } 33 | 34 | // 文本溢出时显示省略号 35 | %ellipsis { 36 | // 隐藏溢出的文本 37 | overflow: hidden; 38 | // 防止文本换行 39 | white-space: nowrap; 40 | // 文本内容溢出容器时,文本末尾显示省略号 41 | text-overflow: ellipsis; 42 | } 43 | -------------------------------------------------------------------------------- /server/internal/service/base/logReg.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "server/internal/global" 8 | modelAuthority "server/internal/model/entity/authority" 9 | "server/internal/pkg" 10 | ) 11 | 12 | // 登录注册相关 13 | 14 | type LogRegService struct{} 15 | 16 | func NewLogRegService() *LogRegService { 17 | return &LogRegService{} 18 | } 19 | 20 | // Login 登陆校验 21 | func (lr *LogRegService) Login(u *modelAuthority.UserModel) (userInter *modelAuthority.UserModel, err error) { 22 | var userModel modelAuthority.UserModel 23 | u.Password = pkg.MD5V([]byte(u.Password)) 24 | err = global.TD27_DB.Where("username = ? AND password = ?", u.Username, u.Password).First(&userModel).Error 25 | if err != nil { 26 | return nil, fmt.Errorf("usrname or password error: %s", err.Error()) 27 | } 28 | if userModel.Active == false { 29 | return nil, errors.New("用户为禁用状态") 30 | } 31 | return &userModel, err 32 | } 33 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/configs/config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type Server struct { 4 | JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"` 5 | Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"` 6 | RotateLogs RotateLogs `mapstructure:"rotateLogs" json:"rotateLogs" yaml:"rotateLogs"` 7 | System System `mapstructure:"system" json:"system" yaml:"system"` 8 | File File `mapstructure:"file" json:"file" yaml:"file"` 9 | Router Router `mapstructure:"router" json:"router" yaml:"router"` 10 | Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"` 11 | Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` 12 | Captcha Captcha `mapstructure:"captcha" json:"captcha" yaml:"captcha"` 13 | Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"` // 跨域配置 14 | Crontab Crontab `mapstructure:"crontab" json:"crontab" yaml:"crontab"` // 计划任务 15 | } 16 | -------------------------------------------------------------------------------- /server/internal/initialize/redis.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/redis/go-redis/v9" 9 | "go.uber.org/zap" 10 | 11 | "server/internal/global" 12 | ) 13 | 14 | func Redis() *redis.Client { 15 | redisCfg := global.TD27_CONFIG.Redis 16 | client := redis.NewClient(&redis.Options{ 17 | Addr: fmt.Sprintf("%s:%d", global.TD27_CONFIG.Redis.Host, global.TD27_CONFIG.Redis.Port), 18 | //Password: redisCfg.Password, // no password set 19 | DB: redisCfg.DB, // use default DB 20 | }) 21 | 22 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 23 | defer cancel() 24 | 25 | pong, err := client.Ping(ctx).Result() 26 | if err != nil { 27 | global.TD27_LOG.Error("redis connect ping failed, err:", zap.Error(err)) 28 | } else { 29 | global.TD27_LOG.Info("redis connect ping response:", zap.String("pong", pong)) 30 | } 31 | 32 | return client 33 | } 34 | -------------------------------------------------------------------------------- /web/src/common/composables/usePagination_n.ts: -------------------------------------------------------------------------------- 1 | interface PaginationData { 2 | total?: number 3 | currentPage?: number 4 | pageSizes?: number[] 5 | pageSize?: number 6 | layout?: string 7 | } 8 | 9 | /** 默认的分页参数 */ 10 | const DEFAULT_PAGINATION_DATA = { 11 | total: 0, 12 | currentPage: 1, 13 | pageSizes: [10, 20, 50], 14 | pageSize: 10, 15 | layout: "total, sizes, prev, pager, next, jumper" 16 | } 17 | 18 | /** 分页 Composable */ 19 | export function usePagination(initPaginationData: PaginationData = {}) { 20 | // 合并分页参数 21 | const paginationData = reactive({ ...DEFAULT_PAGINATION_DATA, ...initPaginationData }) 22 | 23 | // 改变当前页码 24 | const changeCurrentPage = (value: number) => { 25 | paginationData.currentPage = value 26 | } 27 | 28 | // 改变每页显示条数 29 | const changePageSize = (value: number) => { 30 | paginationData.pageSize = value 31 | } 32 | 33 | return { paginationData, changeCurrentPage, changePageSize } 34 | } 35 | -------------------------------------------------------------------------------- /server/internal/core/zap.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | 10 | coreZap "server/internal/core/zap" 11 | "server/internal/global" 12 | "server/internal/pkg" 13 | ) 14 | 15 | func Zap() (logger *zap.Logger) { 16 | // 判断是否有Director文件夹 17 | if ok, _ := pkg.PathExists(global.TD27_CONFIG.Zap.Director); !ok { 18 | fmt.Printf("create %v directory\n", global.TD27_CONFIG.Zap.Director) 19 | _ = os.Mkdir(global.TD27_CONFIG.Zap.Director, os.ModePerm) 20 | } 21 | 22 | levels := global.TD27_CONFIG.Zap.Levels() 23 | length := len(levels) 24 | cores := make([]zapcore.Core, 0, length) 25 | for i := 0; i < length; i++ { 26 | core := coreZap.NewZapCore(levels[i]) 27 | cores = append(cores, core) 28 | } 29 | 30 | logger = zap.New(zapcore.NewTee(cores...)) 31 | 32 | if global.TD27_CONFIG.Zap.ShowLine { 33 | logger = logger.WithOptions(zap.AddCaller()) 34 | } 35 | return logger 36 | } 37 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/config.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/internal/router/sysSet/dictDetail.go: -------------------------------------------------------------------------------- 1 | package sysSet 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/api/sysSet" 7 | "server/internal/middleware" 8 | ) 9 | 10 | type DictDetailRouter struct { 11 | dictDetailApi *sysSet.DictDetailApi 12 | } 13 | 14 | func NewDictDetailRouter() *DictDetailRouter { 15 | return &DictDetailRouter{ 16 | dictDetailApi: sysSet.NewDictDetailApi(), 17 | } 18 | } 19 | 20 | func (ddr *DictDetailRouter) InitDictDetailRouter(rg *gin.RouterGroup) { 21 | base := rg.Group("dictDetail") 22 | record := base.Use(middleware.OperationRecord()) 23 | // record 24 | record.POST("delDictDetail", ddr.dictDetailApi.DelDictDetail) 25 | record.POST("addDictDetail", ddr.dictDetailApi.AddDictDetail) 26 | record.POST("editDictDetail", ddr.dictDetailApi.EditDictDetail) 27 | // not record 28 | base.POST("getDictDetail", ddr.dictDetailApi.GetDictDetail) 29 | base.POST("getDictDetailFlat", ddr.dictDetailApi.GetDictDetailFlat) 30 | } 31 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/file.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/internal/core/zap/rotatelogs.go: -------------------------------------------------------------------------------- 1 | package zap 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "go.uber.org/zap/zapcore" 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | 10 | "server/internal/global" 11 | ) 12 | 13 | type lumberjackLogs struct{} 14 | 15 | var LumberjackLogs = new(lumberjackLogs) 16 | 17 | // GetWriteSyncer 获取 zapcore.WriteSyncer 18 | func (l *lumberjackLogs) GetWriteSyncer(level string) zapcore.WriteSyncer { 19 | fileWriter := &lumberjack.Logger{ 20 | Filename: path.Join(global.TD27_CONFIG.Zap.Director, level+".log"), 21 | MaxSize: global.TD27_CONFIG.RotateLogs.MaxSize, 22 | MaxBackups: global.TD27_CONFIG.RotateLogs.MaxBackups, 23 | MaxAge: global.TD27_CONFIG.RotateLogs.MaxAge, 24 | Compress: global.TD27_CONFIG.RotateLogs.Compress, 25 | } 26 | 27 | if global.TD27_CONFIG.Zap.LogInConsole { 28 | return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(fileWriter)) 29 | } 30 | return zapcore.AddSync(fileWriter) 31 | } 32 | -------------------------------------------------------------------------------- /web/src/api/base/login.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@/http/axios_n" 2 | 3 | export interface LoginRequestData { 4 | /** admin 或 editor */ 5 | username: string 6 | /** 密码 */ 7 | password: string 8 | /** 验证码 */ 9 | captcha: string 10 | captchaId: string 11 | } 12 | 13 | type LoginCodeResponseData = ApiResponseData<{ picPath: string, captchaId: string }> 14 | type LoginResponseData = ApiResponseData<{ token: string }> 15 | 16 | // 获取验证码 17 | export function captcha() { 18 | return request({ 19 | url: "/logReg/captcha", 20 | method: "post" 21 | }) 22 | } 23 | 24 | /** 登录并返回 Token */ 25 | export function loginApi(data: LoginRequestData) { 26 | return request({ 27 | url: "/logReg/login", 28 | method: "post", 29 | data 30 | }) 31 | } 32 | 33 | // 登出 34 | export function logoutApi() { 35 | return request>({ 36 | url: "/logReg/logout", 37 | method: "post", 38 | data: {} 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /web/src/common/components/ThemeSwitch/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | -------------------------------------------------------------------------------- /server/internal/middleware/casbin.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "server/internal/global" 9 | "server/internal/model/common/response" 10 | "server/internal/pkg" 11 | "server/internal/service/base" 12 | ) 13 | 14 | var ( 15 | casbinService = base.NewCasbinService() 16 | ) 17 | 18 | // CasbinHandler 拦截器 19 | func CasbinHandler() gin.HandlerFunc { 20 | return func(c *gin.Context) { 21 | if global.TD27_CONFIG.System.Env != "dev" { 22 | waitUse, _ := pkg.GetClaims(c) 23 | //获取请求的PATH 24 | obj := c.Request.URL.Path 25 | // 获取请求方法 26 | act := c.Request.Method 27 | // 角色ID 28 | sub := strconv.Itoa(int(waitUse.RoleId)) 29 | e := casbinService.Casbin() // 判断策略中是否存在 30 | success, _ := e.Enforce(sub, obj, act) 31 | if !success { 32 | response.FailWithDetailed(gin.H{}, "接口权限不足", c) 33 | global.TD27_LOG.Error("接口权限不足") 34 | c.Abort() 35 | return 36 | } 37 | } 38 | c.Next() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/types/api.d.ts: -------------------------------------------------------------------------------- 1 | /** 所有 api 接口的响应数据都应该准守该格式 */ 2 | interface ApiResponseData { 3 | code: number 4 | data: T 5 | msg: string 6 | } 7 | 8 | /** 请求IDs */ 9 | interface CIds { 10 | ids: number[] 11 | } 12 | 13 | /** 分页 */ 14 | interface PageInfo { 15 | page: number 16 | pageSize: number 17 | } 18 | 19 | /** get list */ 20 | interface ApiListData { 21 | list: T 22 | total: number 23 | page: number 24 | pageSize: number 25 | } 26 | 27 | interface Td27Model { 28 | id: number // 主键ID 29 | createdAt: string // 创建时间 30 | updatedAt: string // 更新时间 31 | deletedAt: string // 删除时间 32 | } 33 | 34 | interface MongoModel { 35 | id: string // 主键ID 36 | createdAt: string // 创建时间 37 | updatedAt: string // 更新时间 38 | } 39 | 40 | /** get list */ 41 | interface ListData { 42 | list: T 43 | total: number 44 | page: number 45 | pageSize: number 46 | } 47 | 48 | /** 请求ID */ 49 | interface CId { 50 | id: number 51 | } 52 | 53 | interface CIds { 54 | ids: number[] 55 | } 56 | -------------------------------------------------------------------------------- /server/internal/pkg/claims.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/global" 7 | baseReq "server/internal/model/entity/base/request" 8 | ) 9 | 10 | func GetUserInfo(c *gin.Context) (*baseReq.CustomClaims, error) { 11 | claims, exists := c.Get("claims") 12 | if !exists { 13 | token := c.Request.Header.Get("x-token") 14 | j := NewJWT() 15 | claims, err := j.ParseToken(token) 16 | if err != nil { 17 | global.TD27_LOG.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在x-token且claims是否为规定结构") 18 | return nil, err 19 | } 20 | return claims, nil 21 | } else { 22 | return claims.(*baseReq.CustomClaims), nil 23 | } 24 | } 25 | 26 | func GetClaims(c *gin.Context) (*baseReq.CustomClaims, error) { 27 | token := c.Request.Header.Get("x-token") 28 | j := NewJWT() 29 | claims, err := j.ParseToken(token) 30 | if err != nil { 31 | global.TD27_LOG.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在x-token且claims是否为规定结构") 32 | } 33 | return claims, err 34 | } 35 | -------------------------------------------------------------------------------- /web/src/common/composables/useFetchSelect.ts: -------------------------------------------------------------------------------- 1 | type OptionValue = string | number 2 | 3 | /** Select 需要的数据格式 */ 4 | interface SelectOption { 5 | value: OptionValue 6 | label: string 7 | disabled?: boolean 8 | } 9 | 10 | /** 接口响应格式 */ 11 | type ApiData = ApiResponseData 12 | 13 | /** 入参格式,暂时只需要传递 api 函数即可 */ 14 | interface FetchSelectProps { 15 | api: () => Promise 16 | } 17 | 18 | /** 下拉选择器 Composable */ 19 | export function useFetchSelect(props: FetchSelectProps) { 20 | const { api } = props 21 | 22 | const loading = ref(false) 23 | 24 | const options = ref([]) 25 | 26 | const value = ref("") 27 | 28 | // 调用接口获取数据 29 | const loadData = () => { 30 | loading.value = true 31 | options.value = [] 32 | api().then((res) => { 33 | options.value = res.data 34 | }).finally(() => { 35 | loading.value = false 36 | }) 37 | } 38 | 39 | onMounted(() => { 40 | loadData() 41 | }) 42 | 43 | return { loading, options, value } 44 | } 45 | -------------------------------------------------------------------------------- /web/src/api/sysSet/dict.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@/http/axios_n" 2 | 3 | interface dictData { 4 | chName: string 5 | enName: string 6 | } 7 | 8 | export interface dictDataModel extends dictData, Td27Model {} 9 | 10 | // List 11 | // export type dictListData = ListData 12 | 13 | export function getDictApi() { 14 | return request>({ 15 | url: "/dict/getDict", 16 | method: "get" 17 | }) 18 | } 19 | 20 | export function addDictApi(data: dictData) { 21 | return request>({ 22 | url: "/dict/addDict", 23 | method: "post", 24 | data 25 | }) 26 | } 27 | 28 | export function delDictApi(data: CId) { 29 | return request>({ 30 | url: "/dict/delDict", 31 | method: "post", 32 | data 33 | }) 34 | } 35 | 36 | export function editDictApi(data: dictData & CId) { 37 | return request>({ 38 | url: "/dict/editDict", 39 | method: "post", 40 | data 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /web/src/common/composables/useFullscreenLoading.ts: -------------------------------------------------------------------------------- 1 | import type { LoadingOptions } from "element-plus" 2 | 3 | interface UseFullscreenLoading { 4 | ) => ReturnType>( 5 | fn: T, 6 | options?: LoadingOptions 7 | ): (...args: Parameters) => Promise> 8 | } 9 | 10 | interface LoadingInstance { 11 | close: () => void 12 | } 13 | 14 | const DEFAULT_OPTIONS = { 15 | lock: true, 16 | text: "加载中..." 17 | } 18 | 19 | /** 20 | * @name 全屏加载 Composable 21 | * @description 传入一个函数 fn,在它执行周期内,加上「全屏」Loading 22 | * @param fn 要执行的函数 23 | * @param options LoadingOptions 24 | * @returns 返回一个新的函数,该函数返回一个 Promise 25 | */ 26 | export const useFullscreenLoading: UseFullscreenLoading = (fn, options = {}) => { 27 | let loadingInstance: LoadingInstance 28 | return async (...args) => { 29 | try { 30 | loadingInstance = ElLoading.service({ ...DEFAULT_OPTIONS, ...options }) 31 | return await fn(...args) 32 | } finally { 33 | loadingInstance.close() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/src/api/fileM/file.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@/http/axios_n" 2 | 3 | interface reqFiles extends PageInfo { 4 | name?: string 5 | orderKey?: string 6 | desc?: boolean 7 | } 8 | 9 | interface fileData { 10 | fileName: string 11 | fullPath: string 12 | mime: string 13 | } 14 | 15 | export interface fileDataModel extends fileData, Td27Model {} 16 | 17 | // List 18 | export type fileListData = ListData 19 | 20 | // 分页获取文件信息 21 | export function getFileListApi(data: reqFiles) { 22 | return request>({ 23 | url: "/file/getFileList", 24 | method: "post", 25 | data 26 | }) 27 | } 28 | 29 | // 下载文件 30 | export function downloadApi(params: { name: string }) { 31 | return request({ 32 | url: "/file/download", 33 | method: "get", 34 | params, 35 | responseType: "blob" 36 | }) 37 | } 38 | 39 | // 删除文件 40 | export function deleteApi(params: { name: string }) { 41 | return request>({ 42 | url: "/file/delete", 43 | method: "get", 44 | params 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/access.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/request/menu.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type Menu struct { 4 | Pid uint `json:"pid"` // 默认0 根目录 5 | Name string `json:"name"` // 名称 6 | Path string `json:"path" binding:"required"` // 路径 7 | Redirect string `json:"redirect"` // 重定向 8 | Component string `json:"component" binding:"required"` // 前端组件 9 | Sort uint `json:"sort" binding:"required"` // 排序 10 | Meta meta `json:"meta"` 11 | } 12 | 13 | type meta struct { 14 | Hidden bool `json:"hidden"` // 隐藏菜单 15 | Title string `json:"title"` // 菜单名 16 | Icon string `json:"icon"` // element图标 17 | Affix bool `json:"affix"` // 组件固定 18 | KeepAlive bool `json:"keepAlive"` // 组件缓存 19 | AlwaysShow bool `json:"alwaysShow"` // 是否一直显示根路由 20 | } 21 | 22 | type EditMenuReq struct { 23 | ID uint `json:"id" binding:"required"` // 菜单ID 24 | Menu 25 | } 26 | 27 | type EditRoleMenu struct { 28 | RoleId uint `json:"roleId"` // 角色ID 29 | Ids []uint `json:"ids"` // 菜单ID 30 | } 31 | -------------------------------------------------------------------------------- /server/internal/model/entity/monitor/operationLog.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "server/internal/global" 5 | ) 6 | 7 | type OperationLogModel struct { 8 | global.TD27_MODEL 9 | Ip string `json:"ip" gorm:"comment:请求ip"` // 请求ip 10 | Method string `json:"method" gorm:"comment:请求方法"` // 请求方法 11 | Path string `json:"path" gorm:"comment:请求路径"` // 请求路径 12 | Status int `json:"status" gorm:"comment:请求状态"` // 请求状态 13 | UserAgent string `json:"userAgent"` // http userAgent 14 | ReqParam string `json:"reqParam" gorm:"type:text;comment:请求Body"` // 请求参数 15 | RespData string `json:"respData" gorm:"type:mediumtext;comment:响应数据"` // 响应数据 16 | RespTime int64 `json:"respTime"` // 响应时间 17 | UserID uint `json:"userID" gorm:"comment:用户id"` // 用户id 18 | UserName string `json:"userName" gorm:"comment:用户名称"` // 用户名称 19 | } 20 | 21 | func (ol *OperationLogModel) TableName() string { 22 | return "monitor_operationLog" 23 | } 24 | -------------------------------------------------------------------------------- /web/src/common/components/WarningBar/warningBar.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 32 | 33 | 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present pddzl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/configs/mysql.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | type Mysql struct { 4 | Host string `mapstructure:"host" json:"host" yaml:"host"` // 地址 5 | Port string `mapstructure:"port" json:"port" yaml:"port"` // 端口 6 | Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置 7 | Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` // 数据库名 8 | Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库用户名 9 | Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码 10 | MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数 11 | MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数 12 | LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志 13 | LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` // 是否通过zap写入日志文件 14 | } 15 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/api.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "server/internal/global" 5 | ) 6 | 7 | type ApiModel struct { 8 | global.TD27_MODEL 9 | Path string `json:"path" gorm:"not null;comment:api路径" binding:"required"` // api路径 10 | Description string `json:"description" gorm:"not null;comment:api中文描述" binding:"required"` // api中文描述 11 | ApiGroup string `json:"apiGroup" gorm:"not null;comment:api组" binding:"required"` // api组 12 | Method string `json:"method" gorm:"not null;default:POST;comment:方法" binding:"required"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE 13 | } 14 | 15 | func (ApiModel) TableName() string { 16 | return "authority_api" 17 | } 18 | 19 | type Children struct { 20 | Key string `json:"key"` // for 前端el-tree node-key (path + method) 21 | ApiGroup string `json:"apiGroup"` // for 前端el-tree label (path + description) 22 | Path string `json:"path"` 23 | Method string `json:"method"` 24 | Description string `json:"description"` 25 | } 26 | 27 | type ApiTree struct { 28 | ApiGroup string `json:"apiGroup"` 29 | Children []Children `json:"children"` 30 | } 31 | -------------------------------------------------------------------------------- /web/src/api/monitor/operationLog.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@/http/axios_n" 2 | 3 | interface orData { 4 | ip: string 5 | method: string 6 | path: string 7 | status: number 8 | userAgent: string 9 | reqParam: string 10 | respData: string 11 | respTime: number 12 | userName: string 13 | } 14 | 15 | export interface orDataModel extends orData, Td27Model {} 16 | 17 | // 数据结构 - List 18 | type orListData = ListData 19 | 20 | interface reqOrList extends PageInfo { 21 | path?: string 22 | method?: string 23 | status?: number 24 | asc?: boolean 25 | } 26 | 27 | // 分页获取操作记录 28 | export function getOrListApi(data: reqOrList) { 29 | return request>({ 30 | url: "/opl/getOplList", 31 | method: "post", 32 | data 33 | }) 34 | } 35 | 36 | // 删除操作记录 37 | export function deleteOrApi(data: CId) { 38 | return request>({ 39 | url: "/opl/deleteOpl", 40 | method: "post", 41 | data 42 | }) 43 | } 44 | 45 | // 批量删除操作记录 46 | export function deleteOrByIdsApi(data: CIds) { 47 | return request>({ 48 | url: "/opl/deleteOplByIds", 49 | method: "post", 50 | data 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/radar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from "@antfu/eslint-config" 2 | 3 | // 更多自定义配置可查阅仓库:https://github.com/antfu/eslint-config 4 | export default antfu( 5 | { 6 | // 使用外部格式化程序格式化 css、html、markdown 等文件 7 | formatters: true, 8 | // 启用样式规则 9 | stylistic: { 10 | // 缩进级别 11 | indent: 2, 12 | // 引号风格 'single' | 'double' 13 | quotes: "double", 14 | // 是否启用分号 15 | semi: false 16 | }, 17 | // 忽略文件 18 | ignores: [] 19 | }, 20 | { 21 | // 对所有文件都生效的规则 22 | rules: { 23 | // vue 24 | "vue/block-order": ["error", { order: ["script", "template", "style"] }], 25 | "vue/attributes-order": "off", 26 | // ts 27 | "ts/no-use-before-define": "off", 28 | // node 29 | "node/prefer-global/process": "off", 30 | // style 31 | "style/comma-dangle": ["error", "never"], 32 | "style/brace-style": ["error", "1tbs"], 33 | // regexp 34 | "regexp/no-unused-capturing-group": "off", 35 | // other 36 | "no-console": "off", 37 | "no-debugger": "off", 38 | "symbol-description": "off", 39 | "antfu/if-newline": "off", 40 | "unicorn/no-instanceof-builtins": "off" 41 | } 42 | } 43 | ) 44 | -------------------------------------------------------------------------------- /web/src/api/authority/role.ts: -------------------------------------------------------------------------------- 1 | import type { MenuData } from "./menu" 2 | import { request } from "@/http/axios_n" 3 | 4 | interface roleData { 5 | roleName: string 6 | menus?: MenuData[] 7 | } 8 | 9 | export interface roleDataModel extends roleData, Td27Model {} 10 | 11 | /** 获取用户详情 */ 12 | export function listRoleApi() { 13 | return request>({ 14 | url: "/role/list", 15 | method: "post", 16 | data: {} 17 | }) 18 | } 19 | 20 | export function createRoleApi(data: roleData) { 21 | return request>({ 22 | url: "/role/create", 23 | method: "post", 24 | data 25 | }) 26 | } 27 | 28 | export function deleteRoleApi(data: CId) { 29 | return request>({ 30 | url: "/role/delete", 31 | method: "post", 32 | data 33 | }) 34 | } 35 | 36 | export function updateRoleApi(data: roleData & CId) { 37 | return request>({ 38 | url: "/role/update", 39 | method: "post", 40 | data 41 | }) 42 | } 43 | 44 | export function editRoleMenuApi(data: { roleId: number } & CIds) { 45 | return request>({ 46 | url: "/role/editRoleMenu", 47 | method: "post", 48 | data 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /server/cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap" 6 | 7 | "server/internal/core" 8 | "server/internal/global" 9 | "server/internal/initialize" 10 | ) 11 | 12 | func main() { 13 | // Load configuration first (everything depends on it) 14 | global.TD27_VP = core.Viper() 15 | 16 | // Setup logger 17 | global.TD27_LOG = core.Zap() 18 | zap.ReplaceGlobals(global.TD27_LOG) 19 | 20 | // Initialize MySQL 21 | global.TD27_DB = initialize.Gorm() 22 | if global.TD27_DB == nil { 23 | global.TD27_LOG.Fatal("mysql connection failed") 24 | } 25 | db, _ := global.TD27_DB.DB() 26 | defer db.Close() 27 | 28 | // Initialize Redis 29 | global.TD27_REDIS = initialize.Redis() 30 | if global.TD27_REDIS == nil { 31 | global.TD27_LOG.Fatal("redis connection failed") 32 | } 33 | 34 | // Initialize Cron AFTER DB/Redis ready 35 | global.TD27_CRON = initialize.InitCron() 36 | initialize.CheckCron() 37 | 38 | // Auto migrate tables AFTER DB is initialized 39 | initialize.RegisterTables(global.TD27_DB) 40 | 41 | // Build router 42 | router := initialize.Routers() 43 | 44 | // Run HTTP server 45 | addr := fmt.Sprintf("%s:%d", global.TD27_CONFIG.System.Host, global.TD27_CONFIG.System.Port) 46 | initialize.RunServer(addr, router) 47 | } 48 | -------------------------------------------------------------------------------- /server/internal/api/base/casbin.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go.uber.org/zap" 6 | 7 | "server/internal/global" 8 | "server/internal/model/common/response" 9 | authorityReq "server/internal/model/entity/base/request" 10 | "server/internal/service/base" 11 | ) 12 | 13 | type CasbinApi struct { 14 | casbinService *base.CasbinService 15 | } 16 | 17 | func NewCasbinApi() *CasbinApi { 18 | return &CasbinApi{casbinService: base.NewCasbinService()} 19 | } 20 | 21 | // EditCasbin 22 | // @Tags CasbinApi 23 | // @Summary 编辑casbin 24 | // @Security ApiKeyAuth 25 | // @accept application/json 26 | // @Produce application/json 27 | // @Param data body authorityReq.ReqCasbin true "请求参数" 28 | // @Success 200 {object} response.Response{msg=string} 29 | // @Router /casbin/editCasbin [post] 30 | func (ca *CasbinApi) EditCasbin(c *gin.Context) { 31 | var reqCasbin authorityReq.ReqCasbin 32 | if err := c.ShouldBindJSON(&reqCasbin); err != nil { 33 | response.FailReq(err.Error(), c) 34 | return 35 | } 36 | 37 | if err := ca.casbinService.EditCasbin(reqCasbin.RoleId, reqCasbin.CasbinInfos); err != nil { 38 | response.Fail(c) 39 | global.TD27_LOG.Error("更新失败", zap.Error(err)) 40 | } else { 41 | response.Ok(c) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/src/common/utils/router_m.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 后端路由转换为RouteRecordRaw格式 3 | * 路由component转换为vite import 4 | * 递归处理子路由 5 | */ 6 | import type { RouteRecordRaw } from "vue-router" 7 | import type { MenuData } from "@/api/authority/menu" 8 | 9 | const modules = import.meta.glob("@/pages/**/*.vue") 10 | 11 | export function dynamicImport(component: string) { 12 | if (component === "Layout") { 13 | return () => import("@/layouts/index.vue") 14 | } 15 | 16 | const mod = modules[`/src/pages/${component}`] 17 | if (!mod) { 18 | console.warn(`组件 ${component} 不存在!`) 19 | return () => import("@/pages/error/404.vue") // 可以默认返回一个404组件 20 | } 21 | return mod 22 | } 23 | 24 | export function formatRouter(menuList: MenuData[], formatMenu: RouteRecordRaw[]) { 25 | for (const menu of menuList) { 26 | const fMenu: RouteRecordRaw = { 27 | name: menu.name, 28 | path: menu.path, 29 | redirect: menu.redirect, 30 | component: dynamicImport(menu.component), 31 | meta: menu.meta, 32 | children: [] 33 | } 34 | formatMenu.push(fMenu) 35 | // 递归处理子路由 36 | if (Array.isArray(menu.children) && menu.children.length > 0) { 37 | formatRouter(menu.children, fMenu.children) 38 | } else { 39 | // @ts-ignore 40 | fMenu.children = null 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/router/config.ts: -------------------------------------------------------------------------------- 1 | import type { RouterHistory } from "vue-router" 2 | import { createWebHashHistory, createWebHistory } from "vue-router" 3 | 4 | /** 路由配置 */ 5 | interface RouterConfig { 6 | /** 7 | * @name 路由模式 8 | * @description hash 模式和 html5 模式 9 | */ 10 | history: RouterHistory 11 | /** 12 | * @name 是否开启动态路由功能 13 | * @description 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段) 14 | * @description 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 dynamic: false 15 | */ 16 | dynamic: boolean 17 | /** 18 | * @name 默认角色 19 | * @description 当动态路由功能关闭时: 20 | * @description 1. 应该将所有路由都写到常驻路由里面(表明所有登录的用户能访问的页面都是一样的) 21 | * @description 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色 22 | */ 23 | defaultRoles: Array 24 | /** 25 | * @name 是否开启三级及其以上路由缓存功能 26 | * @description 1. 开启后会进行路由降级(把三级及其以上的路由转化为二级路由) 27 | * @description 2. 由于都会转成二级路由,所以二级及其以上路由有内嵌子路由将会失效 28 | */ 29 | thirdLevelRouteCache: boolean 30 | } 31 | 32 | const VITE_ROUTER_HISTORY = import.meta.env.VITE_ROUTER_HISTORY 33 | 34 | const VITE_PUBLIC_PATH = import.meta.env.VITE_PUBLIC_PATH 35 | 36 | export const routerConfig: RouterConfig = { 37 | history: VITE_ROUTER_HISTORY === "hash" ? createWebHashHistory(VITE_PUBLIC_PATH) : createWebHistory(VITE_PUBLIC_PATH), 38 | dynamic: true, 39 | defaultRoles: ["DEFAULT_ROLE"], 40 | thirdLevelRouteCache: false 41 | } 42 | -------------------------------------------------------------------------------- /server/internal/initialize/server.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "go.uber.org/zap" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "server/internal/global" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // RunServer starts the HTTP server and handles graceful shutdown 16 | func RunServer(addr string, handler http.Handler) { 17 | srv := &http.Server{ 18 | Addr: addr, 19 | Handler: handler, 20 | ReadTimeout: 120 * time.Second, 21 | WriteTimeout: 120 * time.Second, 22 | MaxHeaderBytes: 1 << 20, 23 | } 24 | 25 | // Start server in a goroutine 26 | go func() { 27 | //global.TD27_LOG.Info("server listening", zap.String("addr", addr)) 28 | if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 29 | global.TD27_LOG.Error("http server failed ", zap.Any("err", err)) 30 | } 31 | }() 32 | 33 | // Wait for interrupt signal 34 | quit := make(chan os.Signal, 1) 35 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 36 | <-quit 37 | global.TD27_LOG.Info("shutting down server...") 38 | 39 | // Graceful shutdown with timeout 40 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 41 | defer cancel() 42 | if err := srv.Shutdown(ctx); err != nil { 43 | global.TD27_LOG.Error("server forced to shutdown", zap.Any("err", err)) 44 | } 45 | 46 | global.TD27_LOG.Info("server exiting") 47 | } 48 | -------------------------------------------------------------------------------- /server/internal/initialize/router.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | swaggerFiles "github.com/swaggo/files" 6 | ginSwagger "github.com/swaggo/gin-swagger" 7 | 8 | _ "server/docs" 9 | "server/internal/global" 10 | "server/internal/middleware" 11 | "server/internal/middleware/log" 12 | "server/internal/router" 13 | ) 14 | 15 | func Routers() *gin.Engine { 16 | if global.TD27_CONFIG.System.Env == "prod" { 17 | gin.SetMode(gin.ReleaseMode) 18 | } 19 | r := gin.New() 20 | r.Use(log.GinLogger(), log.GinRecovery(global.TD27_CONFIG.System.Stack)) 21 | 22 | // 跨域,如需跨域可以打开下面的注释 23 | // global.GVA_LOG.Info("use middleware cors") 24 | // Router.Use(middleware.Cors()) // 直接放行全部跨域请求 25 | // Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求 26 | 27 | // Swagger 28 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 29 | 30 | // Public group 31 | publicGroup := r.Group(global.TD27_CONFIG.Router.Prefix) 32 | publicGroup.GET("/health", func(c *gin.Context) { c.JSON(200, "ok") }) 33 | 34 | // Private group 35 | privateGroup := r.Group(global.TD27_CONFIG.Router.Prefix) 36 | privateGroup.Use(middleware.JWTAuth(), middleware.CasbinHandler()) 37 | 38 | // Automatically load ALL router modules 39 | for _, m := range router.GetAllModules() { 40 | m.InitPublic(publicGroup) 41 | m.InitPrivate(privateGroup) 42 | } 43 | 44 | global.TD27_LOG.Info("router register success") 45 | 46 | return r 47 | } 48 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://www.typescriptlang.org/tsconfig 3 | * @link https://cn.vuejs.org/guide/typescript/overview#configuring-tsconfig-json 4 | * @link https://cn.vite.dev/guide/features#typescript-compiler-options 5 | */ 6 | 7 | { 8 | "compilerOptions": { 9 | "target": "esnext", 10 | "jsx": "preserve", 11 | "jsxImportSource": "vue", 12 | "lib": ["esnext", "dom"], 13 | "useDefineForClassFields": true, 14 | "experimentalDecorators": true, 15 | // baseUrl 用来告诉编译器到哪里去查找模块,使用非相对模块时必须配置此项 16 | "baseUrl": ".", 17 | "module": "esnext", 18 | "moduleResolution": "bundler", 19 | // 非相对模块导入的路径映射配置,根据 baseUrl 配置进行路径计算,与 vite.config 中 alias 配置同步 20 | "paths": { 21 | "@/*": ["src/*"], 22 | "@@/*": ["src/common/*"] 23 | }, 24 | "resolveJsonModule": true, 25 | "types": ["vite/client", "element-plus/global"], 26 | // 允许导入 .ts .mts .tsx 拓展名的文件 27 | "allowImportingTsExtensions": true, 28 | // 允许 JS 29 | "allowJs": true, 30 | // TS 严格模式 31 | "strict": true, 32 | "importHelpers": true, 33 | // 不输出任何编译后的文件,只进行类型检查 34 | "noEmit": true, 35 | "sourceMap": true, 36 | "allowSyntheticDefaultImports": true, 37 | "esModuleInterop": true, 38 | "isolatedModules": true, 39 | "skipLibCheck": true 40 | }, 41 | // 需要被编译的文件列表 42 | "include": ["**/*.ts", "**/*.tsx", "**/*.vue", "**/*.d.ts"], 43 | // 从编译中排除的文件列表 44 | "exclude": ["node_modules", "dist"] 45 | } 46 | -------------------------------------------------------------------------------- /web/src/layouts/components/AppMain/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 32 | 51 | -------------------------------------------------------------------------------- /web/src/common/composables/useRouteListener.ts: -------------------------------------------------------------------------------- 1 | import type { Handler } from "mitt" 2 | import type { RouteLocationNormalizedGeneric } from "vue-router" 3 | import mitt from "mitt" 4 | 5 | /** 回调函数的类型 */ 6 | type Callback = (route: RouteLocationNormalizedGeneric) => void 7 | 8 | const emitter = mitt() 9 | 10 | const key = Symbol("ROUTE_CHANGE") 11 | 12 | let latestRoute: RouteLocationNormalizedGeneric 13 | 14 | /** 设置最新的路由信息,触发路由变化事件 */ 15 | export function setRouteChange(to: RouteLocationNormalizedGeneric) { 16 | // 触发事件 17 | emitter.emit(key, to) 18 | // 缓存最新的路由信息 19 | latestRoute = to 20 | } 21 | 22 | /** 23 | * @name 订阅路由变化 Composable 24 | * @description 1. 单独用 watch 监听路由会浪费渲染性能 25 | * @description 2. 可优先选择使用该发布订阅模式去进行分发管理 26 | */ 27 | export function useRouteListener() { 28 | // 回调函数集合 29 | const callbackList: Callback[] = [] 30 | 31 | // 监听路由变化(可以选择立即执行) 32 | const listenerRouteChange = (callback: Callback, immediate = false) => { 33 | // 缓存回调函数 34 | callbackList.push(callback) 35 | // 监听事件 36 | emitter.on(key, callback as Handler) 37 | // 可以选择立即执行一次回调函数 38 | immediate && latestRoute && callback(latestRoute) 39 | } 40 | 41 | // 移除路由变化事件监听器 42 | const removeRouteListener = (callback: Callback) => { 43 | emitter.off(key, callback as Handler) 44 | } 45 | 46 | // 组件销毁前移除监听器 47 | onBeforeUnmount(() => { 48 | callbackList.forEach(removeRouteListener) 49 | }) 50 | 51 | return { listenerRouteChange, removeRouteListener } 52 | } 53 | -------------------------------------------------------------------------------- /web/src/common/components/SearchMenu/Footer.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | 34 | 55 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | // 全局 CSS 变量 2 | @import "./variables.css"; 3 | // Transition 4 | @import "./transition.scss"; 5 | // Element Plus 6 | @import "./element-plus.css"; 7 | @import "./element-plus.scss"; 8 | // Vxe Table 9 | // @import "./vxe-table.css"; 10 | // @import "./vxe-table.scss"; 11 | // 注册多主题 12 | @import "./theme/register.scss"; 13 | // Mixins 14 | @import "./mixins.scss"; 15 | // View Transition 16 | @import "./view-transition.scss"; 17 | // td27 18 | @import "./td27.scss"; 19 | 20 | // 业务页面几乎都应该在根元素上挂载 class="app-container",以保持页面美观 21 | .app-container { 22 | padding: 8px; 23 | } 24 | 25 | html { 26 | height: 100%; 27 | // 灰色模式 28 | &.grey-mode { 29 | filter: grayscale(1); 30 | } 31 | // 色弱模式 32 | &.color-weakness { 33 | filter: invert(0.8); 34 | } 35 | } 36 | 37 | body { 38 | height: 100%; 39 | color: var(--v3-body-text-color); 40 | background-color: var(--v3-body-bg-color); 41 | -moz-osx-font-smoothing: grayscale; 42 | -webkit-font-smoothing: antialiased; 43 | font-family: 44 | Inter, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, 45 | sans-serif; 46 | @extend %scrollbar; 47 | } 48 | 49 | #app { 50 | height: 100%; 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | } 58 | 59 | a, 60 | a:focus, 61 | a:hover { 62 | color: inherit; 63 | outline: none; 64 | text-decoration: none; 65 | } 66 | 67 | div:focus { 68 | outline: none; 69 | } 70 | -------------------------------------------------------------------------------- /web/src/layouts/config.ts: -------------------------------------------------------------------------------- 1 | import { LayoutModeEnum } from "@@/constants/app-key" 2 | import { getLayoutsConfig } from "@@/utils/cache/local-storage" 3 | 4 | /** 项目配置类型 */ 5 | export interface LayoutsConfig { 6 | /** 是否显示设置按钮和面板 */ 7 | showSettings: boolean 8 | /** 布局模式 */ 9 | layoutMode: LayoutModeEnum 10 | /** 是否显示标签栏 */ 11 | showTagsView: boolean 12 | /** 是否显示 Logo */ 13 | showLogo: boolean 14 | /** 是否固定 Header */ 15 | fixedHeader: boolean 16 | /** 是否显示页脚 */ 17 | showFooter: boolean 18 | /** 是否显示消息通知 */ 19 | showNotify: boolean 20 | /** 是否显示切换主题按钮 */ 21 | showThemeSwitch: boolean 22 | /** 是否显示全屏按钮 */ 23 | showScreenfull: boolean 24 | /** 是否显示搜索按钮 */ 25 | showSearchMenu: boolean 26 | /** 是否缓存标签栏 */ 27 | cacheTagsView: boolean 28 | /** 开启系统水印 */ 29 | showWatermark: boolean 30 | /** 是否显示灰色模式 */ 31 | showGreyMode: boolean 32 | /** 是否显示色弱模式 */ 33 | showColorWeakness: boolean 34 | } 35 | 36 | /** 默认配置 */ 37 | const DEFAULT_CONFIG: LayoutsConfig = { 38 | layoutMode: LayoutModeEnum.Left, 39 | showSettings: true, 40 | showTagsView: true, 41 | fixedHeader: true, 42 | showFooter: true, 43 | showLogo: true, 44 | showNotify: true, 45 | showThemeSwitch: true, 46 | showScreenfull: true, 47 | showSearchMenu: true, 48 | cacheTagsView: false, 49 | showWatermark: true, 50 | showGreyMode: false, 51 | showColorWeakness: false 52 | } 53 | 54 | /** 项目配置 */ 55 | export const layoutsConfig: LayoutsConfig = { ...DEFAULT_CONFIG, ...getLayoutsConfig() } 56 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/bug.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | redis: 4 | container_name: td27_redis 5 | image: redis:6.2.6 6 | command: redis-server /usr/local/etc/redis/redis.conf 7 | volumes: 8 | - ./redis/redis.conf:/usr/local/etc/redis/redis.conf 9 | - ./redis/data:/data 10 | mysql: 11 | container_name: td27_mysql 12 | image: mysql:8.0.28 13 | command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 14 | volumes: 15 | - ./mysql/data/:/var/lib/mysql 16 | - ./mysql/init/:/docker-entrypoint-initdb.d 17 | environment: 18 | MYSQL_ROOT_PASSWORD: td27admin 19 | MYSQL_DATABASE: td27 20 | server: 21 | sysctls: 22 | net.ipv4.ping_group_range: 0 2147483647 23 | container_name: td27_server 24 | build: 25 | context: ../server/ 26 | dockerfile: ./Dockerfile 27 | image: td27_server:2.0 28 | depends_on: 29 | - redis 30 | - mysql 31 | links: 32 | - redis 33 | - mysql 34 | restart: on-failure 35 | web: 36 | container_name: td27_web 37 | build: 38 | context: ../web/ 39 | dockerfile: ./Dockerfile 40 | image: td27_web:2.0 41 | environment: 42 | - BACKEND=server 43 | command: /bin/sh -c "envsubst '$$BACKEND' < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf && sleep 2 && exec nginx -g 'daemon off;'" 44 | ports: 45 | - 8500:8500 46 | depends_on: 47 | - server 48 | restart: on-failure 49 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/dashboard.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/internal/router/enter.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "server/internal/router/authority" 7 | "server/internal/router/base" 8 | "server/internal/router/fileM" 9 | "server/internal/router/monitor" 10 | "server/internal/router/sysSet" 11 | "server/internal/router/sysTool" 12 | ) 13 | 14 | func NewBaseRouterGroup() *base.RouterGroup { 15 | return base.NewRouterGroup() 16 | } 17 | 18 | func NewMonitorRouterGroup() *monitor.RouterGroup { 19 | return monitor.NewRouterGroup() 20 | } 21 | 22 | func NewSysSetRouterGroup() *sysSet.RouterGroup { 23 | return sysSet.NewRouterGroup() 24 | } 25 | 26 | func NewSysToolRouterGroup() *sysTool.RouterGroup { 27 | return sysTool.NewRouterGroup() 28 | } 29 | 30 | func NewAuthorityRouterGroup() *authority.RouterGroup { 31 | return authority.NewRouterGroup() 32 | } 33 | 34 | func NewFileMRouterGroup() *fileM.RouterGroup { 35 | return fileM.NewRouterGroup() 36 | } 37 | 38 | type ModuleRouter interface { 39 | InitPublic(group *gin.RouterGroup) 40 | InitPrivate(group *gin.RouterGroup) 41 | } 42 | 43 | var modules []ModuleRouter 44 | 45 | func Register(m ModuleRouter) { 46 | modules = append(modules, m) 47 | } 48 | 49 | func GetAllModules() []ModuleRouter { 50 | return modules 51 | } 52 | 53 | func init() { 54 | Register(NewAuthorityRouterGroup()) 55 | Register(NewBaseRouterGroup()) 56 | Register(NewFileMRouterGroup()) 57 | Register(NewMonitorRouterGroup()) 58 | Register(NewSysSetRouterGroup()) 59 | Register(NewSysToolRouterGroup()) 60 | } 61 | -------------------------------------------------------------------------------- /web/src/plugins/vxe-table.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import VXETable from "vxe-table" // https://vxetable.cn/#/start/install 3 | 4 | // 全局默认参数 5 | VXETable.setConfig({ 6 | // 全局尺寸 7 | size: "medium", 8 | // 全局 zIndex 起始值,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡 9 | zIndex: 9999, 10 | // 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据 11 | version: 0, 12 | // 全局 loading 提示内容,如果为 null 则不显示文本 13 | loadingText: null, 14 | table: { 15 | showHeader: true, 16 | showOverflow: "tooltip", 17 | showHeaderOverflow: "tooltip", 18 | autoResize: true, 19 | // stripe: false, 20 | border: "inner", 21 | // round: false, 22 | emptyText: "暂无数据", 23 | rowConfig: { 24 | isHover: true, 25 | isCurrent: true, 26 | // 行数据的唯一主键字段名 27 | keyField: "_VXE_ID" 28 | }, 29 | columnConfig: { 30 | resizable: false 31 | }, 32 | align: "center", 33 | headerAlign: "center" 34 | }, 35 | pager: { 36 | // size: "medium", 37 | // 配套的样式 38 | perfect: false, 39 | pageSize: 10, 40 | pagerCount: 7, 41 | pageSizes: [10, 20, 50], 42 | layouts: ["Total", "PrevJump", "PrevPage", "Number", "NextPage", "NextJump", "Sizes", "FullJump"] 43 | }, 44 | modal: { 45 | minWidth: 500, 46 | minHeight: 400, 47 | lockView: true, 48 | mask: true, 49 | // duration: 3000, 50 | // marginSize: 20, 51 | dblclickZoom: false, 52 | showTitleOverflow: true, 53 | transfer: true, 54 | draggable: false 55 | } 56 | }) 57 | 58 | export function installVxeTable(app: App) { 59 | // Vxe Table 组件完整引入 60 | app.use(VXETable) 61 | } 62 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/menu.go: -------------------------------------------------------------------------------- 1 | package authority 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | 7 | "server/internal/global" 8 | ) 9 | 10 | type MenuModel struct { 11 | global.TD27_MODEL 12 | Pid uint `json:"pid"` // 父菜单ID 13 | Name string `json:"name,omitempty"` // 路由名称 14 | Path string `json:"path" gorm:"unique"` // 路由路径 15 | Redirect string `json:"redirect,omitempty"` // 重定向 16 | Component string `json:"component" gorm:"not null"` // 前端组件 17 | Sort uint `json:"sort" gorm:"not null"` // 排序 18 | Meta Meta `json:"meta" gorm:"type:json"` // 元数据 19 | Children []MenuModel `json:"children,omitempty" gorm:"-"` 20 | Roles []*RoleModel `json:"-" gorm:"many2many:role_menus;"` 21 | } 22 | 23 | type Meta struct { 24 | Hidden bool `json:"hidden,omitempty"` // 菜单是否隐藏 25 | Title string `json:"title,omitempty"` // 菜单名 26 | SvgIcon string `json:"svgIcon,omitempty"` // svg图标 27 | ElIcon string `json:"elIcon,omitempty"` // element-plus图标 28 | Affix bool `json:"affix,omitempty"` // 是否固定 29 | KeepAlive bool `json:"keepAlive,omitempty"` 30 | AlwaysShow bool `json:"alwaysShow,omitempty"` // 是否一直显示根路由 31 | } 32 | 33 | func (m Meta) Value() (driver.Value, error) { 34 | b, err := json.Marshal(m) 35 | return string(b), err 36 | } 37 | 38 | func (m *Meta) Scan(input interface{}) error { 39 | return json.Unmarshal(input.([]byte), m) 40 | } 41 | 42 | func (MenuModel) TableName() string { 43 | return "authority_menu" 44 | } 45 | -------------------------------------------------------------------------------- /web/src/pinia/stores/settings.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from "vue" 2 | import type { LayoutsConfig } from "@/layouts/config" 3 | import { setLayoutsConfig } from "@@/utils/cache/local-storage" 4 | import { layoutsConfig } from "@/layouts/config" 5 | import { pinia } from "@/pinia" 6 | 7 | type SettingsStore = { 8 | // 使用映射类型来遍历 LayoutsConfig 对象的键 9 | [Key in keyof LayoutsConfig]: Ref 10 | } 11 | 12 | type SettingsStoreKey = keyof SettingsStore 13 | 14 | export const useSettingsStore = defineStore("settings", () => { 15 | // 状态对象 16 | const state = {} as SettingsStore 17 | 18 | // 遍历 LayoutsConfig 对象的键值对 19 | for (const [key, value] of Object.entries(layoutsConfig)) { 20 | // 使用类型断言来指定 key 的类型,将 value 包装在 ref 函数中,创建一个响应式变量 21 | const refValue = ref(value) 22 | // @ts-expect-error ignore 23 | state[key as SettingsStoreKey] = refValue 24 | // 监听每个响应式变量 25 | watch(refValue, () => { 26 | // 缓存 27 | const settings = getCacheData() 28 | setLayoutsConfig(settings) 29 | }) 30 | } 31 | 32 | // 获取要缓存的数据:将 state 对象转化为 settings 对象 33 | const getCacheData = () => { 34 | const settings = {} as LayoutsConfig 35 | for (const [key, value] of Object.entries(state)) { 36 | // @ts-expect-error ignore 37 | settings[key as SettingsStoreKey] = value.value 38 | } 39 | return settings 40 | } 41 | 42 | return state 43 | }) 44 | 45 | /** 46 | * @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store 47 | * @description 在 SSR 应用中可用于在 setup 外使用 store 48 | */ 49 | export function useSettingsStoreOutside() { 50 | return useSettingsStore(pinia) 51 | } 52 | -------------------------------------------------------------------------------- /web/src/api/sysSet/dictDetail.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@/http/axios_n" 2 | 3 | interface dictDetailData { 4 | label: string 5 | value: string 6 | sort: number 7 | dictId: number 8 | parentId?: number 9 | children?: dictDetailDataModel[] 10 | description: string 11 | } 12 | 13 | export interface dictDetailDataModel extends dictDetailData, Td27Model {} 14 | 15 | // List 16 | export type dictDetailListData = ListData 17 | 18 | interface reqDictDetail extends PageInfo { 19 | dictId: number 20 | } 21 | 22 | export function getDictDetailApi(data: reqDictDetail) { 23 | return request>({ 24 | url: "/dictDetail/getDictDetail", 25 | method: "post", 26 | data 27 | }) 28 | } 29 | 30 | export function getDictDetailFlatApi(data: { dictId: number }) { 31 | return request>({ 32 | url: "/dictDetail/getDictDetailFlat", 33 | method: "post", 34 | data 35 | }) 36 | } 37 | 38 | export function addDictDetailApi(data: dictDetailData) { 39 | return request>({ 40 | url: "/dictDetail/addDictDetail", 41 | method: "post", 42 | data 43 | }) 44 | } 45 | 46 | export function delDictDetailApi(data: CId) { 47 | return request>({ 48 | url: "/dictDetail/delDictDetail", 49 | method: "post", 50 | data 51 | }) 52 | } 53 | 54 | export function editDictDetailApi(data: dictDetailData & CId) { 55 | return request>({ 56 | url: "/dictDetail/editDictDetail", 57 | method: "post", 58 | data 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /web/src/layouts/composables/useResize.ts: -------------------------------------------------------------------------------- 1 | import { useRouteListener } from "@@/composables/useRouteListener" 2 | import { DeviceEnum } from "@@/constants/app-key" 3 | import { useAppStore } from "@/pinia/stores/app" 4 | 5 | /** 参考 Bootstrap 的响应式设计将最大移动端宽度设置为 992 */ 6 | const MAX_MOBILE_WIDTH = 992 7 | 8 | /** 9 | * @name 浏览器宽度变化 Composable 10 | * @description 根据浏览器宽度变化,变换 Layout 布局 11 | */ 12 | export function useResize() { 13 | const appStore = useAppStore() 14 | 15 | const { listenerRouteChange } = useRouteListener() 16 | 17 | // 用于判断当前设备是否为移动端 18 | const isMobile = () => { 19 | const rect = document.body.getBoundingClientRect() 20 | return rect.width - 1 < MAX_MOBILE_WIDTH 21 | } 22 | 23 | // 用于处理窗口大小变化事件 24 | const resizeHandler = () => { 25 | if (!document.hidden) { 26 | const _isMobile = isMobile() 27 | appStore.toggleDevice(_isMobile ? DeviceEnum.Mobile : DeviceEnum.Desktop) 28 | _isMobile && appStore.closeSidebar(true) 29 | } 30 | } 31 | 32 | // 监听路由变化,根据设备类型调整布局 33 | listenerRouteChange(() => { 34 | if (appStore.device === DeviceEnum.Mobile && appStore.sidebar.opened) { 35 | appStore.closeSidebar(false) 36 | } 37 | }) 38 | 39 | // 在组件挂载前添加窗口大小变化事件监听器 40 | onBeforeMount(() => { 41 | window.addEventListener("resize", resizeHandler) 42 | }) 43 | 44 | // 在组件挂载后根据窗口大小判断设备类型并调整布局 45 | onMounted(() => { 46 | if (isMobile()) { 47 | appStore.toggleDevice(DeviceEnum.Mobile) 48 | appStore.closeSidebar(true) 49 | } 50 | }) 51 | 52 | // 在组件卸载前移除窗口大小变化事件监听器 53 | onBeforeUnmount(() => { 54 | window.removeEventListener("resize", resizeHandler) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /server/internal/initialize/cron.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "github.com/robfig/cron/v3" 5 | "go.uber.org/zap" 6 | 7 | "server/configs" 8 | "server/internal/global" 9 | modelSysTool "server/internal/model/entity/sysTool" 10 | "server/internal/pkg" 11 | ) 12 | 13 | // Crontab 添加计划任务 14 | func crontab() { 15 | if global.TD27_CONFIG.Crontab.Open { 16 | ct := cron.New() 17 | for index := range global.TD27_CONFIG.Crontab.Objects { 18 | go func(cObject configs.Object) { 19 | _, err := ct.AddFunc(global.TD27_CONFIG.Crontab.Spec, func() { 20 | err := pkg.ClearTable(global.TD27_DB, cObject.TableName, cObject.CompareField, cObject.Interval) 21 | if err != nil { 22 | global.TD27_LOG.Error("clear table", zap.Error(err)) 23 | } 24 | }) 25 | if err != nil { 26 | global.TD27_LOG.Error("cron add func", zap.Error(err)) 27 | } 28 | }(global.TD27_CONFIG.Crontab.Objects[index]) 29 | } 30 | // 启动cron 31 | ct.Start() 32 | } 33 | } 34 | 35 | // InitCron 初始化Cron 36 | func InitCron() *cron.Cron { 37 | // 配置文件方式cron 38 | crontab() 39 | // 页面方式配置 40 | instance := cron.New(cron.WithSeconds()) // 支持秒 41 | instance.Start() // 启动cron 42 | return instance 43 | } 44 | 45 | func CheckCron() { 46 | var cronModelList []modelSysTool.CronModel 47 | global.TD27_DB.Where("open = ?", 1).Find(&cronModelList) 48 | for _, cronModel := range cronModelList { 49 | entryId, err := global.TD27_CRON.AddJob(cronModel.Expression, &cronModel) 50 | if err != nil { 51 | global.TD27_LOG.Error("CRON", zap.Error(err)) 52 | } else { 53 | global.TD27_DB.Model(cronModel).Update("entryId", entryId) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /web/src/common/components/Notify/List.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | 61 | -------------------------------------------------------------------------------- /web/src/pinia/stores/app.ts: -------------------------------------------------------------------------------- 1 | import { DeviceEnum, SIDEBAR_CLOSED, SIDEBAR_OPENED } from "@@/constants/app-key" 2 | import { getSidebarStatus, setSidebarStatus } from "@@/utils/cache/local-storage" 3 | import { pinia } from "@/pinia" 4 | 5 | interface Sidebar { 6 | opened: boolean 7 | withoutAnimation: boolean 8 | } 9 | 10 | /** 设置侧边栏状态本地缓存 */ 11 | function handleSidebarStatus(opened: boolean) { 12 | opened ? setSidebarStatus(SIDEBAR_OPENED) : setSidebarStatus(SIDEBAR_CLOSED) 13 | } 14 | 15 | export const useAppStore = defineStore("app", () => { 16 | // 侧边栏状态 17 | const sidebar: Sidebar = reactive({ 18 | opened: getSidebarStatus() !== SIDEBAR_CLOSED, 19 | withoutAnimation: false 20 | }) 21 | 22 | // 设备类型 23 | const device = ref(DeviceEnum.Desktop) 24 | 25 | // 监听侧边栏 opened 状态 26 | watch( 27 | () => sidebar.opened, 28 | (opened) => { 29 | handleSidebarStatus(opened) 30 | } 31 | ) 32 | 33 | // 切换侧边栏 34 | const toggleSidebar = (withoutAnimation: boolean) => { 35 | sidebar.opened = !sidebar.opened 36 | sidebar.withoutAnimation = withoutAnimation 37 | } 38 | 39 | // 关闭侧边栏 40 | const closeSidebar = (withoutAnimation: boolean) => { 41 | sidebar.opened = false 42 | sidebar.withoutAnimation = withoutAnimation 43 | } 44 | 45 | // 切换设备类型 46 | const toggleDevice = (value: DeviceEnum) => { 47 | device.value = value 48 | } 49 | 50 | return { device, sidebar, toggleSidebar, closeSidebar, toggleDevice } 51 | }) 52 | 53 | /** 54 | * @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store 55 | * @description 在 SSR 应用中可用于在 setup 外使用 store 56 | */ 57 | export function useAppStoreOutside() { 58 | return useAppStore(pinia) 59 | } 60 | -------------------------------------------------------------------------------- /web/src/pinia/stores/permission_n.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from "vue-router" 2 | import type { MenuData } from "@/api/authority/menu" 3 | import { listMenuApi } from "@/api/authority/menu" 4 | import { formatRouter } from "@/common/utils/router_m" 5 | import { pinia } from "@/pinia" 6 | import { constantRoutes } from "@/router/index_n" 7 | 8 | export const usePermissionStore = defineStore("permission", () => { 9 | const routes = ref([]) 10 | const dynamicRoutes = ref([]) 11 | const asyncRouterList = ref([]) 12 | 13 | const setRoutes = async () => { 14 | // 获取动态路由 15 | const asyncRouterRes = await listMenuApi() 16 | if (asyncRouterRes.code === 0) { 17 | asyncRouterList.value = asyncRouterRes.data 18 | } 19 | 20 | // 格式化后端路由 21 | formatRouter(asyncRouterList.value, dynamicRoutes.value) 22 | 23 | // 404路由放最后 24 | // 添加404 ErrorPage 25 | dynamicRoutes.value.push({ 26 | path: "/:pathMatch(.*)*", 27 | redirect: "/404", 28 | name: "ErrorPage", 29 | meta: { 30 | hidden: true 31 | } 32 | }) 33 | 34 | // 合并静态路由,动态路由 35 | routes.value = constantRoutes.concat(dynamicRoutes.value) 36 | } 37 | 38 | const resetDynamicRouter = () => { 39 | routes.value = [] 40 | dynamicRoutes.value = [] 41 | asyncRouterList.value = [] 42 | } 43 | 44 | return { routes, dynamicRoutes, asyncRouterList, setRoutes, resetDynamicRouter } 45 | }) 46 | 47 | /** 48 | * @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store 49 | * @description 在 SSR 应用中可用于在 setup 外使用 store 50 | */ 51 | export function usePermissionStoreOutside() { 52 | return usePermissionStore(pinia) 53 | } 54 | -------------------------------------------------------------------------------- /web/types/auto/svg-component-global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // biome-ignore format: off 4 | // biome-ignore lint: off 5 | // @ts-nocheck 6 | // Generated by unplugin-svg-component 7 | import 'vue' 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | SvgIcon: import("vue").DefineComponent<{ 11 | name: { 12 | type: import("vue").PropType<"access" | "bug" | "config" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "link" | "load" | "lock" | "menu" | "monitor" | "network" | "plus" | "radar" | "search" | "setting">; 13 | default: string; 14 | required: true; 15 | }; 16 | }, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly; 19 | default: string; 20 | required: true; 21 | }; 22 | }>>, { 23 | name: "access" | "bug" | "config" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "link" | "load" | "lock" | "menu" | "monitor" | "network" | "plus" | "radar" | "search" | "setting"; 24 | }>; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/internal/model/entity/authority/request/user.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/go-playground/validator/v10" 7 | ) 8 | 9 | type AddUser struct { 10 | Username string `json:"username" binding:"required"` // 用户名 11 | Password string `json:"password" binding:"required"` // 密码 12 | Phone string `json:"phone" binding:"omitempty,phone"` // 手机号 13 | Email string `json:"email" binding:"omitempty,email"` // 邮箱 14 | Active bool `json:"active"` // 是否活跃 15 | RoleModelID uint `json:"roleId" binding:"required"` // 角色ID 16 | } 17 | 18 | // PhoneValidation 自定义手机号码校验函数 19 | func PhoneValidation(fl validator.FieldLevel) bool { 20 | phone := fl.Field().String() 21 | re := regexp.MustCompile("^1[0-9]{10}") 22 | return re.MatchString(phone) 23 | } 24 | 25 | type EditUser struct { 26 | ID uint `json:"id" binding:"required"` // 用户ID 27 | Username string `json:"username" binding:"required"` // 用户名 28 | Phone string `json:"phone" binding:"omitempty,phone"` // 手机号 29 | Email string `json:"email" binding:"omitempty,email"` // 邮箱 30 | Active bool `json:"active"` // 是否活跃 31 | RoleModelID uint `json:"roleId" binding:"required"` // 角色ID 32 | } 33 | 34 | type ModifyPass struct { 35 | ID uint `json:"id" binding:"required"` // 用户ID 36 | OldPassword string `json:"oldPassword" binding:"required"` // 旧密码 37 | NewPassword string `json:"newPassword" binding:"required"` // 新密码 38 | } 39 | 40 | type SwitchActive struct { 41 | ID uint `json:"id" binding:"required"` // 用户ID 42 | Active bool `json:"active"` // 是否启用 43 | } 44 | -------------------------------------------------------------------------------- /server/internal/middleware/log/error.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "net/http/httputil" 7 | "os" 8 | "runtime/debug" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.uber.org/zap" 12 | "strings" 13 | 14 | "server/internal/global" 15 | ) 16 | 17 | // GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志 18 | func GinRecovery(stack bool) gin.HandlerFunc { 19 | return func(c *gin.Context) { 20 | defer func() { 21 | if err := recover(); err != nil { 22 | // Check for a broken connection, as it is not really a 23 | // condition that warrants a panic stack trace. 24 | var brokenPipe bool 25 | if ne, ok := err.(*net.OpError); ok { 26 | if se, ok := ne.Err.(*os.SyscallError); ok { 27 | if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { 28 | brokenPipe = true 29 | } 30 | } 31 | } 32 | 33 | httpRequest, _ := httputil.DumpRequest(c.Request, false) 34 | if brokenPipe { 35 | global.TD27_LOG.Debug(c.Request.URL.Path, zap.Any("error", err), zap.String("request", string(httpRequest))) 36 | // If the connection is dead, we can't write a status to it. 37 | _ = c.Error(err.(error)) 38 | c.Abort() 39 | return 40 | } 41 | 42 | if stack { 43 | global.TD27_LOG.Debug("[Recovery from panic]", zap.Any("error", err), zap.String("request", string(httpRequest)), zap.String("stack", string(debug.Stack()))) 44 | } else { 45 | global.TD27_LOG.Debug("[Recovery from panic]", zap.Any("error", err), zap.String("request", string(httpRequest))) 46 | } 47 | c.AbortWithStatus(http.StatusInternalServerError) 48 | } 49 | }() 50 | c.Next() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /web/types/vue-router.d.ts: -------------------------------------------------------------------------------- 1 | import type * as ElementPlusIconsVue from "@element-plus/icons-vue" 2 | import type { SvgName } from "~virtual/svg-component" 3 | import "vue-router" 4 | 5 | export {} 6 | 7 | type ElementPlusIconsName = keyof typeof ElementPlusIconsVue 8 | 9 | declare module "vue-router" { 10 | interface RouteMeta { 11 | /** 12 | * @description 设置该路由在侧边栏和面包屑中展示的名字 13 | */ 14 | title?: string 15 | /** 16 | * @description 设置该路由的图标,记得将 svg 导入 src/common/assets/icons 17 | */ 18 | svgIcon?: SvgName 19 | /** 20 | * @description 设置该路由的图标,直接使用 Element Plus 的 Icon(与 svgIcon 同时设置时,svgIcon 将优先生效) 21 | */ 22 | elIcon?: ElementPlusIconsName 23 | /** 24 | * @description 默认 false,设置 true 的时候该路由不会在侧边栏出现 25 | */ 26 | hidden?: boolean 27 | /** 28 | * @description 设置能进入该路由的角色,支持多个角色叠加 29 | */ 30 | roles?: string[] 31 | /** 32 | * @description 默认 true,如果设置为 false,则不会在面包屑中显示 33 | */ 34 | breadcrumb?: boolean 35 | /** 36 | * @description 默认 false,如果设置为 true,它则会固定在 tags-view 中 37 | */ 38 | affix?: boolean 39 | /** 40 | * @description 当一个路由的 children 属性中声明的非隐藏子路由只有 1 个且该子路由为叶子节点时,会将这个子路由当做父路由显示在侧边栏 41 | * @description 当大于 1 个时,会恢复成嵌套模式 42 | * @description 如果想不管个数总是显示父路由,可以在父路由上设置 alwaysShow: true 43 | */ 44 | alwaysShow?: boolean 45 | /** 46 | * @description 示例: activeMenu: "/xxx/xxx", 47 | * @description 当设置了该属性进入路由时,则会高亮 activeMenu 属性对应的侧边栏 48 | * @description 该属性适合使用在有 hidden: true 属性的路由上 49 | */ 50 | activeMenu?: string 51 | /** 52 | * @description 是否缓存该路由页面 53 | * @description 默认为 false,为 true 时代表需要缓存,此时该路由和该页面都需要设置一致的 Name 54 | */ 55 | keepAlive?: boolean 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /web/src/api/authority/menu.ts: -------------------------------------------------------------------------------- 1 | import type * as ElementPlusIconsVue from "@element-plus/icons-vue" 2 | import type { SvgName } from "~virtual/svg-component" 3 | import { request } from "@/http/axios_n" 4 | 5 | type ElementPlusIconsName = keyof typeof ElementPlusIconsVue 6 | 7 | export interface MenuData { 8 | pid: number 9 | name: string 10 | path: string 11 | redirect?: string 12 | component: string 13 | sort: number 14 | meta: { 15 | hidden?: boolean 16 | title?: string 17 | elIcon?: ElementPlusIconsName 18 | svgIcon?: SvgName 19 | affix?: boolean 20 | keepAlive?: boolean 21 | alwaysShow?: boolean 22 | } 23 | children?: MenuData[] 24 | } 25 | 26 | export interface MenuDataModel extends MenuData, Td27Model {} 27 | 28 | // 获取动态路由 29 | export function listMenuApi() { 30 | return request>({ 31 | url: "/menu/list", 32 | method: "get" 33 | }) 34 | } 35 | 36 | export function createMenuApi(data: MenuData) { 37 | return request>({ 38 | url: "menu/create", 39 | method: "post", 40 | data 41 | }) 42 | } 43 | 44 | export function updateMenuApi(data: MenuData & CId) { 45 | return request>({ 46 | url: "menu/update", 47 | method: "post", 48 | data 49 | }) 50 | } 51 | 52 | export function deleteMenuApi(data: CId) { 53 | return request>({ 54 | url: "menu/delete", 55 | method: "post", 56 | data 57 | }) 58 | } 59 | 60 | interface allMenus { 61 | list: MenuData[] 62 | menuIds: number[] 63 | } 64 | 65 | export function getElTreeMenusApi(data: CId) { 66 | return request>({ 67 | url: "menu/getElTreeMenus", 68 | method: "post", 69 | data 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use workspace TypeScript version 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | 5 | // Show inlay hints for destructured props 6 | "vue.inlayHints.destructuredProps": true, 7 | 8 | // Disable the default formatter, use eslint instead 9 | "prettier.enable": false, 10 | "editor.formatOnSave": false, 11 | 12 | // Auto fix 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.eslint": "explicit", 15 | "source.organizeImports": "never" 16 | }, 17 | 18 | // Silent the stylistic rules in you IDE, but still auto fix them 19 | "eslint.rules.customizations": [ 20 | { "rule": "style/*", "severity": "off", "fixable": true }, 21 | { "rule": "format/*", "severity": "off", "fixable": true }, 22 | { "rule": "*-indent", "severity": "off", "fixable": true }, 23 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 24 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 25 | { "rule": "*-order", "severity": "off", "fixable": true }, 26 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 27 | { "rule": "*-newline", "severity": "off", "fixable": true }, 28 | { "rule": "*quotes", "severity": "off", "fixable": true }, 29 | { "rule": "*semi", "severity": "off", "fixable": true } 30 | ], 31 | 32 | // Enable eslint for all supported languages 33 | "eslint.validate": [ 34 | "javascript", 35 | "javascriptreact", 36 | "typescript", 37 | "typescriptreact", 38 | "vue", 39 | "html", 40 | "markdown", 41 | "json", 42 | "jsonc", 43 | "yaml", 44 | "toml", 45 | "xml", 46 | "gql", 47 | "graphql", 48 | "astro", 49 | "svelte", 50 | "css", 51 | "less", 52 | "scss", 53 | "pcss", 54 | "postcss" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /web/src/layouts/components/Logo/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | 29 | 63 | -------------------------------------------------------------------------------- /web/src/layouts/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 56 | -------------------------------------------------------------------------------- /server/internal/service/sysSet/dict.go: -------------------------------------------------------------------------------- 1 | package sysSet 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | 8 | "server/internal/global" 9 | modelSysSet "server/internal/model/entity/sysSet" 10 | ) 11 | 12 | type DictService struct{} 13 | 14 | func NewDictService() *DictService { 15 | return &DictService{} 16 | } 17 | 18 | func (ds *DictService) GetDict() ([]modelSysSet.DictModel, error) { 19 | var dictList []modelSysSet.DictModel 20 | err := global.TD27_DB.Find(&dictList).Error 21 | 22 | return dictList, err 23 | } 24 | 25 | func (ds *DictService) AddDict(instance *modelSysSet.DictModel) (*modelSysSet.DictModel, error) { 26 | err := global.TD27_DB.Create(instance).Error 27 | return instance, err 28 | 29 | } 30 | 31 | func (ds *DictService) DelDict(id uint) (err error) { 32 | var dictModel modelSysSet.DictModel 33 | 34 | // load dict and details in one query 35 | if err = global.TD27_DB.Preload("DictDetails").First(&dictModel, id).Error; err != nil { 36 | if errors.Is(err, gorm.ErrRecordNotFound) { 37 | return errors.New("dict not found") 38 | } 39 | return err 40 | } 41 | 42 | // prevent deletion if details exist 43 | if len(dictModel.DictDetails) > 0 { 44 | return errors.New("cannot delete: dict has DictDetails") 45 | } 46 | 47 | // delete permanently 48 | if err = global.TD27_DB.Unscoped().Delete(&dictModel).Error; err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (ds *DictService) EditDict(instance *modelSysSet.DictModel) (*modelSysSet.DictModel, error) { 56 | var dictModel modelSysSet.DictModel 57 | if errors.Is(global.TD27_DB.Where("id = ?", instance.ID).First(&dictModel).Error, gorm.ErrRecordNotFound) { 58 | return nil, errors.New("not exist Dict") 59 | } 60 | 61 | err := global.TD27_DB.Model(&dictModel).Update("ch_name", instance.CHName).Error 62 | return instance, err 63 | } 64 | -------------------------------------------------------------------------------- /server/internal/model/common/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type Response struct { 10 | Code int `json:"code"` 11 | Data interface{} `json:"data"` 12 | Msg string `json:"msg"` 13 | } 14 | 15 | const ( 16 | ERROR_RES = 7 // 响应错误 17 | ERROR_REQ = 4 // 请求错误 18 | SUCCESS = 0 19 | ) 20 | 21 | func Result(code int, data interface{}, msg string, c *gin.Context) { 22 | // 开始时间 23 | c.JSON(http.StatusOK, Response{ 24 | code, 25 | data, 26 | msg, 27 | }) 28 | } 29 | 30 | func ResultStatus(status int, code int, data interface{}, msg string, c *gin.Context) { 31 | // 开始时间 32 | c.JSON(status, Response{ 33 | code, 34 | data, 35 | msg, 36 | }) 37 | } 38 | 39 | func Ok(c *gin.Context) { 40 | Result(SUCCESS, map[string]interface{}{}, "操作成功", c) 41 | } 42 | 43 | func OkWithMessage(message string, c *gin.Context) { 44 | Result(SUCCESS, map[string]interface{}{}, message, c) 45 | } 46 | 47 | func OkWithData(data interface{}, c *gin.Context) { 48 | Result(SUCCESS, data, "查询成功", c) 49 | } 50 | 51 | func OkWithDetailed(data interface{}, message string, c *gin.Context) { 52 | Result(SUCCESS, data, message, c) 53 | } 54 | 55 | func Fail(c *gin.Context) { 56 | Result(ERROR_RES, map[string]interface{}{}, "操作失败", c) 57 | } 58 | 59 | func FailReq(message string, c *gin.Context) { 60 | Result(ERROR_REQ, map[string]interface{}{}, message, c) 61 | } 62 | 63 | func FailWithMessage(message string, c *gin.Context) { 64 | Result(ERROR_RES, map[string]interface{}{}, message, c) 65 | } 66 | 67 | func FailWithDetailed(data interface{}, message string, c *gin.Context) { 68 | Result(ERROR_RES, data, message, c) 69 | } 70 | 71 | func FailWithStatusMessage(status int, message string, c *gin.Context) { 72 | ResultStatus(status, ERROR_RES, map[string]interface{}{}, message, c) 73 | } 74 | -------------------------------------------------------------------------------- /web/src/layouts/modes/TopMode.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 76 | -------------------------------------------------------------------------------- /web/src/layouts/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 52 | 53 | 64 | -------------------------------------------------------------------------------- /web/src/pinia/stores/dictionary.ts: -------------------------------------------------------------------------------- 1 | import type { dictDataModel } from "@/api/sysSet/dict" 2 | import type { dictDetailDataModel } from "@/api/sysSet/dictDetail" 3 | import { getDictApi } from "@/api/sysSet/dict" 4 | import { getDictDetailFlatApi } from "@/api/sysSet/dictDetail" 5 | 6 | export const useDictionaryStore = defineStore("dictionary", () => { 7 | const dictionaries = ref([]) 8 | // cache details by dictId 9 | const detailsMap = ref>({}) 10 | 11 | const fetchDictionaries = async () => { 12 | if (dictionaries.value.length > 0) return 13 | try { 14 | const res = await getDictApi() 15 | if (res.code === 0) { 16 | dictionaries.value = res.data 17 | } 18 | } finally { 19 | // 20 | } 21 | } 22 | 23 | const fetchDictionaryDetail = async (dictId: number) => { 24 | if (detailsMap.value[dictId]) return // ✅ cached 25 | try { 26 | const res = await getDictDetailFlatApi({ dictId }) 27 | if (res.code === 0) { 28 | detailsMap.value[dictId] = res.data.list 29 | } 30 | } finally { 31 | // 32 | } 33 | } 34 | 35 | // ✅ Helper: get options by enName 36 | const getOptions = async (enName: string) => { 37 | // find dictId 38 | if (dictionaries.value.length === 0) { 39 | await fetchDictionaries() 40 | } 41 | const dict = dictionaries.value.find(d => d.enName === enName) 42 | if (!dict) return [] 43 | 44 | // fetch details if needed 45 | await fetchDictionaryDetail(dict.id) 46 | 47 | return (detailsMap.value[dict.id] || []).map((item: dictDetailDataModel) => ({ 48 | label: item.label, 49 | value: item.value 50 | })) 51 | } 52 | 53 | return { 54 | dictionaries, 55 | detailsMap, 56 | fetchDictionaries, 57 | fetchDictionaryDetail, 58 | getOptions // ✅ expose helper 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /web/src/router/guard_n.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from "vue-router" 2 | import { setRouteChange } from "@@/composables/useRouteListener" 3 | import { useTitle } from "@@/composables/useTitle" 4 | import NProgress from "nprogress" 5 | import { usePermissionStore } from "@/pinia/stores/permission_n" 6 | import { useUserStore } from "@/pinia/stores/user_n" 7 | import { isWhiteList } from "@/router/whitelist" 8 | 9 | NProgress.configure({ showSpinner: false }) 10 | 11 | const { setTitle } = useTitle() 12 | 13 | const LOGIN_PATH = "/login" 14 | 15 | export function registerNavigationGuard(router: Router) { 16 | // 全局前置守卫 17 | router.beforeEach(async (to, _from) => { 18 | NProgress.start() 19 | const userStore = useUserStore() 20 | const permissionStore = usePermissionStore() 21 | // 如果没有登录 22 | if (!userStore.token) { 23 | // 如果在免登录的白名单中,则直接进入 24 | if (isWhiteList(to)) return true 25 | // 其他没有访问权限的页面将被重定向到登录页面 26 | return LOGIN_PATH 27 | } 28 | // 如果已经登录,并准备进入 Login 页面,则重定向到主页 29 | if (to.path === LOGIN_PATH) return "/" 30 | // 如果用户已经获得其权限角色 31 | if (userStore.userInfo.username !== "") return true 32 | // 否则要重新获取权限角色 33 | try { 34 | // 获取用户信息 35 | await userStore.getInfo() 36 | // 获取路由 37 | await permissionStore.setRoutes() 38 | // 将'有访问权限的动态路由' 添加到 Router 中 39 | permissionStore.dynamicRoutes.forEach((route) => { 40 | router.addRoute(route) 41 | }) 42 | // 设置 replace: true, 因此导航将不会留下历史记录 43 | return { ...to, replace: true } 44 | } catch (error) { 45 | // 过程中发生任何错误,都直接重置 Token,并重定向到登录页面 46 | userStore.resetToken() 47 | ElMessage.error((error as Error).message || "路由守卫发生错误") 48 | return LOGIN_PATH 49 | } 50 | }) 51 | 52 | // 全局后置钩子 53 | router.afterEach((to) => { 54 | setRouteChange(to) 55 | setTitle(to.meta.title) 56 | NProgress.done() 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "td27-admin", 3 | "type": "module", 4 | "version": "3.0", 5 | "description": "An out-of-the-box scaffold developed with Go and Vue.", 6 | "author": "pddzl (https://github.com/pddzl)", 7 | "repository": "https://github.com/pddzl/td27-admin", 8 | "scripts": { 9 | "dev": "vite", 10 | "build:staging": "vue-tsc && vite build --mode staging", 11 | "build": "vue-tsc && vite build", 12 | "preview": "vite preview", 13 | "lint": "eslint . --fix", 14 | "prepare": "husky", 15 | "test": "vitest" 16 | }, 17 | "dependencies": { 18 | "@element-plus/icons-vue": "2.3.1", 19 | "axios": "1.10.0", 20 | "dayjs": "1.11.13", 21 | "element-plus": "2.10.4", 22 | "lodash-es": "4.17.21", 23 | "mitt": "3.0.1", 24 | "normalize.css": "8.0.1", 25 | "nprogress": "0.2.0", 26 | "path-browserify": "1.0.1", 27 | "path-to-regexp": "8.2.0", 28 | "pinia": "3.0.3", 29 | "screenfull": "6.0.2", 30 | "vue": "3.5.17", 31 | "vue-json-pretty": "2.5.0", 32 | "vue-router": "4.5.1", 33 | "vxe-table": "4.6.25" 34 | }, 35 | "devDependencies": { 36 | "@antfu/eslint-config": "4.17.0", 37 | "@types/lodash-es": "4.17.12", 38 | "@types/node": "24.0.14", 39 | "@types/nprogress": "0.2.3", 40 | "@types/path-browserify": "1.0.3", 41 | "@vitejs/plugin-vue": "6.0.0", 42 | "eslint": "9.31.0", 43 | "eslint-plugin-format": "1.0.1", 44 | "happy-dom": "18.0.1", 45 | "husky": "9.1.7", 46 | "lint-staged": "16.1.2", 47 | "sass-embedded": "1.78.0", 48 | "typescript": "5.8.3", 49 | "unocss": "66.3.3", 50 | "unplugin-auto-import": "19.3.0", 51 | "unplugin-svg-component": "0.12.2", 52 | "unplugin-vue-components": "28.8.0", 53 | "vite": "7.0.4", 54 | "vite-svg-loader": "5.1.0", 55 | "vue-tsc": "3.0.1" 56 | }, 57 | "lint-staged": { 58 | "*": "eslint --fix" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/api/authority/user.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@/http/axios_n" 2 | 3 | interface userData { 4 | username: string 5 | phone: string 6 | email: string 7 | active: boolean 8 | roleId: number 9 | } 10 | 11 | export interface userDataModel extends userData, Td27Model { 12 | roleName: string 13 | } 14 | 15 | // 数据结构 - List 16 | export type userListData = ListData 17 | 18 | /** 获取用户详情 */ 19 | export function getUserInfoApi() { 20 | return request>({ 21 | url: "/user/getUserInfo", 22 | method: "get" 23 | }) 24 | } 25 | 26 | /** 获取所有用户 */ 27 | export function listUserApi(data: PageInfo) { 28 | return request>({ 29 | url: "/user/list", 30 | method: "post", 31 | data 32 | }) 33 | } 34 | 35 | // 删除用户 36 | export function deleteUserApi(data: CId) { 37 | return request>({ 38 | url: "/user/delete", 39 | method: "post", 40 | data 41 | }) 42 | } 43 | 44 | // 添加用户 45 | export function createUserApi(data: userData & { password: string }) { 46 | return request>({ 47 | url: "/user/create", 48 | method: "post", 49 | data 50 | }) 51 | } 52 | 53 | // 编辑用户 54 | export function updateUserApi(data: userData & CId) { 55 | return request>({ 56 | url: "/user/update", 57 | method: "post", 58 | data 59 | }) 60 | } 61 | 62 | // 修改用户密码 63 | interface reqModifyPass { 64 | oldPassword: string 65 | newPassword: string 66 | } 67 | 68 | export function modifyPassApi(data: reqModifyPass & CId) { 69 | return request>({ 70 | url: "/user/modifyPass", 71 | method: "post", 72 | data 73 | }) 74 | } 75 | 76 | // 切换用户状态 77 | export function SwitchActiveApi(data: { active: boolean } & CId) { 78 | return request>({ 79 | url: "/user/switchActive", 80 | method: "post", 81 | data 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /server/internal/service/monitor/operationLog.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "server/internal/global" 5 | modelMonitor "server/internal/model/entity/monitor" 6 | monitorReq "server/internal/model/entity/monitor/request" 7 | ) 8 | 9 | type OperationLogService struct{} 10 | 11 | func NewOperationLogService() *OperationLogService { 12 | return &OperationLogService{} 13 | } 14 | 15 | // CreateOperationLog 创建操作日志 16 | func (o *OperationLogService) CreateOperationLog(operationRecord modelMonitor.OperationLogModel) error { 17 | return global.TD27_DB.Create(&operationRecord).Error 18 | } 19 | 20 | // GetOperationLogList 分页获取操作日志 21 | func (o *OperationLogService) GetOperationLogList(orSp monitorReq.OrSearchParams) ([]modelMonitor.OperationLogModel, int64, error) { 22 | limit := orSp.PageSize 23 | offset := orSp.PageSize * (orSp.Page - 1) 24 | db := global.TD27_DB.Model(&modelMonitor.OperationLogModel{}) 25 | var olList []modelMonitor.OperationLogModel 26 | 27 | if orSp.Path != "" { 28 | db = db.Where("path LIKE ?", "%"+orSp.Path+"%") 29 | } 30 | 31 | if orSp.Method != "" { 32 | db = db.Where("method = ?", orSp.Method) 33 | } 34 | 35 | if orSp.Status != 0 { 36 | db = db.Where("status = ?", orSp.Status) 37 | } 38 | 39 | var total int64 40 | err := db.Count(&total).Error 41 | 42 | if err != nil { 43 | return olList, total, err 44 | } else { 45 | db = db.Limit(limit).Offset(offset) 46 | if orSp.Asc { 47 | err = db.Find(&olList).Error 48 | } else { 49 | err = db.Order("id desc").Find(&olList).Error 50 | } 51 | } 52 | return olList, total, err 53 | } 54 | 55 | // DeleteOperationLog 删除操作日志 56 | func (o *OperationLogService) DeleteOperationLog(id uint) error { 57 | return global.TD27_DB.Unscoped().Delete(&modelMonitor.OperationLogModel{}, id).Error 58 | } 59 | 60 | // DeleteOperationLogByIds 批量删除操作日志 61 | func (o *OperationLogService) DeleteOperationLogByIds(ids []uint) error { 62 | return global.TD27_DB.Unscoped().Delete(&[]modelMonitor.OperationLogModel{}, ids).Error 63 | } 64 | -------------------------------------------------------------------------------- /web/.cursor/rules/vue.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | globs: *.vue 3 | alwaysApply: false 4 | --- 5 | 6 | # Vue 开发规范 7 | 8 | - 你是一位前端开发专家,精通 Vue、Vue Router、Pinia、Element Plus 等前端框架 9 | 10 | ## 代码风格 11 | 12 | - 组件: 使用单文件组件 (SFC) 13 | - API: 使用组合式 API (Composition API) 并搭配 `