├── .env.development ├── .env.production ├── .env.staging ├── src ├── vite-env.d.ts ├── store │ ├── keys.ts │ ├── index.ts │ ├── pinia.ts │ ├── modules │ │ ├── cached-routes.ts │ │ ├── app-config.ts │ │ ├── user.ts │ │ └── permission.ts │ ├── plugin │ │ └── persist.ts │ └── types.ts ├── assets │ ├── logo.png │ ├── bg_img.webp │ ├── img_403.png │ ├── img_404.png │ ├── img_500.png │ ├── qrcode.jpg │ ├── the_p.png │ ├── the_w.png │ ├── the_x.png │ ├── img_vue.jpeg │ ├── icon_sex_man.png │ ├── img_angular.jpeg │ ├── img_avatar.gif │ ├── img_login_bg.png │ ├── img_react.jpeg │ ├── img_vip_icon.png │ ├── icon_sex_woman.png │ ├── img_avatar_01.jpeg │ ├── img_avatar_02.jpeg │ ├── img_avatar_default.png │ └── data │ │ └── provinces.json ├── hooks │ ├── useAxios.ts │ ├── useGet.ts │ ├── usePost.ts │ ├── useAppInfo.ts │ ├── useEcharts.ts │ ├── useMenuWidth.ts │ ├── useCreateScript.ts │ └── useDialogDragger.ts ├── icons │ ├── iconfont │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ ├── add.svg │ ├── expand.svg │ ├── menu.svg │ ├── data-money.svg │ └── data-chart.svg ├── shims-vue.d.ts ├── styles │ ├── variables.scss │ ├── index.ts │ ├── index.css │ └── transition.css ├── App.vue ├── router │ ├── guard │ │ ├── index.ts │ │ ├── title.ts │ │ ├── cached.ts │ │ ├── permission.ts │ │ └── visited.ts │ ├── index.ts │ └── routes │ │ ├── async.ts │ │ ├── constants.ts │ │ └── default-routes.ts ├── components │ ├── common │ │ ├── TableBody.vue │ │ ├── AddButton.vue │ │ ├── DeleteButton.vue │ │ ├── TableConfig.vue │ │ ├── SearchContent.vue │ │ ├── RichTextEditor.vue │ │ ├── TableFooter.vue │ │ ├── PopoverMessage.vue │ │ ├── CitySelector.vue │ │ ├── IconSelector.vue │ │ ├── TableHeader.vue │ │ ├── ModalDialog.vue │ │ └── SortableTable.vue │ ├── loading │ │ └── index.vue │ ├── footer │ │ └── index.vue │ ├── Main.vue │ ├── index.ts │ ├── navbar │ │ └── NavBar.vue │ ├── humburger │ │ └── index.vue │ ├── svg-icon │ │ └── index.vue │ ├── logo │ │ └── index.vue │ ├── header │ │ └── index.vue │ ├── sidebar │ │ └── components │ │ │ ├── HorizontalScrollerMenu.vue │ │ │ └── ScrollerMenu.vue │ ├── setting │ │ └── components │ │ │ └── StyleExample.vue │ ├── avatar │ │ └── VAWAvatar.vue │ └── breadcrumb │ │ └── index.vue ├── views │ ├── exception │ │ ├── 403.vue │ │ ├── 404.vue │ │ ├── 500.vue │ │ └── components │ │ │ └── ExceptionStatus.vue │ ├── next │ │ ├── menu1.vue │ │ ├── menu2 │ │ │ ├── menu-2-2.vue │ │ │ └── menu-2-1 │ │ │ │ ├── menu-2-1-2.vue │ │ │ │ └── menu-2-1-1.vue │ │ └── cache-next-child.vue │ ├── redirect │ │ └── index.vue │ ├── other │ │ ├── chart │ │ │ ├── icon-selector.vue │ │ │ ├── icon.vue │ │ │ └── components │ │ │ │ ├── Chart.vue │ │ │ │ ├── IconFont.vue │ │ │ │ └── xicons.vue │ │ ├── big-preview.vue │ │ ├── clipboard.vue │ │ ├── player.vue │ │ ├── print.vue │ │ ├── css-animation.vue │ │ └── city-selector.vue │ ├── editor │ │ ├── markdown.vue │ │ └── rich-text.vue │ ├── index │ │ └── components │ │ │ ├── TrendsItem.vue │ │ │ ├── TodoItem.vue │ │ │ ├── DataItem.vue │ │ │ ├── ProjectItem.vue │ │ │ └── chart │ │ │ ├── OrderChart.vue │ │ │ ├── EnrollmentChannelsChart.vue │ │ │ ├── SalesChart.vue │ │ │ ├── StudentChart.vue │ │ │ └── DepartmentChart.vue │ ├── map │ │ ├── gaode.vue │ │ └── baidu.vue │ ├── result │ │ ├── success.vue │ │ └── fail.vue │ ├── form │ │ ├── step-form.vue │ │ └── components │ │ │ ├── ResultInfo.vue │ │ │ └── PasswordInfo.vue │ ├── list │ │ ├── card-list.vue │ │ └── list.vue │ ├── project │ │ └── infomation.vue │ ├── draggable │ │ └── card-draggable.vue │ └── login │ │ └── index.vue ├── api │ ├── interceptors │ │ └── CustomRequestInterceptor.ts │ ├── url.ts │ ├── axios.config.ts │ └── http.ts ├── main.ts ├── setting │ └── index.ts ├── types │ └── components.ts └── utils │ └── index.ts ├── public ├── favicon.ico └── static │ ├── images │ ├── 1.jpeg │ ├── 2.jpeg │ ├── 3.jpeg │ ├── 4.jpeg │ ├── 5.jpeg │ ├── 6.jpeg │ ├── 7.jpeg │ ├── 8.jpeg │ ├── 9.jpeg │ └── img_avatar_01.jpeg │ └── loading.css ├── mock ├── base.ts ├── index.ts └── user │ └── index.js ├── postcss.config.js ├── .gitignore ├── tailwind.config.js ├── .eslintignore ├── tsconfig.json ├── prettier.config.js ├── .vscode └── settings.json ├── LICENSE ├── index.html ├── vite.config.ts ├── package.json └── .eslintrc.js /.env.development: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV=production -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/store/keys.ts: -------------------------------------------------------------------------------- 1 | export const LAYOUT = () => import('@/components/Layout.vue') 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /mock/base.ts: -------------------------------------------------------------------------------- 1 | export const baseData = { 2 | code: 200, 3 | data: '', 4 | msg: '获取数据成功', 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/bg_img.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/bg_img.webp -------------------------------------------------------------------------------- /src/assets/img_403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_403.png -------------------------------------------------------------------------------- /src/assets/img_404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_404.png -------------------------------------------------------------------------------- /src/assets/img_500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_500.png -------------------------------------------------------------------------------- /src/assets/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/qrcode.jpg -------------------------------------------------------------------------------- /src/assets/the_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/the_p.png -------------------------------------------------------------------------------- /src/assets/the_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/the_w.png -------------------------------------------------------------------------------- /src/assets/the_x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/the_x.png -------------------------------------------------------------------------------- /src/assets/img_vue.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_vue.jpeg -------------------------------------------------------------------------------- /public/static/images/1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/1.jpeg -------------------------------------------------------------------------------- /public/static/images/2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/2.jpeg -------------------------------------------------------------------------------- /public/static/images/3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/3.jpeg -------------------------------------------------------------------------------- /public/static/images/4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/4.jpeg -------------------------------------------------------------------------------- /public/static/images/5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/5.jpeg -------------------------------------------------------------------------------- /public/static/images/6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/6.jpeg -------------------------------------------------------------------------------- /public/static/images/7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/7.jpeg -------------------------------------------------------------------------------- /public/static/images/8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/8.jpeg -------------------------------------------------------------------------------- /public/static/images/9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/9.jpeg -------------------------------------------------------------------------------- /src/assets/icon_sex_man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/icon_sex_man.png -------------------------------------------------------------------------------- /src/assets/img_angular.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_angular.jpeg -------------------------------------------------------------------------------- /src/assets/img_avatar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_avatar.gif -------------------------------------------------------------------------------- /src/assets/img_login_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_login_bg.png -------------------------------------------------------------------------------- /src/assets/img_react.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_react.jpeg -------------------------------------------------------------------------------- /src/assets/img_vip_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_vip_icon.png -------------------------------------------------------------------------------- /src/assets/icon_sex_woman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/icon_sex_woman.png -------------------------------------------------------------------------------- /src/assets/img_avatar_01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_avatar_01.jpeg -------------------------------------------------------------------------------- /src/assets/img_avatar_02.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_avatar_02.jpeg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/hooks/useAxios.ts: -------------------------------------------------------------------------------- 1 | import axios from '../api/axios.config' 2 | export default function () { 3 | return axios 4 | } 5 | -------------------------------------------------------------------------------- /src/hooks/useGet.ts: -------------------------------------------------------------------------------- 1 | import { get } from '../api/http' 2 | export default function usePost() { 3 | return get 4 | } 5 | -------------------------------------------------------------------------------- /src/hooks/usePost.ts: -------------------------------------------------------------------------------- 1 | import { post } from '../api/http' 2 | export default function usePost() { 3 | return post 4 | } 5 | -------------------------------------------------------------------------------- /src/icons/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/icons/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/icons/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/icons/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/assets/img_avatar_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/assets/img_avatar_default.png -------------------------------------------------------------------------------- /src/icons/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/src/icons/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /public/static/images/img_avatar_01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingqingxuan/admin-work/HEAD/public/static/images/img_avatar_01.jpeg -------------------------------------------------------------------------------- /src/hooks/useAppInfo.ts: -------------------------------------------------------------------------------- 1 | import packageInfo from '../../package.json' 2 | export default function useAppInfo() { 3 | return packageInfo 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist-ssr 4 | *.local 5 | /dist 6 | dist 7 | package-lock.json 8 | components.d.ts 9 | pnpm-lock.yaml 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import pinia from './pinia' 3 | 4 | function useAppPinia(app: App) { 5 | app.use(pinia) 6 | } 7 | 8 | export default useAppPinia 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | node_modules 3 | *.md 4 | *.woff 5 | *.ttf 6 | .vscode 7 | .idea 8 | dist 9 | /public 10 | /docs 11 | .husky 12 | .local 13 | /bin 14 | Dockerfile 15 | /dist 16 | -------------------------------------------------------------------------------- /src/store/pinia.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import PersistPlugin from './plugin/persist' 3 | 4 | const pinia = createPinia() 5 | pinia.use(PersistPlugin) 6 | 7 | export default pinia 8 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $tabSplitMenuWidth: 65px; 2 | $menuWidth: var(--menu-width); 3 | $minMenuWidth: 65px; 4 | $transitionTime: 0.3s ease; 5 | $logoHeight: 48px; 6 | $tabHeight: 40px; 7 | $footerHeight: 45px; 8 | -------------------------------------------------------------------------------- /mock/index.ts: -------------------------------------------------------------------------------- 1 | function useMock() { 2 | const modules = import.meta.glob('./**/*.{js,ts}', { eager: true }) 3 | Object.keys(modules).forEach((it) => { 4 | modules[it] 5 | }) 6 | } 7 | export default useMock 8 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import 'vfonts/Lato.css' 2 | import 'vfonts/FiraCode.css' 3 | import 'virtual:svg-icons-register' 4 | import './index.css' 5 | import './transition.css' 6 | import '@/icons/iconfont/iconfont.css' 7 | -------------------------------------------------------------------------------- /src/hooks/useEcharts.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts' 2 | export default function useEcharts(dom: HTMLElement, theme?: string) { 3 | let instance = echarts.getInstanceByDom(dom) 4 | if (!instance) { 5 | instance = echarts.init(dom, theme) 6 | } 7 | return instance 8 | } 9 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import useCachedGuard from './cached' 2 | import usePermissionGuard from './permission' 3 | import usePageTitleGuard from './title' 4 | import useVisitedGuard from './visited' 5 | 6 | export default function useRouterGuard() { 7 | usePermissionGuard() 8 | useVisitedGuard() 9 | useCachedGuard() 10 | usePageTitleGuard() 11 | } 12 | -------------------------------------------------------------------------------- /src/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | /*! @import */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | :root { 6 | --menu-width: 210px; 7 | } 8 | html, 9 | body, 10 | #app { 11 | height: 100vh; 12 | } 13 | * { 14 | box-sizing: border-box; 15 | } 16 | 17 | td .n-button + .n-button { 18 | margin-left: 10px; 19 | } 20 | 21 | .n-card { 22 | border-radius: 10px; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/common/TableBody.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/router/guard/title.ts: -------------------------------------------------------------------------------- 1 | import router from '@/router' 2 | import { projectName } from '@/setting' 3 | 4 | function usePageTitleGuard() { 5 | router.afterEach((to) => { 6 | if (to.meta && to.meta.title) { 7 | const title = to.meta.title 8 | document.title = projectName + ' | ' + title 9 | } else { 10 | document.title = projectName 11 | } 12 | }) 13 | } 14 | 15 | export default usePageTitleGuard 16 | -------------------------------------------------------------------------------- /src/views/exception/403.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/views/exception/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/views/exception/500.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/hooks/useMenuWidth.ts: -------------------------------------------------------------------------------- 1 | export function useMenuWidth() { 2 | const r = document.querySelector(':root') as HTMLElement 3 | const styles = getComputedStyle(r) 4 | const menuWith = styles.getPropertyValue('--menu-width') 5 | return parseInt(menuWith) 6 | } 7 | 8 | export function useChangeMenuWidth(width: Number) { 9 | const r = document.querySelector(':root') as HTMLElement 10 | r.style.setProperty('--menu-width', width + 'px') 11 | } 12 | -------------------------------------------------------------------------------- /src/views/next/menu1.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/views/next/menu2/menu-2-2.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import { mapTwoLevelRouter } from '@/store/help' 3 | import { createRouter, createWebHashHistory } from 'vue-router' 4 | import { constantRoutes } from './routes/constants' 5 | 6 | const router = createRouter({ 7 | history: createWebHashHistory(), 8 | routes: mapTwoLevelRouter([...constantRoutes]), 9 | }) 10 | 11 | export function useAppRouter(app: App) { 12 | app.use(router) 13 | } 14 | 15 | export default router 16 | -------------------------------------------------------------------------------- /src/api/interceptors/CustomRequestInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { useUserStoreContext } from '@/store/modules/user' 2 | import { AxiosRequestConfig } from 'axios' 3 | 4 | export default function (config: AxiosRequestConfig) { 5 | const useStore = useUserStoreContext() 6 | if (config) { 7 | if (!config.headers) { 8 | config.headers = {} 9 | } 10 | if (!config.headers['Auth']) { 11 | config.headers['Auth'] = useStore.token 12 | } 13 | } 14 | return config 15 | } 16 | -------------------------------------------------------------------------------- /src/views/next/menu2/menu-2-1/menu-2-1-2.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /src/components/loading/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /src/views/other/chart/icon-selector.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /src/router/guard/cached.ts: -------------------------------------------------------------------------------- 1 | import { findCachedRoutes } from '@/store/help' 2 | import useCachedRouteStore from '@/store/modules/cached-routes' 3 | import router from '..' 4 | 5 | function useCachedGuard() { 6 | router.beforeEach(() => { 7 | const cachedRouteStore = useCachedRouteStore() 8 | if (cachedRouteStore.getCachedRouteName.length === 0) { 9 | cachedRouteStore.initCachedRoute(findCachedRoutes(router.getRoutes())) 10 | } 11 | return true 12 | }) 13 | } 14 | 15 | export default useCachedGuard 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "lib": ["esnext", "dom"], 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["src/*"] 16 | } 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 19 | } 20 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | vueIndentScriptAndStyle: true, 7 | singleQuote: true, 8 | quoteProps: 'as-needed', 9 | bracketSpacing: true, 10 | trailingComma: 'es5', 11 | jsxBracketSameLine: false, 12 | jsxSingleQuote: false, 13 | arrowParens: 'always', 14 | insertPragma: false, 15 | requirePragma: false, 16 | proseWrap: 'never', 17 | htmlWhitespaceSensitivity: 'strict', 18 | endOfLine: 'auto', 19 | rangeStart: 0, 20 | } 21 | -------------------------------------------------------------------------------- /src/views/next/menu2/menu-2-1/menu-2-1-1.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './styles' 4 | import useGlobalComponents from './components' 5 | import { useAppRouter } from './router' 6 | import useRouterGuard from './router/guard' 7 | import useAppPinia from './store' 8 | import useMock from '../mock' 9 | 10 | function vawBoot() { 11 | const app = createApp(App) 12 | useAppPinia(app) 13 | useAppRouter(app) 14 | useGlobalComponents(app) 15 | useRouterGuard() 16 | useMock() 17 | app.mount('#app') 18 | } 19 | 20 | vawBoot() 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.iconTheme": "material-icon-theme", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "[html]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[javascript]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[typescript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[vue]": { 16 | "editor.defaultFormatter": "Vue.volar" 17 | }, 18 | "git.ignoreLimitWarning": true 19 | } 20 | -------------------------------------------------------------------------------- /src/views/other/big-preview.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/editor/markdown.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /src/hooks/useCreateScript.ts: -------------------------------------------------------------------------------- 1 | import { onMounted } from 'vue' 2 | 3 | export default function userCreateScript(src: string) { 4 | const createScriptPromise = new Promise((resolve, reject) => { 5 | onMounted(() => { 6 | const script = document.createElement('script') 7 | script.type = 'text/javascript' 8 | script.onload = () => { 9 | resolve('') 10 | } 11 | script.onerror = (error) => { 12 | reject(error) 13 | } 14 | script.src = src 15 | document.head.appendChild(script) 16 | }) 17 | }) 18 | return { 19 | createScriptPromise, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/views/next/cache-next-child.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /src/setting/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AppConfigState, 3 | DeviceType, 4 | LayoutMode, 5 | PageAnim, 6 | SideTheme, 7 | ThemeMode, 8 | } from '@/store/types' 9 | 10 | export const projectName = 'Admin Work' 11 | 12 | export default { 13 | theme: ThemeMode.LIGHT, 14 | sideTheme: SideTheme.WHITE, 15 | themeColor: '#409eff', 16 | layoutMode: LayoutMode.LTR, 17 | sideWidth: 210, 18 | deviceType: DeviceType.PC, 19 | pageAnim: PageAnim.OPACITY, 20 | isFixedNavBar: true, 21 | isCollapse: false, 22 | actionBar: { 23 | isShowSearch: true, 24 | isShowMessage: true, 25 | isShowRefresh: true, 26 | isShowFullScreen: true, 27 | }, 28 | } as AppConfigState 29 | -------------------------------------------------------------------------------- /src/components/common/AddButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /src/components/footer/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/views/index/components/TrendsItem.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /src/components/common/DeleteButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /src/icons/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/modules/cached-routes.ts: -------------------------------------------------------------------------------- 1 | import { toHump } from '@/utils' 2 | import { defineStore } from 'pinia' 3 | 4 | const useCachedRouteStore = defineStore('cached-routes', { 5 | state: () => { 6 | return { 7 | cachedRoutes: [] as string[], 8 | } 9 | }, 10 | getters: { 11 | getCachedRouteName(state) { 12 | return state.cachedRoutes 13 | }, 14 | }, 15 | actions: { 16 | initCachedRoute(routes: string[]) { 17 | this.cachedRoutes = routes.map((it) => { 18 | return toHump(it as string) 19 | }) 20 | }, 21 | setCachedRoutes(cachedRoutes: string[] = []) { 22 | this.cachedRoutes = cachedRoutes 23 | }, 24 | resetCachedRoutes() { 25 | this.$reset() 26 | }, 27 | }, 28 | }) 29 | 30 | export default useCachedRouteStore 31 | -------------------------------------------------------------------------------- /src/views/other/chart/icon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | -------------------------------------------------------------------------------- /src/assets/data/provinces.json: -------------------------------------------------------------------------------- 1 | [{"code":"11","name":"北京市"},{"code":"12","name":"天津市"},{"code":"13","name":"河北省"},{"code":"14","name":"山西省"},{"code":"15","name":"内蒙古自治区"},{"code":"21","name":"辽宁省"},{"code":"22","name":"吉林省"},{"code":"23","name":"黑龙江省"},{"code":"31","name":"上海市"},{"code":"32","name":"江苏省"},{"code":"33","name":"浙江省"},{"code":"34","name":"安徽省"},{"code":"35","name":"福建省"},{"code":"36","name":"江西省"},{"code":"37","name":"山东省"},{"code":"41","name":"河南省"},{"code":"42","name":"湖北省"},{"code":"43","name":"湖南省"},{"code":"44","name":"广东省"},{"code":"45","name":"广西壮族自治区"},{"code":"46","name":"海南省"},{"code":"50","name":"重庆市"},{"code":"51","name":"四川省"},{"code":"52","name":"贵州省"},{"code":"53","name":"云南省"},{"code":"54","name":"西藏自治区"},{"code":"61","name":"陕西省"},{"code":"62","name":"甘肃省"},{"code":"63","name":"青海省"},{"code":"64","name":"宁夏回族自治区"},{"code":"65","name":"新疆维吾尔自治区"}] -------------------------------------------------------------------------------- /src/components/Main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /src/api/url.ts: -------------------------------------------------------------------------------- 1 | import { baseURL } from './axios.config' 2 | 3 | export const baseAddress = baseURL 4 | 5 | export const test = '/test' 6 | 7 | export const login = '/login' 8 | 9 | export const updateUserInfo = '/updateUser' 10 | 11 | export const addUserInfo = '/addUser' 12 | 13 | export const getMenuListByRoleId = '/getMenusByRoleId' 14 | 15 | export const getAllMenuByRoleId = '/getAllMenuByRoleId' 16 | 17 | export const deleteUserById = '/deleteUserById' 18 | 19 | export const getDepartmentList = '/getDepartmentList' 20 | 21 | export const addDepartment = '/addDepartment' 22 | 23 | export const getRoleList = '/getRoleList' 24 | 25 | export const getMenuList = '/getMenuList' 26 | 27 | export const getParentMenuList = '/getParentMenuList' 28 | 29 | export const getTableList = '/getTableList' 30 | 31 | export const getCardList = '/getCardList' 32 | 33 | export const getCommentList = '/getCommentList' 34 | -------------------------------------------------------------------------------- /src/router/guard/permission.ts: -------------------------------------------------------------------------------- 1 | import useUserStore from '@/store/modules/user' 2 | import usePermissionStore from '@/store/modules/permission' 3 | import router from '..' 4 | 5 | const whiteRoutes: string[] = ['/login', '/404', '/403', '/500'] 6 | 7 | function usePermissionGuard() { 8 | router.beforeEach(async (to) => { 9 | if (whiteRoutes.includes(to.path)) { 10 | return true 11 | } 12 | const userStore = useUserStore() 13 | if (userStore.isTokenExpire()) { 14 | return { 15 | path: '/login', 16 | query: { redirect: to.fullPath }, 17 | } 18 | } 19 | const permissionStore = usePermissionStore() 20 | const isEmptyRoute = permissionStore.isEmptyPermissionRoute() 21 | if (isEmptyRoute) { 22 | await permissionStore.initPermissionRoute() 23 | return { ...to, replace: true } 24 | } 25 | return true 26 | }) 27 | } 28 | 29 | export default usePermissionGuard 30 | -------------------------------------------------------------------------------- /src/router/routes/async.ts: -------------------------------------------------------------------------------- 1 | import { LAYOUT } from '@/store/keys' 2 | 3 | export const asyncRoutes = [ 4 | { 5 | path: '/index', 6 | component: LAYOUT, 7 | name: 'Index', 8 | meta: { 9 | title: 'Dashboard', 10 | iconPrefix: 'iconfont', 11 | icon: 'dashboard', 12 | }, 13 | children: [ 14 | { 15 | path: 'home', 16 | name: 'Home', 17 | component: () => import('@/views/index/main.vue'), 18 | meta: { 19 | title: '主控台', 20 | affix: true, 21 | cacheable: true, 22 | iconPrefix: 'iconfont', 23 | icon: 'menu', 24 | }, 25 | }, 26 | { 27 | path: 'work-place', 28 | name: 'WorkPlace', 29 | component: () => import('@/views/index/work-place.vue'), 30 | meta: { 31 | title: '工作台', 32 | affix: true, 33 | iconPrefix: 'iconfont', 34 | icon: 'menu', 35 | }, 36 | }, 37 | ], 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /src/router/guard/visited.ts: -------------------------------------------------------------------------------- 1 | import { findAffixedRoutes } from '@/store/help' 2 | import useVisitedRouteStore from '@/store/modules/visited-routes' 3 | import { RouteRecordRaw } from 'vue-router' 4 | import router from '..' 5 | 6 | function useVisitedGuard() { 7 | router.beforeEach((to) => { 8 | if (['404', '500', '403', 'not-found', 'Login'].includes(to.name as string)) { 9 | return true 10 | } 11 | const visitedRouteStore = useVisitedRouteStore() 12 | if (!visitedRouteStore.isLoadAffix) { 13 | const affixRoutes = findAffixedRoutes(router.getRoutes()) 14 | visitedRouteStore.initAffixRoutes(affixRoutes) 15 | } 16 | if (to.path.startsWith('/redirect')) { 17 | return true 18 | } 19 | if (to.meta.noShowTabbar) { 20 | return true 21 | } 22 | if (to.query?.noShowTabbar) { 23 | return true 24 | } 25 | visitedRouteStore.addVisitedRoute(to as unknown as RouteRecordRaw) 26 | return true 27 | }) 28 | } 29 | 30 | export default useVisitedGuard 31 | -------------------------------------------------------------------------------- /src/views/other/clipboard.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /src/views/other/chart/components/Chart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/views/map/gaode.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 清清玄 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | Admin Work 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
Admin Work
28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import { toHump } from '../utils' 3 | 4 | function adapterNaiveCss() { 5 | const meta = document.createElement('meta') 6 | meta.name = 'naive-ui-style' 7 | document.head.appendChild(meta) 8 | } 9 | 10 | function getComponentName(key: string) { 11 | const paths = key.split('/') 12 | const name = paths 13 | .filter((it) => !!it && it !== '.') 14 | .reverse() 15 | .find( 16 | (it) => 17 | it !== 'index.vue' && 18 | it !== 'index.ts' && 19 | it !== 'index.tsx' && 20 | it !== 'index.js' && 21 | it !== 'index.jsx' 22 | ) 23 | ?.replace('.vue', '') 24 | return name || '' 25 | } 26 | 27 | export function registerComponents(app: App) { 28 | const components = import.meta.glob('./**/**.{vue,tsx}', { eager: true }) 29 | Object.keys(components).forEach((it: string) => { 30 | const component = components[it] as any 31 | app.component(component.default.name || toHump(getComponentName(it)), component.default) 32 | }) 33 | } 34 | 35 | function useGlobalComponents(app: App) { 36 | adapterNaiveCss() 37 | registerComponents(app) 38 | } 39 | 40 | export default useGlobalComponents 41 | -------------------------------------------------------------------------------- /src/views/other/player.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/views/map/baidu.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/components/navbar/NavBar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | 29 | 47 | -------------------------------------------------------------------------------- /src/components/common/TableConfig.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 43 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' 3 | import path from 'path' 4 | import vitePluginCompression from 'vite-plugin-compression' 5 | import ViteComponents from 'unplugin-vue-components/vite' 6 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' 7 | 8 | import vueJsx from '@vitejs/plugin-vue-jsx' 9 | 10 | export default () => { 11 | return { 12 | base: '/', 13 | plugins: [ 14 | vue(), 15 | createSvgIconsPlugin({ 16 | iconDirs: [path.resolve(process.cwd(), 'src/icons')], 17 | symbolId: 'icon-[dir]-[name]', 18 | }), 19 | vitePluginCompression({ 20 | threshold: 1024 * 10, 21 | }), 22 | ViteComponents({ 23 | resolvers: [NaiveUiResolver()], 24 | }), 25 | vueJsx(), 26 | ], 27 | css: { 28 | preprocessorOptions: { 29 | scss: { 30 | additionalData: '@use "./src/styles/variables.scss" as *;', 31 | }, 32 | }, 33 | }, 34 | resolve: { 35 | alias: [ 36 | { 37 | find: '@/', 38 | replacement: path.resolve(process.cwd(), 'src') + '/', 39 | }, 40 | ], 41 | }, 42 | server: { 43 | open: true, 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/humburger/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 50 | -------------------------------------------------------------------------------- /src/api/axios.config.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosResponse } from 'axios' 2 | import qs from 'qs' 3 | 4 | export const baseURL = 'http://localhost:8080/' 5 | 6 | export const CONTENT_TYPE = 'Content-Type' 7 | 8 | export const FORM_URLENCODED = 'application/x-www-form-urlencoded; charset=UTF-8' 9 | 10 | export const APPLICATION_JSON = 'application/json; charset=UTF-8' 11 | 12 | export const TEXT_PLAIN = 'text/plain; charset=UTF-8' 13 | 14 | const service = Axios.create({ 15 | baseURL, 16 | timeout: 10 * 60 * 1000, 17 | }) 18 | 19 | service.interceptors.request.use( 20 | (config) => { 21 | !config.headers && (config.headers = {}) 22 | if (!config.headers[CONTENT_TYPE]) { 23 | config.headers[CONTENT_TYPE] = APPLICATION_JSON 24 | } 25 | if (config.headers[CONTENT_TYPE] === FORM_URLENCODED) { 26 | config.data = qs.stringify(config.data) 27 | } 28 | return config 29 | }, 30 | (error) => { 31 | return Promise.reject(error.response) 32 | } 33 | ) 34 | 35 | service.interceptors.response.use( 36 | (response: AxiosResponse): AxiosResponse => { 37 | if (response.status === 200) { 38 | return response 39 | } else { 40 | throw new Error(response.status.toString()) 41 | } 42 | }, 43 | (error) => { 44 | if (import.meta.env.MODE === 'development') { 45 | console.log(error) 46 | } 47 | return Promise.reject({ code: 500, msg: '服务器异常,请稍后重试…' }) 48 | } 49 | ) 50 | 51 | export default service 52 | -------------------------------------------------------------------------------- /src/components/svg-icon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 41 | 42 | 60 | -------------------------------------------------------------------------------- /src/router/routes/constants.ts: -------------------------------------------------------------------------------- 1 | import { LAYOUT } from '@/store/keys' 2 | 3 | export const constantRoutes = [ 4 | { 5 | path: '/login', 6 | name: 'Login', 7 | component: () => import('@/views/login/index.vue'), 8 | meta: { 9 | hidden: true, 10 | }, 11 | }, 12 | { 13 | path: '/redirect', 14 | component: LAYOUT, 15 | meta: { 16 | hidden: true, 17 | noShowTabbar: true, 18 | }, 19 | children: [ 20 | { 21 | path: '/redirect/:path(.*)*', 22 | component: () => import('@/views/redirect/index.vue'), 23 | }, 24 | ], 25 | }, 26 | { 27 | path: '/personal', 28 | name: 'personal', 29 | component: LAYOUT, 30 | meta: { 31 | title: '个人中心', 32 | hidden: true, 33 | }, 34 | children: [ 35 | { 36 | path: 'info', 37 | component: () => import('@/views/personal/index.vue'), 38 | meta: { 39 | title: '个人中心', 40 | }, 41 | }, 42 | ], 43 | }, 44 | { 45 | path: '/404', 46 | name: '404', 47 | component: () => import('@/views/exception/404.vue'), 48 | meta: { 49 | hidden: true, 50 | }, 51 | }, 52 | { 53 | path: '/500', 54 | name: '500', 55 | component: () => import('@/views/exception/500.vue'), 56 | meta: { 57 | hidden: true, 58 | }, 59 | }, 60 | { 61 | path: '/403', 62 | name: '403', 63 | component: () => import('@/views/exception/403.vue'), 64 | meta: { 65 | hidden: true, 66 | }, 67 | }, 68 | ] 69 | -------------------------------------------------------------------------------- /src/store/modules/app-config.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | import defaultSetting from '@/setting' 4 | import { LayoutMode, PageAnim, SideTheme, ThemeMode, DeviceType } from '../types' 5 | 6 | import { useChangeMenuWidth } from '@/hooks/useMenuWidth' 7 | useChangeMenuWidth(defaultSetting.sideWidth) 8 | 9 | const useAppConfigStore = defineStore('app-config', { 10 | state: () => { 11 | return defaultSetting 12 | }, 13 | getters: { 14 | getLayoutMode(state) { 15 | return state.layoutMode 16 | }, 17 | }, 18 | actions: { 19 | changeTheme(theme: ThemeMode) { 20 | this.theme = theme 21 | }, 22 | changeLayoutMode(mode: LayoutMode) { 23 | this.layoutMode = mode 24 | }, 25 | changeDevice(deviceType: DeviceType) { 26 | this.deviceType = deviceType 27 | }, 28 | changeSideBarTheme(sideTheme: SideTheme) { 29 | this.sideTheme = sideTheme 30 | }, 31 | changePageAnim(pageAnim: PageAnim) { 32 | this.pageAnim = pageAnim 33 | }, 34 | changePrimaryColor(color: string) { 35 | this.themeColor = color 36 | }, 37 | changeSideWith(sideWidth: number) { 38 | this.sideWidth = sideWidth 39 | const r = document.querySelector(':root') as HTMLElement 40 | r.style.setProperty('--menu-width', sideWidth + 'px') 41 | }, 42 | toggleCollapse(isCollapse: boolean) { 43 | this.isCollapse = isCollapse 44 | }, 45 | }, 46 | presist: { 47 | enable: true, 48 | resetToState: true, 49 | }, 50 | }) 51 | 52 | export default useAppConfigStore 53 | -------------------------------------------------------------------------------- /src/views/result/success.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | 34 | 57 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { UserState } from '../types' 3 | import store from '../pinia' 4 | 5 | import Avatar from '@/assets/img_avatar.gif' 6 | 7 | const defaultAvatar = Avatar 8 | 9 | const useUserStore = defineStore('user-info', { 10 | state: () => { 11 | return { 12 | userId: 0, 13 | roleId: 0, 14 | token: '', 15 | userName: '', 16 | nickName: '', 17 | avatar: defaultAvatar, 18 | } 19 | }, 20 | actions: { 21 | saveUser(userInfo: UserState) { 22 | return new Promise((resolve) => { 23 | this.userId = userInfo.userId 24 | this.roleId = userInfo.roleId 25 | this.token = userInfo.token 26 | this.userName = userInfo.userName 27 | this.nickName = userInfo.nickName 28 | this.avatar = userInfo.avatar || defaultAvatar 29 | resolve(userInfo) 30 | }) 31 | }, 32 | isTokenExpire() { 33 | return !this.token 34 | }, 35 | changeNickName(newNickName: string) { 36 | this.nickName = newNickName 37 | }, 38 | logout() { 39 | return new Promise((resolve) => { 40 | this.$reset() 41 | localStorage.clear() 42 | sessionStorage.clear() 43 | resolve() 44 | }) 45 | }, 46 | }, 47 | presist: { 48 | enable: true, 49 | resetToState: true, 50 | option: { 51 | exclude: ['userName'], 52 | }, 53 | }, 54 | }) 55 | 56 | export default useUserStore 57 | 58 | export function useUserStoreContext() { 59 | return useUserStore(store) 60 | } 61 | -------------------------------------------------------------------------------- /src/router/routes/default-routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 这里的 defaultRoutes 是为了在一开始对接项目的时候,后端人员还没有准备好菜单接口,导致前端开发者不能进入主页面。 3 | * 所以这里返回默认的菜单数据,同时也向大家说明菜单数据的数据结构。后端的菜单接口一定要按这个格式去返回json数据,否则会解析菜单失败 4 | */ 5 | export default [ 6 | { 7 | menuUrl: '/index', 8 | menuName: 'Dashborad', 9 | routeName: 'dashborad', 10 | icon: 'icon-dashboard', 11 | parentPath: '', 12 | children: [ 13 | { 14 | parentPath: '/index', 15 | menuUrl: '/index/home', 16 | menuName: '主控台', 17 | routeName: 'home', 18 | }, 19 | { 20 | parentPath: '/index', 21 | menuUrl: '/index/work-place', 22 | menuName: '工作台', 23 | routeName: 'workPlace', 24 | }, 25 | ], 26 | }, 27 | { 28 | menuUrl: '/system', 29 | menuName: '系统管理', 30 | icon: 'icon-settings', 31 | parentPath: '', 32 | routeName: 'system', 33 | children: [ 34 | { 35 | parentPath: '/system', 36 | menuUrl: '/system/department', 37 | menuName: '部门管理', 38 | routeName: 'department', 39 | localFilePath: '/system/local-path/department', 40 | }, 41 | { 42 | parentPath: '/system', 43 | menuUrl: '/system/user', 44 | menuName: '用户管理', 45 | routeName: 'user', 46 | isRootPath: true, 47 | }, 48 | { 49 | parentPath: '/system', 50 | menuUrl: '/system/role', 51 | menuName: '角色管理', 52 | }, 53 | { 54 | parentPath: '/system', 55 | menuUrl: '/system/menu', 56 | menuName: '菜单管理', 57 | }, 58 | ], 59 | }, 60 | ] 61 | -------------------------------------------------------------------------------- /src/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/logo/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 42 | 66 | -------------------------------------------------------------------------------- /src/components/header/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | 35 | 62 | -------------------------------------------------------------------------------- /public/static/loading.css: -------------------------------------------------------------------------------- 1 | .first-loading-wrp { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 90vh; 7 | min-height: 90vh; 8 | } 9 | 10 | .loading-wrp{ 11 | display: flex; 12 | } 13 | 14 | .line{ 15 | width: 5px; 16 | height: 50px; 17 | margin: 0 5px; 18 | border-radius: 30px; 19 | } 20 | 21 | .loading-wrp > span:nth-child(even) { 22 | background-color: #eb0f0f; 23 | animation: growAnim 0.4s linear infinite alternate-reverse; 24 | } 25 | 26 | .loading-wrp > span:nth-child(odd) { 27 | background-color: #18a058; 28 | animation: growAnimReverse 0.4s linear infinite alternate-reverse; 29 | } 30 | 31 | @keyframes growAnim { 32 | 0% { 33 | transform: scale(1, 0.3); 34 | -ms-transform: scale(1, 0.3); 35 | -webkit-transform: scale(1, 0.3); 36 | -moz-transform: scale(1, 0.3); 37 | -o-transform: scale(1, 0.3); 38 | } 39 | 100% { 40 | transform: scale(1, 1); 41 | -ms-transform: scale(1, 1); 42 | -webkit-transform: scale(1, 1); 43 | -moz-transform: scale(1, 1); 44 | -o-transform: scale(1, 1); 45 | } 46 | } 47 | 48 | @keyframes growAnimReverse { 49 | from { 50 | transform: scale(1, 1); 51 | -ms-transform: scale(1, 1); 52 | -webkit-transform: scale(1, 1); 53 | -moz-transform: scale(1, 1); 54 | -o-transform: scale(1, 1); 55 | } 56 | to { 57 | transform: scale(1, 0.3); 58 | -ms-transform: scale(1, 0.3); 59 | -webkit-transform: scale(1, 0.3); 60 | -moz-transform: scale(1, 0.3); 61 | -o-transform: scale(1, 0.3); 62 | } 63 | } 64 | 65 | .first-loading-wrp .title{ 66 | font-size: 28px; 67 | font-weight: bold; 68 | text-align: center; 69 | margin-top: 50px; 70 | } -------------------------------------------------------------------------------- /src/views/index/components/TodoItem.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 67 | -------------------------------------------------------------------------------- /src/icons/data-money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/index/components/DataItem.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 55 | 56 | 62 | -------------------------------------------------------------------------------- /src/components/common/SearchContent.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 46 | 47 | 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-work", 3 | "version": "2.1.8", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite --host", 7 | "staging": "vite build --mode staging", 8 | "build": "vite build", 9 | "tsc": "vue-tsc --noEmit", 10 | "serve": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@vueuse/core": "^11.1.0", 14 | "axios": "^1.7.7", 15 | "clipboard": "^2.0.8", 16 | "echarts": "^5.1.2", 17 | "lodash-es": "^4.17.21", 18 | "mockjs": "^1.1.0", 19 | "path-browserify": "^1.0.1", 20 | "pinia": "^2.1.7", 21 | "print-js": "^1.6.0", 22 | "qrcode": "^1.4.4", 23 | "qs": "^6.10.1", 24 | "quill": "^1.3.7", 25 | "screenfull": "^5.1.0", 26 | "tiny-emitter": "^2.1.0", 27 | "vue": "^3.3.9", 28 | "vue-router": "^4.2.5", 29 | "vuedraggable": "^4.0.3", 30 | "xgplayer": "^2.28.0" 31 | }, 32 | "devDependencies": { 33 | "@types/js-cookie": "^2.2.7", 34 | "@types/lodash-es": "^4.17.6", 35 | "@types/node": "*", 36 | "@types/path-browserify": "^1.0.0", 37 | "@types/qrcode": "^1.4.1", 38 | "@types/qs": "^6.9.7", 39 | "@types/quill": "^2.0.9", 40 | "@typescript-eslint/eslint-plugin": "^6.13.1", 41 | "@typescript-eslint/parser": "^6.13.1", 42 | "@vicons/ionicons5": "^0.10.0", 43 | "@vitejs/plugin-vue": "^5.0.3", 44 | "@vitejs/plugin-vue-jsx": "^3.1.0", 45 | "autoprefixer": "^10.4.7", 46 | "eslint": "^7.30.0", 47 | "eslint-config-prettier": "^8.3.0", 48 | "eslint-define-config": "^1.0.9", 49 | "eslint-plugin-prettier": "^3.4.0", 50 | "eslint-plugin-vue": "^7.13.0", 51 | "naive-ui": "^2.40.3", 52 | "postcss": "^8.4.13", 53 | "prettier": "^2.3.2", 54 | "sass": "^1.35.1", 55 | "tailwindcss": "^3.3.5", 56 | "typescript": "^5.3.2", 57 | "unplugin-vue-components": "^0.26.0", 58 | "vfonts": "^0.1.0", 59 | "vite": "^5.2.12", 60 | "vite-plugin-compression": "^0.5.1", 61 | "vite-plugin-svg-icons": "^2.0.1", 62 | "vue-tsc": "^0.3.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/views/exception/components/ExceptionStatus.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 62 | 63 | 79 | -------------------------------------------------------------------------------- /src/views/form/step-form.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 52 | 53 | 67 | -------------------------------------------------------------------------------- /src/components/common/RichTextEditor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 66 | -------------------------------------------------------------------------------- /src/api/http.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios' 2 | import { App } from 'vue' 3 | import request from './axios.config' 4 | 5 | export interface HttpOption { 6 | url: string 7 | data?: any 8 | method?: string 9 | headers?: any 10 | beforeRequest?: () => void 11 | afterRequest?: () => void 12 | } 13 | 14 | export interface Response { 15 | totalSize: number | 0 16 | code: number 17 | msg: string 18 | data: T 19 | } 20 | 21 | function http({ url, data, method, headers, beforeRequest, afterRequest }: HttpOption) { 22 | const successHandler = (res: AxiosResponse>) => { 23 | if (res.data.code === 200) { 24 | return res.data 25 | } 26 | throw new Error(res.data.msg || '请求失败,未知异常') 27 | } 28 | const failHandler = (error: Response) => { 29 | afterRequest && afterRequest() 30 | throw new Error(error.msg || '请求失败,未知异常') 31 | } 32 | beforeRequest && beforeRequest() 33 | method = method || 'GET' 34 | const params = Object.assign(typeof data === 'function' ? data() : data || {}, {}) 35 | return method === 'GET' 36 | ? request.get(url, { params }).then(successHandler, failHandler) 37 | : request.post(url, params, { headers: headers }).then(successHandler, failHandler) 38 | } 39 | 40 | export function get({ 41 | url, 42 | data, 43 | method = 'GET', 44 | beforeRequest, 45 | afterRequest, 46 | }: HttpOption): Promise> { 47 | return http({ 48 | url, 49 | method, 50 | data, 51 | beforeRequest, 52 | afterRequest, 53 | }) 54 | } 55 | 56 | export function post({ 57 | url, 58 | data, 59 | method = 'POST', 60 | headers, 61 | beforeRequest, 62 | afterRequest, 63 | }: HttpOption): Promise> { 64 | return http({ 65 | url, 66 | method, 67 | data, 68 | headers, 69 | beforeRequest, 70 | afterRequest, 71 | }) 72 | } 73 | 74 | function install(app: App): void { 75 | app.config.globalProperties.$http = http 76 | 77 | app.config.globalProperties.$get = get 78 | 79 | app.config.globalProperties.$post = post 80 | } 81 | 82 | export default { 83 | install, 84 | get, 85 | post, 86 | } 87 | -------------------------------------------------------------------------------- /src/store/plugin/persist.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '@vueuse/core' 2 | import { PiniaPluginContext } from 'pinia' 3 | import { toRaw } from 'vue' 4 | 5 | interface PresistType { 6 | enable: boolean 7 | option: Partial<{ 8 | key: string 9 | storage: 'local' | 'session' 10 | include: (keyof S)[] 11 | exclude: (keyof S)[] 12 | }> 13 | resetToState?: ((store: Store) => void) | boolean 14 | } 15 | 16 | declare module 'pinia' { 17 | export interface DefineStoreOptionsBase { 18 | presist?: Partial> 19 | } 20 | } 21 | 22 | export default ({ options, store }: PiniaPluginContext) => { 23 | const presist = options.presist 24 | if (presist && isObject(presist) && presist.enable) { 25 | // 设置默认值 26 | !presist.option && (presist.option = {}) 27 | 28 | const key = presist.option?.key || store.$id 29 | presist.option!.key = key 30 | const storage = presist.option?.storage || 'local' 31 | presist.option!.storage = storage 32 | 33 | // 恢复状态 34 | if (presist.resetToState) { 35 | if (typeof presist.resetToState === 'boolean') { 36 | const json = (window as any)[presist.option?.storage + 'Storage'].getItem( 37 | presist.option?.key 38 | ) 39 | if (json) { 40 | store.$patch(JSON.parse(json)) 41 | } 42 | } else if (typeof presist.resetToState === 'function') { 43 | presist.resetToState.call(presist, store) 44 | } 45 | } 46 | 47 | // 设置监听器 48 | store.$subscribe( 49 | (mutation, state) => { 50 | const toPersistObj = JSON.parse(JSON.stringify(toRaw(state))) 51 | if (presist.option?.include || presist.option?.exclude) { 52 | Object.keys(toPersistObj).forEach((it) => { 53 | if ( 54 | (presist.option?.include && !presist.option?.include?.includes(it)) || 55 | (presist.option?.exclude && presist.option?.exclude?.includes(it)) 56 | ) { 57 | toPersistObj[it] = undefined 58 | } 59 | }) 60 | } 61 | ;(window as any)[storage + 'Storage'].setItem(key, JSON.stringify(toPersistObj)) 62 | }, 63 | { detached: true } 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/views/index/components/ProjectItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 50 | 51 | 85 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true, 7 | }, 8 | parser: 'vue-eslint-parser', 9 | parserOptions: { 10 | parser: '@typescript-eslint/parser', 11 | ecmaVersion: 2020, 12 | sourceType: 'module', 13 | jsxPragma: 'React', 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | }, 18 | extends: [ 19 | 'plugin:vue/vue3-recommended', 20 | 'plugin:@typescript-eslint/recommended', 21 | 'prettier', 22 | 'plugin:prettier/recommended', 23 | ], 24 | rules: { 25 | '@typescript-eslint/ban-ts-ignore': 'off', 26 | '@typescript-eslint/explicit-function-return-type': 'off', 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-var-requires': 'off', 29 | '@typescript-eslint/no-empty-function': 'off', 30 | 'vue/custom-event-name-casing': 'off', 31 | 'no-use-before-define': 'off', 32 | '@typescript-eslint/no-use-before-define': 'off', 33 | '@typescript-eslint/ban-ts-comment': 'off', 34 | '@typescript-eslint/ban-types': 'off', 35 | '@typescript-eslint/no-non-null-assertion': 'off', 36 | '@typescript-eslint/explicit-module-boundary-types': 'off', 37 | '@typescript-eslint/no-unused-vars': [ 38 | 'error', 39 | { 40 | argsIgnorePattern: '^_', 41 | varsIgnorePattern: '^_', 42 | }, 43 | ], 44 | 'no-unused-vars': [ 45 | 'error', 46 | { 47 | argsIgnorePattern: '^_', 48 | varsIgnorePattern: '^_', 49 | }, 50 | ], 51 | 'space-before-function-paren': 'off', 52 | 'vue/attributes-order': 'off', 53 | 'vue/one-component-per-file': 'off', 54 | 'vue/html-closing-bracket-newline': 'off', 55 | 'vue/max-attributes-per-line': 'off', 56 | 'vue/multiline-html-element-content-newline': 'off', 57 | 'vue/singleline-html-element-content-newline': 'off', 58 | 'vue/attribute-hyphenation': 'off', 59 | 'vue/require-default-prop': 'off', 60 | 'vue/script-setup-uses-vars': 'off', 61 | 'vue/html-self-closing': [ 62 | 'error', 63 | { 64 | html: { 65 | void: 'always', 66 | normal: 'never', 67 | component: 'always', 68 | }, 69 | svg: 'always', 70 | math: 'always', 71 | }, 72 | ], 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /src/views/list/card-list.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 67 | 68 | 73 | -------------------------------------------------------------------------------- /src/components/common/TableFooter.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 70 | 79 | -------------------------------------------------------------------------------- /src/views/result/fail.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 40 | 41 | 78 | -------------------------------------------------------------------------------- /src/styles/transition.css: -------------------------------------------------------------------------------- 1 | /* fade-transform */ 2 | .fade-transform-leave-active, 3 | .fade-transform-enter-active { 4 | transition: all 0.5s; 5 | } 6 | 7 | .fade-transform-enter-from { 8 | opacity: 0; 9 | transform: translateX(-50px); 10 | } 11 | 12 | .fade-transform-leave-to { 13 | opacity: 0; 14 | transform: translateX(50px); 15 | } 16 | 17 | /* down-transform */ 18 | .down-transform-leave-active, 19 | .down-transform-enter-active { 20 | transition: all 0.5s; 21 | } 22 | 23 | .down-transform-enter-from { 24 | opacity: 0; 25 | transform: translateY(-50px); 26 | } 27 | 28 | .down-transform-leave-to { 29 | opacity: 0; 30 | transform: translateY(50px); 31 | } 32 | 33 | /* scale-transform */ 34 | .scale-transform-leave-active, 35 | .scale-transform-enter-active { 36 | transition: all 0.5s; 37 | } 38 | 39 | .scale-transform-enter-from { 40 | opacity: 0; 41 | transform: scale(2); 42 | } 43 | 44 | .scale-transform-leave-to { 45 | opacity: 0; 46 | transform: scale(0.5); 47 | } 48 | 49 | /* opacity-transform */ 50 | .opacity-transform-leave-active, 51 | .opacity-transform-enter-active { 52 | transition: all 0.5s; 53 | } 54 | 55 | .opacity-transform-enter-from { 56 | opacity: 0; 57 | } 58 | 59 | .opacity-transform-leave-to { 60 | opacity: 0; 61 | } 62 | 63 | /* breadcrumb transition */ 64 | .breadcrumb-enter-active, 65 | .breadcrumb-leave-active { 66 | transition: all 0.5s; 67 | } 68 | 69 | .breadcrumb-enter-from, 70 | .breadcrumb-leave-active { 71 | opacity: 0; 72 | transform: translateX(20px); 73 | } 74 | 75 | .breadcrumb-move { 76 | transition: all 0.5s; 77 | } 78 | 79 | .breadcrumb-leave-active { 80 | position: absolute; 81 | } 82 | 83 | /* header transition */ 84 | .header-enter-active, 85 | .header-leave-active { 86 | transition: all 0.5s; 87 | } 88 | 89 | .header-enter-from, 90 | .header-leave-active { 91 | opacity: 0; 92 | transform: translateX(100%); 93 | } 94 | 95 | .header-move { 96 | transition: all 0.5s; 97 | } 98 | 99 | /* logo transition */ 100 | .logo-enter-active, 101 | .logo-leave-active { 102 | transition: all 0.5s; 103 | } 104 | 105 | .logo-enter-from, 106 | .logo-leave-active { 107 | opacity: 0; 108 | transform: translateY(-100%); 109 | } 110 | 111 | .logo-move { 112 | transition: all 0.5s; 113 | } 114 | -------------------------------------------------------------------------------- /src/types/components.ts: -------------------------------------------------------------------------------- 1 | import { MessageApi, TreeSelectOption } from 'naive-ui' 2 | import { Ref, VNode } from 'vue' 3 | 4 | export interface HeaderCellStyle { 5 | backgroundColor?: string 6 | color: string 7 | } 8 | 9 | export interface TableConfig { 10 | dataList: Array 11 | stripe: boolean 12 | border: boolean 13 | size: string 14 | headerCellStyle: HeaderCellStyle 15 | height: string | number 16 | tableLoading: boolean 17 | } 18 | 19 | export interface SelectOptionItem { 20 | label: string 21 | value: any 22 | } 23 | 24 | export interface TableSearchItem { 25 | key: string | number 26 | label: string 27 | value: any 28 | placeholder?: string 29 | associatedOption?: string 30 | onChange?: (value: any, associationItem: string) => void 31 | span?: number 32 | } 33 | 34 | export interface FormItem extends TableSearchItem { 35 | required?: boolean 36 | validator?: (value: FormItem, message: MessageApi) => boolean 37 | hidden?: boolean 38 | inputType?: string 39 | maxLength?: number 40 | rows?: number 41 | disabled?: Ref | boolean 42 | optionItems?: Array 43 | path?: string 44 | reset?: (formItem: FormItem) => void 45 | render?: (formItem: FormItem) => VNode 46 | } 47 | 48 | export interface LikeSearchModel { 49 | conditionItems: Array | null 50 | extraParams?: (() => Record) | Record 51 | } 52 | 53 | export interface TablePropsType { 54 | title: string 55 | key: string 56 | sortIndex: number 57 | checked: Ref 58 | } 59 | 60 | // export type ModalDialogType = InstanceType 61 | 62 | export type ModalDialogType = InstanceType< 63 | typeof import('../components/common/ModalDialog.vue').default 64 | > 65 | 66 | export type DataFormType = InstanceType 67 | 68 | export type TableHeaderType = InstanceType< 69 | typeof import('../components/common/TableHeader.vue').default 70 | > 71 | 72 | export type TableFooterType = InstanceType< 73 | typeof import('../components/common/TableFooter.vue').default 74 | > 75 | 76 | export type SvgIconType = InstanceType 77 | 78 | export type SearchContentType = InstanceType< 79 | typeof import('../components/common/SearchContent.vue').default 80 | > 81 | -------------------------------------------------------------------------------- /src/hooks/useDialogDragger.ts: -------------------------------------------------------------------------------- 1 | const range = { 2 | left: 0, 3 | right: 0, 4 | top: 0, 5 | bottom: 0, 6 | } 7 | 8 | const listeners: { name: string; listener: (e: MouseEvent) => void }[] = [] 9 | 10 | export function drag(wrap: HTMLElement) { 11 | wrap.style.cursor = 'move' 12 | const parent = wrap.parentElement 13 | if (!parent) { 14 | return 15 | } 16 | let startX = 0 17 | let startY = 0 18 | let status = '' 19 | wrap.addEventListener('mousedown', (e: MouseEvent) => { 20 | e.preventDefault() 21 | status = 'down' 22 | range.left = -((document.documentElement.clientWidth - parent.clientWidth) / 2) 23 | range.right = Math.abs(range.left) 24 | range.top = -(document.documentElement.clientHeight - parent.clientHeight) / 2 25 | range.bottom = Math.abs(range.top) 26 | startX = e.clientX - (parseInt(parent.style.left) || 0) 27 | startY = e.clientY - (parseInt(parent.style.top) || 0) 28 | const handleMove = (e: MouseEvent) => { 29 | if (status !== 'down') return 30 | const moveX = e.clientX 31 | const moveY = e.clientY 32 | let distX = moveX - startX 33 | let distY = moveY - startY 34 | if (distX <= range.left) { 35 | distX = range.left 36 | } 37 | if (distX >= range.right) { 38 | distX = range.right 39 | } 40 | if (distY <= range.top) { 41 | distY = range.top 42 | } 43 | if (distY >= range.bottom) { 44 | distY = range.bottom 45 | } 46 | parent.style.left = distX + 'px' 47 | parent.style.top = distY + 'px' 48 | } 49 | const handleUp = () => { 50 | status = 'up' 51 | document.removeEventListener('mousemove', handleMove) 52 | document.removeEventListener('mouseup', handleUp) 53 | } 54 | listeners.push( 55 | { 56 | name: 'mousemove', 57 | listener: handleMove, 58 | }, 59 | { 60 | name: 'mouseup', 61 | listener: handleUp, 62 | } 63 | ) 64 | document.addEventListener('mousemove', handleMove) 65 | document.addEventListener('mouseup', handleUp) 66 | wrap.addEventListener('mouseup', () => { 67 | handleUp() 68 | }) 69 | }) 70 | } 71 | 72 | export function unDrag(wrap: HTMLElement) { 73 | listeners.forEach((it: any) => { 74 | wrap.removeEventListener(it.name, it.listener) 75 | }) 76 | listeners.length = 0 77 | } 78 | -------------------------------------------------------------------------------- /src/components/common/PopoverMessage.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 80 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { DataTableColumn, TreeSelectOption } from 'naive-ui' 2 | import { TablePropsType } from '@/types/components' 3 | 4 | export function isExternal(path: string) { 5 | return /^(https?:|mailto:|tel:)/.test(path) 6 | } 7 | 8 | export function uuid() { 9 | const s: Array = [] 10 | const hexDigits = '0123456789abcdef' 11 | for (let i = 0; i < 36; i++) { 12 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) 13 | } 14 | s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010 15 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01 16 | s[8] = s[13] = s[18] = s[23] = '-' 17 | const uuid = s.join('') 18 | return uuid 19 | } 20 | 21 | export function randomString(length: number) { 22 | const str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 23 | let result = '' 24 | for (let i = length; i > 0; --i) { 25 | result += str[Math.floor(Math.random() * str.length)] 26 | } 27 | return result 28 | } 29 | 30 | /** 31 | * 中划线字符驼峰 32 | * @param {*} str 要转换的字符串 33 | * @returns 返回值 34 | */ 35 | export function toHump(str: string): string { 36 | if (!str) return str 37 | return str 38 | .replace(/\-(\w)/g, function (all, letter) { 39 | return letter.toUpperCase() 40 | }) 41 | .replace(/(\s|^)[a-z]/g, function (char) { 42 | return char.toUpperCase() 43 | }) 44 | } 45 | 46 | export function sortColumns(originColumns: DataTableColumn[], newColumns: TablePropsType[]) { 47 | if (!originColumns || !newColumns) { 48 | return 49 | } 50 | if (newColumns.length === 0) { 51 | originColumns.length = 0 52 | } else { 53 | const selectionItem = originColumns.find((it) => it.type === 'selection') 54 | originColumns.length = 0 55 | if (selectionItem) { 56 | originColumns.push(selectionItem) 57 | } 58 | originColumns.push(...newColumns) 59 | } 60 | } 61 | 62 | export function transformTreeSelect( 63 | origin: any[], 64 | labelName: string, 65 | keyName: string 66 | ): TreeSelectOption[] { 67 | const tempSelections: TreeSelectOption[] = [] 68 | origin.forEach((it) => { 69 | const selection = { 70 | label: it[labelName], 71 | key: it[keyName], 72 | } as TreeSelectOption 73 | if (it.children) { 74 | selection.children = transformTreeSelect(it.children, labelName, keyName) 75 | } 76 | tempSelections.push(selection) 77 | }) 78 | return tempSelections 79 | } 80 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | import { Ref, UnwrapRef } from 'vue' 2 | import { RouteMeta, RouteRecordNormalized, RouteRecordRaw } from 'vue-router' 3 | 4 | export enum LayoutMode { 5 | LTR = 'ltr', 6 | LCR = 'lcr', 7 | TTB = 'ttb', 8 | } 9 | 10 | export enum DeviceType { 11 | PC = 'pc', 12 | PAD = 'pad', 13 | MOBILE = 'mobile', 14 | } 15 | 16 | export enum ThemeMode { 17 | LIGHT = 'light', 18 | DARK = 'dark', 19 | } 20 | 21 | export enum SideTheme { 22 | DARK = 'dark', 23 | WHITE = 'white', 24 | BLUE = 'blue', 25 | IMAGE = 'image', 26 | } 27 | 28 | export enum PageAnim { 29 | FADE = 'fade', 30 | OPACITY = 'opacity', 31 | DOWN = 'down', 32 | SCALE = 'scale', 33 | } 34 | 35 | export interface UserState { 36 | userId: number 37 | token: string 38 | roleId: number 39 | roles: string[] | null 40 | userName: string 41 | nickName: string 42 | avatar: string 43 | } 44 | 45 | export interface AppConfigState { 46 | projectName: string 47 | theme: ThemeMode 48 | sideTheme: SideTheme 49 | themeColor: string 50 | layoutMode: LayoutMode 51 | deviceType: DeviceType 52 | sideWidth: number 53 | pageAnim: PageAnim 54 | isFixedNavBar: boolean 55 | isCollapse: boolean 56 | actionBar: { 57 | isShowSearch: boolean 58 | isShowMessage: boolean 59 | isShowRefresh: boolean 60 | isShowFullScreen: boolean 61 | } 62 | } 63 | 64 | export interface VisitedRouteState { 65 | visitedRoutes: RouteRecordNormalized[] 66 | affixRoutes: RouteRecordNormalized[] 67 | } 68 | 69 | export interface CachedRouteState { 70 | cachedRoutes: string[] 71 | } 72 | 73 | export interface OriginRoute { 74 | parentPath?: string 75 | menuUrl: string 76 | menuName?: string 77 | routeName?: string 78 | hidden?: boolean 79 | outLink?: string 80 | affix?: boolean 81 | cacheable?: boolean 82 | isRootPath?: boolean 83 | iconPrefix?: string 84 | icon?: string 85 | badge?: string | number 86 | isSingle?: boolean 87 | localFilePath?: string 88 | children?: Array 89 | } 90 | 91 | export interface RouteMetaType extends RouteMeta { 92 | icon?: string 93 | title?: string 94 | cacheable?: boolean 95 | affix?: boolean 96 | } 97 | 98 | export interface SplitTab { 99 | label: string 100 | iconPrefix?: string | unknown 101 | icon: string 102 | fullPath: string 103 | children?: Array 104 | checked: Ref> 105 | } 106 | -------------------------------------------------------------------------------- /src/views/other/chart/components/IconFont.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 43 | 44 | 82 | -------------------------------------------------------------------------------- /src/components/sidebar/components/HorizontalScrollerMenu.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 66 | 67 | 85 | -------------------------------------------------------------------------------- /src/icons/data-chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/index/components/chart/OrderChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 89 | -------------------------------------------------------------------------------- /src/views/project/infomation.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 78 | -------------------------------------------------------------------------------- /src/components/common/CitySelector.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 83 | -------------------------------------------------------------------------------- /mock/user/index.js: -------------------------------------------------------------------------------- 1 | import { getMenuListByRoleId, getAllMenuByRoleId, login, updateUserInfo } from '@/api/url' 2 | import { randomString } from '@/utils' 3 | import Mock from 'mockjs' 4 | import { baseData } from '../base.ts' 5 | import { adminRoutes, editorRoutes } from '../router' 6 | 7 | Mock.mock(RegExp(login), 'post', function (options) { 8 | const username = JSON.parse(options.body).username 9 | const data = {} 10 | if (username === 'admin') { 11 | baseData.code = 200 12 | baseData.msg = '登录成功' 13 | data.nickName = '超级管理员' 14 | data.userName = 'admin' 15 | data.userId = 1 16 | data.roleId = 1 17 | data.token = randomString(100) 18 | data.roles = [ 19 | { 20 | roleCode: 'ROLE_admin', 21 | roleId: 1, 22 | roleName: '超级管理员', 23 | }, 24 | ] 25 | baseData.data = data 26 | } else if (username === 'editor') { 27 | baseData.code = 200 28 | baseData.msg = '登录成功' 29 | data.nickName = '编辑员' 30 | data.userName = 'editor' 31 | data.userId = 2 32 | data.roleId = 2 33 | data.token = randomString(100) 34 | data.roles = [ 35 | { 36 | roleCode: 'ROLE_editor', 37 | roleId: 2, 38 | roleName: '网站编辑人员', 39 | }, 40 | ] 41 | baseData.data = data 42 | } else { 43 | baseData.code = 500 44 | baseData.data = '' 45 | baseData.msg = '用户名或密码错误' 46 | } 47 | return Mock.mock(baseData) 48 | }) 49 | 50 | Mock.mock(RegExp(getAllMenuByRoleId), 'post', function (options) { 51 | const roleId = JSON.parse(options.body).roleId || '' 52 | if (!roleId) { 53 | return Mock.mock({ code: 500, data: '', msg: '获取菜单列表失败' }) 54 | } 55 | const allRoutes = [...adminRoutes] 56 | allRoutes.forEach((it) => { 57 | it.isSelect = parseInt(roleId) === 1 || it.menuUrl.indexOf('authority') === -1 58 | it.children.forEach((child) => { 59 | child.isSelect = parseInt(roleId) === 1 || child.menuUrl.indexOf('authority') === -1 60 | }) 61 | }) 62 | return Mock.mock({ code: 200, data: allRoutes, msg: '获取菜单列表成功' }) 63 | }) 64 | 65 | Mock.mock(RegExp(getMenuListByRoleId), 'post', function (options) { 66 | const roleId = JSON.parse(options.body).roleId || '' 67 | if (!roleId) { 68 | return Mock.mock({ code: 500, data: '', msg: '获取菜单列表失败' }) 69 | } 70 | if (parseInt(roleId) === 1) { 71 | // 超级管理员 72 | return Mock.mock({ code: 200, data: adminRoutes, msg: '获取菜单列表成功' }) 73 | } else if (parseInt(roleId) === 2) { 74 | // 编辑 75 | return Mock.mock({ 76 | code: 200, 77 | data: editorRoutes, 78 | msg: '获取菜单列表成功', 79 | }) 80 | } else { 81 | return Mock.mock({ 82 | code: 500, 83 | data: '', 84 | msg: '目前仅支持超级管理员和编辑人员菜单', 85 | }) 86 | } 87 | }) 88 | 89 | Mock.mock(RegExp(updateUserInfo), 'post', function () { 90 | return Mock.mock({ ...baseData, msg: '更新信息成功' }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/views/form/components/ResultInfo.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 66 | 67 | 103 | -------------------------------------------------------------------------------- /src/store/modules/permission.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import { defineStore } from 'pinia' 3 | import useUserStore from './user' 4 | import router from '@/router' 5 | import { baseAddress, getMenuListByRoleId } from '@/api/url' 6 | import { post } from '@/api/http' 7 | import defaultRoutes from '@/router/routes/default-routes' 8 | import { findRootPathRoute, generatorRoutes, mapTwoLevelRouter } from '../help' 9 | import { constantRoutes } from '@/router/routes/constants' 10 | 11 | const usePermissionStore = defineStore('permission-route', { 12 | state: () => { 13 | return { 14 | permissionRoutes: [] as RouteRecordRaw[], 15 | } 16 | }, 17 | getters: { 18 | getPermissionSideBar(state) { 19 | return state.permissionRoutes.filter((it) => { 20 | return it.meta && !it.meta.hidden 21 | }) 22 | }, 23 | getPermissionSplitTabs(state) { 24 | return state.permissionRoutes.filter((it) => { 25 | return it.meta && !it.meta.hidden && it.children && it.children.length > 0 26 | }) 27 | }, 28 | }, 29 | actions: { 30 | async getRoutes(data: { userId: number; roleId: number }) { 31 | try { 32 | if (getMenuListByRoleId) { 33 | const res = await post({ 34 | url: baseAddress + getMenuListByRoleId, 35 | // 在实际的开发中,这个地方可以换成 token,让后端解析用户信息获取 userId 和 roleId,前端可以不用传 userId 和 roleId。 36 | // 这样可以增加安全性 37 | data, 38 | }) 39 | return generatorRoutes(res.data) 40 | } else { 41 | return generatorRoutes(defaultRoutes) 42 | } 43 | } catch (error) { 44 | console.log( 45 | '路由加载失败了,请清空一下Cookie和localStorage,重新登录;如果已经采用真实接口的,请确保菜单接口地址真实可用并且返回的数据格式和mock中的一样' 46 | ) 47 | return [] 48 | } 49 | }, 50 | async initPermissionRoute() { 51 | const userStore = useUserStore() 52 | // 加载路由 53 | const accessRoutes = await this.getRoutes({ 54 | roleId: userStore.roleId, 55 | userId: userStore.userId, 56 | }) 57 | const mapRoutes = mapTwoLevelRouter(accessRoutes) 58 | mapRoutes.forEach((it: any) => { 59 | router.addRoute(it) 60 | }) 61 | // 配置 `/` 路由的默认跳转地址 62 | router.addRoute({ 63 | path: '/', 64 | redirect: findRootPathRoute(accessRoutes), 65 | meta: { 66 | hidden: true, 67 | }, 68 | }) 69 | // 这个路由一定要放在最后 70 | router.addRoute({ 71 | path: '/:pathMatch(.*)*', 72 | redirect: '/404', 73 | meta: { 74 | hidden: true, 75 | }, 76 | }) 77 | this.permissionRoutes = [...constantRoutes, ...accessRoutes] 78 | }, 79 | isEmptyPermissionRoute() { 80 | return !this.permissionRoutes || this.permissionRoutes.length === 0 81 | }, 82 | reset() { 83 | this.$reset() 84 | }, 85 | }, 86 | }) 87 | 88 | export default usePermissionStore 89 | -------------------------------------------------------------------------------- /src/views/other/print.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 88 | 89 | 105 | -------------------------------------------------------------------------------- /src/views/other/css-animation.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 91 | 92 | 108 | -------------------------------------------------------------------------------- /src/views/editor/rich-text.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 73 | 74 | 96 | -------------------------------------------------------------------------------- /src/views/other/chart/components/xicons.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 57 | 58 | 96 | -------------------------------------------------------------------------------- /src/views/form/components/PasswordInfo.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 87 | 88 | 102 | -------------------------------------------------------------------------------- /src/components/common/IconSelector.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 88 | 96 | -------------------------------------------------------------------------------- /src/components/setting/components/StyleExample.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 41 | 42 | 117 | -------------------------------------------------------------------------------- /src/components/common/TableHeader.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 104 | -------------------------------------------------------------------------------- /src/views/index/components/chart/EnrollmentChannelsChart.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 99 | 100 | 108 | -------------------------------------------------------------------------------- /src/components/common/ModalDialog.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 112 | -------------------------------------------------------------------------------- /src/views/index/components/chart/SalesChart.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 101 | 102 | 110 | -------------------------------------------------------------------------------- /src/views/index/components/chart/StudentChart.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 102 | 103 | 111 | -------------------------------------------------------------------------------- /src/views/other/city-selector.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 97 | -------------------------------------------------------------------------------- /src/components/avatar/VAWAvatar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 85 | 86 | 123 | -------------------------------------------------------------------------------- /src/views/index/components/chart/DepartmentChart.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 108 | 109 | 117 | -------------------------------------------------------------------------------- /src/views/draggable/card-draggable.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 98 | 99 | 132 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 36 | 63 | 150 | -------------------------------------------------------------------------------- /src/views/list/list.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 77 | 78 | 127 | -------------------------------------------------------------------------------- /src/components/common/SortableTable.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 115 | -------------------------------------------------------------------------------- /src/components/sidebar/components/ScrollerMenu.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 98 | 99 | 122 | -------------------------------------------------------------------------------- /src/components/breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 125 | --------------------------------------------------------------------------------