├── .env.dev ├── .env.prod ├── .env.test ├── public ├── img.png ├── img1.png ├── img10.png ├── img11.png ├── img12.png ├── img13.png ├── img14.png ├── img15.png ├── img16.png ├── img2.png ├── img3.png ├── img4.png ├── img5.png ├── img6.png ├── img7.png ├── img8.png ├── img9.png ├── img_1.png ├── vite.svg └── print-lock.css ├── src ├── service │ ├── list │ │ └── index.js │ ├── user │ │ └── index.js │ └── posts │ │ └── index.js ├── assets │ ├── login-banner.png │ └── vue.svg ├── pages │ ├── test │ │ ├── list.vue │ │ ├── test1.vue │ │ └── test2.vue │ ├── user │ │ ├── list │ │ │ └── index.vue │ │ └── role │ │ │ ├── components │ │ │ └── RoleFormModal.vue │ │ │ └── index.vue │ ├── com │ │ ├── lazy │ │ │ └── index.vue │ │ ├── count-down │ │ │ └── index.vue │ │ ├── modal │ │ │ └── index.vue │ │ ├── radius-tabs │ │ │ └── index.vue │ │ ├── auth │ │ │ └── index.vue │ │ ├── mark-keyword │ │ │ └── index.vue │ │ └── text-ellipsis │ │ │ └── index.vue │ ├── plugin │ │ ├── icons │ │ │ └── index.vue │ │ ├── watermark │ │ │ └── index.vue │ │ ├── check-card │ │ │ └── index.vue │ │ ├── browser-key │ │ │ └── index.vue │ │ ├── org_tree │ │ │ └── index.vue │ │ ├── lodash │ │ │ └── index.vue │ │ ├── sign │ │ │ └── index.vue │ │ ├── zip │ │ │ └── index.vue │ │ ├── excel │ │ │ └── index.vue │ │ ├── dayjs │ │ │ └── index.vue │ │ ├── calendar │ │ │ └── index.vue │ │ └── idcard │ │ │ └── index.vue │ ├── setting │ │ ├── menu-enter │ │ │ ├── components │ │ │ │ ├── HeaderSearch.vue │ │ │ │ ├── MenuFormModal.vue │ │ │ │ └── List.vue │ │ │ └── index.vue │ │ └── profile │ │ │ └── index.vue │ ├── login │ │ ├── index.vue │ │ └── components │ │ │ ├── login-form.vue │ │ │ └── banner.vue │ ├── dashboard │ │ ├── components │ │ │ ├── slot_prop.vue │ │ │ ├── child.vue │ │ │ └── sun.vue │ │ └── index.vue │ ├── exception │ │ ├── 500-page.vue │ │ ├── 401-page.vue │ │ └── 404-page.vue │ ├── case │ │ └── tab │ │ │ └── index.vue │ ├── list │ │ ├── card-list │ │ │ └── index.vue │ │ └── search-list │ │ │ └── index.vue │ ├── print │ │ └── index.vue │ └── charts │ │ ├── g2-column │ │ └── index.vue │ │ ├── line │ │ └── index.vue │ │ └── pie │ │ └── chart │ │ └── p.vue ├── layout │ ├── sider │ │ ├── index.js │ │ ├── components │ │ │ ├── Logo.vue │ │ │ ├── util.js │ │ │ ├── Menu.vue │ │ │ └── ColumnTabs.vue │ │ └── index.vue │ ├── header │ │ ├── components │ │ │ ├── Lock.vue │ │ │ └── Avatar.vue │ │ └── index.vue │ └── index.vue ├── config │ ├── index.js │ └── icons.js ├── router │ ├── index.js │ ├── group │ │ ├── home.js │ │ ├── print.js │ │ ├── case.js │ │ ├── user.js │ │ ├── list.js │ │ ├── setting.js │ │ ├── exception.js │ │ ├── chart.js │ │ ├── com.js │ │ └── plugin.js │ └── routes.js ├── components │ ├── Footer │ │ └── index.vue │ ├── ConfigProvider │ │ └── index.vue │ ├── DynamicIcons │ │ └── index.vue │ ├── global.component.js │ ├── TextEllipsis │ │ └── index.vue │ ├── FullScreen │ │ └── index.vue │ ├── ModalFooter │ │ └── index.vue │ ├── TextMark │ │ └── index.vue │ ├── RadiusTabs │ │ └── index.vue │ ├── Lock │ │ └── index.vue │ ├── ProTable │ │ └── index.vue │ ├── SearchBox │ │ └── index.vue │ └── CheckCard │ │ └── index.vue ├── store │ ├── counter.js │ ├── setting.js │ ├── user-info.js │ ├── side-menu.js │ └── dynamic-router.js ├── directive │ ├── index.js │ ├── lazyImg.js │ ├── scrollbar.js │ ├── loading.js │ └── hasAuth.js ├── utils │ ├── get.js │ ├── router-listener.js │ ├── index.js │ ├── print │ │ └── index.js │ ├── table.js │ ├── storage.js │ └── date.js ├── App.vue ├── main.js ├── hooks │ ├── util.js │ ├── useModal │ │ ├── index.js │ │ └── useModal.md │ ├── useFetch │ │ ├── demo.md │ │ └── index.js │ └── useCountDown │ │ └── index.js ├── vendor │ ├── zip.js │ └── excel.js ├── styles │ ├── variables.less │ └── global.less ├── lib │ ├── echarts.js │ └── theme │ │ └── walden.project.json ├── mock │ └── data.js └── bootstrap.js ├── env.d.ts ├── vercel.json ├── .editorconfig ├── .gitignore ├── uno.config.js ├── prettier.config.js ├── index.html ├── vite.config.js ├── package.json └── README.md /.env.dev: -------------------------------------------------------------------------------- 1 | VITE_BASE_API = https://mock.apifox.com/m1/3887514-0-default 2 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | VITE_BASE_API = https://mock.apifox.com/m1/3887514-0-default 2 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | VITE_BASE_API = https://mock.apifox.com/m1/3887514-0-default 2 | -------------------------------------------------------------------------------- /public/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img.png -------------------------------------------------------------------------------- /public/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img1.png -------------------------------------------------------------------------------- /public/img10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img10.png -------------------------------------------------------------------------------- /public/img11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img11.png -------------------------------------------------------------------------------- /public/img12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img12.png -------------------------------------------------------------------------------- /public/img13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img13.png -------------------------------------------------------------------------------- /public/img14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img14.png -------------------------------------------------------------------------------- /public/img15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img15.png -------------------------------------------------------------------------------- /public/img16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img16.png -------------------------------------------------------------------------------- /public/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img2.png -------------------------------------------------------------------------------- /public/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img3.png -------------------------------------------------------------------------------- /public/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img4.png -------------------------------------------------------------------------------- /public/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img5.png -------------------------------------------------------------------------------- /public/img6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img6.png -------------------------------------------------------------------------------- /public/img7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img7.png -------------------------------------------------------------------------------- /public/img8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img8.png -------------------------------------------------------------------------------- /public/img9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img9.png -------------------------------------------------------------------------------- /public/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img_1.png -------------------------------------------------------------------------------- /src/service/list/index.js: -------------------------------------------------------------------------------- 1 | export const API_LIST = { 2 | // 列表 3 | list: '/list/card', 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/login-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/src/assets/login-banner.png -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | // Auto generate by env-parse 3 | readonly VITE_BASE_API: string 4 | } -------------------------------------------------------------------------------- /src/pages/test/list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/user/list/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/pages/test/test1.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/service/user/index.js: -------------------------------------------------------------------------------- 1 | export const API_USER = { 2 | // 角色动态权限 3 | roleRoutes: '/user/routes', 4 | 5 | // 用户角色列表 6 | roleList: '/role/list' 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/test/test2.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /src/service/posts/index.js: -------------------------------------------------------------------------------- 1 | export const API_POSTS = { 2 | // 列表 3 | photoList: 'https://jsonplaceholder.typicode.com/photos', 4 | 5 | // 详情 6 | photoDetail: '' 7 | } 8 | -------------------------------------------------------------------------------- /src/layout/sider/index.js: -------------------------------------------------------------------------------- 1 | import ColumnTabs from '@/layout/sider/components/ColumnTabs.vue' 2 | import Menu from '@/layout/sider/components/Menu.vue' 3 | 4 | export { ColumnTabs, Menu } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /src/layout/sider/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Vue3 Vite', 3 | 4 | onlyMenu: true, 5 | 6 | /** 自定义滚动条 */ 7 | useCustomScrollBar: true, 8 | 9 | /** 后台动态路由权限 */ 10 | useDynamicRoute: false 11 | } 12 | -------------------------------------------------------------------------------- /src/layout/sider/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/pages/com/lazy/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /src/pages/plugin/icons/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/pages/setting/menu-enter/components/HeaderSearch.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/pages/login/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import { BASE_ROUTE } from './routes.js' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(), 6 | routes: BASE_ROUTE, 7 | scrollBehavior: () => ({ left: 0, top: 0 }) 8 | }) 9 | 10 | export default router 11 | -------------------------------------------------------------------------------- /src/components/Footer/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/slot_prop.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/child.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /src/components/ConfigProvider/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/store/counter.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | const useCounterStore = defineStore('counter', { 4 | state: () => ({ count: 0 }), 5 | getters: { 6 | double: (state) => state.count * 2 7 | }, 8 | actions: { 9 | increment() { 10 | this.count++ 11 | } 12 | } 13 | }) 14 | 15 | export default useCounterStore 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/pages/plugin/watermark/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /src/pages/exception/500-page.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/pages/exception/401-page.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/directive/index.js: -------------------------------------------------------------------------------- 1 | import scrollbar from './scrollbar.js' 2 | import auth from './hasAuth.js' 3 | import loading from './loading.js' 4 | import lazyImg from './lazyImg.js' 5 | 6 | const directives = { 7 | scrollbar, 8 | auth, 9 | loading, 10 | lazyImg 11 | } 12 | 13 | export default { 14 | install(app) { 15 | Object.keys(directives).forEach((key) => { 16 | app.directive(key, directives[key]) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/setting/menu-enter/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /src/pages/exception/404-page.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /src/layout/header/components/Lock.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/sun.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /uno.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetUno } from 'unocss' 2 | import presetIcons from '@unocss/preset-icons/browser' 3 | import safelist from './src/config/icons.js' 4 | 5 | export default defineConfig({ 6 | presets: [ 7 | presetUno(), 8 | presetIcons({ 9 | scale: 1.2, 10 | collections: { 11 | carbon: () => import('@iconify-json/carbon/icons.json').then((i) => i.default) 12 | } 13 | }) 14 | ], 15 | safelist 16 | }) 17 | -------------------------------------------------------------------------------- /src/pages/plugin/check-card/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/directive/lazyImg.js: -------------------------------------------------------------------------------- 1 | import { useIntersectionObserver } from '@vueuse/core' 2 | 3 | /** 4 | v-lazyImg="'https://via.placeholder.com/250'" 5 | */ 6 | export default { 7 | mounted(el, binding) { 8 | const { stop } = useIntersectionObserver( 9 | el, 10 | ([{ isIntersecting }], observerElement) => { 11 | if (isIntersecting) { 12 | stop() 13 | 14 | el.src = binding.value 15 | } 16 | }, 17 | { threshold: 0 } 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/DynamicIcons/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /src/utils/get.js: -------------------------------------------------------------------------------- 1 | /** 2 | 性能对比: at > slice > length 3 | */ 4 | export const getValByIndex = (arr, index) => { 5 | if (Object.prototype.toString.call(arr) !== '[object Array]') { 6 | console.error('arr 不是一个数组!') 7 | return 8 | } 9 | if (arr.at) { 10 | return arr.at(index) 11 | } 12 | 13 | return arr.slice(index)[0] 14 | } 15 | 16 | export const getLast = (arr) => { 17 | return getValByIndex(arr, -1) 18 | } 19 | 20 | export const getFirst = (arr) => { 21 | return getValByIndex(arr, 0) 22 | } 23 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import directive from './directive' 4 | import globalComponent from '@/components/global.component.js' 5 | import router from '@/router/index.js' 6 | import 'virtual:uno.css' 7 | import './styles/global.less' 8 | import App from './App.vue' 9 | 10 | import './bootstrap.js' 11 | 12 | const app = createApp(App) 13 | const pinia = createPinia() 14 | app.use(pinia) 15 | app.use(directive) 16 | app.use(globalComponent) 17 | app.use(router) 18 | app.mount('#app') 19 | -------------------------------------------------------------------------------- /src/pages/com/count-down/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | arrowParens: 'always', 3 | bracketSameLine: false, 4 | bracketSpacing: true, 5 | embeddedLanguageFormatting: 'auto', 6 | htmlWhitespaceSensitivity: 'ignore', 7 | insertPragma: false, 8 | jsxSingleQuote: false, 9 | printWidth: 120, 10 | proseWrap: 'preserve', 11 | quoteProps: 'as-needed', 12 | requirePragma: false, 13 | trailingComma: 'none', 14 | semi: false, 15 | singleQuote: true, 16 | tabWidth: 2, 17 | singleAttributePerLine: true, 18 | useTabs: false, 19 | vueIndentScriptAndStyle: true, 20 | } 21 | -------------------------------------------------------------------------------- /src/router/group/home.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | export default [ 4 | { 5 | path: '/', 6 | component: Layout, 7 | name: 'Base', 8 | redirect: '/home', 9 | meta: { 10 | title: '首页', 11 | isGroup: true, 12 | icon: 'i-carbon-cloud-monitoring' 13 | }, 14 | children: [ 15 | { 16 | path: '/home', 17 | name: 'Home', 18 | component: () => import('@/pages/dashboard/index.vue'), 19 | meta: { 20 | title: '首页' 21 | } 22 | } 23 | ] 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /src/pages/plugin/browser-key/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 20 | -------------------------------------------------------------------------------- /src/utils/router-listener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。 3 | */ 4 | import mitt from 'mitt' 5 | 6 | const emitter = mitt() 7 | 8 | const EMIT_KEY = 'ROUTE_CHANGE' 9 | 10 | let latestRoute 11 | 12 | export function setRouteEmitter(to) { 13 | emitter.emit(EMIT_KEY, to) 14 | latestRoute = to 15 | } 16 | 17 | export function listenerRouteChange(handler, immediate = true) { 18 | emitter.on(EMIT_KEY, handler) 19 | if (immediate && latestRoute) { 20 | handler(latestRoute) 21 | } 22 | } 23 | 24 | export function removeRouteListener() { 25 | emitter.off(EMIT_KEY) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/global.component.js: -------------------------------------------------------------------------------- 1 | import ProTable from './ProTable/index.vue' 2 | import SearchBox from './SearchBox/index.vue' 3 | import ModalFooter from './ModalFooter/index.vue' 4 | 5 | import { setupCalendar } from 'v-calendar' 6 | import vue3TreeOrg from 'vue3-tree-org' 7 | import 'vue3-tree-org/lib/vue3-tree-org.css' 8 | 9 | export default { 10 | install(app) { 11 | app.component('ProTable', ProTable) 12 | app.component('SearchBox', SearchBox) 13 | app.component('ModalFooter', ModalFooter) 14 | 15 | app.use(vue3TreeOrg) 16 | app.use(setupCalendar, {}) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/router/group/print.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/print' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'Print', 10 | meta: { 11 | title: '打印', 12 | isGroup: true, 13 | icon: 'i-carbon-printer' 14 | }, 15 | children: [ 16 | { 17 | path: `${BASE_URL}/list`, 18 | name: 'Print-List', 19 | component: () => import('@/pages/print/index.vue'), 20 | meta: { 21 | title: '打印-1', 22 | icon: 'i-carbon-printer' 23 | } 24 | } 25 | ] 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /src/pages/com/modal/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /src/router/group/case.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/case' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'Case', 10 | meta: { 11 | title: '案例', 12 | isGroup: true, 13 | icon: 'i-carbon-location-heart' 14 | }, 15 | children: [ 16 | { 17 | path: `${BASE_URL}/iso-no`, 18 | name: 'IsoNo', 19 | component: () => import('@/pages/case/tab/index.vue'), 20 | meta: { 21 | title: '非同源Tab', 22 | icon: 'i-carbon-data-check' 23 | } 24 | } 25 | ] 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /src/components/TextEllipsis/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 31 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import Scrollbar from 'smooth-scrollbar' 2 | import config from '@/config/index.js' 3 | import { isFunction } from 'lodash-es' 4 | 5 | export const scrollToByEl = (options) => { 6 | const { x = 0, y = 0, el } = options 7 | 8 | if (!el) { 9 | throw new Error('scroll is by el, el is not defined!') 10 | } 11 | const scrollDom = document.querySelector(el) 12 | 13 | if (config.useCustomScrollBar) { 14 | Scrollbar.get(scrollDom)?.scrollTo(x, y) 15 | } else { 16 | scrollDom?.scrollTo(x, y) 17 | } 18 | } 19 | 20 | export const isVueComponent = (obj) => { 21 | return isFunction(obj.render) || isFunction(obj.setup) 22 | } 23 | -------------------------------------------------------------------------------- /src/layout/sider/components/util.js: -------------------------------------------------------------------------------- 1 | export const getTabMenu = (routes) => { 2 | const tabMenu = [] 3 | routes.forEach((it) => { 4 | const { isGroup, hideInMenu } = it.meta || {} 5 | if (isGroup && !hideInMenu) { 6 | tabMenu.push(it) 7 | } 8 | }) 9 | 10 | return tabMenu 11 | } 12 | 13 | export const getRedirectPath = (route) => { 14 | if (!route) return '' 15 | 16 | let path = route.path 17 | const getPath = (child) => { 18 | if (child[0].children && child[0].children.length) { 19 | getPath(child[0].children) 20 | } else { 21 | path = child[0].path 22 | } 23 | } 24 | getPath([route]) 25 | 26 | return path 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/util.js: -------------------------------------------------------------------------------- 1 | export const inBrowser = typeof window !== 'undefined' 2 | 3 | // Keep forward compatible 4 | // should be removed in next major version 5 | export const supportsPassive = true 6 | 7 | /** 8 | * @param fn FrameRequestCallback 9 | * @returns { number } 10 | */ 11 | export function raf(fn) { 12 | return inBrowser ? requestAnimationFrame(fn) : -1 13 | } 14 | 15 | /** 16 | * @param id { number } 17 | */ 18 | export function cancelRaf(id) { 19 | if (inBrowser) { 20 | cancelAnimationFrame(id) 21 | } 22 | } 23 | 24 | /** 25 | * double raf for animation 26 | * @param fn { FrameRequestCallback } 27 | */ 28 | export function doubleRaf(fn) { 29 | raf(() => raf(fn)) 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/com/radius-tabs/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | -------------------------------------------------------------------------------- /src/components/FullScreen/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /src/store/setting.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getLock, setLock } from '@/utils/storage.js' 3 | 4 | const useSettingStore = defineStore('setting', { 5 | state: () => ({ 6 | collapsed: false, 7 | openKeys: [], 8 | selectedKeys: [], 9 | 10 | lockScreen: getLock() 11 | }), 12 | actions: { 13 | toggleCollapsed() { 14 | this.collapsed = !this.collapsed 15 | }, 16 | 17 | changeMenuHighlight(openKeys, selectedKeys) { 18 | this.openKeys = openKeys 19 | this.selectedKeys = selectedKeys 20 | }, 21 | 22 | toggleLock() { 23 | this.lockScreen = !this.lockScreen 24 | setLock(this.lockScreen) 25 | } 26 | } 27 | }) 28 | 29 | export default useSettingStore 30 | -------------------------------------------------------------------------------- /src/vendor/zip.js: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver' 2 | import JSZip from 'jszip' 3 | 4 | export function export_txt_to_zip(th, jsonData, txtName, zipName) { 5 | const zip = new JSZip() 6 | const txt_name = txtName || 'file' 7 | const zip_name = zipName || 'file' 8 | const data = jsonData 9 | let txtData = `${th}\r\n` 10 | data.forEach((row) => { 11 | let tempStr = '' 12 | tempStr = row.toString() 13 | txtData += `${tempStr}\r\n` 14 | }) 15 | zip.file(`${txt_name}.txt`, txtData) 16 | zip 17 | .generateAsync({ 18 | type: 'blob' 19 | }) 20 | .then( 21 | (blob) => { 22 | saveAs(blob, `${zip_name}.zip`) 23 | }, 24 | (err) => { 25 | alert('导出失败') 26 | } 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/variables.less: -------------------------------------------------------------------------------- 1 | @primary-color: #2546f0; 2 | 3 | @header-height: 60px; 4 | 5 | @menu-width: 184px; 6 | 7 | @box-shadow: 8 | 0 1px 2px 0 rgba(0, 0, 0, 0.03), 9 | 0 1px 6px -1px rgba(0, 0, 0, 0.02), 10 | 0 2px 4px 0 rgba(0, 0, 0, 0.02); 11 | 12 | @scroll-content-min-width: 600px; 13 | 14 | @main-bg: rgb(242, 243, 245); 15 | 16 | // ==============breakpoint============ 17 | 18 | // Extra small screen / phone 19 | @screen-xs: 480px; 20 | 21 | // Small screen / tablet 22 | @screen-sm: 576px; 23 | 24 | // Medium screen / desktop 25 | @screen-md: 768px; 26 | 27 | // Large screen / wide desktop 28 | @screen-lg: 992px; 29 | 30 | // Extra large screen / full hd 31 | @screen-xl: 1200px; 32 | 33 | // Extra extra large screen / large desktop 34 | @screen-xxl: 1600px; 35 | -------------------------------------------------------------------------------- /src/pages/case/tab/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 32 | -------------------------------------------------------------------------------- /src/components/ModalFooter/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | -------------------------------------------------------------------------------- /src/utils/print/index.js: -------------------------------------------------------------------------------- 1 | import { hiprint, disAutoConnect } from 'vue-plugin-hiprint' 2 | import tem1 from '@/utils/print/template/tem1.js' 3 | 4 | disAutoConnect() 5 | 6 | const templates = { 7 | tem1 8 | } 9 | 10 | class Print { 11 | constructor(template) { 12 | this.hiprintTemplate = new hiprint.PrintTemplate({ 13 | template: templates[template] 14 | }) 15 | } 16 | 17 | toPdf(printData, name = 'pdf') { 18 | this.hiprintTemplate.toPdf(printData, name) 19 | } 20 | 21 | print(printData) { 22 | this.hiprintTemplate.print( 23 | printData, 24 | {}, 25 | { 26 | callback: () => { 27 | console.log('浏览器打印窗口已打开') 28 | } 29 | } 30 | ) 31 | } 32 | 33 | getPreviewHtml(printData) { 34 | return this.hiprintTemplate.getHtml(printData)[0].outerHTML 35 | } 36 | } 37 | 38 | export default Print 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | 17 | 22 | 28 | Vite + Vue 29 | 30 | 31 |
32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/router/group/user.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/user' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'User', 10 | meta: { 11 | title: '用户', 12 | isGroup: true, 13 | icon: 'i-carbon-user-multiple' 14 | }, 15 | children: [ 16 | { 17 | path: `${BASE_URL}/list`, 18 | name: 'User_List', 19 | component: () => import('@/pages/user/list/index.vue'), 20 | meta: { 21 | title: '用户列表', 22 | icon: 'i-carbon-user-profile' 23 | } 24 | }, 25 | { 26 | path: `${BASE_URL}/role`, 27 | name: 'User_Role', 28 | component: () => import('@/pages/user/role/index.vue'), 29 | meta: { 30 | title: '角色列表', 31 | icon: 'i-carbon-user-role' 32 | } 33 | } 34 | ] 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /src/router/group/list.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/list' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'List', 10 | meta: { 11 | title: '列表', 12 | isGroup: true, 13 | icon: 'i-carbon-ibm-cloud-transit-gateway' 14 | }, 15 | children: [ 16 | { 17 | path: `${BASE_URL}/search`, 18 | name: 'List-Search', 19 | component: () => import('@/pages/list/search-list/index.vue'), 20 | meta: { 21 | title: '查询表格', 22 | icon: 'i-carbon-list' 23 | } 24 | }, 25 | { 26 | path: `${BASE_URL}/card`, 27 | name: 'List-Card', 28 | component: () => import('@/pages/list/card-list/index.vue'), 29 | meta: { 30 | title: '卡片表格', 31 | icon: 'i-carbon-list-boxes' 32 | } 33 | } 34 | ] 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /src/router/group/setting.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/setting' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'Setting', 10 | meta: { 11 | title: '设置', 12 | isGroup: true, 13 | icon: 'i-carbon-settings' 14 | }, 15 | children: [ 16 | { 17 | path: `${BASE_URL}/menu`, 18 | name: 'Setting_Menu', 19 | component: () => import('@/pages/setting/menu-enter/index.vue'), 20 | meta: { 21 | title: '菜单列表', 22 | icon: 'i-carbon-list' 23 | } 24 | }, 25 | { 26 | path: `${BASE_URL}/profile`, 27 | name: 'Setting_Profile', 28 | component: () => import('@/pages/setting/profile/index.vue'), 29 | meta: { 30 | title: '个人中心', 31 | icon: 'i-carbon-user-data' 32 | } 33 | } 34 | ] 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /src/store/user-info.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import useDynamicRouterStore from '@/store/dynamic-router.js' 3 | import { removeRouteListener } from '@/utils/router-listener.js' 4 | import { getUserInfo, removeToken, removeUserInfo, setToken, setUserInfo } from '@/utils/storage.js' 5 | 6 | const useUserInfoStore = defineStore('userInfo', { 7 | state: () => ({ 8 | userInfo: getUserInfo() 9 | }), 10 | actions: { 11 | login() { 12 | const us = { name: 'Ealien', age: 18 } 13 | 14 | this.userInfo = us 15 | setUserInfo(us) 16 | setToken('asdasd') 17 | }, 18 | logout() { 19 | const dynamicRouter = useDynamicRouterStore() 20 | 21 | removeToken() 22 | this.userInfo = {} 23 | removeUserInfo() 24 | removeRouteListener() 25 | dynamicRouter.syncRoutes = false 26 | dynamicRouter.roleRoutes = [] 27 | } 28 | } 29 | }) 30 | 31 | export default useUserInfoStore 32 | -------------------------------------------------------------------------------- /src/hooks/useModal/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { Modal, message } from 'ant-design-vue' 3 | import { isVueComponent } from '@/utils/index.js' 4 | 5 | export const useModal = (modalProps) => { 6 | const open = (component, props) => { 7 | if (!isVueComponent(component)) { 8 | message.warn('这里强制是VueComponent,否则你不应该使用它!') 9 | } 10 | 11 | const modal = Modal.confirm({ 12 | title: 'useModal 请设置title覆盖', 13 | content: h( 14 | component, 15 | { 16 | ...props, 17 | ok: (val) => { 18 | props.ok && props.ok(val) 19 | modal.destroy() 20 | }, 21 | cancel: () => { 22 | modal.destroy() 23 | } 24 | }, 25 | () => null 26 | ), 27 | icon: null, 28 | closable: true, 29 | ...modalProps, 30 | footer: null, 31 | wrapClassName: 'use-modal' 32 | }) 33 | } 34 | 35 | return { 36 | open 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | 24 | 44 | -------------------------------------------------------------------------------- /src/lib/echarts.js: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/core' 2 | import { 3 | DatasetComponent, 4 | TooltipComponent, 5 | GridComponent, 6 | LegendComponent, 7 | TitleComponent, 8 | GraphicComponent, 9 | ToolboxComponent 10 | } from 'echarts/components' 11 | import { 12 | BarChart, 13 | LineChart, 14 | PieChart, 15 | GaugeChart, 16 | CustomChart, 17 | GraphChart, 18 | LinesChart, 19 | PictorialBarChart 20 | } from 'echarts/charts' 21 | import { CanvasRenderer } from 'echarts/renderers' 22 | import walden from './theme/walden.project.json' 23 | 24 | const components = [ 25 | ToolboxComponent, 26 | DatasetComponent, 27 | TooltipComponent, 28 | GridComponent, 29 | LegendComponent, 30 | TitleComponent, 31 | GraphicComponent 32 | ] 33 | 34 | const charts = [BarChart, LineChart, PieChart, GaugeChart, CustomChart, GraphChart, LinesChart, PictorialBarChart] 35 | 36 | echarts.use([...components, ...charts, CanvasRenderer]) 37 | 38 | echarts.registerTheme('walden', walden.theme) 39 | 40 | export default echarts 41 | -------------------------------------------------------------------------------- /src/hooks/useModal/useModal.md: -------------------------------------------------------------------------------- 1 | ### 示例 2 | 3 | ```js 4 | const modal = useModal({ title: '你好', width: '800px' }) 5 | const handleFormModal = () => { 6 | modal.open(FormMenu, { 7 | id: 1, 8 | limit: 10, 9 | /// 对应FormMenu组件的ok,函数名称固定 10 | ok: (val) => { 11 | console.log('=>(List.vue:61) val', val) 12 | console.log('操作了ok') 13 | } 14 | }) 15 | } 16 | ``` 17 | 18 | ### MenuFormModal 组件props(弹窗组件 + 弹窗组件按钮) 19 | 20 | ```js 21 | const props = defineProps({ 22 | id: { 23 | type: Number 24 | }, 25 | limit: { 26 | type: Number 27 | }, 28 | ok: { 29 | type: Function 30 | }, 31 | cancel: { 32 | type: Function 33 | } 34 | }) 35 | 36 | const ok = (val) => { 37 | // 校验等。。。 38 | 39 | // 这里固定格式 40 | props.ok && props.ok(val) 41 | } 42 | 43 | const handleCancel = () => { 44 | // 取消函数内部固定格式,暂时无法优化到不用写取消函数 45 | props.cancel && props.cancel() 46 | } 47 | ``` 48 | 49 | ### 弹窗组件底部按钮 50 | 51 | ```vue 52 | 53 | ``` 54 | -------------------------------------------------------------------------------- /src/config/icons.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'i-carbon-home', 3 | 'i-carbon-list-boxes', 4 | 'i-carbon-ibm-cloud-transit-gateway', 5 | 'i-carbon-list', 6 | 'i-carbon-location-heart', 7 | 'i-carbon-cloud-monitoring', 8 | 'i-carbon-data-check', 9 | 'i-carbon-ibm-content-services', 10 | 'i-carbon-encryption', 11 | 'i-carbon-chart-bar', 12 | 'i-carbon-chart-line', 13 | 'i-carbon-chart-combo', 14 | 'i-carbon-chart-pie', 15 | 'i-carbon-printer', 16 | 'i-carbon-decision-tree', 17 | 'i-carbon-calendar', 18 | 'i-carbon-id-management', 19 | 'i-carbon-contour-draw', 20 | 'i-carbon-event-schedule', 21 | 'i-carbon-time', 22 | 'i-carbon-model-alt', 23 | 'i-carbon-settings', 24 | 'i-carbon-user-multiple', 25 | 'i-carbon-user-role', 26 | 'i-carbon-user-profile', 27 | 'i-carbon-list', 28 | 'i-carbon-ibm-cloud-key-protect', 29 | 'i-carbon-user-data', 30 | 'i-carbon-ibm-toolchain', 31 | 'i-carbon-dot-mark', 32 | 'i-carbon-text-footnote', 33 | 'i-carbon-image', 34 | 'i-carbon-table-alias', 35 | 'i-carbon-zip', 36 | 'i-carbon-document' 37 | ] 38 | -------------------------------------------------------------------------------- /src/pages/list/card-list/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /src/utils/table.js: -------------------------------------------------------------------------------- 1 | /** 2 | * const columns = setTableColumn([ 3 | * { no: '集合编号' }, 4 | * { name: '集合名称' }, 5 | * { type: '内容体裁' }, 6 | * { fs: '筛选方式' }, 7 | * { action: { title: '操作', width: '130px', ... } } 8 | * ]) 9 | * @param simpleArr 10 | */ 11 | export const setTableColumn = (simpleArr = []) => { 12 | if (!simpleArr || !Array.isArray(simpleArr) || !simpleArr?.length) { 13 | return [] 14 | } 15 | 16 | return simpleArr.map((it) => { 17 | const key = Object.keys(it)[0] 18 | const val = it[key] 19 | 20 | return typeof val === 'string' 21 | ? { 22 | title: val, 23 | dataIndex: key, 24 | key 25 | } 26 | : { 27 | dataIndex: key, 28 | key, 29 | ...val 30 | } 31 | }) 32 | } 33 | 34 | export const getScrollHeight = (className, extraBottom) => { 35 | try { 36 | const rect = document.querySelector(className).getBoundingClientRect() 37 | return window.innerHeight - rect.top - extraBottom 38 | } catch { 39 | console.log('滚动区域高度计算失败') 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/com/auth/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 49 | -------------------------------------------------------------------------------- /src/pages/plugin/org_tree/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 45 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import path from 'path' 3 | import vue from '@vitejs/plugin-vue' 4 | import { envParse } from 'vite-plugin-env-parse' 5 | import UnoCSS from 'unocss/vite' 6 | import Components from 'unplugin-vue-components/vite' 7 | import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | resolve: { 12 | alias: { 13 | '@': path.resolve(__dirname, 'src') 14 | } 15 | }, 16 | plugins: [ 17 | vue(), 18 | envParse(), 19 | UnoCSS(), 20 | Components({ 21 | resolvers: [ 22 | AntDesignVueResolver({ 23 | importStyle: false // css in js 24 | }) 25 | ] 26 | }) 27 | ], 28 | server: { 29 | warmup: { 30 | clientFiles: ['./*.html', './src/global.component.js'] 31 | } 32 | }, 33 | css: { 34 | preprocessorOptions: { 35 | less: { 36 | modifyVars: { 37 | hack: `true; @import (reference) "${path.resolve('src/styles/variables.less')}";` 38 | }, 39 | javascriptEnabled: true 40 | } 41 | } 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /src/pages/setting/profile/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 48 | -------------------------------------------------------------------------------- /src/router/group/exception.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/exception' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'Exception', 10 | redirect: `${BASE_URL}/404-page`, 11 | meta: { 12 | title: '异常页', 13 | isGroup: true, 14 | icon: 'i-carbon-ibm-cloud-transit-gateway' 15 | }, 16 | children: [ 17 | { 18 | path: `${BASE_URL}/404-page`, 19 | name: '404-PAGE', 20 | component: () => import('@/pages/exception/404-page.vue'), 21 | meta: { 22 | title: '404', 23 | icon: 'i-carbon-list-boxes' 24 | } 25 | }, 26 | { 27 | path: `${BASE_URL}/401-page`, 28 | name: '401-PAGE', 29 | component: () => import('@/pages/exception/401-page.vue'), 30 | meta: { 31 | title: '401', 32 | icon: 'i-carbon-list-boxes' 33 | } 34 | }, 35 | { 36 | path: `${BASE_URL}/500-page`, 37 | name: '500-PAGE', 38 | component: () => import('@/pages/exception/500-page.vue'), 39 | meta: { 40 | title: '50x', 41 | icon: 'i-carbon-list-boxes' 42 | } 43 | } 44 | ] 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | // 屏幕锁 2 | const LOCK_KEY = 'lock_screen' 3 | 4 | export const setLock = (isLock) => { 5 | localStorage.setItem(LOCK_KEY, isLock) 6 | } 7 | 8 | export const getLock = () => { 9 | return JSON.parse(localStorage.getItem(LOCK_KEY) || 'false') 10 | } 11 | 12 | // 锁屏密码 13 | const LOCK_PAS_KEY = 'lock_pas' 14 | 15 | export const setLockPas = (pas = '123456') => { 16 | return localStorage.setItem(LOCK_PAS_KEY, pas) 17 | } 18 | 19 | export const getLockPas = () => { 20 | return localStorage.getItem(LOCK_PAS_KEY) 21 | } 22 | 23 | // 用户TOKEN 24 | const TOKEN_KEY = 'tk' 25 | 26 | export const setToken = (token) => { 27 | localStorage.setItem(TOKEN_KEY, token) 28 | } 29 | 30 | export const getToken = () => { 31 | return localStorage.getItem(TOKEN_KEY) 32 | } 33 | 34 | export const removeToken = () => { 35 | localStorage.removeItem(TOKEN_KEY) 36 | } 37 | 38 | // 用户信息 39 | const USER_INFO_KEY = 'usi' 40 | 41 | export const setUserInfo = (userInfo) => { 42 | localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo || {})) 43 | } 44 | 45 | export const getUserInfo = () => { 46 | return JSON.parse(localStorage.getItem(USER_INFO_KEY)) 47 | } 48 | 49 | export const removeUserInfo = () => { 50 | localStorage.removeItem(USER_INFO_KEY) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/TextMark/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 44 | 45 | 50 | -------------------------------------------------------------------------------- /src/hooks/useFetch/demo.md: -------------------------------------------------------------------------------- 1 | ## 可以不使用await, 这样不会阻塞页面 2 | 3 | ## useFetch 没有对get请求参数做很好的处理(query参数只能手动拼接到url中),所以二次封装了 4 | 5 | ### 原来get请求url拼接示例 6 | ```js 7 | useFetch('https://www.baidu.com/asd?name=1&age=18').json() 8 | ``` 9 | 10 | ### 封装后的get请求参数处理 11 | ### 示例1 响应式查询参数 12 | ```js 13 | const inputVal = ref({ id: '1', name: null, age: undefined, as: '' }) 14 | const { data, isFetching, execute } = await useFetch(API_POSTS.photos, { 15 | params: inputVal 16 | }).json() 17 | ``` 18 | 19 | 20 | ### 示例2 响应式查询参数 21 | ```js 22 | const state = reactive({ 23 | page_index: 1, 24 | page_size: 2, 25 | id: 1 26 | }) 27 | 28 | const { data, isFetching, execute } = await useFetch(API_POSTS.photos, { 29 | params: state 30 | }).json() 31 | ``` 32 | 33 | ### 示例3 响应式payload, 使用reactive定义参数;继续执行execute数据会是最新的 34 | ```js 35 | const state = reactive({ 36 | page_index: 1, 37 | page_size: 2, 38 | id: 1 39 | }) 40 | 41 | const { data, isFetching, execute } = await useFetch(API_POSTS.photos).post(state).json() 42 | ``` 43 | 44 | ### 示例4 payload只初始化一次 45 | ```js 46 | const state = ref({ 47 | page_index: 1, 48 | page_size: 2, 49 | id: 1 50 | }) 51 | 52 | const { data, isFetching, execute } = await useFetch(API_POSTS.photos).post({ ...state.value }).json() 53 | ``` 54 | -------------------------------------------------------------------------------- /src/pages/plugin/lodash/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/pages/print/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/components/RadiusTabs/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 56 | -------------------------------------------------------------------------------- /src/pages/charts/g2-column/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/directive/scrollbar.js: -------------------------------------------------------------------------------- 1 | import Scrollbar from 'smooth-scrollbar' 2 | import config from '@/config/index.js' 3 | 4 | const extractProp = (prop) => (obj) => (typeof obj === 'undefined' ? undefined : obj[prop]) 5 | const extractOptions = extractProp('options') 6 | const extractEl = extractProp('el') 7 | 8 | const bestMatch = (extractor) => (possibilities) => 9 | extractor(possibilities.find((p) => typeof extractor(p) !== 'undefined')) 10 | const bestEl = bestMatch(extractEl) 11 | const bestOptions = bestMatch(extractOptions) 12 | 13 | /** 14 | v-scrollbar 15 | v-scrollbar="{ el: "" }" 16 | */ 17 | export default { 18 | mounted(el, binding) { 19 | if (config.useCustomScrollBar) { 20 | const possibilities = [binding.value] 21 | const targetEl = bestEl(possibilities) 22 | const config = bestOptions(possibilities) 23 | 24 | const scrollY = binding.modifiers.y 25 | const scrollX = binding.modifiers.x 26 | Scrollbar.init(targetEl ? document.querySelector(targetEl) : el) 27 | } 28 | }, 29 | 30 | updated(el, binding, vnode, prevVnode) {}, 31 | 32 | unmounted(el, binding) { 33 | if (config.useCustomScrollBar) { 34 | const possibilities = [binding.value] 35 | const targetEl = bestEl(possibilities) 36 | Scrollbar.destroy(targetEl ? document.querySelector(targetEl) : el, {}) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/plugin/sign/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/layout/sider/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 37 | 38 | 55 | -------------------------------------------------------------------------------- /src/router/group/chart.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/chart' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'Chart', 10 | meta: { 11 | title: '图表', 12 | isGroup: true, 13 | icon: 'i-carbon-chart-combo' 14 | }, 15 | children: [ 16 | { 17 | path: `${BASE_URL}/bar`, 18 | name: 'Chart_Bar', 19 | component: () => import('@/pages/charts/bar/index.vue'), 20 | meta: { 21 | title: '柱状图', 22 | icon: 'i-carbon-chart-bar' 23 | } 24 | }, 25 | { 26 | path: `${BASE_URL}/line`, 27 | name: 'Chart_Line', 28 | component: () => import('@/pages/charts/line/index.vue'), 29 | meta: { 30 | title: '折线图', 31 | icon: 'i-carbon-chart-line' 32 | } 33 | }, 34 | { 35 | path: `${BASE_URL}/pie`, 36 | name: 'Chart_Pie', 37 | component: () => import('@/pages/charts/pie/index.vue'), 38 | meta: { 39 | title: '饼图', 40 | icon: 'i-carbon-chart-pie' 41 | } 42 | }, 43 | { 44 | path: `${BASE_URL}/column`, 45 | name: 'Chart2_Pie', 46 | component: () => import('@/pages/charts/g2-column/index.vue'), 47 | meta: { 48 | title: 'G2-堆叠', 49 | icon: 'i-carbon-chart-pie' 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/com/mark-keyword/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/layout/header/components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 20 | 44 | 45 | 51 | -------------------------------------------------------------------------------- /src/directive/loading.js: -------------------------------------------------------------------------------- 1 | const loading = `` 2 | 3 | /** 4 | * 通过自定义样式(global.less),对 primary 类型按钮,和官方示例一样。事件只执行一次 5 | * 6 | * 默认值1500毫秒 7 | * v-loading="2000" 8 | * v-loading === v-loading="1500" 9 | */ 10 | export default { 11 | mounted(el, binding) { 12 | const originInnerHtml = el.innerHTML 13 | 14 | if (binding.value && typeof binding.value !== 'number') { 15 | console.error('自定义时间应为数字 例: v-loading="2000"') 16 | return 17 | } 18 | 19 | el.addEventListener( 20 | 'click', 21 | () => { 22 | if (!el.disabled) { 23 | el.disabled = true 24 | el.innerHTML = `${loading}${originInnerHtml}` 25 | 26 | setTimeout(() => { 27 | el.innerHTML = originInnerHtml 28 | el.disabled = false 29 | }, binding.value || 1500) 30 | } 31 | }, 32 | false 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/mock/data.js: -------------------------------------------------------------------------------- 1 | export const typeEnum = { 2 | '图文': 'https://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image', 3 | '横版短视频': 4 | 'https://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77721e365eb2ab786c889682cbc721c1.svg~tplv-49unhts6dw-image.image', 5 | '竖版短视频': 6 | 'https://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/ea8b09190046da0ea7e070d83c5d1731.svg~tplv-49unhts6dw-image.image' 7 | } 8 | 9 | /// 通过name匹配的, 其他信息可传入后覆盖, 更改为Apifox云端mock获取 10 | export const adminRoutes = [ 11 | { 12 | id: 1, 13 | name: 'Chart', 14 | path: '/chart', 15 | title: '图标', 16 | status: 1, 17 | children: [ 18 | { 19 | id: 2, 20 | title: '柱状图', 21 | path: '/chart/bar', 22 | name: 'Chart_Bar', 23 | status: 1 24 | }, 25 | { 26 | id: 3, 27 | title: '折线图', 28 | path: '/chart/line', 29 | name: 'Chart_Line', 30 | status: 0 31 | }, 32 | { 33 | id: 4, 34 | title: '饼图', 35 | path: '/chart/pie', 36 | name: 'Chart2_Pie', 37 | status: 1 38 | } 39 | ] 40 | }, 41 | { 42 | id: 5, 43 | name: 'Print', 44 | path: '/print', 45 | title: '打印', 46 | status: 1, 47 | children: [ 48 | { 49 | id: 6, 50 | status: 1, 51 | title: '打印列表', 52 | name: 'Print-List', 53 | path: '/print/list' 54 | } 55 | ] 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | import HomeGroup from './group/home.js' 3 | import ComGroup from './group/com.js' 4 | import PluginGroup from './group/plugin.js' 5 | import CaseGroup from './group/case.js' 6 | import ListGroup from './group/list.js' 7 | import ExceptionGroup from './group/exception.js' 8 | import ChartGroup from './group/chart.js' 9 | import PrintGroup from './group/print.js' 10 | import UserGroup from './group/user.js' 11 | import SettingGroup from './group/setting.js' 12 | 13 | /** 14 | * meta: { icon, hideInMenu, title } 15 | * 16 | * TIP path必须写完整的路径,要做跳转匹配, path必填项-要匹配路由 17 | */ 18 | export const BASE_ROUTE = [ 19 | { 20 | path: '/login', 21 | name: 'Login', 22 | component: () => import('@/pages/login/index.vue'), 23 | meta: { 24 | title: '登录' 25 | } 26 | }, 27 | ...HomeGroup, 28 | ...ExceptionGroup, 29 | { 30 | path: '/:path(.*)*', 31 | name: '404-page', 32 | component: Layout, 33 | meta: { 34 | title: '404页面找不到' 35 | }, 36 | children: [ 37 | { 38 | path: '/:path(.*)*', 39 | name: '404-child-page', 40 | component: () => import('@/pages/exception/404-page.vue'), 41 | meta: { 42 | title: '404页面找不到' 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | 49 | export const DYNAMIC_ROUTE = [ 50 | ...ComGroup, 51 | ...PluginGroup, 52 | ...CaseGroup, 53 | ...ChartGroup, 54 | ...UserGroup, 55 | ...ListGroup, 56 | ...PrintGroup, 57 | ...SettingGroup 58 | ] 59 | -------------------------------------------------------------------------------- /src/store/side-menu.js: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import config from '@/config/index.js' 4 | 5 | const generator = (routerMap) => { 6 | return routerMap.map((item) => { 7 | const { title, hideInMenu, icon } = item.meta || {} 8 | const currentRouter = { 9 | label: title, 10 | key: item.path, 11 | icon: icon ? h('i', { class: icon }) : null 12 | // router警告组件是响应式时可使用 shallowRef 包裹 13 | // https://cn.vuejs.org/api/reactivity-advanced.html#shallowref 14 | } 15 | 16 | item.redirect && (currentRouter.redirect = item.redirect) 17 | 18 | if (item.children && item.children.length > 0) { 19 | currentRouter.children = generator(item.children) 20 | } 21 | return hideInMenu ? null : currentRouter 22 | }) 23 | } 24 | 25 | const emptyMenu = [{ label: '', key: '' }] 26 | 27 | const useSideMenuStore = defineStore('sideMenu', { 28 | state: () => ({ menus: emptyMenu, menuMap: new Map() }), 29 | getters: { 30 | onlyMenu() { 31 | return config.onlyMenu 32 | ? this.menus.length > 1 || (this.menus[0] && this.menus[0].children && this.menus[0].children.length) 33 | : true 34 | } 35 | }, 36 | actions: { 37 | changeSide(side) { 38 | if(!side) return 39 | 40 | if (!this.menuMap.has(side.path)) { 41 | this.menuMap.set(side.path, generator(side.children) || []) 42 | } 43 | 44 | this.menus = this.menuMap.get(side.path) 45 | } 46 | } 47 | }) 48 | 49 | export default useSideMenuStore 50 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | import router from '@/router/index.js' 2 | import config from '@/config/index.js' 3 | import NProgress from 'nprogress' 4 | import 'nprogress/nprogress.css' 5 | import useDynamicRouterStore from '@/store/dynamic-router.js' 6 | import { scrollToByEl } from '@/utils/index.js' 7 | import { getToken, setLockPas, getLockPas } from '@/utils/storage.js' 8 | import { setRouteEmitter } from '@/utils/router-listener.js' 9 | 10 | const LOGIN_PATH = '/login' 11 | 12 | /// set default lock screen pas 13 | if (!getLockPas()) { 14 | setLockPas() 15 | } 16 | 17 | NProgress.configure({ showSpinner: false }) 18 | 19 | router.beforeEach(async (to) => { 20 | setRouteEmitter(to) 21 | 22 | NProgress.start() 23 | 24 | document.title = to.meta.title || config.title 25 | 26 | const token = getToken() 27 | if (token) { 28 | if (to.path === LOGIN_PATH) { 29 | router.replace('/') 30 | } else { 31 | const dynamicRouter = useDynamicRouterStore() 32 | if (!dynamicRouter.syncRoutes) { 33 | await dynamicRouter.getUserRoutes() 34 | 35 | const resultRoute = await dynamicRouter.generator() 36 | 37 | resultRoute.forEach((route) => router.addRoute(route)) 38 | 39 | dynamicRouter.syncRoutes = true 40 | 41 | // TIP: 如果想做登录后的redirect,需要检测路径是否存在(权限变更,路径不存在等问题) 42 | router.replace(to.fullPath) 43 | } 44 | } 45 | } else { 46 | to.path !== LOGIN_PATH && router.replace(`${LOGIN_PATH}?redirect=${to.fullPath}`) 47 | } 48 | 49 | scrollToByEl({ el: '.content_wrap' }) 50 | NProgress.done() 51 | }) 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --mode dev", 8 | "build:dev": "vite build --mode dev", 9 | "build:test": "vite build --mode test", 10 | "build:prod": "vite build --mode prod", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@antv/g2": "^5.1.10", 15 | "@fingerprintjs/fingerprintjs": "^4.1.0", 16 | "@popperjs/core": "^2.11.8", 17 | "@vueuse/core": "^12.2.0", 18 | "ant-design-vue": "^4.2.6", 19 | "codemirror": "^5.65.16", 20 | "codemirror-editor-vue3": "^2.4.1", 21 | "dayjs": "^1.11.13", 22 | "echarts": "^5.4.3", 23 | "file-saver": "^2.0.5", 24 | "idcard": "^4.2.0", 25 | "jszip": "^3.10.1", 26 | "lodash-es": "^4.17.21", 27 | "mitt": "^3.0.1", 28 | "nprogress": "^0.2.0", 29 | "pinia": "^2.3.0", 30 | "smooth-scrollbar": "^8.8.4", 31 | "v-calendar": "^3.1.2", 32 | "vue": "^3.5.13", 33 | "vue-esign": "^1.1.4", 34 | "vue-json-pretty": "^2.4.0", 35 | "vue-plugin-hiprint": "^0.0.56", 36 | "vue-router": "^4.5.0", 37 | "vue3-tree-org": "^4.2.2", 38 | "xlsx": "^0.18.5" 39 | }, 40 | "devDependencies": { 41 | "@iconify-json/carbon": "^1.2.5", 42 | "@unocss/preset-icons": "^0.65.2", 43 | "@vitejs/plugin-vue": "^5.2.1", 44 | "less": "^4.2.1", 45 | "prettier": "^3.4.2", 46 | "unocss": "^0.65.2", 47 | "unplugin-vue-components": "^0.28.0", 48 | "vite": "^6.0.5", 49 | "vite-plugin-env-parse": "^1.0.15" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/plugin/zip/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 59 | -------------------------------------------------------------------------------- /src/pages/com/text-ellipsis/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 62 | -------------------------------------------------------------------------------- /src/layout/header/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 30 | 31 | 69 | -------------------------------------------------------------------------------- /src/pages/setting/menu-enter/components/MenuFormModal.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 72 | -------------------------------------------------------------------------------- /src/styles/global.less: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | width: 100%; 5 | margin: 0; 6 | 7 | // 防止左右滑动返回 8 | overscroll-behavior: none; 9 | } 10 | 11 | #app { 12 | width: 100%; 13 | height: 100%; 14 | overflow: hidden; 15 | } 16 | 17 | .header_title { 18 | font-size: 16px; 19 | font-weight: 500; 20 | color: rgb(29, 33, 41); 21 | margin-bottom: 16px; 22 | } 23 | 24 | // reset smooth-scrollbar 25 | .scrollbar-track { 26 | background: transparent !important; 27 | } 28 | 29 | .scrollbar-track-y, 30 | .scrollbar-thumb.scrollbar-thumb-y { 31 | width: 6px !important; 32 | } 33 | 34 | .scrollbar-track-x, 35 | .scrollbar-thumb.scrollbar-thumb-x { 36 | height: 6px !important; 37 | } 38 | 39 | //textarea:-webkit-autofill 40 | //select:-webkit-autofill 41 | input:-webkit-autofill { 42 | -webkit-box-shadow: 0 0 0 1000px transparent inset !important; 43 | background-color: transparent !important; 44 | background-image: none; 45 | transition: background-color 50000s ease-in-out 0s; 46 | -webkit-text-fill-color: black !important; 47 | } 48 | 49 | // 覆盖useModal 50 | .ant-modal-wrap.use-modal .ant-modal-confirm-content { 51 | max-width: 100% !important; 52 | } 53 | 54 | .ant-modal-wrap.use-modal .ant-modal-confirm-title { 55 | margin-bottom: 10px; 56 | } 57 | 58 | // 覆盖自定义 v-loading 59 | button:disabled.ant-btn-primary { 60 | color: #ffffff; 61 | background-color: #2546f0; 62 | box-shadow: 0 2px 0 rgba(5, 145, 255, 0.1); 63 | opacity: 0.65; 64 | } 65 | 66 | button:disabled.ant-btn-default { 67 | color: rgba(0, 0, 0, 0.88); 68 | background-color: #ffffff; 69 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.02); 70 | opacity: 0.65; 71 | } 72 | 73 | .center_spin { 74 | position: absolute; 75 | top: 40%; 76 | left: 48%; 77 | z-index: 1; 78 | } 79 | -------------------------------------------------------------------------------- /src/pages/user/role/components/RoleFormModal.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 77 | -------------------------------------------------------------------------------- /src/store/dynamic-router.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import useFetch from '@/hooks/useFetch/index.js' 3 | import config from '@/config/index.js' 4 | import { DYNAMIC_ROUTE } from '@/router/routes.js' 5 | import { API_USER } from '@/service/user/index.js' 6 | 7 | const HAS_KEY = 'path' 8 | const hasPermission = (flattenApiRoutes, route) => { 9 | if (route[HAS_KEY]) { 10 | return flattenApiRoutes.findIndex((systemRoute) => systemRoute[HAS_KEY] === route[HAS_KEY]) >= 0 11 | } else { 12 | return false 13 | } 14 | } 15 | 16 | const filterAsyncRoutes = (flattenApiRoutes, systemRoutes) => { 17 | const res = [] 18 | 19 | systemRoutes.forEach((route) => { 20 | const tmp = { ...route } 21 | if (hasPermission(flattenApiRoutes, tmp)) { 22 | if (tmp.children) { 23 | tmp.children = filterAsyncRoutes(flattenApiRoutes, tmp.children) 24 | } 25 | res.push(tmp) 26 | } 27 | }) 28 | 29 | return res 30 | } 31 | 32 | const flattenSystemRoutes = (routes) => { 33 | const res = [] 34 | 35 | const filterRoutes = (routes) => { 36 | routes.forEach((item) => { 37 | if (item.children && item.children.length) { 38 | filterRoutes(item.children) 39 | item.children = [] 40 | } 41 | res.push(item) 42 | }) 43 | } 44 | filterRoutes(routes) 45 | 46 | return res 47 | } 48 | 49 | const useDynamicRouterStore = defineStore('dynamicRouter', { 50 | state: () => ({ 51 | syncRoutes: false, 52 | roleRoutes: [] 53 | }), 54 | actions: { 55 | async generator() { 56 | if (config.useDynamicRoute) { 57 | return filterAsyncRoutes(flattenSystemRoutes(this.roleRoutes), DYNAMIC_ROUTE) 58 | } 59 | 60 | return DYNAMIC_ROUTE 61 | }, 62 | 63 | async getUserRoutes() { 64 | if (config.useDynamicRoute) { 65 | const { data } = await useFetch(API_USER.roleRoutes).json() 66 | this.roleRoutes = data.value.routes 67 | } 68 | } 69 | } 70 | }) 71 | 72 | export default useDynamicRouterStore 73 | -------------------------------------------------------------------------------- /src/hooks/useFetch/index.js: -------------------------------------------------------------------------------- 1 | import { unref } from 'vue' 2 | import { createFetch } from '@vueuse/core' 3 | import { message } from 'ant-design-vue' 4 | 5 | // useFetch 没有很方便的query传参 6 | export const montageUrl = (originUrl, query) => { 7 | const params = new URLSearchParams() 8 | for (let q in query) { 9 | const val = query[q] 10 | 11 | if ((val ?? '') !== '') { 12 | params.append(q, val) 13 | } 14 | } 15 | 16 | const connector = originUrl.includes('?') ? '&' : '?' 17 | 18 | return `${originUrl}${connector}${params.toString()}` 19 | } 20 | 21 | const codeMessage = { 22 | 200: '服务器成功返回请求的数据。', 23 | 201: '新建或修改数据成功。', 24 | 202: '一个请求已经进入后台排队(异步任务)。', 25 | 204: '删除数据成功。', 26 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', 27 | 401: '用户没有权限(令牌、用户名、密码错误)。', 28 | 403: '用户得到授权,但是访问是被禁止的。', 29 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', 30 | 406: '请求的格式不可得。', 31 | 410: '请求的资源被永久删除,且不会再得到的。', 32 | 422: '当创建一个对象时,发生一个验证错误。', 33 | 500: '服务器发生错误,请检查服务器。', 34 | 502: '网关错误。', 35 | 503: '服务不可用,服务器暂时过载或维护。', 36 | 504: '网关超时。' 37 | } 38 | 39 | const useFetch = createFetch({ 40 | baseUrl: import.meta.env.VITE_BASE_API, 41 | options: { 42 | async beforeFetch(ctx) { 43 | const { options } = ctx 44 | 45 | options.headers.Authorization = `Bearer custom` 46 | options.headers.apifoxToken = 'IbvbVFA8uGdREezk4bfv9' 47 | 48 | const { params } = options 49 | // 自定义 Query String Parameters 携带请求参数。 一般是get请求 50 | if (params && typeof params !== 'string') { 51 | ctx.url = montageUrl(ctx.url, unref(params)) 52 | } 53 | 54 | return ctx 55 | }, 56 | 57 | async afterFetch(ctx) { 58 | const { code, msg } = ctx.data 59 | 60 | if (code === 200) { 61 | return ctx.data 62 | } 63 | 64 | // 兼容多个api 65 | if (code !== 200 && msg) { 66 | message.error(msg || '服务器错误!') 67 | } 68 | 69 | return ctx 70 | }, 71 | 72 | async onFetchError({ response }) { 73 | message.error(codeMessage[response?.status] || '服务器错误,请联系管理员!') 74 | } 75 | }, 76 | fetchOptions: { 77 | mode: 'cors' 78 | } 79 | }) 80 | 81 | export default useFetch 82 | -------------------------------------------------------------------------------- /src/pages/setting/menu-enter/components/List.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 81 | -------------------------------------------------------------------------------- /src/components/Lock/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 55 | 56 | 90 | -------------------------------------------------------------------------------- /src/pages/user/role/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 83 | -------------------------------------------------------------------------------- /src/components/ProTable/index.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 91 | -------------------------------------------------------------------------------- /src/pages/plugin/excel/index.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 94 | -------------------------------------------------------------------------------- /src/components/SearchBox/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 78 | 79 | 102 | -------------------------------------------------------------------------------- /src/pages/plugin/dayjs/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 87 | -------------------------------------------------------------------------------- /src/pages/plugin/calendar/index.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/pages/plugin/idcard/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 104 | -------------------------------------------------------------------------------- /src/utils/date.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { message } from 'ant-design-vue' 3 | 4 | const _getDayOrigin = (date) => { 5 | const curDay = dayjs(date) 6 | if (!curDay.isValid()) { 7 | const err = `存在不合法的日期格式:${date}` 8 | message.error(err) 9 | console.error(err) 10 | } 11 | 12 | return curDay 13 | } 14 | 15 | /** 16 | * 17 | * 某个日期的 - 几天前后 18 | * A few days ago or so 19 | * @param dayNum 20 | * @param date 21 | * @returns {string} 22 | * @example 23 | * 24 | * afterDay(1) 25 | * afterDay(-7) 26 | */ 27 | export const dayAgoOrSo = (dayNum = 0, date) => { 28 | return _getDayOrigin(date).add(dayNum, 'day').format('YYYY-MM-DD') 29 | } 30 | 31 | /** 32 | * 日期补充00:00:00 33 | * @param date 34 | * @returns {string} 35 | * 36 | * @example 37 | * dayStartTime('2023-01-01') - 2023-01-01:00:00:00 38 | * dayStartTime('2023-01-01 12:00:11') - 2023-01-01:00:00:00 39 | */ 40 | export const dayStartTime = (date) => { 41 | return _getDayOrigin(date).startOf('day').format('YYYY-MM-DD HH:mm:ss') 42 | } 43 | 44 | /** 45 | * 日期补充23:59:59 46 | * @param date 47 | * @returns {string} 48 | * 49 | * @example 50 | * dayEndTime('2023-01-01') - 2023-01-01:00:00:00 51 | * dayEndTime('2023-01-01 12:00:11') - 2023-01-01:00:00:00 52 | */ 53 | export const dayEndTime = (date) => { 54 | return _getDayOrigin(date).endOf('day').format('YYYY-MM-DD HH:mm:ss') 55 | } 56 | 57 | /** 58 | * 获取日期区间的开始结束日期补充 00:00:00 - 23:59:59 59 | * @param dateArr 60 | * @returns {[string,string]|*[]} 61 | */ 62 | export const dayStartEndTime = (dateArr) => { 63 | if (!Array.isArray(dateArr)) { 64 | console.error("dayStartEndTime 入参是一个日期数组 例:['2023-01-01', '2023-01-22']") 65 | return [] 66 | } 67 | 68 | if (dateArr.length === 0) { 69 | return [] 70 | } 71 | 72 | return [dayStartTime(dateArr[0]), dayEndTime(dateArr[1])] 73 | } 74 | 75 | /** 76 | * 获取 UNIX 时间戳,10位数字,秒 77 | * @param date 78 | * @returns {number} 79 | */ 80 | export const getUnixTime = (date) => { 81 | return _getDayOrigin(date).unix() 82 | } 83 | 84 | /** 85 | * 获取 UNIX 时间戳,13位数字,毫秒 86 | * @param date 87 | * @returns {number} 88 | */ 89 | export const getTimeMill = (date) => { 90 | return _getDayOrigin(date).valueOf() 91 | } 92 | 93 | /** 94 | * 95 | * @param date date小于当前时间 96 | */ 97 | const ONE_HOUR = 60 * 60 98 | export const fromCurrentTime = (date) => { 99 | const dateUnixTime = getUnixTime(date) 100 | const currentUnixTime = getUnixTime() 101 | 102 | const spaceTime = Math.abs(currentUnixTime - dateUnixTime) 103 | 104 | if (spaceTime < ONE_HOUR) { 105 | return `${parseInt(String(spaceTime / 60))}分钟前` 106 | } else if (spaceTime < ONE_HOUR * 24) { 107 | return `${parseInt(String(spaceTime / ONE_HOUR))}小时前` 108 | } else if (spaceTime < ONE_HOUR * 24 * 2) { 109 | return '昨天' 110 | } else { 111 | return `${parseInt(String(spaceTime / (ONE_HOUR * 24)))}天前` 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/pages/login/components/login-form.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 77 | 78 | 104 | -------------------------------------------------------------------------------- /src/router/group/com.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/com' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'Com', 10 | meta: { 11 | title: '组件', 12 | isGroup: true, 13 | icon: 'i-carbon-ibm-cloud-transit-gateway' 14 | }, 15 | children: [ 16 | { 17 | path: `${BASE_URL}/list-1`, 18 | name: 'List-1', 19 | meta: { 20 | title: '列表-1', 21 | icon: 'i-carbon-list-boxes' 22 | }, 23 | children: [ 24 | { 25 | path: `${BASE_URL}/list-1/list2-1`, 26 | name: 'List2-1', 27 | component: () => import('@/pages/test/test2.vue'), 28 | meta: { 29 | title: '列表2-1' 30 | } 31 | }, 32 | { 33 | path: `${BASE_URL}/list-1/list3-1`, 34 | name: 'List3-1', 35 | component: () => import('@/pages/exception/404-page.vue'), 36 | meta: { 37 | title: '列表3-1' 38 | } 39 | } 40 | ] 41 | }, 42 | { 43 | path: `${BASE_URL}/count-down`, 44 | name: 'CountDown', 45 | component: () => import('@/pages/com/count-down/index.vue'), 46 | meta: { 47 | title: '倒计时', 48 | icon: 'i-carbon-time' 49 | } 50 | }, 51 | { 52 | path: `${BASE_URL}/modal`, 53 | name: 'Modal', 54 | component: () => import('@/pages/com/modal/index.vue'), 55 | meta: { 56 | title: 'Modal', 57 | icon: 'i-carbon-model-alt' 58 | } 59 | }, 60 | { 61 | path: `${BASE_URL}/auto`, 62 | name: 'BtnAuth', 63 | component: () => import('@/pages/com/auth/index.vue'), 64 | meta: { 65 | title: '按钮权限', 66 | icon: 'i-carbon-model-alt' 67 | } 68 | }, 69 | { 70 | path: `${BASE_URL}/mark`, 71 | name: 'TextMark', 72 | component: () => import('@/pages/com/mark-keyword/index.vue'), 73 | meta: { 74 | title: '关键词高亮', 75 | icon: 'i-carbon-dot-mark' 76 | } 77 | }, 78 | { 79 | path: `${BASE_URL}/ellipsis`, 80 | name: 'TextEllipsis', 81 | component: () => import('@/pages/com/text-ellipsis/index.vue'), 82 | meta: { 83 | title: '文本溢出', 84 | icon: 'i-carbon-text-footnote' 85 | } 86 | }, 87 | { 88 | path: `${BASE_URL}/tabs`, 89 | name: 'RadiusTabs', 90 | component: () => import('@/pages/com/radius-tabs/index.vue'), 91 | meta: { 92 | title: '反圆角Tabs', 93 | icon: 'i-carbon-table-alias' 94 | } 95 | }, 96 | { 97 | path: `${BASE_URL}/lazy`, 98 | name: 'LazyImage', 99 | component: () => import('@/pages/com/lazy/index.vue'), 100 | meta: { 101 | title: '图片懒加载', 102 | icon: 'i-carbon-image' 103 | } 104 | } 105 | ] 106 | } 107 | ] 108 | -------------------------------------------------------------------------------- /src/pages/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 140 | -------------------------------------------------------------------------------- /src/router/group/plugin.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/index.vue' 2 | 3 | const BASE_URL = '/plugin' 4 | 5 | export default [ 6 | { 7 | path: BASE_URL, 8 | component: Layout, 9 | name: 'Plugin', 10 | meta: { 11 | title: '插件', 12 | isGroup: true, 13 | icon: 'i-carbon-location-heart' 14 | }, 15 | children: [ 16 | { 17 | path: `${BASE_URL}/check`, 18 | name: 'PluginList', 19 | component: () => import('@/pages/plugin/check-card/index.vue'), 20 | meta: { 21 | title: 'CheckCard', 22 | icon: 'i-carbon-data-check' 23 | } 24 | }, 25 | { 26 | path: `${BASE_URL}/watermark`, 27 | name: 'Watermark', 28 | component: () => import('@/pages/plugin/watermark/index.vue'), 29 | meta: { 30 | title: '水印', 31 | icon: 'i-carbon-encryption' 32 | } 33 | }, 34 | { 35 | path: `${BASE_URL}/idcard`, 36 | name: 'IdCard', 37 | component: () => import('@/pages/plugin/idcard/index.vue'), 38 | meta: { 39 | title: 'idcard', 40 | icon: 'i-carbon-id-management' 41 | } 42 | }, 43 | { 44 | path: `${BASE_URL}/icons`, 45 | name: 'Icons', 46 | component: () => import('@/pages/plugin/icons/index.vue'), 47 | meta: { 48 | title: '图标', 49 | icon: 'i-carbon-ibm-content-services' 50 | } 51 | }, 52 | { 53 | path: `${BASE_URL}/org`, 54 | name: 'OrgTree', 55 | component: () => import('@/pages/plugin/org_tree/index.vue'), 56 | meta: { 57 | title: 'OrgTree', 58 | icon: 'i-carbon-decision-tree' 59 | } 60 | }, 61 | { 62 | path: `${BASE_URL}/calendar`, 63 | name: 'Calendar', 64 | component: () => import('@/pages/plugin/calendar/index.vue'), 65 | meta: { 66 | title: '日历', 67 | icon: 'i-carbon-calendar' 68 | } 69 | }, 70 | { 71 | path: `${BASE_URL}/sign`, 72 | name: 'Sign', 73 | component: () => import('@/pages/plugin/sign/index.vue'), 74 | meta: { 75 | title: '签字板', 76 | icon: 'i-carbon-contour-draw' 77 | } 78 | }, 79 | { 80 | path: `${BASE_URL}/day`, 81 | name: 'DayJS', 82 | component: () => import('@/pages/plugin/dayjs/index.vue'), 83 | meta: { 84 | title: 'dayjs', 85 | icon: 'i-carbon-event-schedule' 86 | } 87 | }, 88 | { 89 | path: `${BASE_URL}/key`, 90 | name: 'BRKey', 91 | component: () => import('@/pages/plugin/browser-key/index.vue'), 92 | meta: { 93 | title: '浏览器唯一标识', 94 | icon: 'i-carbon-ibm-cloud-key-protect' 95 | } 96 | }, 97 | { 98 | path: `${BASE_URL}/lodash`, 99 | name: 'Lodash', 100 | component: () => import('@/pages/plugin/lodash/index.vue'), 101 | meta: { 102 | title: 'LODASH常用', 103 | icon: 'i-carbon-ibm-toolchain' 104 | } 105 | }, 106 | { 107 | path: `${BASE_URL}/zip`, 108 | name: 'ExportZip', 109 | component: () => import('@/pages/plugin/zip/index.vue'), 110 | meta: { 111 | title: 'Zip', 112 | icon: 'i-carbon-zip' 113 | } 114 | }, 115 | { 116 | path: `${BASE_URL}/excel`, 117 | name: 'ExportExcel', 118 | component: () => import('@/pages/plugin/excel/index.vue'), 119 | meta: { 120 | title: 'Excel', 121 | icon: 'i-carbon-document' 122 | } 123 | } 124 | ] 125 | } 126 | ] 127 | -------------------------------------------------------------------------------- /src/layout/sider/components/ColumnTabs.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 82 | 83 | 128 | 129 | 139 | -------------------------------------------------------------------------------- /src/pages/login/components/banner.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 74 | 75 | 158 | -------------------------------------------------------------------------------- /src/components/CheckCard/index.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 87 | 88 | 157 | -------------------------------------------------------------------------------- /src/hooks/useCountDown/index.js: -------------------------------------------------------------------------------- 1 | import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue' 2 | import { raf, cancelRaf, inBrowser } from '../util.js' 3 | 4 | // export type CurrentTime = { 5 | // days: number; 6 | // hours: number; 7 | // total: number; 8 | // minutes: number; 9 | // seconds: number; 10 | // milliseconds: number; 11 | // }; 12 | 13 | const SECOND = 1000 14 | const MINUTE = 60 * SECOND 15 | const HOUR = 60 * MINUTE 16 | const DAY = 24 * HOUR 17 | 18 | /** 19 | * 20 | * @param time { number } 21 | * @returns {{ milliseconds: number, total: number, hours: number, seconds: number, minutes: number, days: number }} 22 | */ 23 | function parseTime(time) { 24 | const days = Math.floor(time / DAY) 25 | const hours = Math.floor((time % DAY) / HOUR) 26 | const minutes = Math.floor((time % HOUR) / MINUTE) 27 | const seconds = Math.floor((time % MINUTE) / SECOND) 28 | const milliseconds = Math.floor(time % SECOND) 29 | 30 | return { 31 | total: time, 32 | days, 33 | hours, 34 | minutes, 35 | seconds, 36 | milliseconds 37 | } 38 | } 39 | 40 | /** 41 | * 42 | * @param time1 { number } 43 | * @param time2 { number } 44 | * @returns { boolean } 45 | */ 46 | function isSameSecond(time1, time2) { 47 | return Math.floor(time1 / 1000) === Math.floor(time2 / 1000) 48 | } 49 | 50 | /** 51 | * @param options UseCountDownOptions = { 52 | * time: number; 53 | * millisecond?: boolean; 54 | * onChange?: (current: CurrentTime) => void; 55 | * onFinish?: () => void; 56 | * } 57 | * @returns {{current: ComputedRef<{CurrentTime}>, start: (function(): void), reset: (function(number=): void), pause: (function(): void)}} 58 | */ 59 | // 文档详情: https://vant-ui.github.io/vant/#/zh-CN/count-down 60 | export function useCountDown(options) { 61 | let rafId 62 | let endTime 63 | let counting 64 | let deactivated 65 | 66 | const remain = ref(options.time) 67 | const current = computed(() => parseTime(remain.value)) 68 | 69 | const pause = () => { 70 | counting = false 71 | cancelRaf(rafId) 72 | } 73 | 74 | const getCurrentRemain = () => Math.max(endTime - Date.now(), 0) 75 | 76 | /** 77 | * @param value { number } 78 | */ 79 | const setRemain = (value) => { 80 | remain.value = value 81 | options.onChange?.(current.value) 82 | 83 | if (value === 0) { 84 | pause() 85 | options.onFinish?.() 86 | } 87 | } 88 | 89 | const microTick = () => { 90 | rafId = raf(() => { 91 | // in case of call reset immediately after finish 92 | if (counting) { 93 | setRemain(getCurrentRemain()) 94 | 95 | if (remain.value > 0) { 96 | microTick() 97 | } 98 | } 99 | }) 100 | } 101 | 102 | const macroTick = () => { 103 | rafId = raf(() => { 104 | // in case of call reset immediately after finish 105 | if (counting) { 106 | const remainRemain = getCurrentRemain() 107 | 108 | if (!isSameSecond(remainRemain, remain.value) || remainRemain === 0) { 109 | setRemain(remainRemain) 110 | } 111 | 112 | if (remain.value > 0) { 113 | macroTick() 114 | } 115 | } 116 | }) 117 | } 118 | 119 | const tick = () => { 120 | // should not start counting in server 121 | // see: https://github.com/vant-ui/vant/issues/7807 122 | if (!inBrowser) { 123 | return 124 | } 125 | 126 | if (options.millisecond) { 127 | microTick() 128 | } else { 129 | macroTick() 130 | } 131 | } 132 | 133 | const start = () => { 134 | if (!counting) { 135 | endTime = Date.now() + remain.value 136 | counting = true 137 | tick() 138 | } 139 | } 140 | 141 | /** 142 | * 143 | * @param totalTime { number } 144 | */ 145 | const reset = (totalTime = options.time) => { 146 | pause() 147 | remain.value = totalTime 148 | } 149 | 150 | onBeforeUnmount(pause) 151 | 152 | onActivated(() => { 153 | if (deactivated) { 154 | counting = true 155 | deactivated = false 156 | tick() 157 | } 158 | }) 159 | 160 | onDeactivated(() => { 161 | if (counting) { 162 | pause() 163 | deactivated = true 164 | } 165 | }) 166 | 167 | return { 168 | start, 169 | pause, 170 | reset, 171 | current 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/pages/list/search-list/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 155 | -------------------------------------------------------------------------------- /src/directive/hasAuth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 设计场景 3 | * 4 | * 1、后台新增权限时选择类型是否是按钮,选择按钮类型。登录后调取接口查出所有按钮类型权限:(我使用接口作为唯一标识) 5 | * response = ['/user/list', '/user/add', '/user/detail/add', '/user/detail/edit'] 6 | * 7 | * 指令使用格式 8 | * v-auth="'/user/list'" 9 | * v-auth="['/user/list', '/user/detail/edit']" 10 | * 11 | * 12 | * 2、按照菜单权限层级返回。类似 mock 中的 adminRoutes,再增加类型区分是否是按钮权限即可。 13 | * response = [{ key: 'user', children: [{ key: 'user/list', children: [{ type: 'btn', key: 'api/user/list' }] }] }] 14 | * 15 | * 使用(.[user]修饰符用来快速定位查找,也可以起到命名空间的作用) 16 | * 17 | * 找到命名空间内的 18 | * v-auth.user="'api/user/list'" 19 | * v-auth.user="['api/user/list', 'api/user/list']" 20 | * v-auth="{ user: ["api/user/list", "api/user/add"], setting: [""] }" 21 | * v-auth="{ user: "", setting: "" }" 22 | * 23 | * user和setting模块中任意找到 24 | * v-auth.user.setting="api/user/list" 25 | * v-auth.user.setting="['api/user/list', 'api/user/add']" 26 | * 27 | * tip:要是有 user下面,或者setting下面有某个权限都可以显示按钮这种场景该怎么办 28 | * 29 | * 30 | * 31 | */ 32 | 33 | import { isArray, isString, isPlainObject } from 'lodash-es' 34 | 35 | const _mockResRouteData = [ 36 | { 37 | key: 'user', 38 | name: '用户管理', 39 | children: [ 40 | { 41 | key: 'user/list', 42 | name: '用户列表', 43 | children: [ 44 | { type: 'btn', key: 'api/user/list', name: '用户列表查看' }, 45 | { type: 'btn', key: 'api/user/detail', name: '用户详情' }, 46 | { type: 'btn', key: 'api/user/auth-edit', name: '用户权限编辑' } 47 | ] 48 | }, 49 | { 50 | key: 'user/list1', 51 | name: '用户列表1', 52 | children: [ 53 | { type: 'btn', key: 'api/user/list1', name: '用户列表查看1' }, 54 | { type: 'btn', key: 'api/user/detail1', name: '用户详情1' }, 55 | { type: 'btn', key: 'api/user/auth-edit1', name: '用户权限编辑1' } 56 | ] 57 | } 58 | ] 59 | }, 60 | { 61 | key: 'setting', 62 | name: '设置', 63 | children: [ 64 | { 65 | key: 'setting/auth', 66 | name: '权限设置', 67 | children: [ 68 | { type: 'btn', key: 'api/auth/add', name: '新增权限' }, 69 | { type: 'btn', key: 'api/auth/edit', name: '编辑权限' }, 70 | { type: 'btn', key: 'api/auth/list', name: '权限列表' } 71 | ] 72 | } 73 | ] 74 | }, 75 | { 76 | key: '404', 77 | name: '异常页面', 78 | children: [ 79 | { 80 | key: 'exception/404', 81 | name: '404页面', 82 | children: [ 83 | { type: 'btn', key: 'api/exception/add', name: '新增' }, 84 | { type: 'btn', key: 'api/exception/edit', name: '编辑' } 85 | ] 86 | }, 87 | { 88 | key: 'exception/503', 89 | name: '503页面' 90 | } 91 | ] 92 | } 93 | ] 94 | 95 | // 模块唯一标识key 96 | const KEY_NAME = 'key' 97 | const findNamesRoutes = (moduleName) => { 98 | return (_mockResRouteData.find((route) => route[KEY_NAME] === moduleName) || {}).children || [] 99 | } 100 | 101 | const btnKeys = (routes) => { 102 | const keys = [] 103 | 104 | function find(arr) { 105 | arr.forEach((it) => { 106 | // 按钮类型的唯一key 107 | if (it.type === 'btn') { 108 | keys.push(it[KEY_NAME]) 109 | } 110 | 111 | if (it.children && it.children.length) { 112 | find(it.children) 113 | } 114 | }) 115 | } 116 | find(routes) 117 | 118 | return keys 119 | } 120 | 121 | /** 122 | * 比对是否有相同项,只要找到一个有相同的,就立即返回(或的关系,所以可以提前返回) 123 | * 124 | * arrModuleValue 必然存在 125 | */ 126 | const hasDuplicates = (arr1, arrModuleValue) => { 127 | for (let i = 0, len = arrModuleValue.length; i < len; i++) { 128 | if (arr1.includes(arrModuleValue[i])) { 129 | return true 130 | } 131 | } 132 | 133 | return false 134 | } 135 | 136 | const hasPer = (moduleName, moduleValue) => { 137 | const keys = btnKeys(findNamesRoutes(moduleName)) 138 | 139 | if (isString(moduleValue)) { 140 | return keys.includes(moduleValue) 141 | } 142 | 143 | if (isArray(moduleValue) && moduleValue.length > 0) { 144 | return hasDuplicates(keys, moduleValue) 145 | } 146 | 147 | return false 148 | } 149 | 150 | const DOM_MARK = 'data-auth' 151 | const hasMark = (el) => { 152 | return el.getAttribute(DOM_MARK) === 'true' 153 | } 154 | 155 | const setMark = (el) => { 156 | el.setAttribute(DOM_MARK, true) 157 | } 158 | 159 | const removeEl = (el) => { 160 | el && el.parentNode && el.parentNode.removeChild(el) 161 | } 162 | 163 | /** 164 | * 场景2方式实现 165 | */ 166 | export default { 167 | mounted(el, binding) { 168 | const { modifiers, value } = binding 169 | 170 | const valueIsPlainObj = isPlainObject(value) 171 | const routeModules = Object.keys(valueIsPlainObj ? value : modifiers) 172 | 173 | if (routeModules.length) { 174 | try { 175 | routeModules.forEach((module) => { 176 | const curModuleValue = valueIsPlainObj ? value[module] : value 177 | if (hasPer(module, curModuleValue)) { 178 | setMark(el) 179 | throw new Error('当前el已打标可立即跳出') 180 | } 181 | }) 182 | } catch {} 183 | } else { 184 | // 没有命名空间直接删除,例:v-auth='"api/list"' 185 | removeEl(el) 186 | return 187 | } 188 | 189 | if (!hasMark(el)) { 190 | removeEl(el) 191 | } 192 | }, 193 | 194 | updated() {}, 195 | 196 | unmounted() {} 197 | } 198 | -------------------------------------------------------------------------------- /public/print-lock.css: -------------------------------------------------------------------------------- 1 | @media print{body{margin:0px;padding:0px;}}@page{margin:0;}.hiprint-printPaper *{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;}.hiprint-printPaper *:focus{outline:-webkit-focus-ring-color auto 0px;}.hiprint-printPaper{position:relative;padding:0 0 0 0;page-break-after:always;-webkit-user-select:none;-moz-user-select:none;user-select:none;overflow-x:hidden;overflow:hidden;}.hiprint-printPaper .hiprint-printPaper-content{position:relative;}@-moz-document url-prefix(){.hiprint-printPaper .hiprint-printPaper-content{position:relative;margin-top:20px;top:-20px;}}.hiprint-printPaper.design{overflow:visible;}.hiprint-printTemplate .hiprint-printPanel{page-break-after:always;}.hiprint-printPaper,hiprint-printPanel{box-sizing:border-box;border:0px;}.hiprint-printPanel .hiprint-printPaper:last-child{page-break-after:avoid;}.hiprint-printTemplate .hiprint-printPanel:last-child{page-break-after:avoid;}.hiprint-printPaper .hideheaderLinetarget{border-top:0px dashed rgb(201,190,190) !important;}.hiprint-printPaper .hidefooterLinetarget{border-top:0px dashed rgb(201,190,190) !important;}.hiprint-printPaper.design{border:1px dashed rgba(170,170,170,0.7);}.design .hiprint-printElement-table-content,.design .hiprint-printElement-longText-content{overflow:hidden;box-sizing:border-box;}.design .resize-panel{box-sizing:border-box;border:1px dotted;}.hiprint-printElement-text{background-color:transparent;background-repeat:repeat;padding:0 0 0 0;border:0.75pt none rgb(0,0,0);direction:ltr;font-family:'SimSun';font-size:9pt;font-style:normal;font-weight:normal;padding-bottom:0pt;padding-left:0pt;padding-right:0pt;padding-top:0pt;text-align:left;text-decoration:none;line-height:9.75pt;box-sizing:border-box;word-wrap:break-word;word-break:break-all;}.design .hiprint-printElement-text-content{border:1px dashed rgb(206,188,188);box-sizing:border-box;}.hiprint-printElement-longText{background-color:transparent;background-repeat:repeat;border:0.75pt none rgb(0,0,0);direction:ltr;font-family:'SimSun';font-size:9pt;font-style:normal;font-weight:normal;padding-bottom:0pt;padding-left:0pt;padding-right:0pt;padding-top:0pt;text-align:left;text-decoration:none;line-height:9.75pt;box-sizing:border-box;word-wrap:break-word;word-break:break-all;}.hiprint-printElement-table{background-color:transparent;background-repeat:repeat;color:rgb(0,0,0);border-color:rgb(0,0,0);border-style:none;direction:ltr;font-family:'SimSun';font-size:9pt;font-style:normal;font-weight:normal;padding-bottom:0pt;padding-left:0pt;padding-right:0pt;padding-top:0pt;text-align:left;text-decoration:none;padding:0 0 0 0;box-sizing:border-box;line-height:9.75pt;}.hiprint-printElement-table thead{background:#e8e8e8;font-weight:700;}table.hiprint-printElement-tableTarget{width:100%;}.hiprint-printElement-tableTarget,.hiprint-printElement-tableTarget tr,.hiprint-printElement-tableTarget td{border-color:rgb(0,0,0);font-weight:normal;direction:ltr;padding-bottom:0pt;padding-left:4pt;padding-right:4pt;padding-top:0pt;text-decoration:none;vertical-align:middle;box-sizing:border-box;word-wrap:break-word;word-break:break-all;}.hiprint-printElement-tableTarget-border-all{border:1px solid;}.hiprint-printElement-tableTarget-border-none{border:0px solid;}.hiprint-printElement-tableTarget-border-lr{border-left:1px solid;border-right:1px solid;}.hiprint-printElement-tableTarget-border-left{border-left:1px solid;}.hiprint-printElement-tableTarget-border-right{border-right:1px solid;}.hiprint-printElement-tableTarget-border-tb{border-top:1px solid;border-bottom:1px solid;}.hiprint-printElement-tableTarget-border-top{border-top:1px solid;}.hiprint-printElement-tableTarget-border-bottom{border-bottom:1px solid;}.hiprint-printElement-tableTarget-border-td-none td{border:0px solid;}.hiprint-printElement-tableTarget-border-td-all td:not(:nth-last-child(-n + 2)){border-right:1px solid;}.hiprint-printElement-tableTarget-border-td-all td:last-child{border-left:1px solid;}.hiprint-printElement-tableTarget-border-td-all td:last-child:first-child{border-left:none;}.hiprint-printElement-tableTarget td{height:18pt;}.hiprint-printPaper .hiprint-paperNumber{font-size:9pt;}.design .hiprint-printElement-table-handle{position:absolute;height:21pt;width:21pt;background:red;z-index:1;}.hiprint-printPaper .hiprint-paperNumber-disabled{float:right !important;right:0 !important;color:gainsboro !important;}.hiprint-printElement-vline,.hiprint-printElement-hline{border:0px none rgb(0,0,0);}.hiprint-printElement-vline{border-left:0.75pt solid #000;border-right:0px none rgb(0,0,0) !important;border-bottom:0px none rgb(0,0,0) !important;border-top:0px none rgb(0,0,0) !important;}.hiprint-printElement-hline{border-top:0.75pt solid #000;border-right:0px none rgb(0,0,0) !important;border-bottom:0px none rgb(0,0,0) !important;border-left:0px none rgb(0,0,0) !important;}.hiprint-printElement-oval,.hiprint-printElement-rect{border:0.75pt solid #000;}.hiprint-text-content-middle{}.hiprint-text-content-middle > div{display:grid;align-items:center;}.hiprint-text-content-bottom{}.hiprint-text-content-bottom > div{display:grid;align-items:flex-end;}.hiprint-text-content-wrap{}.hiprint-text-content-wrap .hiprint-text-content-wrap-nowrap{white-space:nowrap;}.hiprint-text-content-wrap .hiprint-text-content-wrap-clip{white-space:nowrap;overflow:hidden;text-overflow:clip;}.hiprint-text-content-wrap .hiprint-text-content-wrap-ellipsis{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.hi-grid-row{position:relative;height:auto;margin-right:0;margin-left:0;zoom:1;display:block;box-sizing:border-box;}.hi-grid-row::after,.hi-grid-row::before{display:table;content:'';box-sizing:border-box;}.hi-grid-col{display:block;box-sizing:border-box;position:relative;float:left;flex:0 0 auto;}.table-grid-row{margin-left:-0pt;margin-right:-0pt;}.tableGridColumnsGutterRow{padding-left:0pt;padding-right:0pt;}.hiprint-gridColumnsFooter{text-align:left;clear:both;} -------------------------------------------------------------------------------- /src/lib/theme/walden.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "themeName": "walden", 4 | "theme": { 5 | "seriesCnt": "12", 6 | "backgroundColor": "rgba(252,252,252,0)", 7 | "titleColor": "#666666", 8 | "subtitleColor": "#999999", 9 | "textColorShow": false, 10 | "textColor": "#333", 11 | "markTextColor": "#ffffff", 12 | "color": [ 13 | "#3fb1e3", 14 | "#6be6c1", 15 | "#626c91", 16 | "#a0a7e6", 17 | "#c4ebad", 18 | "#96dee8" 19 | ], 20 | "borderColor": "#ccc", 21 | "borderWidth": 0, 22 | "visualMapColor": [ 23 | "#2a99c9", 24 | "#afe8ff" 25 | ], 26 | "legendTextColor": "#999999", 27 | "kColor": "#e6a0d2", 28 | "kColor0": "transparent", 29 | "kBorderColor": "#e6a0d2", 30 | "kBorderColor0": "#3fb1e3", 31 | "kBorderWidth": "2", 32 | "lineWidth": "3", 33 | "symbolSize": "8", 34 | "symbol": "emptyCircle", 35 | "symbolBorderWidth": "2", 36 | "lineSmooth": false, 37 | "graphLineWidth": "1", 38 | "graphLineColor": "#cccccc", 39 | "mapLabelColor": "#ffffff", 40 | "mapLabelColorE": "#3fb1e3", 41 | "mapBorderColor": "#aaaaaa", 42 | "mapBorderColorE": "#3fb1e3", 43 | "mapBorderWidth": 0.5, 44 | "mapBorderWidthE": 1, 45 | "mapAreaColor": "#eeeeee", 46 | "mapAreaColorE": "rgba(63,177,227,0.25)", 47 | "axes": [ 48 | { 49 | "type": "all", 50 | "name": "通用坐标轴", 51 | "axisLineShow": true, 52 | "axisLineColor": "#cccccc", 53 | "axisTickShow": false, 54 | "axisTickColor": "#333", 55 | "axisLabelShow": true, 56 | "axisLabelColor": "#999999", 57 | "splitLineShow": true, 58 | "splitLineColor": [ 59 | "#eeeeee" 60 | ], 61 | "splitAreaShow": false, 62 | "splitAreaColor": [ 63 | "rgba(250,250,250,0.05)", 64 | "rgba(200,200,200,0.02)" 65 | ] 66 | }, 67 | { 68 | "type": "category", 69 | "name": "类目坐标轴", 70 | "axisLineShow": true, 71 | "axisLineColor": "#333", 72 | "axisTickShow": true, 73 | "axisTickColor": "#333", 74 | "axisLabelShow": true, 75 | "axisLabelColor": "#333", 76 | "splitLineShow": false, 77 | "splitLineColor": [ 78 | "#ccc" 79 | ], 80 | "splitAreaShow": false, 81 | "splitAreaColor": [ 82 | "rgba(250,250,250,0.3)", 83 | "rgba(200,200,200,0.3)" 84 | ] 85 | }, 86 | { 87 | "type": "value", 88 | "name": "数值坐标轴", 89 | "axisLineShow": true, 90 | "axisLineColor": "#333", 91 | "axisTickShow": true, 92 | "axisTickColor": "#333", 93 | "axisLabelShow": true, 94 | "axisLabelColor": "#333", 95 | "splitLineShow": true, 96 | "splitLineColor": [ 97 | "#ccc" 98 | ], 99 | "splitAreaShow": false, 100 | "splitAreaColor": [ 101 | "rgba(250,250,250,0.3)", 102 | "rgba(200,200,200,0.3)" 103 | ] 104 | }, 105 | { 106 | "type": "log", 107 | "name": "对数坐标轴", 108 | "axisLineShow": true, 109 | "axisLineColor": "#333", 110 | "axisTickShow": true, 111 | "axisTickColor": "#333", 112 | "axisLabelShow": true, 113 | "axisLabelColor": "#333", 114 | "splitLineShow": true, 115 | "splitLineColor": [ 116 | "#ccc" 117 | ], 118 | "splitAreaShow": false, 119 | "splitAreaColor": [ 120 | "rgba(250,250,250,0.3)", 121 | "rgba(200,200,200,0.3)" 122 | ] 123 | }, 124 | { 125 | "type": "time", 126 | "name": "时间坐标轴", 127 | "axisLineShow": true, 128 | "axisLineColor": "#333", 129 | "axisTickShow": true, 130 | "axisTickColor": "#333", 131 | "axisLabelShow": true, 132 | "axisLabelColor": "#333", 133 | "splitLineShow": true, 134 | "splitLineColor": [ 135 | "#ccc" 136 | ], 137 | "splitAreaShow": false, 138 | "splitAreaColor": [ 139 | "rgba(250,250,250,0.3)", 140 | "rgba(200,200,200,0.3)" 141 | ] 142 | } 143 | ], 144 | "axisSeperateSetting": false, 145 | "toolboxColor": "#999999", 146 | "toolboxEmphasisColor": "#666666", 147 | "tooltipAxisColor": "#cccccc", 148 | "tooltipAxisWidth": 1, 149 | "timelineLineColor": "#626c91", 150 | "timelineLineWidth": 1, 151 | "timelineItemColor": "#626c91", 152 | "timelineItemColorE": "#626c91", 153 | "timelineCheckColor": "#3fb1e3", 154 | "timelineCheckBorderColor": "#3fb1e3", 155 | "timelineItemBorderWidth": 1, 156 | "timelineControlColor": "#626c91", 157 | "timelineControlBorderColor": "#626c91", 158 | "timelineControlBorderWidth": 0.5, 159 | "timelineLabelColor": "#626c91", 160 | "datazoomBackgroundColor": "rgba(255,255,255,0)", 161 | "datazoomDataColor": "rgba(222,222,222,1)", 162 | "datazoomFillColor": "rgba(114,230,212,0.25)", 163 | "datazoomHandleColor": "#cccccc", 164 | "datazoomHandleWidth": "100", 165 | "datazoomLabelColor": "#999999" 166 | } 167 | } -------------------------------------------------------------------------------- /src/pages/charts/line/index.vue: -------------------------------------------------------------------------------- 1 | 232 | 233 | 239 | -------------------------------------------------------------------------------- /src/vendor/excel.js: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver' 2 | import { writeXLSX, utils, SSF } from 'xlsx' 3 | import { isObject } from 'lodash-es' 4 | 5 | function generateArray(table) { 6 | let out = [] 7 | let rows = table.querySelectorAll('tr') 8 | let ranges = [] 9 | for (let R = 0; R < rows.length; ++R) { 10 | let outRow = [] 11 | let row = rows[R] 12 | let columns = row.querySelectorAll('td') 13 | for (let C = 0; C < columns.length; ++C) { 14 | let cell = columns[C] 15 | let colspan = cell.getAttribute('colspan') 16 | let rowspan = cell.getAttribute('rowspan') 17 | let cellValue = cell.innerText 18 | if (cellValue !== '' && cellValue == +cellValue) cellValue = +cellValue 19 | 20 | //Skip ranges 21 | ranges.forEach(function (range) { 22 | if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) { 23 | for (let i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null) 24 | } 25 | }) 26 | 27 | //Handle Row Span 28 | if (rowspan || colspan) { 29 | rowspan = rowspan || 1 30 | colspan = colspan || 1 31 | ranges.push({ 32 | s: { 33 | r: R, 34 | c: outRow.length 35 | }, 36 | e: { 37 | r: R + rowspan - 1, 38 | c: outRow.length + colspan - 1 39 | } 40 | }) 41 | } 42 | 43 | //Handle Value 44 | outRow.push(cellValue !== '' ? cellValue : null) 45 | 46 | //Handle Colspan 47 | if (colspan) for (let k = 0; k < colspan - 1; ++k) outRow.push(null) 48 | } 49 | out.push(outRow) 50 | } 51 | return [out, ranges] 52 | } 53 | 54 | function datenum(v, date1904) { 55 | if (date1904) v += 1462 56 | let epoch = Date.parse(v) 57 | return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000) 58 | } 59 | 60 | function sheet_from_array_of_arrays(data, opts) { 61 | let ws = {} 62 | let range = { 63 | s: { 64 | c: 10000000, 65 | r: 10000000 66 | }, 67 | e: { 68 | c: 0, 69 | r: 0 70 | } 71 | } 72 | for (let R = 0; R != data.length; ++R) { 73 | for (let C = 0; C != data[R].length; ++C) { 74 | if (range.s.r > R) range.s.r = R 75 | if (range.s.c > C) range.s.c = C 76 | if (range.e.r < R) range.e.r = R 77 | if (range.e.c < C) range.e.c = C 78 | let cell = { 79 | v: data[R][C] 80 | } 81 | if (cell.v == null) continue 82 | let cell_ref = utils.encode_cell({ 83 | c: C, 84 | r: R 85 | }) 86 | 87 | if (typeof cell.v === 'number') cell.t = 'n' 88 | else if (typeof cell.v === 'boolean') cell.t = 'b' 89 | else if (cell.v instanceof Date) { 90 | cell.t = 'n' 91 | cell.z = SSF._table[14] 92 | cell.v = datenum(cell.v) 93 | } else cell.t = 's' 94 | 95 | ws[cell_ref] = cell 96 | } 97 | } 98 | if (range.s.c < 10000000) ws['!ref'] = utils.encode_range(range) 99 | return ws 100 | } 101 | 102 | function Workbook() { 103 | if (!(this instanceof Workbook)) return new Workbook() 104 | this.SheetNames = [] 105 | this.Sheets = {} 106 | } 107 | 108 | function s2ab(s) { 109 | let buf = new ArrayBuffer(s.length) 110 | let view = new Uint8Array(buf) 111 | for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff 112 | return buf 113 | } 114 | 115 | export function export_table_to_excel(id) { 116 | let theTable = document.getElementById(id) 117 | let oo = generateArray(theTable) 118 | let ranges = oo[1] 119 | 120 | /* original data */ 121 | let data = oo[0] 122 | let ws_name = 'SheetJS' 123 | 124 | let wb = new Workbook(), 125 | ws = sheet_from_array_of_arrays(data) 126 | 127 | /* add ranges to worksheet */ 128 | // ws['!cols'] = ['apple', 'banan']; 129 | ws['!merges'] = ranges 130 | 131 | /* add worksheet to workbook */ 132 | wb.SheetNames.push(ws_name) 133 | wb.Sheets[ws_name] = ws 134 | 135 | let wbout = writeXLSX(wb, { 136 | bookType: 'xlsx', 137 | bookSST: false, 138 | type: 'binary' 139 | }) 140 | 141 | saveAs( 142 | new Blob([s2ab(wbout)], { 143 | type: 'application/octet-stream' 144 | }), 145 | 'test.xlsx' 146 | ) 147 | } 148 | 149 | export function export_json_to_excel({ 150 | multiHeader = [], 151 | header, 152 | data, 153 | filename, 154 | merges = [], 155 | autoWidth = true, 156 | bookType = 'xlsx' 157 | } = {}) { 158 | /* original data */ 159 | filename = filename || 'excel-list' 160 | data = [...data] 161 | data.unshift(header) 162 | 163 | for (let i = multiHeader.length - 1; i > -1; i--) { 164 | data.unshift(multiHeader[i]) 165 | } 166 | 167 | let ws_name = 'SheetJS' 168 | let wb = new Workbook(), 169 | ws = sheet_from_array_of_arrays(data) 170 | 171 | if (merges.length > 0) { 172 | if (!ws['!merges']) ws['!merges'] = [] 173 | merges.forEach((item) => { 174 | ws['!merges'].push(utils.decode_range(item)) 175 | }) 176 | } 177 | 178 | if (autoWidth) { 179 | /*设置worksheet每列的最大宽度*/ 180 | const colWidth = data.map((row) => 181 | row.map((val) => { 182 | /*先判断是否为null/undefined*/ 183 | if (val == null) { 184 | return { 185 | wch: 10 186 | } 187 | } else if (val.toString().charCodeAt(0) > 255) { 188 | /*再判断是否为中文*/ 189 | return { 190 | wch: val.toString().length * 2 191 | } 192 | } else { 193 | return { 194 | wch: val.toString().length 195 | } 196 | } 197 | }) 198 | ) 199 | /*以第一行为初始值*/ 200 | let result = colWidth[0] 201 | for (let i = 1; i < colWidth.length; i++) { 202 | for (let j = 0; j < colWidth[i].length; j++) { 203 | if (result[j]['wch'] < colWidth[i][j]['wch']) { 204 | result[j]['wch'] = colWidth[i][j]['wch'] 205 | } 206 | } 207 | } 208 | ws['!cols'] = result 209 | } 210 | 211 | /* add worksheet to workbook */ 212 | wb.SheetNames.push(ws_name) 213 | wb.Sheets[ws_name] = ws 214 | 215 | let wbout = writeXLSX(wb, { 216 | bookType: bookType, 217 | bookSST: false, 218 | type: 'binary' 219 | }) 220 | saveAs( 221 | new Blob([s2ab(wbout)], { 222 | type: 'application/octet-stream' 223 | }), 224 | `${filename}.${bookType}` 225 | ) 226 | } 227 | 228 | /** 229 | 230 | @params filterVal: ['id', 'name', 'age'] 231 | 232 | @params jsonData: [ 233 | { id: 1, name: 'ealien', age: 18 }, 234 | { id: 2, name: '哇咔咔', age: 99 }, 235 | { id: 3, name: '124', age: 18 }, 236 | { id: 4, name: '1', age: 99 }, 237 | { id: 5, name: 'nihao', age: 18 }, 238 | { id: 6, name: '1838', age: 99 } 239 | ] 240 | 241 | 242 | const tHeader = ['ID', '姓名', '年龄'] 243 | @return [ 244 | [1, 'ealien', 18], 245 | [2, '哇咔咔', 99], 246 | [3, '124', 18], 247 | [4, '1', 99], 248 | [5, 'nihao', 18], 249 | [6, '1838', 99] 250 | ] 251 | */ 252 | export const formatJson = (filterVal, jsonData) => { 253 | return jsonData.map((v) => filterVal.map((j) => v[j])) 254 | } 255 | 256 | /** 257 | const columns = [{ id: 'ID' }, { name: '姓名' }, { age: '年龄' }] 258 | 259 | OR 260 | 261 | const columns = [{ id: 'ID' }, { name: '姓名' }, { action: { title: '操作’, width: '120px' } }] 262 | 263 | return [['ID', '姓名', '年龄'], ['id', 'name', 'age']] 264 | 265 | getFormatDataByColumns(columns) 266 | 267 | getFormatDataByColumns(columns, ['name']) 268 | 269 | */ 270 | export const getFormatDataByColumns = (columns, excludeKeys = []) => { 271 | let header = [] 272 | let keys = [] 273 | columns.forEach((it) => { 274 | const key = Object.keys(it)[0] 275 | const value = Object.values(it)[0] 276 | 277 | if (!excludeKeys.includes(key)) { 278 | header.push(isObject(value) ? value.title : value) 279 | keys.push(key) 280 | } 281 | }) 282 | 283 | console.log(header, keys) 284 | return [header, keys] 285 | } 286 | -------------------------------------------------------------------------------- /src/pages/charts/pie/chart/p.vue: -------------------------------------------------------------------------------- 1 | 410 | 411 | 417 | 418 | 419 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 运行 2 | 3 | ### ProTable 封装可以参考 4 | ### https://github.com/TopAlien/base-admin-naive 5 | ### https://github.com/TopAlien/vue3-admin-cli 6 | ### https://procomponents.ant.design/components/table 7 | 8 | ### 1、yarn install 9 | 10 | ### 2、yarn dev 11 | 12 | ## 构建 13 | 14 | ### 1、build:dev 打包开发环境 15 | 16 | ### 2、build:test 打包测试环境 17 | 18 | ### 3、build:prod 打包正式环境 19 | 20 | ... 自定义可以增加 .env.[mode] 21 | ... package.json scripts 对应 --mode [mode] 22 | 23 | ### 功能/技术栈 24 | 25 | - vue@3 26 | - pinia 27 | - vite@6 28 | - less css预处理器, 变量 29 | - 请求使用原生支持的fetch(vueuse/useFetch hook)useFetch二次封装, 不再使用axios 30 | - vue-router@4 31 | - unocss - safelist加载动态icon class 32 | - vueuse 33 | - ant-design-vue@4.x - zh-cn 组件二次封装slot, attrs透传 34 | - smooth-scrollbar自定义滚动条(自定义指令) 35 | - 可配置右侧content接口请加载动画(如图7),见service/index.js showLoading配置 36 | - 图表库 Echarts-v5 37 | - 图表库 G2-5.0 38 | - 简单易用的打印,局部打印 hiprint,直接预览,导出pdf 39 | - lodash-es版 方便vite tree shake, 减少包体积;所以我们在选择第三方库时,要尽可能使用 ESM 版本,可以提升不少性能! 40 | - 切换页面回到顶部,区域滚动router无效 41 | - Org Tree 42 | - Calendar 日历 43 | - idcard 校验身份证[第二代]合法性,获取身份证详细信息(可15位转18位) 44 | - NProgress 页面切换进度条 45 | - 后端动态路由匹配 config 开启后查看示例, 权限更新后刷新即可,无需重新登录 46 | - 基于Modal封装useModal函数式调用 47 | - 浏览器唯一标识,用于游客记录等 48 | - json美化预览/编辑 49 | - 按钮 v-loading(loading动画)自定义指令 - 节流 50 | - 按钮级权限指令 v-auth.[moduleName] 51 | - 关键词高亮组件TextMark组件 52 | - 文本溢出显示...组件 53 | - 数据使用[Apifox云端Mock](https://apifox.com/help/api-mock/cloud-mock) 54 | - Tabs反圆角 55 | - Table区域滚动(自动计算) 56 | - 图片懒加载指令 57 | 58 | ### import.meta.env 访问环境变量,自定义 VITE\_ 开头 59 | 60 | ### 项目部分截图 61 | 62 | ![img14.png](public/img14.png) 63 | ![img15.png](public/img15.png) 64 | ![img16.png](public/img16.png) 65 | ![img.png](public/img.png) 66 | ![img.png](public/img1.png) 67 | ![img.png](public/img2.png) 68 | ![img.png](public/img3.png) 69 | ![img.png](public/img4.png) 70 | ![img5.png](public/img5.png) 71 | ![img.png](public/img6.png) 72 | ![img7.png](public/img7.png) 73 | ![img8.png](public/img8.png) 74 | ![img9.png](public/img9.png) 75 | ![img10.png](public/img10.png) 76 | ![img11.png](public/img11.png) 77 | ![img12.png](public/img12.png) 78 | ![img13.png](public/img13.png) 79 | 80 | ### 路由配置 81 | 82 | ```js 83 | const BASE_URL = '/other'[ 84 | { 85 | // path必须写完整的路径,要做跳转匹配 86 | path: BASE_URL, 87 | component: Layout, 88 | name: 'Com', 89 | redirect: `${BASE_URL}/list-1/list2-1`, // 不再需要,自动重定向第一个 90 | // icon 为carbon时在,safelist中添加class 91 | // meta: { icon, hideInMenu, title } 92 | meta: { 93 | title: '组件', 94 | // 需要显示到column tab中的分组 95 | isGroup: true, 96 | icon: 'i-carbon-ibm-cloud-transit-gateway' 97 | }, 98 | children: [ 99 | { 100 | path: `${BASE_URL}/list-1`, 101 | redirect: `${BASE_URL}/list-1/list2-1`, 102 | name: 'List-1', 103 | meta: { 104 | title: '列表-1', 105 | icon: 'i-carbon-list-boxes' 106 | } 107 | } 108 | ] 109 | } 110 | ] 111 | ``` 112 | 113 | ## 真香👍 114 | 115 | ### [图表来自](http://ppchart.com/#/) 116 | 117 | ### [插画](https://undraw.co/illustrations) 118 | 119 | ### [404插画](https://error404.fun/) 120 | 121 | ### [打印](https://www.npmjs.com/package/vue-plugin-hiprint) 122 | 123 | ### [数据mock-Apifox](https://apifox.com/) 124 | 125 | ## 开发经验/优化 126 | 127 | 1. 避免整体监听对象,会隐式触发deep。分清楚watch({}), 和 watch(() => {}) 使用场景 128 | 129 | ```js 130 | const watState = reactive({ arr: [], count: 1, str: '123', bo: true }) 131 | 132 | // watch(watState.str, () => {}) 133 | // 原始值不能直接监听,需要用getter函数 134 | // 引用可以直接监听,会隐式创建deep,用到getter函数,需显示deep监听,否则需要整体替换才触发watch 例: watState.arr = [] 135 | watch( 136 | () => watState.arr, 137 | (newVal, oldVal) => { 138 | message.success('触发!') 139 | console.log('-> newVal, oldVal', newVal, oldVal) 140 | } 141 | ) 142 | 143 | const onWatch = () => { 144 | watState.arr = [{ name: 'ealien', age: '123', sex: '1' }] 145 | } 146 | 147 | const counter = ref(0) 148 | // 不是原始值不能直接监听吗?啊这...。 别忘了ref访问需要 .value呀。souga 149 | watch(counter, (newVal, oldVal) => { 150 | console.log('-> newVal, oldVal', newVal, oldVal) 151 | }) 152 | ``` 153 | 154 | 2. 子组件想知道emit父级到底传没传? 155 | 156 | ```vue 157 | 158 | 159 | 160 | 161 | const emit = defineEmits(['confirm', 'cancel']) 162 | 163 | 164 | const props = defineProps({ onConfirm: { type: Function }, onCancel: { type: Function }, }) 165 | 166 | 167 | props.onConfirm props.onCancel 168 | ``` 169 | 170 | ## 指令 171 | 172 | 1. v-loading 173 | 174 | ```js 175 | const loading = `` 176 | 177 | /** 178 | * 通过自定义样式(global.less),对 primary 类型按钮,和官方示例一样。事件只执行一次 179 | * 180 | * 默认值1500毫秒 181 | * v-loading="2000" 182 | * v-loading == v-loading="1500" 183 | */ 184 | export default { 185 | mounted(el, binding) { 186 | const originInnerHtml = el.innerHTML 187 | 188 | if (binding.value && typeof binding.value !== 'number') { 189 | console.error('自定义时间应为数字 例: v-loading="2000"') 190 | return 191 | } 192 | 193 | el.addEventListener( 194 | 'click', 195 | () => { 196 | if (!el.disabled) { 197 | el.disabled = true 198 | el.innerHTML = `${loading}${originInnerHtml}` 199 | 200 | setTimeout(() => { 201 | el.innerHTML = originInnerHtml 202 | el.disabled = false 203 | }, binding.value || 1500) 204 | } 205 | }, 206 | false 207 | ) 208 | } 209 | } 210 | ``` 211 | 212 | 2. v-auth 按钮权限指令 213 | 214 | ```js 215 | /** 216 | * 设计场景 217 | * 218 | * 1、后台新增权限时选择类型是否是按钮,选择按钮类型。登录后调取接口查出所有按钮类型权限:(我使用接口作为唯一标识) 219 | * response = ['/user/list', '/user/add', '/user/detail/add', '/user/detail/edit'] 220 | * 221 | * 指令使用格式 222 | * v-auth="'/user/list'" 223 | * v-auth="['/user/list', '/user/detail/edit']" 224 | * 225 | * 226 | * 2、按照菜单权限层级返回。类似 mock 中的 adminRoutes,再增加类型区分是否是按钮权限即可。 227 | * response = [{ key: 'user', children: [{ key: 'user/list', children: [{ type: 'btn', key: 'api/user/list' }] }] }] 228 | * 229 | * 使用(.[user]修饰符用来快速定位查找,也可以起到命名空间的作用) 230 | * 231 | * 找到命名空间内的 232 | * v-auth.user="'api/user/list'" 233 | * v-auth.user="['api/user/list', 'api/user/list']" 234 | * v-auth="{ user: ["api/user/list", "api/user/add"], setting: [""] }" 235 | * v-auth="{ user: "", setting: "" }" 236 | * 237 | * user和setting模块中任意找到 238 | * v-auth.user.setting="api/user/list" 239 | * v-auth.user.setting="['api/user/list', 'api/user/add']" 240 | * 241 | * tip:要是有 user下面,或者setting下面有某个权限都可以显示按钮这种场景该怎么办 242 | * 243 | * 244 | * 245 | */ 246 | 247 | import { isArray, isString, isPlainObject } from 'lodash-es' 248 | 249 | const _mockResRouteData = [ 250 | { 251 | key: 'user', 252 | name: '用户管理', 253 | children: [ 254 | { 255 | key: 'user/list', 256 | name: '用户列表', 257 | children: [ 258 | { type: 'btn', key: 'api/user/list', name: '用户列表查看' }, 259 | { type: 'btn', key: 'api/user/detail', name: '用户详情' }, 260 | { type: 'btn', key: 'api/user/auth-edit', name: '用户权限编辑' } 261 | ] 262 | }, 263 | { 264 | key: 'user/list1', 265 | name: '用户列表1', 266 | children: [ 267 | { type: 'btn', key: 'api/user/list1', name: '用户列表查看1' }, 268 | { type: 'btn', key: 'api/user/detail1', name: '用户详情1' }, 269 | { type: 'btn', key: 'api/user/auth-edit1', name: '用户权限编辑1' } 270 | ] 271 | } 272 | ] 273 | }, 274 | { 275 | key: 'setting', 276 | name: '设置', 277 | children: [ 278 | { 279 | key: 'setting/auth', 280 | name: '权限设置', 281 | children: [ 282 | { type: 'btn', key: 'api/auth/add', name: '新增权限' }, 283 | { type: 'btn', key: 'api/auth/edit', name: '编辑权限' }, 284 | { type: 'btn', key: 'api/auth/list', name: '权限列表' } 285 | ] 286 | } 287 | ] 288 | }, 289 | { 290 | key: '404', 291 | name: '异常页面', 292 | children: [ 293 | { 294 | key: 'exception/404', 295 | name: '404页面', 296 | children: [ 297 | { type: 'btn', key: 'api/exception/add', name: '新增' }, 298 | { type: 'btn', key: 'api/exception/edit', name: '编辑' } 299 | ] 300 | }, 301 | { 302 | key: 'exception/503', 303 | name: '503页面' 304 | } 305 | ] 306 | } 307 | ] 308 | 309 | // 模块唯一标识key 310 | const KEY_NAME = 'key' 311 | const findNamesRoutes = (moduleName) => { 312 | return (_mockResRouteData.find((route) => route[KEY_NAME] === moduleName) || {}).children || [] 313 | } 314 | 315 | const btnKeys = (routes) => { 316 | const keys = [] 317 | 318 | function find(arr) { 319 | arr.forEach((it) => { 320 | // 按钮类型的唯一key 321 | if (it.type === 'btn') { 322 | keys.push(it[KEY_NAME]) 323 | } 324 | 325 | if (it.children && it.children.length) { 326 | find(it.children) 327 | } 328 | }) 329 | } 330 | 331 | find(routes) 332 | 333 | return keys 334 | } 335 | 336 | /** 337 | * 比对是否有相同项,只要找到一个有相同的,就立即返回(或的关系,所以可以提前返回) 338 | * 339 | * arrModuleValue 必然存在 340 | */ 341 | const hasDuplicates = (arr1, arrModuleValue) => { 342 | for (let i = 0, len = arrModuleValue.length; i < len; i++) { 343 | if (arr1.includes(arrModuleValue[i])) { 344 | return true 345 | } 346 | } 347 | 348 | return false 349 | } 350 | 351 | const hasPer = (moduleName, moduleValue) => { 352 | const keys = btnKeys(findNamesRoutes(moduleName)) 353 | 354 | if (isString(moduleValue)) { 355 | return keys.includes(moduleValue) 356 | } 357 | 358 | if (isArray(moduleValue) && moduleValue.length > 0) { 359 | return hasDuplicates(keys, moduleValue) 360 | } 361 | 362 | return false 363 | } 364 | 365 | const DOM_MARK = 'data-auth' 366 | const hasMark = (el) => { 367 | return el.getAttribute(DOM_MARK) === 'true' 368 | } 369 | 370 | const setMark = (el) => { 371 | el.setAttribute(DOM_MARK, true) 372 | } 373 | 374 | const removeEl = (el) => { 375 | el && el.parentNode && el.parentNode.removeChild(el) 376 | } 377 | 378 | /** 379 | * 场景2方式实现 380 | */ 381 | export default { 382 | mounted(el, binding) { 383 | const { modifiers, value } = binding 384 | 385 | const valueIsPlainObj = isPlainObject(value) 386 | const routeModules = Object.keys(valueIsPlainObj ? value : modifiers) 387 | 388 | if (routeModules.length) { 389 | try { 390 | routeModules.forEach((module) => { 391 | const curModuleValue = valueIsPlainObj ? value[module] : value 392 | if (hasPer(module, curModuleValue)) { 393 | setMark(el) 394 | throw new Error('当前el已打标可立即跳出') 395 | } 396 | }) 397 | } catch {} 398 | } else { 399 | // 没有命名空间直接删除,例:v-auth='"api/list"' 400 | removeEl(el) 401 | return 402 | } 403 | 404 | if (!hasMark(el)) { 405 | removeEl(el) 406 | } 407 | }, 408 | 409 | updated() {}, 410 | 411 | unmounted() {} 412 | } 413 | ``` 414 | 415 | 3. v-scrollbar 自定义scrollbar样式,类似mac滚动条 416 | 417 | ```js 418 | import Scrollbar from 'smooth-scrollbar' 419 | import config from '@/config/index.js' 420 | 421 | const extractProp = (prop) => (obj) => (typeof obj === 'undefined' ? undefined : obj[prop]) 422 | const extractOptions = extractProp('options') 423 | const extractEl = extractProp('el') 424 | 425 | const bestMatch = (extractor) => (possibilities) => 426 | extractor(possibilities.find((p) => typeof extractor(p) !== 'undefined')) 427 | const bestEl = bestMatch(extractEl) 428 | const bestOptions = bestMatch(extractOptions) 429 | 430 | /** 431 | v-scrollbar 432 | v-scrollbar="{ el: "" }" 433 | */ 434 | export default { 435 | mounted(el, binding) { 436 | if (config.useCustomScrollBar) { 437 | const possibilities = [binding.value] 438 | const targetEl = bestEl(possibilities) 439 | const config = bestOptions(possibilities) 440 | 441 | const scrollY = binding.modifiers.y 442 | const scrollX = binding.modifiers.x 443 | Scrollbar.init(targetEl ? document.querySelector(targetEl) : el) 444 | } 445 | }, 446 | 447 | updated(el, binding, vnode, prevVnode) {}, 448 | 449 | unmounted(el, binding) { 450 | if (config.useCustomScrollBar) { 451 | const possibilities = [binding.value] 452 | const targetEl = bestEl(possibilities) 453 | Scrollbar.destroy(targetEl ? document.querySelector(targetEl) : el, {}) 454 | } 455 | } 456 | } 457 | ``` 458 | 459 | 4. v-lazyImg 图片懒加载 460 | 461 | ``` 462 | import { useIntersectionObserver } from '@vueuse/core' 463 | 464 | /** 465 | v-lazyImg="'https://via.placeholder.com/250'" 466 | */ 467 | export default { 468 | mounted(el, binding) { 469 | const { stop } = useIntersectionObserver( 470 | el, 471 | ([{ isIntersecting }], observerElement) => { 472 | if (isIntersecting) { 473 | stop() 474 | 475 | el.src = binding.value 476 | } 477 | }, 478 | { threshold: 0 } 479 | ) 480 | } 481 | } 482 | ``` 483 | --------------------------------------------------------------------------------