├── .node-version ├── src ├── assets │ ├── styles │ │ ├── resources │ │ │ ├── variables.scss │ │ │ └── utils.scss │ │ └── globals.css │ ├── images │ │ ├── logo.png │ │ └── login-banner.png │ └── icons │ │ ├── toolbar-collapse.svg │ │ ├── image-load-fail.svg │ │ └── 404.svg ├── store │ ├── index.ts │ └── modules │ │ ├── window.ts │ │ ├── user.ts │ │ ├── settings.ts │ │ └── menu.ts ├── utils │ ├── eventBus.ts │ ├── dayjs.ts │ ├── composables │ │ ├── useGlobalProperties.ts │ │ ├── useViewTransition.ts │ │ ├── useWindow.ts │ │ ├── useMenu.ts │ │ └── useAuth.ts │ ├── directive.ts │ └── system.copyright.ts ├── views │ ├── windows │ │ ├── WindowExampleNewWindow │ │ │ └── index.vue │ │ ├── WindowExampleParams2 │ │ │ └── index.vue │ │ ├── WindowExampleRemove │ │ │ └── index.vue │ │ ├── WindowExampleAdd │ │ │ └── index.vue │ │ ├── WindowExampleParams │ │ │ └── index.vue │ │ └── registerWindowComponent.ts │ ├── ui-kit │ │ ├── HDropdown.vue │ │ ├── HKbd.vue │ │ ├── HTooltip.vue │ │ ├── HInput.vue │ │ ├── HButton.vue │ │ ├── HDropdownMenu.vue │ │ ├── HCheckList.vue │ │ ├── HToggle.vue │ │ ├── HBadge.vue │ │ ├── HTabList.vue │ │ ├── HSelect.vue │ │ ├── HDialog.vue │ │ └── HSlideover.vue │ ├── components │ │ ├── Breadcrumb │ │ │ ├── index.vue │ │ │ └── item.vue │ │ ├── Topbar │ │ │ ├── Fullscreen │ │ │ │ └── index.vue │ │ │ ├── leftSide.vue │ │ │ ├── PreviewWindows │ │ │ │ └── index.vue │ │ │ ├── NavSearch │ │ │ │ └── index.vue │ │ │ ├── index.vue │ │ │ ├── rightSide.vue │ │ │ └── ColorScheme │ │ │ │ └── index.vue │ │ ├── Logo │ │ │ └── index.vue │ │ ├── Menu │ │ │ ├── types.ts │ │ │ ├── item.vue │ │ │ └── index.vue │ │ ├── Copyright │ │ │ └── index.vue │ │ ├── HotkeysIntro │ │ │ └── index.vue │ │ ├── MainSidebar │ │ │ └── index.vue │ │ ├── SubSidebar │ │ │ └── index.vue │ │ └── Header │ │ │ └── index.vue │ ├── 404.vue │ ├── login.vue │ └── index.vue ├── iconify │ ├── index.json │ └── index.ts ├── api │ ├── modules │ │ ├── app.ts │ │ └── user.ts │ └── index.ts ├── menu │ ├── index.ts │ └── modules │ │ └── window.example.ts ├── settings.ts ├── ui-provider │ ├── index.vue │ └── index.ts ├── types │ ├── shims.d.ts │ ├── components.d.ts │ ├── global.d.ts │ └── auto-imports.d.ts ├── components │ ├── Auth │ │ └── index.vue │ ├── ActionContainer │ │ └── index.vue │ ├── PageHeader │ │ └── index.vue │ ├── Trend │ │ └── index.vue │ ├── SearchBar │ │ └── index.vue │ ├── PageMain │ │ └── index.vue │ ├── ImagePreview │ │ └── index.vue │ ├── SystemInfo │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── FileUpload │ │ └── index.vue │ ├── ResetPasswordForm │ │ └── index.vue │ ├── PcasCascader │ │ └── index.vue │ ├── RegisterForm │ │ └── index.vue │ └── LoginForm │ │ └── index.vue ├── App.vue ├── settings.default.ts ├── mock │ ├── app.ts │ └── user.ts ├── main.ts └── router │ └── index.ts ├── .npmrc ├── public ├── favicon.ico └── browser_upgrade │ ├── edge.png │ ├── chrome.png │ └── index.css ├── .lintstagedrc ├── postcss.config.js ├── .gitignore ├── tsconfig.json ├── .editorconfig ├── .vscode ├── extensions.json └── settings.json ├── plop-templates ├── store │ ├── index.hbs │ └── prompt.js ├── page │ ├── index.hbs │ └── prompt.js └── component │ ├── index.hbs │ └── prompt.js ├── .env.development ├── eslint.config.js ├── plopfile.js ├── .env.test ├── .env.production ├── tsconfig.node.json ├── .github └── workflows │ ├── sync.yml │ └── release.yml ├── stylelint.config.js ├── LICENSE ├── tsconfig.app.json ├── index.html ├── vite.config.ts ├── scripts └── generate.icons.ts ├── unocss.config.ts ├── themes └── index.ts ├── .commitlintrc.js ├── package.json ├── README.md ├── vite └── plugins.ts └── loading.html /.node-version: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /src/assets/styles/resources/variables.scss: -------------------------------------------------------------------------------- 1 | // 全局变量 2 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | const pinia = createPinia() 2 | 3 | export default pinia 4 | -------------------------------------------------------------------------------- /src/utils/eventBus.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | export default mitt() 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | engine-strict=true 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/one-step-admin/basic/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/one-step-admin/basic/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /public/browser_upgrade/edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/one-step-admin/basic/HEAD/public/browser_upgrade/edge.png -------------------------------------------------------------------------------- /public/browser_upgrade/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/one-step-admin/basic/HEAD/public/browser_upgrade/chrome.png -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,tsx,vue}": "eslint --cache --fix", 3 | "*.{css,scss,vue}": "stylelint --cache --fix" 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'autoprefixer': {}, 4 | 'postcss-nested': {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/images/login-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/one-step-admin/basic/HEAD/src/assets/images/login-banner.png -------------------------------------------------------------------------------- /src/views/windows/WindowExampleNewWindow/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/utils/dayjs.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import 'dayjs/locale/zh-cn' 3 | 4 | dayjs.locale('zh-cn') 5 | 6 | export default dayjs 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist* 4 | dist-ssr 5 | *.local 6 | *.tsbuildinfo 7 | .eslintcache 8 | .stylelintcache 9 | public/icons 10 | -------------------------------------------------------------------------------- /src/iconify/index.json: -------------------------------------------------------------------------------- 1 | { "collections": ["ant-design", "ep", "flagpack", "icon-park", "mdi", "ri", "logos", "twemoji", "vscode-icons"], "isOfflineUse": false } 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.app.json" }, 4 | { "path": "./tsconfig.node.json" } 5 | ], 6 | "files": [] 7 | } 8 | -------------------------------------------------------------------------------- /src/api/modules/app.ts: -------------------------------------------------------------------------------- 1 | import api from '../index' 2 | 3 | export default { 4 | // 后端获取导航数据 5 | menuList: () => api.get('app/menu/list', { 6 | baseURL: '/mock/', 7 | }), 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/views/windows/WindowExampleParams2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "mikestead.dotenv", 5 | "dbaeumer.vscode-eslint", 6 | "stylelint.vscode-stylelint", 7 | "Vue.volar", 8 | "antfu.unocss" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/menu/index.ts: -------------------------------------------------------------------------------- 1 | import WindowExample from './modules/window.example' 2 | 3 | const menu = [ 4 | { 5 | title: '演示', 6 | icon: 'i-uim:box', 7 | children: [ 8 | WindowExample, 9 | ], 10 | }, 11 | ] 12 | 13 | export default menu 14 | -------------------------------------------------------------------------------- /plop-templates/store/index.hbs: -------------------------------------------------------------------------------- 1 | const use{{ properCase name }}Store = defineStore( 2 | // 唯一ID 3 | '{{ camelCase name }}', 4 | { 5 | state: () => ({}), 6 | getters: {}, 7 | actions: {}, 8 | }, 9 | ) 10 | 11 | export default use{{ properCase name }}Store 12 | -------------------------------------------------------------------------------- /plop-templates/page/index.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/utils/composables/useGlobalProperties.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentInternalInstance } from 'vue' 2 | 3 | export default function useGlobalProperties() { 4 | const { appContext } = getCurrentInstance() as ComponentInternalInstance 5 | return appContext.config.globalProperties 6 | } 7 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import settingsDefault from '@/settings.default' 2 | import { defaultsDeep } from 'lodash-es' 3 | 4 | const globalSettings: Settings.all = { 5 | // 请在此处编写或粘贴配置代码 6 | } 7 | 8 | export default defaultsDeep(globalSettings, settingsDefault) as RecursiveRequired 9 | -------------------------------------------------------------------------------- /src/ui-provider/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/windows/WindowExampleRemove/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/ui-provider/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import ElementPlus from 'element-plus' 3 | import 'element-plus/dist/index.css' 4 | import 'element-plus/theme-chalk/dark/css-vars.css' 5 | 6 | function install(app: App) { 7 | app.use(ElementPlus) 8 | } 9 | 10 | export default { install } 11 | -------------------------------------------------------------------------------- /plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /src/views/ui-kit/HDropdown.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/views/ui-kit/HKbd.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/types/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | webkitDevicePixelRatio: any 3 | mozDevicePixelRatio: any 4 | } 5 | 6 | declare const __SYSTEM_INFO__: { 7 | pkg: { 8 | version: string 9 | dependencies: Recordable 10 | devDependencies: Recordable 11 | } 12 | lastBuildTime: string 13 | } 14 | -------------------------------------------------------------------------------- /src/views/windows/WindowExampleAdd/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 应用配置面板 2 | VITE_APP_SETTING = true 3 | # 页面标题 4 | VITE_APP_TITLE = One-step-admin 基础版 5 | # 接口请求地址,会设置到 axios 的 baseURL 参数上 6 | VITE_APP_API_BASEURL = / 7 | # 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空 8 | VITE_APP_DEBUG_TOOL = 9 | # 是否禁用开发者工具,可防止被调试 10 | VITE_APP_DISABLE_DEVTOOL = false 11 | 12 | # 是否开启代理 13 | VITE_OPEN_PROXY = false 14 | -------------------------------------------------------------------------------- /src/iconify/index.ts: -------------------------------------------------------------------------------- 1 | import { addCollection } from '@iconify/vue' 2 | import data from './data.json' 3 | 4 | export async function downloadAndInstall(name: string) { 5 | const data = Object.freeze(await fetch(`./icons/${name}-raw.json`).then(r => r.json())) 6 | addCollection(data) 7 | } 8 | 9 | export const icons = data.sort((a, b) => a.info.name.localeCompare(b.info.name)) 10 | -------------------------------------------------------------------------------- /src/views/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /src/views/windows/WindowExampleParams/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/utils/directive.ts: -------------------------------------------------------------------------------- 1 | import type { App, DirectiveBinding } from 'vue' 2 | 3 | export default function directive(app: App) { 4 | app.directive('auth', (el: HTMLElement, binding: DirectiveBinding) => { 5 | if (binding.modifiers.all ? useAuth().authAll(binding.value) : useAuth().auth(binding.value)) { 6 | el.style.display = '' 7 | } 8 | else { 9 | el.style.display = 'none' 10 | } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/menu/modules/window.example.ts: -------------------------------------------------------------------------------- 1 | const menus: Menu.recordRaw = { 2 | title: '窗口功能', 3 | icon: 'i-ri:window-2-line', 4 | children: [ 5 | { 6 | title: '打开新窗口', 7 | windowName: 'WindowExampleAdd', 8 | }, 9 | { 10 | title: '关闭窗口', 11 | windowName: 'WindowExampleRemove', 12 | }, 13 | { 14 | title: '带参窗口', 15 | windowName: 'WindowExampleParams', 16 | }, 17 | ], 18 | } 19 | 20 | export default menus 21 | -------------------------------------------------------------------------------- /src/views/components/Topbar/Fullscreen/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu( 4 | { 5 | unocss: true, 6 | ignores: [ 7 | 'public', 8 | 'dist*', 9 | ], 10 | }, 11 | { 12 | rules: { 13 | 'eslint-comments/no-unlimited-disable': 'off', 14 | 'curly': ['error', 'all'], 15 | 'ts/no-unused-expressions': ['error', { 16 | allowShortCircuit: true, 17 | allowTernary: true, 18 | }], 19 | }, 20 | }, 21 | ) 22 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | 3 | export default async function (plop) { 4 | plop.setWelcomeMessage('请选择需要创建的模式:') 5 | const items = await fs.readdir('./plop-templates') 6 | for (const item of items) { 7 | const stat = await fs.lstat(`./plop-templates/${item}`) 8 | if (stat.isDirectory()) { 9 | const prompt = await import(`./plop-templates/${item}/prompt.js`) 10 | plop.setGenerator(item, prompt.default) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/views/components/Topbar/leftSide.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/views/windows/registerWindowComponent.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { defineAsyncComponent } from 'vue' 3 | 4 | export default function registerWindowComponent(app: App) { 5 | const componentsContext = import.meta.glob('./*/index.vue') 6 | for (const path in componentsContext) { 7 | let name = path.replace('./', '') 8 | name = name.slice(0, name.indexOf('/')) 9 | app.component(name, defineAsyncComponent(componentsContext[path] as any)) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/composables/useViewTransition.ts: -------------------------------------------------------------------------------- 1 | export default function useViewTransition(callback: () => void) { 2 | function startViewTransition() { 3 | if (!document.startViewTransition || window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 4 | callback() 5 | return 6 | } 7 | return document.startViewTransition(async () => { 8 | await Promise.resolve(callback()) 9 | }) 10 | } 11 | 12 | return { 13 | startViewTransition, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Auth/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 24 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 应用配置面板 2 | VITE_APP_SETTING = false 3 | # 页面标题 4 | VITE_APP_TITLE = One-step-admin 基础版 5 | # 接口请求地址,会设置到 axios 的 baseURL 参数上 6 | VITE_APP_API_BASEURL = / 7 | # 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空 8 | VITE_APP_DEBUG_TOOL = 9 | # 是否禁用开发者工具,可防止被调试 10 | VITE_APP_DISABLE_DEVTOOL = false 11 | 12 | # 是否在打包时启用 Mock 13 | VITE_BUILD_MOCK = true 14 | # 是否在打包时生成 sourcemap 15 | VITE_BUILD_SOURCEMAP = false 16 | # 是否在打包时开启压缩,支持 gzip 和 brotli 17 | VITE_BUILD_COMPRESS = gzip,brotli 18 | # 是否在打包后生成存档,支持 zip 和 tar 19 | VITE_BUILD_ARCHIVE = 20 | -------------------------------------------------------------------------------- /src/views/components/Breadcrumb/item.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 应用配置面板 2 | VITE_APP_SETTING = false 3 | # 页面标题 4 | VITE_APP_TITLE = One-step-admin 基础版 5 | # 接口请求地址,会设置到 axios 的 baseURL 参数上 6 | VITE_APP_API_BASEURL = / 7 | # 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空 8 | VITE_APP_DEBUG_TOOL = 9 | # 是否禁用开发者工具,可防止被调试 10 | VITE_APP_DISABLE_DEVTOOL = false 11 | 12 | # 是否在打包时启用 Mock 13 | VITE_BUILD_MOCK = false 14 | # 是否在打包时生成 sourcemap 15 | VITE_BUILD_SOURCEMAP = false 16 | # 是否在打包时开启压缩,支持 gzip 和 brotli 17 | VITE_BUILD_COMPRESS = gzip,brotli 18 | # 是否在打包后生成存档,支持 zip 和 tar 19 | VITE_BUILD_ARCHIVE = 20 | -------------------------------------------------------------------------------- /src/api/modules/user.ts: -------------------------------------------------------------------------------- 1 | import api from '../index' 2 | 3 | export default { 4 | // 登录 5 | login: (data: { 6 | account: string 7 | password: string 8 | }) => api.post('user/login', data, { 9 | baseURL: '/mock/', 10 | }), 11 | 12 | // 获取权限 13 | permission: () => api.get('user/permission', { 14 | baseURL: '/mock/', 15 | }), 16 | 17 | // 修改密码 18 | passwordEdit: (data: { 19 | password: string 20 | newpassword: string 21 | }) => api.post('user/password/edit', data, { 22 | baseURL: '/mock/', 23 | }), 24 | } 25 | -------------------------------------------------------------------------------- /src/views/ui-kit/HTooltip.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "moduleDetection": "force", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "allowImportingTsExtensions": true, 9 | "strict": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noEmit": true, 14 | "isolatedModules": true, 15 | "skipLibCheck": true 16 | }, 17 | "include": [ 18 | "package.json", 19 | "vite.config.ts", 20 | "vite/**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.useFlatConfig": true, 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit", 7 | "source.fixAll.stylelint": "explicit", 8 | "source.organizeImports": "never" 9 | }, 10 | "stylelint.validate": [ 11 | "css", 12 | "postcss", 13 | "scss", 14 | "vue" 15 | ], 16 | "eslint.validate": [ 17 | "javascript", 18 | "javascriptreact", 19 | "typescript", 20 | "typescriptreact", 21 | "vue", 22 | "html", 23 | "markdown", 24 | "json", 25 | "jsonc", 26 | "yaml" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /plop-templates/store/prompt.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: '创建全局状态', 3 | prompts: [ 4 | { 5 | type: 'input', 6 | name: 'name', 7 | message: '请输入模块名称', 8 | validate: (v) => { 9 | if (!v || v.trim === '') { 10 | return '模块名称不能为空' 11 | } 12 | else { 13 | return true 14 | } 15 | }, 16 | }, 17 | ], 18 | actions: () => { 19 | const actions = [ 20 | { 21 | type: 'add', 22 | path: 'src/store/modules/{{camelCase name}}.ts', 23 | templateFile: 'plop-templates/store/index.hbs', 24 | }, 25 | ] 26 | return actions 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: branches-sync 2 | 3 | on: 4 | # 每天 00:15 自动同步 5 | schedule: 6 | - cron: '15 0 * * *' 7 | # 手动触发部署 8 | workflow_dispatch: 9 | 10 | jobs: 11 | sync-to-gitee: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Sync to Gitee 15 | uses: wearerequired/git-mirror-action@master 16 | env: 17 | # 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY 18 | SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }} 19 | with: 20 | # 注意替换为你的 GitHub 源仓库地址 21 | source-repo: git@github.com:one-step-admin/basic.git 22 | # 注意替换为你的 Gitee 目标仓库地址 23 | destination-repo: git@gitee.com:one-step-admin/basic.git 24 | -------------------------------------------------------------------------------- /src/components/ActionContainer/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/utils/system.copyright.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // 请勿删除 3 | if (import.meta.env.PROD) { 4 | const copyright_common_style = 'font-size: 14px; margin-bottom: 2px; padding: 6px 8px; color: #fff;' 5 | const copyright_main_style = `${copyright_common_style} background: #e24329;` 6 | const copyright_sub_style = `${copyright_common_style} background: #707070;` 7 | if ((navigator.language).toLowerCase() === 'zh-cn') { 8 | console.info('%c由%cOne-step-admin%c提供技术支持', copyright_sub_style, copyright_main_style, copyright_sub_style, '\nhttps://one-step-admin.hurui.me') 9 | } 10 | else { 11 | console.info('%cPowered by%cOne-step-admin', copyright_sub_style, copyright_main_style, '\nhttps://one-step-admin.hurui.me') 12 | } 13 | } 14 | 15 | export {} 16 | -------------------------------------------------------------------------------- /src/views/components/Logo/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 29 | -------------------------------------------------------------------------------- /src/views/components/Topbar/PreviewWindows/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/utils/composables/useWindow.ts: -------------------------------------------------------------------------------- 1 | import useWindowStore from '@/store/modules/window' 2 | import eventBus from '@/utils/eventBus' 3 | 4 | export default function useAuth() { 5 | const windowStore = useWindowStore() 6 | 7 | // 新增窗口 8 | function add(windowName: string | window) { 9 | windowStore.add(windowName) 10 | eventBus.emit('scrollToWindow', typeof windowName === 'string' ? windowName : windowName.name) 11 | } 12 | 13 | // 关闭窗口 14 | function remove(windowName: string) { 15 | windowStore.remove(windowName) 16 | } 17 | 18 | // 窗口刷新 19 | function reload(windowName: string) { 20 | windowStore.reload(windowName) 21 | setTimeout(() => { 22 | windowStore.reload(windowName) 23 | }, 0) 24 | } 25 | 26 | return { 27 | add, 28 | remove, 29 | reload, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/views/ui-kit/HInput.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /src/components/PageHeader/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: [ 3 | 'stylelint-config-standard-scss', 4 | 'stylelint-config-standard-vue/scss', 5 | 'stylelint-config-recess-order', 6 | '@stylistic/stylelint-config', 7 | ], 8 | plugins: [ 9 | 'stylelint-scss', 10 | ], 11 | rules: { 12 | 'at-rule-no-unknown': null, 13 | 'no-descending-specificity': null, 14 | 'property-no-unknown': null, 15 | 'font-family-no-missing-generic-family-keyword': null, 16 | 'selector-class-pattern': null, 17 | 'scss/double-slash-comment-empty-line-before': null, 18 | 'scss/no-global-function-names': null, 19 | '@stylistic/max-line-length': null, 20 | '@stylistic/block-closing-brace-newline-after': [ 21 | 'always', 22 | { 23 | ignoreAtRules: ['if', 'else'], 24 | }, 25 | ], 26 | }, 27 | allowEmptyInput: true, 28 | ignoreFiles: [ 29 | 'node_modules/**/*', 30 | 'dist*/**/*', 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Trend/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 39 | -------------------------------------------------------------------------------- /src/views/components/Topbar/NavSearch/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/settings.default.ts: -------------------------------------------------------------------------------- 1 | // 该文件为系统默认配置,请勿修改!!! 2 | const globalSettingsDefault: RecursiveRequired = { 3 | app: { 4 | colorScheme: 'light', 5 | enableMournMode: false, 6 | enableColorAmblyopiaMode: false, 7 | enablePermission: false, 8 | }, 9 | menu: { 10 | baseOn: 'frontend', 11 | mode: 'side', 12 | switchMainMenuAndOpenWindow: false, 13 | subMenuUniqueOpened: true, 14 | subMenuCollapse: false, 15 | enableSubMenuCollapseButton: false, 16 | enableHotkeys: false, 17 | }, 18 | toolbar: { 19 | previewWindows: true, 20 | navSearch: true, 21 | fullscreen: false, 22 | colorScheme: false, 23 | }, 24 | navSearch: { 25 | enableHotkeys: true, 26 | }, 27 | window: { 28 | defaultWidth: 1000, 29 | enableHotkeys: true, 30 | }, 31 | copyright: { 32 | enable: false, 33 | dates: '', 34 | company: '', 35 | website: '', 36 | beian: '', 37 | }, 38 | } 39 | 40 | export default globalSettingsDefault 41 | -------------------------------------------------------------------------------- /src/assets/icons/toolbar-collapse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/composables/useMenu.ts: -------------------------------------------------------------------------------- 1 | import useMenuStore from '@/store/modules/menu' 2 | import useSettingsStore from '@/store/modules/settings' 3 | 4 | export default function useMenu() { 5 | const settingsStore = useSettingsStore() 6 | const menuStore = useMenuStore() 7 | 8 | const appWindow = useWindow() 9 | 10 | function switchTo(index: number) { 11 | menuStore.setActived(index) 12 | if (settingsStore.settings.menu.switchMainMenuAndOpenWindow) { 13 | const windowName = getDeepestWindow(menuStore.sidebarMenus[0]).windowName 14 | if (windowName) { 15 | if (/^(?:https?:|mailto:|tel:)/.test(windowName)) { 16 | window.open(windowName) 17 | } 18 | else { 19 | appWindow.add(windowName) 20 | } 21 | } 22 | } 23 | } 24 | 25 | function getDeepestWindow(menu: Menu.recordRaw): Menu.recordRaw { 26 | return menu.children ? getDeepestWindow(menu.children[0]) : menu 27 | } 28 | 29 | return { 30 | switchTo, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/composables/useAuth.ts: -------------------------------------------------------------------------------- 1 | import useSettingsStore from '@/store/modules/settings' 2 | import useUserStore from '@/store/modules/user' 3 | 4 | export default function useAuth() { 5 | function hasPermission(permission: string) { 6 | const settingsStore = useSettingsStore() 7 | const userStore = useUserStore() 8 | if (settingsStore.settings.app.enablePermission) { 9 | return userStore.permissions.includes(permission) 10 | } 11 | else { 12 | return true 13 | } 14 | } 15 | 16 | function auth(value: string | string[]) { 17 | let auth 18 | if (typeof value === 'string') { 19 | auth = hasPermission(value) 20 | } 21 | else { 22 | auth = value.some((item) => { 23 | return hasPermission(item) 24 | }) 25 | } 26 | return auth 27 | } 28 | 29 | function authAll(value: string[]) { 30 | const auth = value.every((item) => { 31 | return hasPermission(item) 32 | }) 33 | return auth 34 | } 35 | 36 | return { 37 | auth, 38 | authAll, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/browser_upgrade/index.css: -------------------------------------------------------------------------------- 1 | #browser-upgrade { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | z-index: 10001; 6 | display: none; 7 | width: 100%; 8 | height: 100%; 9 | color: #736477; 10 | user-select: none; 11 | background-color: snow; 12 | } 13 | 14 | #browser-upgrade .title { 15 | margin: 40px 0; 16 | font-size: 24px; 17 | text-align: center; 18 | } 19 | 20 | #browser-upgrade .browsers { 21 | text-align: center; 22 | } 23 | 24 | #browser-upgrade .browsers .browser { 25 | display: inline-block; 26 | margin: 0 20px; 27 | text-decoration: none; 28 | cursor: pointer; 29 | } 30 | 31 | #browser-upgrade .browsers .browser .browser-icon { 32 | display: block; 33 | width: 50px; 34 | height: 50px; 35 | margin: 0 auto; 36 | border: none; 37 | } 38 | 39 | #browser-upgrade .browsers .browser .browser-name { 40 | padding-bottom: 2px; 41 | margin-top: 10px; 42 | color: #736477; 43 | text-align: center; 44 | border-bottom: 1px solid transparent; 45 | } 46 | 47 | #browser-upgrade .browsers .browser:hover .browser-name { 48 | border-bottom: 1px solid #736477; 49 | } 50 | -------------------------------------------------------------------------------- /src/mock/app.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from 'vite-plugin-fake-server/client' 2 | 3 | export default defineFakeRoute([ 4 | { 5 | url: '/mock/app/menu/list', 6 | method: 'get', 7 | response: () => { 8 | return { 9 | error: '', 10 | status: 1, 11 | data: [ 12 | { 13 | title: '演示', 14 | icon: 'sidebar-default', 15 | children: [ 16 | { 17 | title: '窗口功能', 18 | icon: 'sidebar-window', 19 | children: [ 20 | { 21 | title: '打开新窗口', 22 | windowName: 'WindowExampleAdd', 23 | }, 24 | { 25 | title: '关闭窗口', 26 | windowName: 'WindowExampleRemove', 27 | }, 28 | { 29 | title: '带参窗口', 30 | windowName: 'WindowExampleParams', 31 | }, 32 | ], 33 | }, 34 | ], 35 | }, 36 | ], 37 | } 38 | }, 39 | }, 40 | ]) 41 | -------------------------------------------------------------------------------- /src/views/components/Topbar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /src/views/ui-kit/HButton.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 One-step-admin 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 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": [ 6 | "ESNext", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "moduleDetection": "force", 11 | "useDefineForClassFields": true, 12 | "baseUrl": "./", 13 | "module": "ESNext", 14 | "moduleResolution": "Bundler", 15 | "paths": { 16 | "@/*": [ 17 | "src/*" 18 | ], 19 | "#/*": [ 20 | "src/types/*" 21 | ] 22 | }, 23 | "resolveJsonModule": true, 24 | "types": [ 25 | "vite/client", 26 | "vite-plugin-app-loading/client", 27 | "element-plus/global" 28 | ], 29 | "allowImportingTsExtensions": true, 30 | "allowJs": false, 31 | "strict": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "noUnusedLocals": true, 34 | "noUnusedParameters": true, 35 | "noEmit": true, 36 | "sourceMap": true, 37 | "esModuleInterop": true, 38 | "isolatedModules": true, 39 | "skipLibCheck": true 40 | }, 41 | "include": [ 42 | "src/**/*.ts", 43 | "src/**/*.d.ts", 44 | "src/**/*.tsx", 45 | "src/**/*.vue" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/views/ui-kit/HDropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /src/assets/styles/resources/utils.scss: -------------------------------------------------------------------------------- 1 | // 文字超出隐藏,默认为单行超出隐藏,可设置多行 2 | @mixin text-overflow($line: 1, $fixed-width: true) { 3 | @if $line == 1 and $fixed-width == true { 4 | overflow: hidden; 5 | text-overflow: ellipsis; 6 | white-space: nowrap; 7 | } @else { 8 | /* stylelint-disable-next-line value-no-vendor-prefix */ 9 | display: -webkit-box; 10 | overflow: hidden; 11 | -webkit-box-orient: vertical; 12 | -webkit-line-clamp: $line; 13 | } 14 | } 15 | 16 | // 定位居中,默认水平居中,可选择垂直居中,或者水平垂直都居中 17 | @mixin position-center($type: x) { 18 | position: absolute; 19 | 20 | @if $type == x { 21 | left: 50%; 22 | transform: translateX(-50%); 23 | } 24 | 25 | @if $type == y { 26 | top: 50%; 27 | transform: translateY(-50%); 28 | } 29 | 30 | @if $type == xy { 31 | top: 50%; 32 | left: 50%; 33 | transform: translateX(-50%) translateY(-50%); 34 | } 35 | } 36 | 37 | // 文字两端对齐 38 | %justify-align { 39 | text-align: justify; 40 | text-align-last: justify; 41 | } 42 | 43 | // 清除浮动 44 | %clearfix { 45 | zoom: 1; 46 | 47 | &::before, 48 | &::after { 49 | display: block; 50 | clear: both; 51 | content: ""; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import registerWindowComponent from '@/views/windows/registerWindowComponent' 2 | 3 | import FloatingVue from 'floating-vue' 4 | import Message from 'vue-m-message' 5 | 6 | import App from './App.vue' 7 | import router from './router' 8 | 9 | import pinia from './store' 10 | 11 | import ui from './ui-provider' 12 | import '@/utils/system.copyright' 13 | import 'floating-vue/dist/style.css' 14 | import 'vue-m-message/dist/style.css' 15 | import 'overlayscrollbars/overlayscrollbars.css' 16 | 17 | // 自定义指令 18 | import directive from '@/utils/directive' 19 | 20 | // 加载 svg 图标 21 | import 'virtual:svg-icons-register' 22 | 23 | // 加载 iconify 图标 24 | import { downloadAndInstall } from '@/iconify' 25 | import icons from '@/iconify/index.json' 26 | 27 | import 'virtual:uno.css' 28 | 29 | // 全局样式 30 | import '@/assets/styles/globals.css' 31 | 32 | const app = createApp(App) 33 | app.use(FloatingVue, { 34 | distance: 12, 35 | }) 36 | app.use(Message) 37 | app.use(pinia) 38 | app.use(router) 39 | app.use(ui) 40 | registerWindowComponent(app) 41 | directive(app) 42 | if (icons.isOfflineUse) { 43 | for (const info of icons.collections) { 44 | downloadAndInstall(info) 45 | } 46 | } 47 | 48 | app.mount('#app') 49 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 48 | -------------------------------------------------------------------------------- /src/assets/icons/image-load-fail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/ui-kit/HCheckList.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %VITE_APP_TITLE% 9 | 10 | 11 |
12 |
13 |
为了您的体验,推荐使用以下浏览器
14 | 24 |
25 |
26 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/views/components/Menu/types.ts: -------------------------------------------------------------------------------- 1 | export interface MenuItem { 2 | index: string 3 | indexPath: string[] 4 | active?: boolean 5 | } 6 | 7 | export interface MenuProps { 8 | menu: Menu.recordRaw[] 9 | value: string 10 | accordion?: boolean 11 | defaultOpeneds?: string[] 12 | mode?: 'horizontal' | 'vertical' 13 | collapse?: boolean 14 | showCollapseName?: boolean 15 | } 16 | 17 | export interface MenuInjection { 18 | props: MenuProps 19 | items: Record 20 | subMenus: Record 21 | activeIndex: MenuProps['value'] 22 | openedMenus: string[] 23 | mouseInMenu: string[] 24 | isMenuPopup: boolean 25 | openMenu: (index: string, indexPath: string[]) => void 26 | closeMenu: (index: string | string[]) => void 27 | handleMenuItemClick: (index: string, windowName?: Menu.recordRaw['windowName']) => void 28 | handleSubMenuClick: (index: string, indexPath: string[]) => void 29 | } 30 | 31 | export const rootMenuInjectionKey = Symbol('rootMenu') as InjectionKey 32 | 33 | export interface SubMenuProps { 34 | uniqueKey: string[] 35 | menu: Menu.recordRaw 36 | level?: number 37 | } 38 | 39 | export interface SubMenuItemProps { 40 | uniqueKey: string[] 41 | item: Menu.recordRaw 42 | level?: number 43 | subMenu?: boolean 44 | expand?: boolean 45 | } 46 | -------------------------------------------------------------------------------- /src/views/ui-kit/HToggle.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /src/components/PageMain/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 48 | -------------------------------------------------------------------------------- /src/views/ui-kit/HBadge.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 43 | -------------------------------------------------------------------------------- /plop-templates/page/prompt.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | 4 | function getFolder(path) { 5 | const components = [] 6 | const files = fs.readdirSync(path) 7 | files.forEach((item) => { 8 | const stat = fs.lstatSync(`${path}/${item}`) 9 | if (stat.isDirectory() === true && item !== 'components') { 10 | components.push(`${path}/${item}`) 11 | components.push(...getFolder(`${path}/${item}`)) 12 | } 13 | }) 14 | return components 15 | } 16 | 17 | export default { 18 | description: '创建页面', 19 | prompts: [ 20 | { 21 | type: 'list', 22 | name: 'path', 23 | message: '请选择页面创建目录', 24 | choices: getFolder('src/views'), 25 | }, 26 | { 27 | type: 'input', 28 | name: 'name', 29 | message: '请输入文件名', 30 | validate: (v) => { 31 | if (!v || v.trim === '') { 32 | return '文件名不能为空' 33 | } 34 | else { 35 | return true 36 | } 37 | }, 38 | }, 39 | ], 40 | actions: (data) => { 41 | const relativePath = path.relative('src/views', data.path) 42 | const actions = [ 43 | { 44 | type: 'add', 45 | path: `${data.path}/{{dotCase name}}.vue`, 46 | templateFile: 'plop-templates/page/index.hbs', 47 | data: { 48 | componentName: `${relativePath} ${data.name}`, 49 | }, 50 | }, 51 | ] 52 | return actions 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /src/views/components/Copyright/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 39 | -------------------------------------------------------------------------------- /src/mock/user.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { defineFakeRoute } from 'vite-plugin-fake-server/client' 3 | 4 | export default defineFakeRoute([ 5 | { 6 | url: '/mock/user/login', 7 | method: 'post', 8 | response: ({ body }) => { 9 | return { 10 | error: '', 11 | status: 1, 12 | data: Mock.mock({ 13 | account: body.account, 14 | token: `${body.account}_@string`, 15 | avatar: 'https://one-step-admin.hurui.me/logo.png', 16 | }), 17 | } 18 | }, 19 | }, 20 | { 21 | url: '/mock/user/permission', 22 | method: 'get', 23 | response: ({ headers }) => { 24 | let permissions: string[] = [] 25 | if (headers.token?.indexOf('admin') === 0) { 26 | permissions = [ 27 | 'permission.browse', 28 | 'permission.create', 29 | 'permission.edit', 30 | 'permission.remove', 31 | ] 32 | } 33 | else if (headers.token?.indexOf('test') === 0) { 34 | permissions = [ 35 | 'permission.browse', 36 | ] 37 | } 38 | return { 39 | error: '', 40 | status: 1, 41 | data: { 42 | permissions, 43 | }, 44 | } 45 | }, 46 | }, 47 | { 48 | url: '/mock/user/password/edit', 49 | method: 'post', 50 | response: () => { 51 | return { 52 | error: '', 53 | status: 1, 54 | data: { 55 | isSuccess: true, 56 | }, 57 | } 58 | }, 59 | }, 60 | ]) 61 | -------------------------------------------------------------------------------- /src/components/ImagePreview/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 36 | 37 | 64 | -------------------------------------------------------------------------------- /plop-templates/component/prompt.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | 3 | function getFolder(path) { 4 | const components = [] 5 | const files = fs.readdirSync(path) 6 | files.forEach((item) => { 7 | const stat = fs.lstatSync(`${path}/${item}`) 8 | if (stat.isDirectory() === true && item !== 'components') { 9 | components.push(`${path}/${item}`) 10 | components.push(...getFolder(`${path}/${item}`)) 11 | } 12 | }) 13 | return components 14 | } 15 | 16 | export default { 17 | description: '创建组件', 18 | prompts: [ 19 | { 20 | type: 'confirm', 21 | name: 'isGlobal', 22 | message: '是否为全局组件', 23 | default: false, 24 | }, 25 | { 26 | type: 'list', 27 | name: 'path', 28 | message: '请选择组件创建目录', 29 | choices: getFolder('src/views'), 30 | when: (answers) => { 31 | return !answers.isGlobal 32 | }, 33 | }, 34 | { 35 | type: 'input', 36 | name: 'name', 37 | message: '请输入组件名称', 38 | validate: (v) => { 39 | if (!v || v.trim === '') { 40 | return '组件名称不能为空' 41 | } 42 | else { 43 | return true 44 | } 45 | }, 46 | }, 47 | ], 48 | actions: (data) => { 49 | let path = '' 50 | if (data.isGlobal) { 51 | path = 'src/components/{{properCase name}}/index.vue' 52 | } 53 | else { 54 | path = `${data.path}/components/{{properCase name}}/index.vue` 55 | } 56 | const actions = [ 57 | { 58 | type: 'add', 59 | path, 60 | templateFile: 'plop-templates/component/index.hbs', 61 | }, 62 | ] 63 | return actions 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /src/store/modules/window.ts: -------------------------------------------------------------------------------- 1 | import useMenuStore from './menu' 2 | 3 | export const useWindowStore = defineStore( 4 | // 唯一ID 5 | 'windows', 6 | () => { 7 | const list = ref([]) 8 | 9 | function add(data: string | window) { 10 | let preData: window 11 | if (typeof data === 'string') { 12 | const menuStore = useMenuStore() 13 | preData = { 14 | name: data, 15 | title: menuStore.flatMenu[data].title, 16 | params: menuStore.flatMenu[data].params, 17 | breadcrumbNeste: menuStore.flatMenu[data].breadcrumbNeste, 18 | reload: false, 19 | } 20 | } 21 | else { 22 | preData = { 23 | name: data.name, 24 | title: data.title, 25 | params: data.params, 26 | breadcrumbNeste: [], 27 | reload: false, 28 | } 29 | } 30 | // 无则添加,有则更新 31 | const index = list.value.findIndex(item => item.name === preData.name) 32 | if (index < 0) { 33 | list.value.push(preData) 34 | } 35 | else { 36 | Object.assign(list.value[index], preData) 37 | } 38 | } 39 | function remove(name: string) { 40 | list.value = list.value.filter(item => item.name !== name) 41 | } 42 | function removeAll() { 43 | list.value = [] 44 | } 45 | function reload(name: string) { 46 | list.value.map((item) => { 47 | if (item.name === name) { 48 | item.reload = !item.reload 49 | } 50 | return item 51 | }) 52 | } 53 | 54 | return { 55 | list, 56 | add, 57 | remove, 58 | removeAll, 59 | reload, 60 | } 61 | }, 62 | ) 63 | 64 | export default useWindowStore 65 | -------------------------------------------------------------------------------- /src/views/ui-kit/HTabList.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 53 | -------------------------------------------------------------------------------- /src/views/components/Topbar/rightSide.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 52 | -------------------------------------------------------------------------------- /src/components/SystemInfo/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 65 | -------------------------------------------------------------------------------- /src/views/ui-kit/HSelect.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 49 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import process from 'node:process' 4 | import dayjs from 'dayjs' 5 | import { defineConfig, loadEnv } from 'vite' 6 | import pkg from './package.json' 7 | import createVitePlugins from './vite/plugins' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig(({ mode, command }) => { 11 | const env = loadEnv(mode, process.cwd()) 12 | // 全局 scss 资源 13 | const scssResources: string[] = [] 14 | fs.readdirSync('src/assets/styles/resources').forEach((dirname) => { 15 | if (fs.statSync(`src/assets/styles/resources/${dirname}`).isFile()) { 16 | scssResources.push(`@use "/src/assets/styles/resources/${dirname}" as *;`) 17 | } 18 | }) 19 | return { 20 | // 开发服务器选项 https://cn.vitejs.dev/config/server-options 21 | server: { 22 | open: true, 23 | port: 9000, 24 | proxy: { 25 | '/proxy': { 26 | target: env.VITE_APP_API_BASEURL, 27 | changeOrigin: command === 'serve' && env.VITE_OPEN_PROXY === 'true', 28 | rewrite: path => path.replace(/\/proxy/, ''), 29 | }, 30 | }, 31 | }, 32 | // 构建选项 https://cn.vitejs.dev/config/build-options 33 | build: { 34 | outDir: mode === 'production' ? 'dist' : `dist-${mode}`, 35 | sourcemap: env.VITE_BUILD_SOURCEMAP === 'true', 36 | }, 37 | define: { 38 | __SYSTEM_INFO__: JSON.stringify({ 39 | pkg: { 40 | version: pkg.version, 41 | dependencies: pkg.dependencies, 42 | devDependencies: pkg.devDependencies, 43 | }, 44 | lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), 45 | }), 46 | }, 47 | plugins: createVitePlugins(mode, command === 'build'), 48 | resolve: { 49 | alias: { 50 | '@': path.resolve(__dirname, 'src'), 51 | }, 52 | }, 53 | css: { 54 | preprocessorOptions: { 55 | scss: { 56 | api: 'modern-compiler', 57 | additionalData: scssResources.join(''), 58 | }, 59 | }, 60 | }, 61 | } 62 | }) 63 | -------------------------------------------------------------------------------- /src/views/components/Topbar/ColorScheme/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 61 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import apiUser from '@/api/modules/user' 2 | import router from '@/router' 3 | import useMenuStore from './menu' 4 | import useWindowStore from './window' 5 | 6 | export const useUserStore = defineStore( 7 | // 唯一ID 8 | 'user', 9 | () => { 10 | const account = ref(sessionStorage.getItem('account') ?? '') 11 | const token = ref(sessionStorage.getItem('token') ?? '') 12 | const avatar = ref(sessionStorage.getItem('avatar') ?? '') 13 | const permissions = ref([]) 14 | const isLogin = computed(() => { 15 | if (token.value) { 16 | return true 17 | } 18 | return false 19 | }) 20 | 21 | async function login(data: { 22 | account: string 23 | password: string 24 | }) { 25 | const res = await apiUser.login(data) 26 | sessionStorage.setItem('account', res.data.account) 27 | sessionStorage.setItem('token', res.data.token) 28 | sessionStorage.setItem('avatar', res.data.avatar) 29 | account.value = res.data.account 30 | token.value = res.data.token 31 | avatar.value = res.data.avatar 32 | } 33 | async function logout() { 34 | const menuStore = useMenuStore() 35 | const windowStore = useWindowStore() 36 | sessionStorage.removeItem('account') 37 | sessionStorage.removeItem('token') 38 | sessionStorage.removeItem('avatar') 39 | account.value = '' 40 | token.value = '' 41 | avatar.value = '' 42 | menuStore.setActived(0) 43 | menuStore.removeMenus() 44 | windowStore.removeAll() 45 | router.push({ 46 | name: 'login', 47 | }) 48 | } 49 | // 获取我的权限 50 | async function getPermissions() { 51 | const res = await apiUser.permission() 52 | permissions.value = res.data.permissions 53 | return permissions.value 54 | } 55 | async function editPassword(data: { 56 | password: string 57 | newpassword: string 58 | }) { 59 | await apiUser.passwordEdit(data) 60 | } 61 | 62 | return { 63 | account, 64 | token, 65 | avatar, 66 | permissions, 67 | isLogin, 68 | login, 69 | logout, 70 | getPermissions, 71 | editPassword, 72 | } 73 | }, 74 | ) 75 | 76 | export default useUserStore 77 | -------------------------------------------------------------------------------- /src/views/components/HotkeysIntro/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 69 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import useMenuStore from '@/store/modules/menu' 2 | import useSettingsStore from '@/store/modules/settings' 3 | import useUserStore from '@/store/modules/user' 4 | import { loadingFadeOut } from 'virtual:app-loading' 5 | import { createRouter, createWebHashHistory } from 'vue-router' 6 | 7 | // 路由数据 8 | const routes = [ 9 | { 10 | path: '/login', 11 | name: 'login', 12 | component: () => import('@/views/login.vue'), 13 | meta: { 14 | title: '登录', 15 | }, 16 | }, 17 | { 18 | path: '/', 19 | name: 'index', 20 | component: () => import('@/views/index.vue'), 21 | }, 22 | { 23 | path: '/:pathMatch(.*)*', 24 | component: () => import('@/views/404.vue'), 25 | meta: { 26 | title: '找不到页面', 27 | }, 28 | }, 29 | ] 30 | 31 | // 免登录白名单 32 | const noLoginWhitelist = [ 33 | '/login', 34 | ] 35 | 36 | const router = createRouter({ 37 | history: createWebHashHistory(), 38 | routes, 39 | }) 40 | 41 | router.beforeEach((to, _from, next) => { 42 | const settingsStore = useSettingsStore() 43 | const userStore = useUserStore() 44 | const menuStore = useMenuStore() 45 | if (userStore.isLogin) { 46 | if (!menuStore.isGenerate) { 47 | if (settingsStore.settings.menu.baseOn === 'frontend') { 48 | menuStore.generateMenusAtFront() 49 | } 50 | else { 51 | menuStore.generateMenusAtBack() 52 | } 53 | } 54 | if (to.name) { 55 | if (to.matched.length !== 0) { 56 | // 如果已登录状态下,进入登录页会强制跳转到控制台页面 57 | if (to.name === 'login') { 58 | next({ 59 | name: 'index', 60 | replace: true, 61 | }) 62 | } 63 | else { 64 | next() 65 | } 66 | } 67 | else { 68 | // 如果是通过 name 跳转,并且 name 对应的路由没有权限时,需要做这步处理,手动指向到 404 页面 69 | next({ 70 | path: '/404', 71 | }) 72 | } 73 | } 74 | else { 75 | next() 76 | } 77 | } 78 | else { 79 | if (!noLoginWhitelist.includes(to.path)) { 80 | next({ 81 | name: 'login', 82 | query: { 83 | redirect: to.fullPath, 84 | }, 85 | }) 86 | } 87 | else { 88 | next() 89 | } 90 | } 91 | }) 92 | 93 | router.isReady().then(() => { 94 | loadingFadeOut() 95 | }) 96 | 97 | export default router 98 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import router from '@/router/index' 2 | 3 | import useUserStore from '@/store/modules/user' 4 | import axios from 'axios' 5 | // import qs from 'qs' 6 | import Message from 'vue-m-message' 7 | 8 | function toLogin() { 9 | router.push({ 10 | path: '/login', 11 | query: { 12 | redirect: router.currentRoute.value.path !== '/login' ? router.currentRoute.value.fullPath : undefined, 13 | }, 14 | }) 15 | } 16 | 17 | const api = axios.create({ 18 | baseURL: (import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY === 'true') ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL, 19 | timeout: 10000, 20 | responseType: 'json', 21 | }) 22 | 23 | api.interceptors.request.use( 24 | (request) => { 25 | const userStore = useUserStore() 26 | /** 27 | * 全局拦截请求发送前提交的参数 28 | * 以下代码为示例,在请求头里带上 token 信息 29 | */ 30 | if (userStore.isLogin && request.headers) { 31 | request.headers.Token = userStore.token 32 | } 33 | // 是否将 POST 请求参数进行字符串化处理 34 | if (request.method === 'post') { 35 | // request.data = qs.stringify(request.data, { 36 | // arrayFormat: 'brackets', 37 | // }) 38 | } 39 | return request 40 | }, 41 | ) 42 | 43 | api.interceptors.response.use( 44 | (response) => { 45 | /** 46 | * 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示 47 | * 假设返回数据格式为:{ status: 1, error: '', data: '' } 48 | * 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录 49 | * 请求出错时 error 会返回错误信息 50 | */ 51 | if (response.data.status === 1) { 52 | if (response.data.error !== '') { 53 | // 错误提示 54 | Message.error(response.data.error, { 55 | zIndex: 2000, 56 | }) 57 | return Promise.reject(response.data) 58 | } 59 | } 60 | else { 61 | toLogin() 62 | } 63 | return Promise.resolve(response.data) 64 | }, 65 | (error) => { 66 | let message = error.message 67 | if (message === 'Network Error') { 68 | message = '后端网络故障' 69 | } 70 | else if (message.includes('timeout')) { 71 | message = '接口请求超时' 72 | } 73 | else if (message.includes('Request failed with status code')) { 74 | message = `接口${message.substr(message.length - 3)}异常` 75 | } 76 | Message.error(message, { 77 | zIndex: 2000, 78 | }) 79 | return Promise.reject(error) 80 | }, 81 | ) 82 | 83 | export default api 84 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 22 19 | 20 | - run: npx changelogithub # or changelogithub@0.12 if ensure the stable result 21 | env: 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | 24 | upload-archive: 25 | needs: release 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: Get Release 33 | id: last_release 34 | uses: joutvhu/get-release@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | latest: true 39 | 40 | - name: Create Archive 41 | uses: thedoctor0/zip-release@main 42 | with: 43 | type: zip 44 | filename: one-step-admin.${{ steps.last_release.outputs.tag_name }}.zip 45 | exclusions: '/.git/* /.github/*' 46 | 47 | - name: Upload Archive To Release 48 | uses: xresloader/upload-to-github-release@v1 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | with: 52 | release_id: ${{ steps.last_release.outputs.id }} 53 | draft: false 54 | file: one-step-admin.${{ steps.last_release.outputs.tag_name }}.zip 55 | 56 | upload-archive-example: 57 | needs: release 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v4 61 | with: 62 | fetch-depth: 0 63 | ref: example 64 | 65 | - name: Get Release 66 | id: last_release 67 | uses: joutvhu/get-release@v1 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | latest: true 72 | 73 | - name: Create Archive 74 | uses: thedoctor0/zip-release@main 75 | with: 76 | type: zip 77 | filename: one-step-admin.example.${{ steps.last_release.outputs.tag_name }}.zip 78 | exclusions: '/.git/* /.github/*' 79 | 80 | - name: Upload Archive To Release 81 | uses: xresloader/upload-to-github-release@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | release_id: ${{ steps.last_release.outputs.id }} 86 | draft: false 87 | file: one-step-admin.example.${{ steps.last_release.outputs.tag_name }}.zip 88 | -------------------------------------------------------------------------------- /scripts/generate.icons.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process' 2 | import path from 'node:path' 3 | import process from 'node:process' 4 | import { lookupCollection, lookupCollections } from '@iconify/json' 5 | import fs from 'fs-extra' 6 | import inquirer from 'inquirer' 7 | 8 | async function generateIcons() { 9 | // 拿到全部图标集的原始数据 10 | const raw = await lookupCollections() 11 | 12 | let lastChoose = fs.readFileSync(path.resolve(process.cwd(), 'src/iconify/index.json'), 'utf-8') 13 | lastChoose = JSON.parse(lastChoose) 14 | 15 | // 取出可使用的图标集数据用于 inquirer 选择,并按名称排序 16 | const collections = Object.entries(raw).map(([id, item]) => ({ 17 | ...item, 18 | id, 19 | })).sort((a, b) => a.name.localeCompare(b.name)) 20 | 21 | /** 22 | * 分别会在对应目录下生成以下文件,其中(1)(3)用于离线下载并安装图标,(2)用于图标选择器使用 23 | * (1) src/iconify/index.json 记录用户 inquirer 的交互信息 24 | * (2) src/iconify/data.json 包含多个图标集数据,仅记录图标名 25 | * (3) public/icons/*-raw.json 多个图标集的原始数据,独立存放,用于离线使用 26 | */ 27 | inquirer.prompt([ 28 | { 29 | type: 'checkbox', 30 | message: '请选择需要生成的图标集', 31 | name: 'collections', 32 | choices: collections.map(item => ({ 33 | name: `${item.name} (${item.total} icons)`, 34 | value: item.id, 35 | })), 36 | default: lastChoose.collections, 37 | }, 38 | { 39 | type: 'confirm', 40 | name: 'isOfflineUse', 41 | message: '是否需要离线使用', 42 | default: false, 43 | }, 44 | ]).then(async (answers) => { 45 | await fs.writeJSON( 46 | path.resolve(process.cwd(), 'src/iconify/index.json'), 47 | { 48 | collections: answers.collections, 49 | isOfflineUse: answers.isOfflineUse, 50 | }, 51 | ) 52 | 53 | const outputDir = path.resolve(process.cwd(), 'public/icons') 54 | await fs.ensureDir(outputDir) 55 | await fs.emptyDir(outputDir) 56 | 57 | const collectionsMeta: object[] = [] 58 | for (const info of answers.collections) { 59 | const setData = await lookupCollection(info) 60 | 61 | collectionsMeta.push({ 62 | prefix: setData.prefix, 63 | info: setData.info, 64 | icons: Object.keys(setData.icons), 65 | }) 66 | 67 | const offlineFilePath = path.join(outputDir, `${info}-raw.json`) 68 | 69 | if (answers.isOfflineUse) { 70 | await fs.writeJSON(offlineFilePath, setData) 71 | } 72 | } 73 | 74 | await fs.writeJSON( 75 | path.resolve(process.cwd(), 'src/iconify/data.json'), 76 | collectionsMeta, 77 | ) 78 | 79 | exec('eslint src/iconify/data.json src/iconify/index.json --cache --fix') 80 | }) 81 | } 82 | 83 | generateIcons() 84 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from 'unocss/preset-uno' 2 | import { entriesToCss, toArray } from '@unocss/core' 3 | import { 4 | defineConfig, 5 | presetAttributify, 6 | presetIcons, 7 | presetTypography, 8 | presetUno, 9 | transformerCompileClass, 10 | transformerDirectives, 11 | transformerVariantGroup, 12 | } from 'unocss' 13 | import { presetScrollbar } from 'unocss-preset-scrollbar' 14 | import { darkTheme, lightTheme } from './themes' 15 | 16 | export default defineConfig({ 17 | content: { 18 | pipeline: { 19 | include: [ 20 | /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/, 21 | 'src/**/*.{js,ts}', 22 | ], 23 | }, 24 | }, 25 | shortcuts: [ 26 | [/^flex-?(col)?-(start|end|center|baseline|stretch)-?(start|end|center|between|around|evenly|left|right)?$/, ([, col, items, justify]) => { 27 | const cls = ['flex'] 28 | if (col === 'col') { 29 | cls.push('flex-col') 30 | } 31 | if (items === 'center' && !justify) { 32 | cls.push('items-center') 33 | cls.push('justify-center') 34 | } 35 | else { 36 | cls.push(`items-${items}`) 37 | if (justify) { 38 | cls.push(`justify-${justify}`) 39 | } 40 | } 41 | return cls.join(' ') 42 | }], 43 | ], 44 | preflights: [ 45 | { 46 | getCSS: () => { 47 | const returnCss: string[] = [] 48 | // 明亮主题 49 | const lightCss = entriesToCss(Object.entries(lightTheme)) 50 | const lightRoots = toArray([`*,::before,::after`, `::backdrop`]) 51 | returnCss.push(lightRoots.map(root => `${root}{${lightCss}}`).join('')) 52 | // 暗黑主题 53 | const darkCss = entriesToCss(Object.entries(darkTheme)) 54 | const darkRoots = toArray([`html.dark,html.dark *,html.dark ::before,html.dark ::after`, `html.dark ::backdrop`]) 55 | returnCss.push(darkRoots.map(root => `${root}{${darkCss}}`).join('')) 56 | 57 | return returnCss.join('') 58 | }, 59 | }, 60 | ], 61 | theme: { 62 | colors: { 63 | 'ui-primary': 'rgb(var(--ui-primary))', 64 | 'ui-text': 'rgb(var(--ui-text))', 65 | }, 66 | }, 67 | presets: [ 68 | presetUno(), 69 | presetAttributify(), 70 | presetIcons({ 71 | extraProperties: { 72 | 'display': 'inline-block', 73 | 'vertical-align': 'middle', 74 | }, 75 | }), 76 | presetTypography(), 77 | presetScrollbar(), 78 | ], 79 | transformers: [ 80 | transformerDirectives(), 81 | transformerVariantGroup(), 82 | transformerCompileClass(), 83 | ], 84 | configDeps: [ 85 | 'themes/index.ts', 86 | ], 87 | }) 88 | -------------------------------------------------------------------------------- /src/types/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | ActionContainer: typeof import('./../components/ActionContainer/index.vue')['default'] 11 | Auth: typeof import('./../components/Auth/index.vue')['default'] 12 | FileUpload: typeof import('./../components/FileUpload/index.vue')['default'] 13 | HBadge: typeof import('./../views/ui-kit/HBadge.vue')['default'] 14 | HButton: typeof import('./../views/ui-kit/HButton.vue')['default'] 15 | HCheckList: typeof import('./../views/ui-kit/HCheckList.vue')['default'] 16 | HDialog: typeof import('./../views/ui-kit/HDialog.vue')['default'] 17 | HDropdown: typeof import('./../views/ui-kit/HDropdown.vue')['default'] 18 | HDropdownMenu: typeof import('./../views/ui-kit/HDropdownMenu.vue')['default'] 19 | HInput: typeof import('./../views/ui-kit/HInput.vue')['default'] 20 | HKbd: typeof import('./../views/ui-kit/HKbd.vue')['default'] 21 | HSelect: typeof import('./../views/ui-kit/HSelect.vue')['default'] 22 | HSlideover: typeof import('./../views/ui-kit/HSlideover.vue')['default'] 23 | HTabList: typeof import('./../views/ui-kit/HTabList.vue')['default'] 24 | HToggle: typeof import('./../views/ui-kit/HToggle.vue')['default'] 25 | HTooltip: typeof import('./../views/ui-kit/HTooltip.vue')['default'] 26 | ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default'] 27 | ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default'] 28 | ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default'] 29 | LoginForm: typeof import('./../components/LoginForm/index.vue')['default'] 30 | PageHeader: typeof import('./../components/PageHeader/index.vue')['default'] 31 | PageMain: typeof import('./../components/PageMain/index.vue')['default'] 32 | PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default'] 33 | RegisterForm: typeof import('./../components/RegisterForm/index.vue')['default'] 34 | ResetPasswordForm: typeof import('./../components/ResetPasswordForm/index.vue')['default'] 35 | RouterLink: typeof import('vue-router')['RouterLink'] 36 | RouterView: typeof import('vue-router')['RouterView'] 37 | SearchBar: typeof import('./../components/SearchBar/index.vue')['default'] 38 | SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default'] 39 | SystemInfo: typeof import('./../components/SystemInfo/index.vue')['default'] 40 | Trend: typeof import('./../components/Trend/index.vue')['default'] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /themes/index.ts: -------------------------------------------------------------------------------- 1 | import { hex2rgba } from '@unocss/preset-mini/utils' 2 | 3 | export const lightTheme = { 4 | 'color-scheme': 'light', 5 | // 内置 UI 6 | '--ui-primary': hex2rgba('#0f0f0f')!.join(' '), 7 | '--ui-text': hex2rgba('#fcfcfc')!.join(' '), 8 | // 主体 9 | '--g-app-bg': '#fff', 10 | '--g-main-bg': '#f2f2f2', 11 | '--g-border-color': '#f2f2f2', 12 | // 头部 13 | '--g-header-bg': '#fff', 14 | '--g-header-color': '#0f0f0f', 15 | '--g-header-menu-color': '#0f0f0f', 16 | '--g-header-menu-hover-bg': '#dde1e3', 17 | '--g-header-menu-hover-color': '#0f0f0f', 18 | '--g-header-menu-active-bg': '#0f0f0f', 19 | '--g-header-menu-active-color': '#fff', 20 | // 主导航 21 | '--g-main-sidebar-bg': '#f2f2f2', 22 | '--g-main-sidebar-menu-color': '#0f0f0f', 23 | '--g-main-sidebar-menu-hover-bg': '#dde1e3', 24 | '--g-main-sidebar-menu-hover-color': '#0f0f0f', 25 | '--g-main-sidebar-menu-active-bg': '#0f0f0f', 26 | '--g-main-sidebar-menu-active-color': '#fff', 27 | // 次导航 28 | '--g-sub-sidebar-bg': '#fff', 29 | '--g-sub-sidebar-logo-bg': '#0f0f0f', 30 | '--g-sub-sidebar-logo-color': '#fff', 31 | '--g-sub-sidebar-menu-color': '#0f0f0f', 32 | '--g-sub-sidebar-menu-hover-bg': '#dde1e3', 33 | '--g-sub-sidebar-menu-hover-color': '#0f0f0f', 34 | '--g-sub-sidebar-menu-active-bg': '#0f0f0f', 35 | '--g-sub-sidebar-menu-active-color': '#fff', 36 | // 工具栏 37 | '--g-toolbar-bg': '#fff', 38 | } 39 | 40 | export const darkTheme = { 41 | 'color-scheme': 'dark', 42 | // 内置 UI 43 | '--ui-primary': hex2rgba('#e5e5e5')!.join(' '), 44 | '--ui-text': hex2rgba('#0f0f0f')!.join(' '), 45 | // 主体 46 | '--g-app-bg': '#141414', 47 | '--g-main-bg': '#0a0a0a', 48 | '--g-border-color': '#15191e', 49 | // 头部 50 | '--g-header-bg': '#141414', 51 | '--g-header-color': '#e5e5e5', 52 | '--g-header-menu-color': '#a8a29e', 53 | '--g-header-menu-hover-bg': '#141414', 54 | '--g-header-menu-hover-color': '#e5e5e5', 55 | '--g-header-menu-active-bg': '#e5e5e5', 56 | '--g-header-menu-active-color': '#0a0a0a', 57 | // 主导航 58 | '--g-main-sidebar-bg': '#0a0a0a', 59 | '--g-main-sidebar-menu-color': '#a8a29e', 60 | '--g-main-sidebar-menu-hover-bg': '#141414', 61 | '--g-main-sidebar-menu-hover-color': '#e5e5e5', 62 | '--g-main-sidebar-menu-active-bg': '#e5e5e5', 63 | '--g-main-sidebar-menu-active-color': '#0a0a0a', 64 | // 次导航 65 | '--g-sub-sidebar-bg': '#141414', 66 | '--g-sub-sidebar-logo-bg': '#0f0f0f', 67 | '--g-sub-sidebar-logo-color': '#e5e5e5', 68 | '--g-sub-sidebar-menu-color': '#a8a29e', 69 | '--g-sub-sidebar-menu-hover-bg': '#0a0a0a', 70 | '--g-sub-sidebar-menu-hover-color': '#e5e5e5', 71 | '--g-sub-sidebar-menu-active-bg': '#e5e5e5', 72 | '--g-sub-sidebar-menu-active-color': '#0a0a0a', 73 | // 工具栏 74 | '--g-toolbar-bg': '#141414', 75 | } 76 | -------------------------------------------------------------------------------- /src/views/ui-kit/HDialog.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 83 | -------------------------------------------------------------------------------- /src/views/login.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 50 | 51 | 109 | -------------------------------------------------------------------------------- /src/views/ui-kit/HSlideover.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 84 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('cz-git').UserConfig} */ 2 | export default { 3 | rules: { 4 | // @see: https://commitlint.js.org/#/reference-rules 5 | }, 6 | prompt: { 7 | alias: { fd: 'docs: fix typos' }, 8 | messages: { 9 | type: '选择你要提交的类型 :', 10 | scope: '选择一个提交范围(可选):', 11 | customScope: '请输入自定义的提交范围 :', 12 | subject: '填写简短精炼的变更描述 :\n', 13 | body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n', 14 | breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n', 15 | footerPrefixsSelect: '选择关联issue前缀(可选):', 16 | customFooterPrefixs: '输入自定义issue前缀 :', 17 | footer: '列举关联issue (可选) 例如: #31, #I3244 :\n', 18 | confirmCommit: '是否提交或修改commit ?', 19 | }, 20 | types: [ 21 | { value: 'feat', name: 'feat: ✨ 新增功能 | A new feature', emoji: ':sparkles:' }, 22 | { value: 'fix', name: 'fix: 🐛 修复缺陷 | A bug fix', emoji: ':bug:' }, 23 | { value: 'docs', name: 'docs: 📝 文档更新 | Documentation only changes', emoji: ':memo:' }, 24 | { value: 'style', name: 'style: 💄 代码格式 | Changes that do not affect the meaning of the code', emoji: ':lipstick:' }, 25 | { value: 'refactor', name: 'refactor: ♻️ 代码重构 | A code change that neither fixes a bug nor adds a feature', emoji: ':recycle:' }, 26 | { value: 'perf', name: 'perf: ⚡️ 性能提升 | A code change that improves performance', emoji: ':zap:' }, 27 | { value: 'test', name: 'test: ✅ 测试相关 | Adding missing tests or correcting existing tests', emoji: ':white_check_mark:' }, 28 | { value: 'build', name: 'build: 📦️ 构建相关 | Changes that affect the build system or external dependencies', emoji: ':package:' }, 29 | { value: 'ci', name: 'ci: 🎡 持续集成 | Changes to our CI configuration files and scripts', emoji: ':ferris_wheel:' }, 30 | { value: 'revert', name: 'revert: ⏪️ 回退代码 | Revert to a commit', emoji: ':rewind:' }, 31 | { value: 'chore', name: 'chore: 🔨 其他修改 | Other changes that do not modify src or test files', emoji: ':hammer:' }, 32 | ], 33 | useEmoji: false, 34 | emojiAlign: 'center', 35 | themeColorCode: '', 36 | scopes: [], 37 | allowCustomScopes: true, 38 | allowEmptyScopes: true, 39 | customScopesAlign: 'bottom', 40 | customScopesAlias: 'custom', 41 | emptyScopesAlias: 'empty', 42 | upperCaseSubject: false, 43 | markBreakingChangeMode: true, 44 | allowBreakingChanges: ['feat', 'fix'], 45 | breaklineNumber: 100, 46 | breaklineChar: '|', 47 | skipQuestions: [], 48 | issuePrefixs: [ 49 | // 如果使用 gitee 作为开发管理 50 | { value: 'link', name: 'link: 链接 ISSUES 进行中' }, 51 | { value: 'closed', name: 'closed: 标记 ISSUES 已完成' }, 52 | ], 53 | customIssuePrefixsAlign: 'top', 54 | emptyIssuePrefixsAlias: 'skip', 55 | customIssuePrefixsAlias: 'custom', 56 | allowCustomIssuePrefixs: true, 57 | allowEmptyIssuePrefixs: true, 58 | confirmColorize: true, 59 | maxHeaderLength: Number.POSITIVE_INFINITY, 60 | maxSubjectLength: Number.POSITIVE_INFINITY, 61 | minSubjectLength: 0, 62 | scopeOverrides: undefined, 63 | defaultBody: '', 64 | defaultIssues: '', 65 | defaultScope: '', 66 | defaultSubject: '', 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /src/store/modules/settings.ts: -------------------------------------------------------------------------------- 1 | import settingsDefault from '@/settings' 2 | import { defaultsDeep } from 'lodash-es' 3 | 4 | export const useSettingsStore = defineStore( 5 | // 唯一ID 6 | 'settings', 7 | () => { 8 | const settings = ref(settingsDefault) 9 | 10 | const prefersColorScheme = window.matchMedia('(prefers-color-scheme: dark)') 11 | const currentColorScheme = ref>() 12 | watch(() => settings.value.app.colorScheme, (val) => { 13 | if (val === '') { 14 | prefersColorScheme.addEventListener('change', updateTheme) 15 | } 16 | else { 17 | prefersColorScheme.removeEventListener('change', updateTheme) 18 | } 19 | }, { 20 | immediate: true, 21 | }) 22 | watch(() => settings.value.app.colorScheme, updateTheme, { 23 | immediate: true, 24 | }) 25 | function updateTheme() { 26 | let colorScheme = settings.value.app.colorScheme 27 | if (colorScheme === '') { 28 | colorScheme = prefersColorScheme.matches ? 'dark' : 'light' 29 | } 30 | currentColorScheme.value = colorScheme 31 | switch (colorScheme) { 32 | case 'dark': 33 | document.documentElement.classList.add('dark') 34 | break 35 | case 'light': 36 | document.documentElement.classList.remove('dark') 37 | break 38 | } 39 | } 40 | watch([ 41 | () => settings.value.app.enableMournMode, 42 | () => settings.value.app.enableColorAmblyopiaMode, 43 | ], (val) => { 44 | document.documentElement.style.removeProperty('filter') 45 | if (val[0] && val[1]) { 46 | document.documentElement.style.setProperty('filter', 'grayscale(100%) invert(80%)') 47 | } 48 | else if (val[0]) { 49 | document.documentElement.style.setProperty('filter', 'grayscale(100%)') 50 | } 51 | else if (val[1]) { 52 | document.documentElement.style.setProperty('filter', 'invert(80%)') 53 | } 54 | }, { 55 | immediate: true, 56 | }) 57 | 58 | watch(() => settings.value.menu.mode, (val) => { 59 | document.body.setAttribute('data-menu-mode', val) 60 | }, { 61 | immediate: true, 62 | }) 63 | 64 | // 操作系统 65 | const os = ref<'mac' | 'windows' | 'linux' | 'other'>('other') 66 | const agent = navigator.userAgent.toLowerCase() 67 | switch (true) { 68 | case agent.includes('mac os'): 69 | os.value = 'mac' 70 | break 71 | case agent.includes('windows'): 72 | os.value = 'windows' 73 | break 74 | case agent.includes('linux'): 75 | os.value = 'linux' 76 | break 77 | } 78 | 79 | const title = ref('') 80 | const previewAllWindows = ref(false) 81 | 82 | // 切换侧边栏导航展开/收起 83 | function toggleSidebarCollapse() { 84 | settings.value.menu.subMenuCollapse = !settings.value.menu.subMenuCollapse 85 | } 86 | // 设置主题颜色模式 87 | function setColorScheme(color: Required['colorScheme']) { 88 | settings.value.app.colorScheme = color 89 | } 90 | // 更新主题配置 91 | function updateSettings(data: Settings.all) { 92 | settings.value = defaultsDeep(data, settings.value) 93 | } 94 | 95 | return { 96 | settings, 97 | currentColorScheme, 98 | os, 99 | title, 100 | previewAllWindows, 101 | toggleSidebarCollapse, 102 | setColorScheme, 103 | updateSettings, 104 | } 105 | }, 106 | ) 107 | 108 | export default useSettingsStore 109 | -------------------------------------------------------------------------------- /src/assets/styles/globals.css: -------------------------------------------------------------------------------- 1 | /* 页面布局 CSS 变量 */ 2 | :root { 3 | /* 头部高度 */ 4 | --g-header-height: 60px; 5 | 6 | /* 侧边栏宽度 */ 7 | --g-main-sidebar-width: 80px; 8 | --g-sub-sidebar-width: 220px; 9 | --g-sub-sidebar-collapse-width: 64px; 10 | 11 | /* 侧边栏Logo高度 */ 12 | --g-sidebar-logo-height: 50px; 13 | 14 | /* 顶栏高度 */ 15 | --g-topbar-height: 50px; 16 | 17 | /* 窗口高度(仅在窗口预览时使用) */ 18 | --g-window-height: 800px; 19 | 20 | /* 窗口预览缩放系数 */ 21 | --g-window-perview-scale: 0.5; 22 | } 23 | 24 | /* 明暗模式 CSS 变量 */ 25 | /* stylelint-disable-next-line no-duplicate-selectors */ 26 | :root { 27 | --g-box-shadow-color: rgb(0 0 0 / 12%); 28 | 29 | &::view-transition-old(root), 30 | &::view-transition-new(root) { 31 | mix-blend-mode: normal; 32 | animation: none; 33 | } 34 | 35 | &::view-transition-old(root) { 36 | z-index: 1; 37 | } 38 | 39 | &::view-transition-new(root) { 40 | z-index: 9999; 41 | } 42 | 43 | &.dark { 44 | --g-box-shadow-color: rgb(0 0 0 / 72%); 45 | 46 | &::view-transition-old(root) { 47 | z-index: 9999; 48 | } 49 | 50 | &::view-transition-new(root) { 51 | z-index: 1; 52 | } 53 | } 54 | } 55 | 56 | ::-webkit-scrollbar { 57 | width: 12px; 58 | height: 12px; 59 | } 60 | 61 | ::-webkit-scrollbar-thumb { 62 | background-color: rgb(0 0 0 / 40%); 63 | background-clip: padding-box; 64 | border: 3px solid transparent; 65 | border-radius: 6px; 66 | } 67 | 68 | ::-webkit-scrollbar-thumb:hover { 69 | background-color: rgb(0 0 0 / 50%); 70 | } 71 | 72 | ::-webkit-scrollbar-track { 73 | background-color: transparent; 74 | } 75 | 76 | html, 77 | body { 78 | height: 100%; 79 | } 80 | 81 | body { 82 | box-sizing: border-box; 83 | margin: 0; 84 | font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif; 85 | background-color: var(--g-app-bg); 86 | -webkit-tap-highlight-color: transparent; 87 | 88 | &.overflow-hidden { 89 | overflow: hidden; 90 | } 91 | } 92 | 93 | * { 94 | box-sizing: inherit; 95 | } 96 | 97 | #app { 98 | height: 100%; 99 | } 100 | 101 | /* 右侧内容区针对fixed元素,有横向铺满的需求,可在fixed元素上设置 [data-fixed-calc-width] */ 102 | [data-fixed-calc-width] { 103 | position: fixed; 104 | right: 0; 105 | left: 50%; 106 | width: calc(100% - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width)); 107 | transform: translateX(-50%) translateX(calc(var(--g-main-sidebar-actual-width) / 2)) translateX(calc(var(--g-sub-sidebar-actual-width) / 2)); 108 | } 109 | 110 | /* textarea 字体跟随系统 */ 111 | textarea { 112 | font-family: inherit; 113 | } 114 | 115 | /* Overrides Floating Vue */ 116 | .v-popper--theme-dropdown, 117 | .v-popper--theme-tooltip { 118 | --uno: inline-flex; 119 | } 120 | 121 | .v-popper--theme-dropdown .v-popper__inner, 122 | .v-popper--theme-tooltip .v-popper__inner { 123 | --uno: bg-white dark-bg-stone-8 text-dark dark-text-white rounded shadow ring-1 ring-gray-200 dark-ring-gray-800 border border-solid border-stone/20 text-xs font-normal; 124 | 125 | box-shadow: 0 6px 30px rgb(0 0 0 / 10%); 126 | } 127 | 128 | .v-popper--theme-tooltip .v-popper__arrow-inner, 129 | .v-popper--theme-dropdown .v-popper__arrow-inner { 130 | visibility: visible; 131 | 132 | --uno: border-white dark-border-stone-8; 133 | } 134 | 135 | .v-popper--theme-tooltip .v-popper__arrow-outer, 136 | .v-popper--theme-dropdown .v-popper__arrow-outer { 137 | --uno: border-stone/20; 138 | } 139 | 140 | .v-popper--theme-tooltip.v-popper--shown, 141 | .v-popper--theme-tooltip.v-popper--shown * { 142 | transition: none !important; 143 | } 144 | 145 | [data-overlayscrollbars-contents] { 146 | overscroll-behavior: contain; 147 | } 148 | -------------------------------------------------------------------------------- /src/components/FileUpload/index.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 116 | 117 | 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "4.8.1", 4 | "engines": { 5 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 6 | }, 7 | "scripts": { 8 | "dev": "vite", 9 | "build:test": "vue-tsc -b && vite build --mode test", 10 | "build": "vue-tsc -b && vite build", 11 | "serve:test": "http-server ./dist-test -o", 12 | "serve": "http-server ./dist -o", 13 | "svgo": "svgo -f src/assets/icons", 14 | "new": "plop", 15 | "generate:icons": "esno ./scripts/generate.icons.ts", 16 | "lint": "npm-run-all -s lint:tsc lint:eslint lint:stylelint", 17 | "lint:tsc": "vue-tsc -b", 18 | "lint:eslint": "eslint . --cache --fix", 19 | "lint:stylelint": "stylelint \"src/**/*.{css,scss,vue}\" --cache --fix", 20 | "postinstall": "simple-git-hooks", 21 | "preinstall": "npx only-allow pnpm", 22 | "commit": "git cz", 23 | "release": "bumpp" 24 | }, 25 | "dependencies": { 26 | "@headlessui/vue": "^1.7.23", 27 | "@vueuse/components": "^12.0.0", 28 | "@vueuse/core": "^12.0.0", 29 | "@vueuse/integrations": "^12.0.0", 30 | "axios": "^1.7.9", 31 | "dayjs": "^1.11.13", 32 | "disable-devtool": "^0.3.8", 33 | "element-plus": "^2.9.0", 34 | "eruda": "^3.4.1", 35 | "floating-vue": "5.2.2", 36 | "hotkeys-js": "^3.13.7", 37 | "lodash-es": "^4.17.21", 38 | "mitt": "^3.0.1", 39 | "mockjs": "^1.1.0", 40 | "overlayscrollbars": "^2.10.1", 41 | "overlayscrollbars-vue": "^0.5.9", 42 | "pinia": "^2.3.0", 43 | "qs": "^6.13.1", 44 | "vconsole": "^3.15.1", 45 | "vue": "^3.5.13", 46 | "vue-m-message": "^4.0.2", 47 | "vue-router": "^4.5.0" 48 | }, 49 | "devDependencies": { 50 | "@antfu/eslint-config": "3.11.2", 51 | "@iconify/json": "^2.2.280", 52 | "@iconify/vue": "^4.2.0", 53 | "@stylistic/stylelint-config": "^2.0.0", 54 | "@types/lodash-es": "^4.17.12", 55 | "@types/mockjs": "^1.0.10", 56 | "@types/qs": "^6.9.17", 57 | "@unocss/eslint-plugin": "^0.65.1", 58 | "@vitejs/plugin-vue": "^5.2.1", 59 | "@vitejs/plugin-vue-jsx": "^4.1.1", 60 | "autoprefixer": "^10.4.20", 61 | "boxen": "^8.0.1", 62 | "bumpp": "^9.9.0", 63 | "cz-git": "^1.11.0", 64 | "eslint": "^9.16.0", 65 | "esno": "^4.8.0", 66 | "fs-extra": "^11.2.0", 67 | "http-server": "^14.1.1", 68 | "inquirer": "^12.2.0", 69 | "lint-staged": "^15.2.10", 70 | "npm-run-all": "^4.1.5", 71 | "picocolors": "^1.1.1", 72 | "plop": "^4.0.1", 73 | "postcss": "^8.4.49", 74 | "postcss-html": "^1.7.0", 75 | "postcss-nested": "^7.0.2", 76 | "sass-embedded": "^1.82.0", 77 | "simple-git-hooks": "^2.11.1", 78 | "stylelint": "^16.11.0", 79 | "stylelint-config-recess-order": "^5.1.1", 80 | "stylelint-config-standard-scss": "^14.0.0", 81 | "stylelint-config-standard-vue": "^1.0.0", 82 | "stylelint-scss": "^6.10.0", 83 | "svgo": "^3.3.2", 84 | "typescript": "^5.6.3", 85 | "unocss": "^0.65.1", 86 | "unocss-preset-scrollbar": "^0.3.1", 87 | "unplugin-auto-import": "^0.18.6", 88 | "unplugin-turbo-console": "^1.10.6", 89 | "unplugin-vue-components": "^0.27.5", 90 | "vite": "^6.0.3", 91 | "vite-plugin-app-loading": "^0.3.0", 92 | "vite-plugin-archiver": "^0.1.1", 93 | "vite-plugin-banner": "^0.8.0", 94 | "vite-plugin-compression2": "^1.3.3", 95 | "vite-plugin-fake-server": "^2.1.4", 96 | "vite-plugin-svg-icons": "^2.0.1", 97 | "vue-tsc": "^2.1.10" 98 | }, 99 | "simple-git-hooks": { 100 | "pre-commit": "pnpm lint-staged", 101 | "preserveUnused": true 102 | }, 103 | "config": { 104 | "commitizen": { 105 | "path": "node_modules/cz-git" 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/components/ResetPasswordForm/index.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 93 | 94 | 127 | -------------------------------------------------------------------------------- /src/components/PcasCascader/index.vue: -------------------------------------------------------------------------------- 1 | 140 | 141 | 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

One-step-admin

6 | 7 |

一款干啥都快人一步的 Vue 中后台管理系统框架

8 | 9 |

10 | 官网 11 |  |  12 | 备用地址 13 |

14 | 15 |

16 | 17 | 18 |

19 | 20 | ## 特点 21 | 22 | - 可自由替换 UI 组件库,默认使用 Element Plus 23 | - 高效的交互方式,让使用人员可以跨模块的多线操作 24 | - 丰富的布局与主题 25 | - 提供系统配置文件,轻松实现个性化定制 26 | - 根据配置自动生成导航栏 27 | - 支持全方位权限验证 28 | - 轻松实现国际化多语言适配 29 | 30 | ## 分支说明 31 | 32 | - `main` 框架源码分支,不含示例代码,可直接用于实际开发 33 | - `example` 演示源码分支,同演示站,包含大量示例,可用于参考学习 34 | 35 | ## 预览 36 | 37 | > 预览截图为专业版 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | ## 支持 53 | 54 | 如果觉得 One-step-admin 这个框架不错,或者已经在使用了,希望你可以去 **Github** 或者 **Gitee(码云)** 帮我点个 ⭐ ,这将对我是极大的鼓励。 55 | 56 | [![star](https://img.shields.io/github/stars/one-step-admin/basic?style=social)](https://github.com/one-step-admin/basic) 57 | 58 | [![star](https://gitee.com/one-step-admin/basic/badge/star.svg?theme=dark)](https://gitee.com/one-step-admin/basic) 59 | 60 |
61 | Github Stars 曲线 62 | 63 | [![Stargazers over time](https://starchart.cc/one-step-admin/basic.svg)](https://starchart.cc/one-step-admin/basic) 64 |
65 | 66 | ## 生态 67 | 68 | 69 | 70 | 73 | 74 | 75 | 78 | 79 |
71 | Fantastic-startkit 72 |
76 | 一款简单好用的 Vue3 项目启动套件 77 |
80 | 81 | 82 | 83 | 86 | 87 | 88 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
84 | Fantastic-admin 85 |
89 | 一款开箱即用的 Vue 中后台管理系统框架 90 |
103 | 104 | 105 | 106 | 109 | 110 | 111 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
107 | Fantastic-mobile 108 |
112 | 一款自成一派的移动端 H5 框架 113 |
122 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | type RecursiveRequired = { 2 | [P in keyof T]-?: RecursiveRequired 3 | } 4 | type RecursivePartial = { 5 | [P in keyof T]?: RecursivePartial 6 | } 7 | 8 | declare namespace Settings { 9 | interface app { 10 | /** 11 | * 颜色方案 12 | * @默认值 `''` 跟随系统 13 | * @可选值 `'light'` 明亮模式 14 | * @可选值 `'dark'` 暗黑模式 15 | */ 16 | colorScheme?: '' | 'light' | 'dark' 17 | /** 18 | * 是否开启哀悼模式 19 | * @默认值 `false` 20 | */ 21 | enableMournMode?: boolean 22 | /** 23 | * 是否开启色弱模式 24 | * @默认值 `false` 25 | */ 26 | enableColorAmblyopiaMode?: boolean 27 | /** 28 | * 是否开启权限功能 29 | * @默认值 `false` 30 | */ 31 | enablePermission?: boolean 32 | } 33 | interface menu { 34 | /** 35 | * 导航栏数据来源,当 `app.routeBaseOn: 'filesystem'` 时生效 36 | * @默认值 `'frontend'` 前端 37 | * @可选值 `'backend'` 后端 38 | */ 39 | baseOn?: 'frontend' | 'backend' 40 | /** 41 | * 导航栏模式 42 | * @默认值 `'side'` 侧边栏模式(有主导航) 43 | * @可选值 `'head'` 顶部模式 44 | * @可选值 `'single'` 侧边栏模式(无主导航) 45 | */ 46 | mode?: 'side' | 'head' | 'single' 47 | /** 48 | * 切换主导航同时打开窗口 49 | * @默认值 `false` 50 | */ 51 | switchMainMenuAndOpenWindow?: boolean 52 | /** 53 | * 次导航是否只保持一个子项的展开 54 | * @默认值 `true` 55 | */ 56 | subMenuUniqueOpened?: boolean 57 | /** 58 | * 次导航是否收起 59 | * @默认值 `false` 60 | */ 61 | subMenuCollapse?: boolean 62 | /** 63 | * 是否开启次导航的展开/收起按钮 64 | * @默认值 `false` 65 | */ 66 | enableSubMenuCollapseButton?: boolean 67 | /** 68 | * 是否开启主导航切换快捷键 69 | * @默认值 `false` 70 | */ 71 | enableHotkeys?: boolean 72 | } 73 | interface toolbar { 74 | /** 75 | * 是否开启窗口预览 76 | * @默认值 `true` 77 | */ 78 | previewWindows?: boolean 79 | /** 80 | * 是否开启导航搜索 81 | * @默认值 `true` 82 | */ 83 | navSearch?: boolean 84 | /** 85 | * 是否开启全屏 86 | * @默认值 `false` 87 | */ 88 | fullscreen?: boolean 89 | /** 90 | * 是否开启颜色主题 91 | * @默认值 `false` 92 | */ 93 | colorScheme?: boolean 94 | } 95 | interface navSearch { 96 | /** 97 | * 是否开启导航搜索快捷键 98 | * @默认值 `true` 99 | */ 100 | enableHotkeys?: boolean 101 | } 102 | interface window { 103 | /** 104 | * 窗口默认宽度,设置为数字时单位为 px 105 | * @默认值 `1000` 106 | */ 107 | defaultWidth?: string | number 108 | /** 109 | * 是否开启窗口快捷键 110 | * @默认值 `true` 111 | */ 112 | enableHotkeys?: boolean 113 | } 114 | interface copyright { 115 | /** 116 | * 是否开启底部版权 117 | * @默认值 `false` 118 | */ 119 | enable?: boolean 120 | /** 121 | * 网站运行日期 122 | * @默认值 `''` 123 | */ 124 | dates?: string 125 | /** 126 | * 公司名称 127 | * @默认值 `''` 128 | */ 129 | company?: string 130 | /** 131 | * 网站地址 132 | * @默认值 `''` 133 | */ 134 | website?: string 135 | /** 136 | * 网站备案号 137 | * @默认值 `''` 138 | */ 139 | beian?: string 140 | } 141 | interface all { 142 | /** 应用设置 */ 143 | app?: app 144 | /** 导航栏设置 */ 145 | menu?: menu 146 | /** 工具栏设置 */ 147 | toolbar?: toolbar 148 | /** 导航搜索设置 */ 149 | navSearch?: navSearch 150 | /** 窗口设置 */ 151 | window?: window 152 | /** 底部版权设置 */ 153 | copyright?: copyright 154 | } 155 | } 156 | 157 | declare namespace Menu { 158 | /** 原始 */ 159 | interface recordRaw { 160 | title?: string | (() => string) 161 | icon?: string 162 | auth?: string | string[] 163 | params?: object 164 | windowName?: string 165 | breadcrumbNeste?: Menu.breadcrumb[] 166 | children?: recordRaw[] 167 | } 168 | /** 主导航 */ 169 | interface recordMainRaw { 170 | title?: string | (() => string) 171 | icon?: string 172 | auth?: string | string[] 173 | children: recordRaw[] 174 | } 175 | interface breadcrumb { 176 | title?: string | (() => string) 177 | } 178 | } 179 | 180 | interface window { 181 | name: string 182 | title?: string | (() => string) 183 | params?: object 184 | breadcrumbNeste?: any[] 185 | reload?: boolean 186 | } 187 | -------------------------------------------------------------------------------- /src/views/components/MainSidebar/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 48 | 49 | 111 | -------------------------------------------------------------------------------- /src/components/RegisterForm/index.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 115 | 116 | 149 | -------------------------------------------------------------------------------- /src/views/components/Menu/item.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 97 | -------------------------------------------------------------------------------- /vite/plugins.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import process from 'node:process' 3 | import vue from '@vitejs/plugin-vue' 4 | import vueJsx from '@vitejs/plugin-vue-jsx' 5 | import boxen from 'boxen' 6 | import picocolors from 'picocolors' 7 | import Unocss from 'unocss/vite' 8 | import autoImport from 'unplugin-auto-import/vite' 9 | import TurboConsole from 'unplugin-turbo-console/vite' 10 | import components from 'unplugin-vue-components/vite' 11 | import { loadEnv, type PluginOption } from 'vite' 12 | import AppLoading from 'vite-plugin-app-loading' 13 | import Archiver from 'vite-plugin-archiver' 14 | import banner from 'vite-plugin-banner' 15 | import { compression } from 'vite-plugin-compression2' 16 | import { vitePluginFakeServer } from 'vite-plugin-fake-server' 17 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' 18 | 19 | export default function createVitePlugins(mode: string, isBuild = false) { 20 | const viteEnv = loadEnv(mode, process.cwd()) 21 | const vitePlugins: (PluginOption | PluginOption[])[] = [ 22 | vue(), 23 | vueJsx(), 24 | 25 | // https://github.com/unplugin/unplugin-auto-import 26 | autoImport({ 27 | imports: [ 28 | 'vue', 29 | 'vue-router', 30 | 'pinia', 31 | ], 32 | dts: './src/types/auto-imports.d.ts', 33 | dirs: [ 34 | './src/utils/composables/**', 35 | ], 36 | }), 37 | 38 | // https://github.com/unplugin/unplugin-vue-components 39 | components({ 40 | dirs: [ 41 | 'src/components', 42 | 'src/views/ui-kit', 43 | ], 44 | dts: './src/types/components.d.ts', 45 | }), 46 | 47 | Unocss(), 48 | 49 | // https://github.com/vbenjs/vite-plugin-svg-icons 50 | createSvgIconsPlugin({ 51 | iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/')], 52 | symbolId: 'icon-[dir]-[name]', 53 | svgoOptions: isBuild, 54 | }), 55 | 56 | // https://github.com/condorheroblog/vite-plugin-fake-server 57 | vitePluginFakeServer({ 58 | logger: !isBuild, 59 | include: 'src/mock', 60 | infixName: false, 61 | enableProd: isBuild && viteEnv.VITE_BUILD_MOCK === 'true', 62 | }), 63 | 64 | // https://github.com/nonzzz/vite-plugin-compression 65 | viteEnv.VITE_BUILD_COMPRESS?.split(',').includes('gzip') && compression(), 66 | viteEnv.VITE_BUILD_COMPRESS?.split(',').includes('brotli') && compression({ 67 | exclude: [/\.(br)$/, /\.(gz)$/], 68 | algorithm: 'brotliCompress', 69 | }), 70 | 71 | viteEnv.VITE_BUILD_ARCHIVE && Archiver({ 72 | archiveType: viteEnv.VITE_BUILD_ARCHIVE, 73 | }), 74 | 75 | AppLoading('loading.html'), 76 | 77 | // https://github.com/unplugin/unplugin-turbo-console 78 | TurboConsole(), 79 | 80 | // https://github.com/chengpeiquan/vite-plugin-banner 81 | banner(` 82 | /** 83 | * 由 One-step-admin 提供技术支持 84 | * Powered by One-step-admin 85 | * https://one-step-admin.hurui.me 86 | */ 87 | `), 88 | 89 | { 90 | name: 'vite-plugin-debug-plugin', 91 | enforce: 'pre', 92 | transform: (code, id) => { 93 | if (/src\/main.ts$/.test(id)) { 94 | if (viteEnv.VITE_APP_DEBUG_TOOL === 'eruda') { 95 | code = code.concat(` 96 | import eruda from 'eruda' 97 | eruda.init() 98 | `) 99 | } 100 | else if (viteEnv.VITE_APP_DEBUG_TOOL === 'vconsole') { 101 | code = code.concat(` 102 | import VConsole from 'vconsole' 103 | new VConsole() 104 | `) 105 | } 106 | return { 107 | code, 108 | map: null, 109 | } 110 | } 111 | }, 112 | }, 113 | 114 | { 115 | name: 'vite-plugin-disable-devtool', 116 | enforce: 'pre', 117 | transform: (code, id) => { 118 | if (/src\/main.ts$/.test(id)) { 119 | if (viteEnv.VITE_APP_DISABLE_DEVTOOL === 'true') { 120 | code = code.concat(` 121 | import DisableDevtool from 'disable-devtool' 122 | DisableDevtool() 123 | `) 124 | } 125 | return { 126 | code, 127 | map: null, 128 | } 129 | } 130 | }, 131 | }, 132 | 133 | { 134 | name: 'vite-plugin-terminal-info', 135 | apply: 'serve', 136 | async buildStart() { 137 | const { bold, green, cyan, bgGreen, underline } = picocolors 138 | // eslint-disable-next-line no-console 139 | console.log( 140 | boxen( 141 | `${bold(green(`由 ${bgGreen('One-step-admin')} 驱动`))}\n\n${underline('https://one-step-admin.hurui.me')}\n\n当前使用:${cyan('基础版')}`, 142 | { 143 | padding: 1, 144 | margin: 1, 145 | borderStyle: 'double', 146 | textAlignment: 'center', 147 | }, 148 | ), 149 | ) 150 | }, 151 | }, 152 | ] 153 | return vitePlugins 154 | } 155 | -------------------------------------------------------------------------------- /src/views/components/SubSidebar/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 79 | 80 | 156 | -------------------------------------------------------------------------------- /src/views/components/Header/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 63 | 64 | 157 | -------------------------------------------------------------------------------- /src/components/LoginForm/index.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 134 | 135 | 173 | -------------------------------------------------------------------------------- /src/types/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 11 | const computed: typeof import('vue')['computed'] 12 | const createApp: typeof import('vue')['createApp'] 13 | const createPinia: typeof import('pinia')['createPinia'] 14 | const customRef: typeof import('vue')['customRef'] 15 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 16 | const defineComponent: typeof import('vue')['defineComponent'] 17 | const defineStore: typeof import('pinia')['defineStore'] 18 | const effectScope: typeof import('vue')['effectScope'] 19 | const getActivePinia: typeof import('pinia')['getActivePinia'] 20 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 21 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 22 | const h: typeof import('vue')['h'] 23 | const inject: typeof import('vue')['inject'] 24 | const isProxy: typeof import('vue')['isProxy'] 25 | const isReactive: typeof import('vue')['isReactive'] 26 | const isReadonly: typeof import('vue')['isReadonly'] 27 | const isRef: typeof import('vue')['isRef'] 28 | const mapActions: typeof import('pinia')['mapActions'] 29 | const mapGetters: typeof import('pinia')['mapGetters'] 30 | const mapState: typeof import('pinia')['mapState'] 31 | const mapStores: typeof import('pinia')['mapStores'] 32 | const mapWritableState: typeof import('pinia')['mapWritableState'] 33 | const markRaw: typeof import('vue')['markRaw'] 34 | const nextTick: typeof import('vue')['nextTick'] 35 | const onActivated: typeof import('vue')['onActivated'] 36 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 37 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 38 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 39 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 40 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 41 | const onDeactivated: typeof import('vue')['onDeactivated'] 42 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 43 | const onMounted: typeof import('vue')['onMounted'] 44 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 45 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 46 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 47 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 48 | const onUnmounted: typeof import('vue')['onUnmounted'] 49 | const onUpdated: typeof import('vue')['onUpdated'] 50 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 51 | const provide: typeof import('vue')['provide'] 52 | const reactive: typeof import('vue')['reactive'] 53 | const readonly: typeof import('vue')['readonly'] 54 | const ref: typeof import('vue')['ref'] 55 | const resolveComponent: typeof import('vue')['resolveComponent'] 56 | const setActivePinia: typeof import('pinia')['setActivePinia'] 57 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 58 | const shallowReactive: typeof import('vue')['shallowReactive'] 59 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 60 | const shallowRef: typeof import('vue')['shallowRef'] 61 | const storeToRefs: typeof import('pinia')['storeToRefs'] 62 | const toRaw: typeof import('vue')['toRaw'] 63 | const toRef: typeof import('vue')['toRef'] 64 | const toRefs: typeof import('vue')['toRefs'] 65 | const toValue: typeof import('vue')['toValue'] 66 | const triggerRef: typeof import('vue')['triggerRef'] 67 | const unref: typeof import('vue')['unref'] 68 | const useAttrs: typeof import('vue')['useAttrs'] 69 | const useAuth: typeof import('../utils/composables/useAuth')['default'] 70 | const useCssModule: typeof import('vue')['useCssModule'] 71 | const useCssVars: typeof import('vue')['useCssVars'] 72 | const useGlobalProperties: typeof import('../utils/composables/useGlobalProperties')['default'] 73 | const useId: typeof import('vue')['useId'] 74 | const useLink: typeof import('vue-router')['useLink'] 75 | const useMenu: typeof import('../utils/composables/useMenu')['default'] 76 | const useModel: typeof import('vue')['useModel'] 77 | const useRoute: typeof import('vue-router')['useRoute'] 78 | const useRouter: typeof import('vue-router')['useRouter'] 79 | const useSlots: typeof import('vue')['useSlots'] 80 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 81 | const useViewTransition: typeof import('../utils/composables/useViewTransition')['default'] 82 | const useWindow: typeof import('../utils/composables/useWindow')['default'] 83 | const watch: typeof import('vue')['watch'] 84 | const watchEffect: typeof import('vue')['watchEffect'] 85 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 86 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 87 | } 88 | // for type re-export 89 | declare global { 90 | // @ts-ignore 91 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 92 | import('vue') 93 | } 94 | -------------------------------------------------------------------------------- /src/views/components/Menu/index.vue: -------------------------------------------------------------------------------- 1 | 169 | 170 | 183 | -------------------------------------------------------------------------------- /src/store/modules/menu.ts: -------------------------------------------------------------------------------- 1 | import apiApp from '@/api/modules/app' 2 | import menu from '@/menu' 3 | import { cloneDeep } from 'lodash-es' 4 | import useSettingsStore from './settings' 5 | import useUserStore from './user' 6 | 7 | const useMenuStore = defineStore( 8 | // 唯一ID 9 | 'menu', 10 | () => { 11 | const isGenerate = ref(false) 12 | const menus = ref([]) 13 | const actived = ref(0) 14 | 15 | function flatMenus(menus: Menu.recordRaw[], breadcrumb: Menu.breadcrumb[] = [], icon = '') { 16 | const res: Menu.recordRaw[] = [] 17 | menus.forEach((menu) => { 18 | const tmpMenu = cloneDeep(menu) 19 | // 处理面包屑导航 20 | const tmpBreadcrumb = cloneDeep(breadcrumb) 21 | tmpBreadcrumb.push({ 22 | title: tmpMenu.title, 23 | }) 24 | if (!tmpMenu.icon) { 25 | tmpMenu.icon = icon 26 | } 27 | if (tmpMenu.children) { 28 | const childrenMenu = flatMenus(tmpMenu.children, tmpBreadcrumb, tmpMenu.icon) 29 | res.push(...childrenMenu) 30 | } 31 | else { 32 | tmpMenu.breadcrumbNeste = tmpBreadcrumb 33 | res.push(tmpMenu) 34 | } 35 | }) 36 | return res 37 | } 38 | // 判断是否有权限 39 | function hasPermission(permissions: string[], menu: Menu.recordMainRaw | Menu.recordRaw) { 40 | let isAuth = false 41 | if (menu.auth) { 42 | isAuth = permissions.some((auth) => { 43 | return typeof menu.auth == 'string' 44 | ? menu.auth === auth 45 | : typeof menu.auth === 'object' 46 | ? menu.auth.includes(auth) 47 | : false 48 | }) 49 | } 50 | else { 51 | isAuth = true 52 | } 53 | return isAuth 54 | } 55 | // 根据权限过滤导航 56 | function filterAsyncMenus(menus: T, permissions: string[]): T { 57 | const res: any = [] 58 | menus.forEach((menu) => { 59 | const tmpMenu = cloneDeep(menu) 60 | if (hasPermission(permissions, tmpMenu)) { 61 | if (tmpMenu.children) { 62 | tmpMenu.children = filterAsyncMenus(tmpMenu.children, permissions) 63 | tmpMenu.children.length && res.push(tmpMenu) 64 | } 65 | else { 66 | res.push(tmpMenu) 67 | } 68 | } 69 | }) 70 | return res 71 | } 72 | 73 | const allMenus = computed(() => { 74 | let returnMenus: Menu.recordMainRaw[] 75 | const settingsStore = useSettingsStore() 76 | if (settingsStore.settings.menu.mode === 'single') { 77 | returnMenus = [{ children: [] }] 78 | menus.value.forEach((item) => { 79 | returnMenus[0].children.push(...item.children) 80 | }) 81 | } 82 | else { 83 | returnMenus = menus.value 84 | } 85 | return returnMenus 86 | }) 87 | const sidebarMenus = computed(() => { 88 | return allMenus.value.length > 0 89 | ? allMenus.value[actived.value].children 90 | : [] 91 | }) 92 | const flatMenu = computed(() => { 93 | const returnMenus: Menu.recordRaw[] = [] 94 | menus.value.forEach((item) => { 95 | returnMenus.push(...flatMenus(item.children)) 96 | }) 97 | const map: { 98 | [key: string]: Menu.recordRaw 99 | } = {} 100 | returnMenus.forEach((item) => { 101 | if (item.windowName) { 102 | map[item.windowName] = item 103 | } 104 | }) 105 | return map 106 | }) 107 | 108 | // 根据权限动态生成菜单(前端生成) 109 | async function generateMenusAtFront() { 110 | const settingsStore = useSettingsStore() 111 | const userStore = useUserStore() 112 | let accessedMenus 113 | // 如果权限功能开启,则需要对路由数据进行筛选过滤 114 | if (settingsStore.settings.app.enablePermission) { 115 | const permissions = await userStore.getPermissions() 116 | accessedMenus = filterAsyncMenus(menu, permissions) 117 | } 118 | else { 119 | accessedMenus = cloneDeep(menu) 120 | } 121 | // 设置 menus 数据 122 | isGenerate.value = true 123 | const newMenus = cloneDeep(accessedMenus) 124 | menus.value = newMenus.filter((item) => { 125 | return item.children.length !== 0 126 | }) 127 | } 128 | // 根据权限动态生成菜单(后端获取) 129 | async function generateMenusAtBack() { 130 | await apiApp.menuList().then(async (res) => { 131 | const settingsStore = useSettingsStore() 132 | const userStore = useUserStore() 133 | let accessedMenus: Menu.recordMainRaw[] 134 | // 如果权限功能开启,则需要对路由数据进行筛选过滤 135 | if (settingsStore.settings.app.enablePermission) { 136 | const permissions = await userStore.getPermissions() 137 | accessedMenus = filterAsyncMenus(res.data, permissions) 138 | } 139 | else { 140 | accessedMenus = cloneDeep(res.data) 141 | } 142 | // 设置 menus 数据 143 | isGenerate.value = true 144 | const newMenus = cloneDeep(accessedMenus) 145 | menus.value = newMenus.filter((item) => { 146 | return item.children.length !== 0 147 | }) 148 | }) 149 | } 150 | // 设置主导航 151 | function setActived(index: number) { 152 | actived.value = index 153 | } 154 | function removeMenus() { 155 | isGenerate.value = false 156 | menus.value = [] 157 | } 158 | 159 | return { 160 | isGenerate, 161 | menus, 162 | actived, 163 | allMenus, 164 | sidebarMenus, 165 | flatMenu, 166 | generateMenusAtFront, 167 | generateMenusAtBack, 168 | setActived, 169 | removeMenus, 170 | } 171 | }, 172 | ) 173 | 174 | export default useMenuStore 175 | -------------------------------------------------------------------------------- /loading.html: -------------------------------------------------------------------------------- 1 | 187 | 188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
%VITE_APP_TITLE%
196 |
载入中
197 |
198 | -------------------------------------------------------------------------------- /src/assets/icons/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/index.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 89 | 90 | 230 | --------------------------------------------------------------------------------