├── public ├── new.xlsx ├── t.xls ├── icon.png ├── t2.xls ├── favicon.ico ├── serverConfig.json └── logo.svg ├── .stylelintignore ├── .npmrc ├── src ├── directives │ ├── index.ts │ ├── auth │ │ └── index.ts │ └── elResizeDetector │ │ └── index.ts ├── assets │ ├── login │ │ ├── bg.png │ │ └── avatar.svg │ ├── iconfont │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.woff2 │ │ ├── iconfont.css │ │ └── iconfont.json │ └── svg │ │ ├── dark.svg │ │ ├── full_screen.svg │ │ ├── exit_screen.svg │ │ ├── enter_outlined.svg │ │ ├── keyboard_esc.svg │ │ ├── day.svg │ │ └── back_top.svg ├── components │ ├── ReAuth │ │ ├── index.ts │ │ └── src │ │ │ └── auth.tsx │ ├── ReSelect │ │ ├── index.ts │ │ └── src │ │ │ └── select.vue │ ├── ReUpload │ │ ├── index.ts │ │ └── src │ │ │ └── upload.vue │ ├── TableRender │ │ ├── index.ts │ │ ├── types.ts │ │ └── TLables.ts │ ├── RePureTableBar │ │ ├── src │ │ │ └── svg │ │ │ │ ├── expand.svg │ │ │ │ ├── refresh.svg │ │ │ │ ├── drag.svg │ │ │ │ ├── collapse.svg │ │ │ │ └── settings.svg │ │ └── index.ts │ ├── ReTable │ │ └── index.ts │ ├── Form │ │ ├── types.ts │ │ ├── XInput.vue │ │ ├── index.ts │ │ ├── XTextarea.vue │ │ ├── XNumber.vue │ │ ├── XOptions.vue │ │ ├── XDate.vue │ │ ├── XFile.vue │ │ ├── XImg.vue │ │ ├── XCertForm.vue │ │ ├── XSelect.vue │ │ ├── XMultipleSelect.vue │ │ └── XDiv.vue │ ├── ReIcon │ │ ├── index.ts │ │ └── src │ │ │ ├── offlineIcon.ts │ │ │ ├── types.ts │ │ │ ├── iconifyIconOnline.ts │ │ │ ├── iconifyIconOffline.ts │ │ │ ├── iconfont.ts │ │ │ └── hooks.ts │ ├── Fields │ │ ├── TMixed.vue │ │ ├── TImg.vue │ │ ├── TOptions.vue │ │ ├── TFile.vue │ │ ├── TSwitch.vue │ │ └── TInput.vue │ ├── ReCol │ │ └── index.ts │ └── ReDialog │ │ ├── index.ts │ │ └── footer.ts ├── hooks │ ├── useWatch.ts │ ├── useUpload.ts │ ├── useAPI.ts │ ├── useTabs.ts │ ├── useDownload.ts │ ├── useCommonEditConfig.ts │ ├── useSystemConfig.ts │ ├── useDialog.tsx │ ├── useOSS.ts │ ├── usePermission.ts │ ├── useGenColumns.ts │ ├── useRestAPI.ts │ ├── useModelConfig.tsx │ └── useTables.ts ├── layout │ ├── components │ │ ├── search │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── SearchFooter.vue │ │ │ │ └── SearchResult.vue │ │ │ └── index.vue │ │ ├── sidebar │ │ │ ├── extraIcon.vue │ │ │ ├── topCollapse.vue │ │ │ ├── leftCollapse.vue │ │ │ ├── logo.vue │ │ │ ├── breadCrumb.vue │ │ │ ├── horizontal.vue │ │ │ └── vertical.vue │ │ ├── notice │ │ │ ├── noticeList.vue │ │ │ └── index.vue │ │ ├── appMain.vue │ │ ├── navbar.vue │ │ └── panel │ │ │ └── index.vue │ ├── redirect.vue │ ├── hooks │ │ ├── useBoolean.ts │ │ ├── useLayout.ts │ │ ├── useDataThemeChange.ts │ │ └── useNav.ts │ ├── frameView.vue │ └── types.ts ├── store │ ├── unstorage.ts │ ├── index.ts │ └── modules │ │ ├── types.ts │ │ ├── settings.ts │ │ ├── epTheme.ts │ │ ├── permission.ts │ │ ├── app.ts │ │ └── user.ts ├── views │ ├── login │ │ └── utils │ │ │ ├── static.ts │ │ │ ├── rule.ts │ │ │ └── motion.ts │ ├── store │ │ ├── index.vue │ │ └── components │ │ │ └── StoreForm.vue │ ├── welcome │ │ └── components │ │ │ ├── preload.js │ │ │ └── StoreForm.vue │ ├── error │ │ ├── 404.vue │ │ ├── 500.vue │ │ └── 403.vue │ └── permission │ │ ├── page │ │ └── index.vue │ │ └── button │ │ └── index.vue ├── api │ ├── routes.ts │ └── user.ts ├── utils │ ├── globalPolyfills.ts │ ├── progress │ │ └── index.ts │ ├── mitt.ts │ ├── propTypes.ts │ ├── http │ │ └── types.d.ts │ ├── responsive.ts │ ├── sso.ts │ ├── message.ts │ └── auth.ts ├── style │ ├── index.scss │ ├── tailwind.css │ ├── transition.scss │ ├── login.css │ └── dark.scss ├── mockProdServer.ts ├── router │ └── modules │ │ ├── home.ts │ │ └── remaining.ts ├── App.vue ├── plugins │ ├── echarts │ │ └── index.ts │ └── element-plus │ │ └── index.ts ├── config │ └── index.ts └── main.ts ├── .env ├── .husky ├── commit-msg ├── common.sh ├── pre-commit └── lintstagedrc.js ├── .prettierrc.js ├── CHANGELOG.md ├── .eslintignore ├── .env.development ├── .markdownlint.json ├── postcss.config.js ├── types ├── shims-vue.d.ts ├── shims-tsx.d.ts └── index.d.ts ├── tsconfig.node.json ├── electron ├── electron-env.d.ts └── preload │ └── index.ts ├── .editorconfig ├── .gitignore ├── .vscode ├── vue3.2.code-snippets ├── extensions.json ├── vue3.0.code-snippets └── settings.json ├── .env.production ├── tailwind.config.js ├── .env.staging ├── mock ├── asyncRoutes.ts ├── refreshToken.ts └── login.ts ├── commitlint.config.js ├── .github └── workflows │ └── release.yml ├── LICENSE ├── tsconfig.json ├── .electron-builder.config.js ├── README.en-US.md ├── stylelint.config.js ├── index.html ├── vite.config.ts ├── README.md └── .eslintrc.js /public/new.xlsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | public/* 4 | src/style/reset.scss -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | shell-emulator=true -------------------------------------------------------------------------------- /public/t.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/mt-store-proxy/HEAD/public/t.xls -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/mt-store-proxy/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/t2.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/mt-store-proxy/HEAD/public/t2.xls -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./elResizeDetector"; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/mt-store-proxy/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/login/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/mt-store-proxy/HEAD/src/assets/login/bg.png -------------------------------------------------------------------------------- /src/components/ReAuth/index.ts: -------------------------------------------------------------------------------- 1 | import auth from "./src/auth"; 2 | 3 | const Auth = auth; 4 | 5 | export { Auth }; 6 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/mt-store-proxy/HEAD/src/assets/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/hooks/useWatch.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | 3 | export function useWatch(model: Ref, props: any) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/mt-store-proxy/HEAD/src/assets/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoxiunique/mt-store-proxy/HEAD/src/assets/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /src/layout/components/search/components/index.ts: -------------------------------------------------------------------------------- 1 | import SearchModal from "./SearchModal.vue"; 2 | 3 | export { SearchModal }; 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 平台本地运行端口号 2 | VITE_PORT = 8848 3 | 4 | # 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置) 5 | VITE_HIDE_HOME = false 6 | -------------------------------------------------------------------------------- /src/components/ReSelect/index.ts: -------------------------------------------------------------------------------- 1 | import select from "./src/select.vue"; 2 | 3 | const Select = select; 4 | 5 | export { Select }; 6 | -------------------------------------------------------------------------------- /src/components/ReUpload/index.ts: -------------------------------------------------------------------------------- 1 | import upload from "./src/upload.vue"; 2 | 3 | const Upload = upload; 4 | 5 | export { Upload }; 6 | -------------------------------------------------------------------------------- /src/components/TableRender/index.ts: -------------------------------------------------------------------------------- 1 | import TLables from "@/components/TableRender/TLables"; 2 | 3 | export { 4 | TLables 5 | }; 6 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck source=./_/husky.sh 4 | . "$(dirname "$0")/_/husky.sh" 5 | 6 | npx --no-install commitlint --edit "$1" 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | singleQuote: false, 4 | arrowParens: "avoid", 5 | trailingComma: "none" 6 | }; 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.1.0 (2023-08-01) 2 | 3 | ### 🎫 Feat 4 | 5 | - 项目启动和打包添加桌面端和浏览器端区分 6 | - 添加桌面端菜单栏通用配置以及不同平台打包 `icon` 配置 7 | - 使用 `GitHub Actions` 打包出兼容 `macOS`、`Linux` 和 `Windows` 三端的软件安装包,可直接下载安装使用 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | dist 3 | *.d.ts 4 | /src/assets 5 | package.json 6 | .eslintrc.js 7 | .prettierrc.js 8 | commitlint.config.js 9 | postcss.config.js 10 | tailwind.config.js 11 | stylelint.config.js -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReTable/index.ts: -------------------------------------------------------------------------------- 1 | import tableProBar from "./src/bar"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** table-crud组件 */ 5 | export const TableProBar = withInstall(tableProBar); 6 | -------------------------------------------------------------------------------- /src/components/TableRender/types.ts: -------------------------------------------------------------------------------- 1 | export interface FormProps { 2 | formKey?: string; 3 | options?: any[]; 4 | prop: any; 5 | label: any; 6 | k?: string; 7 | v?: string; 8 | api?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/store/unstorage.ts: -------------------------------------------------------------------------------- 1 | // import { createStorage } from "unstorage"; 2 | // import fsDriver from "unstorage/drivers/fs"; 3 | // 4 | // export const G = createStorage({ 5 | // driver: fsDriver({ base: "./tmp" }) 6 | // }); 7 | -------------------------------------------------------------------------------- /src/components/Form/types.ts: -------------------------------------------------------------------------------- 1 | export interface FormProps { 2 | formKey?: string; 3 | options?: any[]; 4 | prop: any; 5 | label: any; 6 | k?: string; 7 | v?: string; 8 | api?: string; 9 | init?: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 平台本地运行端口号 2 | VITE_PORT = 8848 3 | 4 | # 开发环境读取配置文件路径 5 | VITE_PUBLIC_PATH = / 6 | 7 | # 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 8 | VITE_ROUTER_HISTORY = "hash" 9 | -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command_exists () { 3 | command -v "$1" >/dev/null 2>&1 4 | } 5 | 6 | # Workaround for Windows 10, Git Bash and Pnpm 7 | if command_exists winpty && test -t 1; then 8 | exec < /dev/tty 9 | fi 10 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import { createPinia } from "pinia"; 3 | const store = createPinia(); 4 | 5 | export function setupStore(app: App) { 6 | app.use(store); 7 | } 8 | 9 | export { store }; 10 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD003": false, 4 | "MD033": false, 5 | "MD013": false, 6 | "MD001": false, 7 | "MD025": false, 8 | "MD024": false, 9 | "MD007": { "indent": 4 }, 10 | "no-hard-tabs": false 11 | } 12 | -------------------------------------------------------------------------------- /src/views/login/utils/static.ts: -------------------------------------------------------------------------------- 1 | import bg from "@/assets/login/bg.png"; 2 | import avatar from "@/assets/login/avatar.svg?component"; 3 | import illustration from "@/assets/login/illustration.svg?component"; 4 | 5 | export { bg, avatar, illustration }; 6 | -------------------------------------------------------------------------------- /src/api/routes.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | type Result = { 4 | success: boolean; 5 | data: Array; 6 | }; 7 | 8 | export const getAsyncRoutes = () => { 9 | return http.request("get", "/getAsyncRoutes"); 10 | }; 11 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}) 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | [ -n "$CI" ] && exit 0 6 | 7 | # Format and submit code according to lintstagedrc.js configuration 8 | npm run lint:lint-staged 9 | 10 | npm run lint:pretty 11 | -------------------------------------------------------------------------------- /src/assets/svg/dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/index.ts: -------------------------------------------------------------------------------- 1 | import pureTableBar from "./src/bar"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */ 5 | export const PureTableBar = withInstall(pureTableBar); 6 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | 7 | declare module "*.scss" { 8 | const scss: Record; 9 | export default scss; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts", "package.json", "electron"] 10 | } 11 | -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: "true"; 6 | DIST_ELECTRON: string; 7 | DIST: string; 8 | /** /dist/ or /public/ */ 9 | PUBLIC: string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/globalPolyfills.ts: -------------------------------------------------------------------------------- 1 | // 如果项目出现 `global is not defined` 报错,可能是您引入某个库的问题,比如 aws-sdk-js https://github.com/aws/aws-sdk-js 2 | // 解决办法就是将该文件引入 src/main.ts 即可 import "@/utils/globalPolyfills"; 3 | if (typeof (window as any).global === "undefined") { 4 | (window as any).global = window; 5 | } 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /src/assets/svg/full_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/exit_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/enter_outlined.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache 7 | report.html 8 | 9 | yarn-error.log 10 | npm-debug.log* 11 | .pnpm-error.log* 12 | .pnpm-debug.log 13 | tests/**/coverage/ 14 | 15 | # Editor directories and files 16 | .idea 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | tsconfig.tsbuildinfo 22 | 23 | dist-electron 24 | release -------------------------------------------------------------------------------- /src/utils/progress/index.ts: -------------------------------------------------------------------------------- 1 | import NProgress from "nprogress"; 2 | import "nprogress/nprogress.css"; 3 | 4 | NProgress.configure({ 5 | // 动画方式 6 | easing: "ease", 7 | // 递增进度条的速度 8 | speed: 500, 9 | // 是否显示加载ico 10 | showSpinner: false, 11 | // 自动递增间隔 12 | trickleSpeed: 200, 13 | // 初始化时的最小百分比 14 | minimum: 0.3 15 | }); 16 | 17 | export default NProgress; 18 | -------------------------------------------------------------------------------- /.vscode/vue3.2.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.2+快速生成模板": { 3 | "prefix": "Vue3.2+", 4 | "body": [ 5 | "\n", 7 | "\n", 11 | "", 13 | "$2" 14 | ], 15 | "description": "Vue3.2+" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- 1 | @import "./transition"; 2 | @import "./element-plus"; 3 | @import "./sidebar"; 4 | @import "./dark"; 5 | 6 | /* 自定义全局 CssVar */ 7 | :root { 8 | /* 左侧菜单展开、收起动画时长 */ 9 | --pure-transition-duration: 0.3s; 10 | } 11 | 12 | /* 灰色模式 */ 13 | .html-grey { 14 | filter: grayscale(100%); 15 | } 16 | 17 | /* 色弱模式 */ 18 | .html-weakness { 19 | filter: invert(80%); 20 | } 21 | -------------------------------------------------------------------------------- /.husky/lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], 3 | "{!(package)*.json}": ["prettier --write--parser json"], 4 | "package.json": ["prettier --write"], 5 | "*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"], 6 | "*.{vue,css,scss,postcss,less}": ["stylelint --fix", "prettier --write"], 7 | "*.md": ["prettier --write"] 8 | }; 9 | -------------------------------------------------------------------------------- /src/assets/svg/keyboard_esc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mockProdServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer"; 2 | 3 | const modules: Record = import.meta.glob("../mock/*.ts", { 4 | eager: true 5 | }); 6 | const mockModules = []; 7 | 8 | Object.keys(modules).forEach(key => { 9 | mockModules.push(...modules[key].default); 10 | }); 11 | 12 | export function setupProdMockServer() { 13 | createProdMockServer(mockModules); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ReIcon/index.ts: -------------------------------------------------------------------------------- 1 | import iconifyIconOffline from "./src/iconifyIconOffline"; 2 | import iconifyIconOnline from "./src/iconifyIconOnline"; 3 | import fontIcon from "./src/iconfont"; 4 | 5 | /** 本地图标组件 */ 6 | const IconifyIconOffline = iconifyIconOffline; 7 | /** 在线图标组件 */ 8 | const IconifyIconOnline = iconifyIconOnline; 9 | /** iconfont组件 */ 10 | const FontIcon = fontIcon; 11 | 12 | export { IconifyIconOffline, IconifyIconOnline, FontIcon }; 13 | -------------------------------------------------------------------------------- /src/views/store/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /src/components/Form/XInput.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /src/style/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .flex-c { 7 | @apply flex justify-center items-center; 8 | } 9 | 10 | .flex-ac { 11 | @apply flex justify-around items-center; 12 | } 13 | 14 | .flex-bc { 15 | @apply flex justify-between items-center; 16 | } 17 | 18 | .navbar-bg-hover { 19 | @apply dark:text-white dark:hover:!bg-[#242424]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Fields/TMixed.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/components/Form/index.ts: -------------------------------------------------------------------------------- 1 | import XDate from "./XDate.vue"; 2 | import XInput from "./XInput.vue"; 3 | import XOptions from "./XOptions.vue"; 4 | import XNumber from "./XNumber.vue"; 5 | import XSelect from "./XSelect.vue"; 6 | import XMultipleSelect from "./XMultipleSelect.vue"; 7 | import XCertForm from "@/components/Form/XCertForm.vue"; 8 | import XImg from './XImg.vue'; 9 | 10 | export { XDate, XInput, XOptions, XNumber, XSelect, XMultipleSelect, XCertForm, XImg}; 11 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/collapse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/directives/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { hasAuth } from "@/router/utils"; 2 | import { Directive, type DirectiveBinding } from "vue"; 3 | 4 | export const auth: Directive = { 5 | mounted(el: HTMLElement, binding: DirectiveBinding) { 6 | const { value } = binding; 7 | if (value) { 8 | !hasAuth(value) && el.parentNode?.removeChild(el); 9 | } else { 10 | throw new Error("need auths! Like v-auth=\"['btn.add','btn.edit']\""); 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/Form/XTextarea.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /src/assets/svg/day.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/useUpload.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage, UploadProps } from "element-plus"; 2 | 3 | export const beforeUpload: UploadProps["beforeUpload"] = rawFile => { 4 | if ( 5 | rawFile.type !== 6 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" 7 | ) { 8 | ElMessage.error("请上传 xlsx 文件"); 9 | return false; 10 | } else if (rawFile.size / 1024 / 1024 > 2) { 11 | ElMessage.error("文件大小不能超过 2MB"); 12 | return false; 13 | } 14 | return true; 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | import type { Emitter } from "mitt"; 2 | import mitt from "mitt"; 3 | 4 | type Events = { 5 | resize: { 6 | detail: { 7 | width: number; 8 | height: number; 9 | }; 10 | }; 11 | openPanel: string; 12 | tagViewsChange: string; 13 | tagViewsShowModel: string; 14 | logoChange: boolean; 15 | changLayoutRoute: { 16 | indexPath: string; 17 | parentPath: string; 18 | }; 19 | }; 20 | 21 | export const emitter: Emitter = mitt(); 22 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境平台打包路径 2 | VITE_PUBLIC_PATH = ./ 3 | 4 | # 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 5 | VITE_ROUTER_HISTORY = "hash" 6 | 7 | # 是否在打包时使用cdn替换本地库 替换 true 不替换 false 8 | VITE_CDN = false 9 | 10 | # 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件) 11 | # 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 12 | # 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 13 | VITE_COMPRESSION = "none" -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "christian-kohler.path-intellisense", 4 | "vscode-icons-team.vscode-icons", 5 | "davidanson.vscode-markdownlint", 6 | "stylelint.vscode-stylelint", 7 | "bradlc.vscode-tailwindcss", 8 | "dbaeumer.vscode-eslint", 9 | "esbenp.prettier-vscode", 10 | "redhat.vscode-yaml", 11 | "csstools.postcss", 12 | "mikestead.dotenv", 13 | "eamodio.gitlens", 14 | "antfu.iconify", 15 | "Vue.volar" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/vue3.0.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.0快速生成模板": { 3 | "prefix": "Vue3.0", 4 | "body": [ 5 | "\n", 9 | "\n", 16 | "", 18 | "$2" 19 | ], 20 | "description": "Vue3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ReAuth/src/auth.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, Fragment } from "vue"; 2 | import { hasAuth } from "@/router/utils"; 3 | 4 | export default defineComponent({ 5 | name: "Auth", 6 | props: { 7 | value: { 8 | type: undefined, 9 | default: [] 10 | } 11 | }, 12 | setup(props, { slots }) { 13 | return () => { 14 | if (!slots) return null; 15 | return hasAuth(props.value) ? ( 16 | {slots.default?.()} 17 | ) : null; 18 | }; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/layout/redirect.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/hooks/useAPI.ts: -------------------------------------------------------------------------------- 1 | import { useAxios } from "@vueuse/integrations/useAxios"; 2 | import { http } from "@/utils/http"; 3 | import { AxiosRequestConfig } from "axios"; 4 | import { onMounted } from "vue"; 5 | 6 | export function useAPI() { 7 | return useAxios(http.getIns()); 8 | } 9 | 10 | export function useAPIIns(config: AxiosRequestConfig) { 11 | const { execute, ...other } = useAxios(config, http.getIns()); 12 | onMounted(() => { 13 | void execute(config.url); 14 | }); 15 | return { execute, ...other }; 16 | } 17 | -------------------------------------------------------------------------------- /src/views/welcome/components/preload.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require("electron"); 2 | 3 | // setInterval(() => { 4 | // console.log("preload.js loaded"); 5 | // const poi = localStorage.getItem("poiPrintTemplates"); 6 | // 7 | // ipcRenderer.send("message-from-renderer", { 8 | // cookie: document.cookie, 9 | // poi 10 | // }); 11 | // }, 1000); 12 | 13 | setTimeout(() => { 14 | // 这里上传测试数据 15 | ipcRenderer.send("message-from-webview", { 16 | cookie: 17 | '', 18 | poi: [] 19 | }); 20 | }, 2000); 21 | -------------------------------------------------------------------------------- /src/layout/hooks/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export function useBoolean(initValue = false) { 4 | const bool = ref(initValue); 5 | 6 | function setBool(value: boolean) { 7 | bool.value = value; 8 | } 9 | function setTrue() { 10 | setBool(true); 11 | } 12 | function setFalse() { 13 | setBool(false); 14 | } 15 | function toggle() { 16 | setBool(!bool.value); 17 | } 18 | 19 | return { 20 | bool, 21 | setBool, 22 | setTrue, 23 | setFalse, 24 | toggle 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/offlineIcon.ts: -------------------------------------------------------------------------------- 1 | import { addIcon } from "@iconify/vue/dist/offline"; 2 | 3 | /** 4 | * 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载 5 | */ 6 | 7 | // 本地菜单图标,后端在路由的icon中返回对应的图标字符串并且前端在此处使用addIcon添加即可渲染菜单图标 8 | import HomeFilled from "@iconify-icons/ep/home-filled"; 9 | import InformationLine from "@iconify-icons/ri/information-line"; 10 | import Lollipop from "@iconify-icons/ep/lollipop"; 11 | 12 | addIcon("homeFilled", HomeFilled); 13 | addIcon("informationLine", InformationLine); 14 | addIcon("lollipop", Lollipop); 15 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/extraIcon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: "class", 4 | corePlugins: { 5 | preflight: false 6 | }, 7 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 8 | theme: { 9 | extend: { 10 | colors: { 11 | bg_color: "var(--el-bg-color)", 12 | primary: "var(--el-color-primary)", 13 | text_color_primary: "var(--el-text-color-primary)", 14 | text_color_regular: "var(--el-text-color-regular)" 15 | } 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare module "*.tsx" { 4 | import Vue from "compatible-vue"; 5 | export default Vue; 6 | } 7 | 8 | declare global { 9 | namespace JSX { 10 | interface Element extends VNode {} 11 | interface ElementClass extends Vue {} 12 | interface ElementAttributesProperty { 13 | $props: any; 14 | } 15 | interface IntrinsicElements { 16 | [elem: string]: any; 17 | } 18 | interface IntrinsicAttributes { 19 | [elem: string]: any; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface iconType { 2 | // iconify (https://docs.iconify.design/icon-components/vue/#properties) 3 | inline?: boolean; 4 | width?: string | number; 5 | height?: string | number; 6 | horizontalFlip?: boolean; 7 | verticalFlip?: boolean; 8 | flip?: string; 9 | rotate?: number | string; 10 | color?: string; 11 | horizontalAlign?: boolean; 12 | verticalAlign?: boolean; 13 | align?: string; 14 | onLoad?: Function; 15 | includes?: Function; 16 | 17 | // all icon 18 | style?: object; 19 | } 20 | -------------------------------------------------------------------------------- /src/views/store/components/StoreForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /src/views/welcome/components/StoreForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /src/components/Fields/TImg.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /src/components/Fields/TOptions.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /src/components/Fields/TFile.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /src/components/Form/XNumber.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | # 预发布也需要生产环境的行为 2 | # https://cn.vitejs.dev/guide/env-and-mode.html#modes 3 | NODE_ENV=production 4 | 5 | VITE_PUBLIC_PATH = ./ 6 | 7 | # 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 8 | VITE_ROUTER_HISTORY = "hash" 9 | 10 | # 是否在打包时使用cdn替换本地库 替换 true 不替换 false 11 | VITE_CDN = true 12 | 13 | # 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件) 14 | # 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 15 | # 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 16 | VITE_COMPRESSION = "none" 17 | -------------------------------------------------------------------------------- /src/assets/svg/back_top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/notice/noticeList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /src/layout/components/search/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/hooks/useTabs.ts: -------------------------------------------------------------------------------- 1 | import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; 2 | import { useRouter, useRoute } from "vue-router"; 3 | 4 | type UseDetailParams = { 5 | path: string; 6 | name: string; 7 | query: string; 8 | params: string; 9 | meta: string; 10 | }; 11 | 12 | export function useTabs(body: UseDetailParams) { 13 | const router = useRouter(); 14 | 15 | function goto() { 16 | // 保存信息到标签页 17 | useMultiTagsStoreHook().handleTags("push", body); 18 | // 路由跳转 19 | void router.push({ name: body.name, query: body.query }); 20 | } 21 | 22 | return { 23 | goto 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /public/serverConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "4.5.0", 3 | "Title": "美团", 4 | "FixedHeader": true, 5 | "HiddenSideBar": false, 6 | "sidebar": false, 7 | "MultiTagsCache": false, 8 | "KeepAlive": true, 9 | "Layout": "vertical", 10 | "Theme": "default", 11 | "DarkMode": false, 12 | "Grey": false, 13 | "Weak": false, 14 | "HideTabs": false, 15 | "SidebarStatus": true, 16 | "EpThemeColor": "#3650d3", 17 | "ShowLogo": true, 18 | "ShowModel": "smart", 19 | "MenuArrowIconNoTransition": true, 20 | "CachingAsyncRoutes": false, 21 | "TooltipEffect": "light", 22 | "ResponsiveStorageNameSpace": "responsive-" 23 | } 24 | -------------------------------------------------------------------------------- /src/router/modules/home.ts: -------------------------------------------------------------------------------- 1 | const { VITE_HIDE_HOME } = import.meta.env; 2 | const Layout = () => import("@/layout/index.vue"); 3 | 4 | export default { 5 | path: "/", 6 | name: "Home", 7 | component: Layout, 8 | redirect: "/welcome", 9 | meta: { 10 | icon: "homeFilled", 11 | title: "首页", 12 | rank: 0 13 | }, 14 | children: [ 15 | { 16 | path: "/welcome", 17 | name: "Welcome", 18 | component: () => import("@/views/welcome/index.vue"), 19 | meta: { 20 | title: "首页", 21 | showLink: VITE_HIDE_HOME === "true" ? false : true 22 | } 23 | } 24 | ] 25 | } as RouteConfigsTable; 26 | -------------------------------------------------------------------------------- /src/components/Form/XOptions.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/components/ReCol/index.ts: -------------------------------------------------------------------------------- 1 | import { ElCol } from "element-plus"; 2 | import { h, defineComponent } from "vue"; 3 | 4 | // 封装element-plus的el-col组件 5 | export default defineComponent({ 6 | name: "ReCol", 7 | props: { 8 | value: { 9 | type: Number, 10 | default: 24 11 | } 12 | }, 13 | render() { 14 | const attrs = this.$attrs; 15 | const val = this.value; 16 | return h( 17 | ElCol, 18 | { 19 | xs: val, 20 | sm: val, 21 | md: val, 22 | lg: val, 23 | xl: val, 24 | ...attrs 25 | }, 26 | { default: () => this.$slots.default() } 27 | ); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | -------------------------------------------------------------------------------- /src/components/Form/XDate.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Form/XFile.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2208059 */ 3 | src: url("iconfont.woff2?t=1671895108120") format("woff2"), 4 | url("iconfont.woff?t=1671895108120") format("woff"), 5 | url("iconfont.ttf?t=1671895108120") format("truetype"); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .pure-iconfont-tabs:before { 17 | content: "\e63e"; 18 | } 19 | 20 | .pure-iconfont-logo:before { 21 | content: "\e620"; 22 | } 23 | 24 | .pure-iconfont-new:before { 25 | content: "\e615"; 26 | } 27 | -------------------------------------------------------------------------------- /src/assets/login/avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mock/asyncRoutes.ts: -------------------------------------------------------------------------------- 1 | // 模拟后端动态生成路由 2 | import { MockMethod } from "vite-plugin-mock"; 3 | 4 | /** 5 | * roles:页面级别权限,这里模拟二种 "admin"、"common" 6 | * admin:管理员角色 7 | * common:普通角色 8 | */ 9 | 10 | const permissionRouter = { 11 | path: "/store", 12 | meta: { 13 | title: "门店管理", 14 | icon: "lollipop", 15 | rank: 10 16 | }, 17 | children: [ 18 | { 19 | path: "/store/index", 20 | name: "门店管理", 21 | meta: {} 22 | } 23 | ] 24 | }; 25 | 26 | export default [ 27 | { 28 | url: "/getAsyncRoutes", 29 | method: "get", 30 | response: () => { 31 | return { 32 | success: true, 33 | data: [permissionRouter] 34 | }; 35 | } 36 | } 37 | ] as MockMethod[]; 38 | -------------------------------------------------------------------------------- /src/components/Fields/TSwitch.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/router/modules/remaining.ts: -------------------------------------------------------------------------------- 1 | const Layout = () => import("@/layout/index.vue"); 2 | 3 | export default [ 4 | { 5 | path: "/login", 6 | name: "Login", 7 | component: () => import("@/views/login/index.vue"), 8 | meta: { 9 | title: "登录", 10 | showLink: false, 11 | rank: 101 12 | } 13 | }, 14 | { 15 | path: "/redirect", 16 | component: Layout, 17 | meta: { 18 | title: "加载中...", 19 | showLink: false, 20 | rank: 102 21 | }, 22 | children: [ 23 | { 24 | path: "/redirect/:path(.*)", 25 | name: "Redirect", 26 | component: () => import("@/layout/redirect.vue") 27 | } 28 | ] 29 | } 30 | ] as Array; 31 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2208059", 3 | "name": "pure-admin", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "pure-iconfont-", 6 | "description": "pure-admin-iconfont", 7 | "glyphs": [ 8 | { 9 | "icon_id": "20594647", 10 | "name": "Tabs", 11 | "font_class": "tabs", 12 | "unicode": "e63e", 13 | "unicode_decimal": 58942 14 | }, 15 | { 16 | "icon_id": "22129506", 17 | "name": "PureLogo", 18 | "font_class": "logo", 19 | "unicode": "e620", 20 | "unicode_decimal": 58912 21 | }, 22 | { 23 | "icon_id": "7795615", 24 | "name": "New", 25 | "font_class": "new", 26 | "unicode": "e615", 27 | "unicode_decimal": 58901 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Fields/TInput.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/iconifyIconOnline.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | import { Icon as IconifyIcon } from "@iconify/vue"; 3 | 4 | // Iconify Icon在Vue里在线使用(用于外网环境) 5 | export default defineComponent({ 6 | name: "IconifyIconOnline", 7 | components: { IconifyIcon }, 8 | props: { 9 | icon: { 10 | type: String, 11 | default: "" 12 | } 13 | }, 14 | render() { 15 | const attrs = this.$attrs; 16 | return h( 17 | IconifyIcon, 18 | { 19 | icon: `${this.icon}`, 20 | style: attrs?.style 21 | ? Object.assign(attrs.style, { outline: "none" }) 22 | : { outline: "none" }, 23 | ...attrs 24 | }, 25 | { 26 | default: () => [] 27 | } 28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignores: [commit => commit.includes("init")], 3 | extends: ["@commitlint/config-conventional"], 4 | rules: { 5 | "body-leading-blank": [2, "always"], 6 | "footer-leading-blank": [1, "always"], 7 | "header-max-length": [2, "always", 108], 8 | "subject-empty": [2, "never"], 9 | "type-empty": [2, "never"], 10 | "type-enum": [ 11 | 2, 12 | "always", 13 | [ 14 | "feat", 15 | "fix", 16 | "perf", 17 | "style", 18 | "docs", 19 | "test", 20 | "refactor", 21 | "build", 22 | "ci", 23 | "chore", 24 | "revert", 25 | "wip", 26 | "workflow", 27 | "types", 28 | "release" 29 | ] 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/hooks/useDownload.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import JSZip from "jszip"; 3 | import FileSaver from "file-saver"; 4 | 5 | export async function batchDownload( 6 | title: string, 7 | urls: { 8 | title: string; 9 | url: { 10 | name: string; 11 | url: string; 12 | } 13 | }[] 14 | ) { 15 | const zip = new JSZip(); 16 | const promises = urls.map(url => 17 | axios.get(url.url.url, { responseType: "blob" }) 18 | ); 19 | const responses = await Promise.all(promises); 20 | responses.forEach((response, index) => { 21 | const { data } = response; 22 | const filename = urls[index].title; 23 | zip.file(filename, data); 24 | }); 25 | zip.generateAsync({ type: "blob" }).then(content => { 26 | FileSaver.saveAs(content, `${title}.zip`); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /mock/refreshToken.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from "vite-plugin-mock"; 2 | 3 | // 模拟刷新token接口 4 | export default [ 5 | { 6 | url: "/refreshToken", 7 | method: "post", 8 | response: ({ body }) => { 9 | if (body.refreshToken) { 10 | return { 11 | success: true, 12 | data: { 13 | accessToken: "eyJhbGciOiJIUzUxMiJ9.newAdmin", 14 | refreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh", 15 | // `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。 16 | expires: "2023/10/30 23:59:59" 17 | } 18 | }; 19 | } else { 20 | return { 21 | success: false, 22 | data: {} 23 | }; 24 | } 25 | } 26 | } 27 | ] as MockMethod[]; 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | platform: [macos-latest, ubuntu-latest, windows-latest] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - name: Yarn setup 17 | uses: DerYeger/yarn-setup-action@master 18 | with: 19 | node-version: 16 20 | 21 | - name: Build and release 22 | uses: nick-fields/retry@v2 23 | with: 24 | timeout_minutes: 15 25 | max_attempts: 2 26 | retry_wait_seconds: 15 27 | retry_on: error 28 | shell: "bash" 29 | command: yarn build --publish always 30 | env: 31 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /src/views/login/utils/rule.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | 4 | /** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */ 5 | export const REGEXP_PWD = 6 | /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/; 7 | 8 | /** 登录校验 */ 9 | const loginRules = reactive({ 10 | password: [ 11 | { 12 | validator: (rule, value, callback) => { 13 | if (value === "") { 14 | callback(new Error("请输入密码")); 15 | } else if (!REGEXP_PWD.test(value)) { 16 | callback( 17 | new Error("密码格式应为8-18位数字、字母、符号的任意两种组合") 18 | ); 19 | } else { 20 | callback(); 21 | } 22 | }, 23 | trigger: "blur" 24 | } 25 | ] 26 | }); 27 | 28 | export { loginRules }; 29 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/topCollapse.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /src/store/modules/types.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordName } from "vue-router"; 2 | 3 | export type cacheType = { 4 | mode: string; 5 | name?: RouteRecordName; 6 | }; 7 | 8 | export type positionType = { 9 | startIndex?: number; 10 | length?: number; 11 | }; 12 | 13 | export type appType = { 14 | sidebar: { 15 | opened: boolean; 16 | withoutAnimation: boolean; 17 | // 判断是否手动点击Collapse 18 | isClickCollapse: boolean; 19 | }; 20 | layout: string; 21 | device: string; 22 | }; 23 | 24 | export type multiType = { 25 | path: string; 26 | name: string; 27 | meta: any; 28 | query?: object; 29 | params?: object; 30 | }; 31 | 32 | export type setType = { 33 | title: string; 34 | fixedHeader: boolean; 35 | hiddenSideBar: boolean; 36 | sidebar: boolean; 37 | }; 38 | 39 | export type userType = { 40 | username?: string; 41 | roles?: Array; 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/iconifyIconOffline.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline"; 3 | 4 | // Iconify Icon在Vue里本地使用(用于内网环境)https://docs.iconify.design/icon-components/vue/offline.html 5 | export default defineComponent({ 6 | name: "IconifyIconOffline", 7 | components: { IconifyIcon }, 8 | props: { 9 | icon: { 10 | default: null 11 | } 12 | }, 13 | render() { 14 | if (typeof this.icon === "object") addIcon(this.icon, this.icon); 15 | const attrs = this.$attrs; 16 | return h( 17 | IconifyIcon, 18 | { 19 | icon: this.icon, 20 | style: attrs?.style 21 | ? Object.assign(attrs.style, { outline: "none" }) 22 | : { outline: "none" }, 23 | ...attrs 24 | }, 25 | { 26 | default: () => [] 27 | } 28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Form/XImg.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /src/directives/elResizeDetector/index.ts: -------------------------------------------------------------------------------- 1 | import { Directive, type DirectiveBinding, type VNode } from "vue"; 2 | import elementResizeDetectorMaker from "element-resize-detector"; 3 | import type { Erd } from "element-resize-detector"; 4 | import { emitter } from "@/utils/mitt"; 5 | 6 | const erd: Erd = elementResizeDetectorMaker({ 7 | strategy: "scroll" 8 | }); 9 | 10 | export const resize: Directive = { 11 | mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) { 12 | erd.listenTo(el, elem => { 13 | const width = elem.offsetWidth; 14 | const height = elem.offsetHeight; 15 | if (binding?.instance) { 16 | emitter.emit("resize", { detail: { width, height } }); 17 | } else { 18 | vnode.el.dispatchEvent( 19 | new CustomEvent("resize", { detail: { width, height } }) 20 | ); 21 | } 22 | }); 23 | }, 24 | unmounted(el: HTMLElement) { 25 | erd.uninstall(el); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/views/login/utils/motion.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent, withDirectives, resolveDirective } from "vue"; 2 | 3 | /** 封装@vueuse/motion动画库中的自定义指令v-motion */ 4 | export default defineComponent({ 5 | name: "Motion", 6 | props: { 7 | delay: { 8 | type: Number, 9 | default: 50 10 | } 11 | }, 12 | render() { 13 | const { delay } = this; 14 | const motion = resolveDirective("motion"); 15 | return withDirectives( 16 | h( 17 | "div", 18 | {}, 19 | { 20 | default: () => [this.$slots.default()] 21 | } 22 | ), 23 | [ 24 | [ 25 | motion, 26 | { 27 | initial: { opacity: 0, y: 100 }, 28 | enter: { 29 | opacity: 1, 30 | y: 0, 31 | transition: { 32 | delay 33 | } 34 | } 35 | } 36 | ] 37 | ] 38 | ); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnType": true, 3 | "editor.formatOnSave": true, 4 | "[vue]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "editor.tabSize": 2, 8 | "editor.formatOnPaste": true, 9 | "editor.guides.bracketPairs": "active", 10 | "files.autoSave": "afterDelay", 11 | "git.confirmSync": false, 12 | "workbench.startupEditor": "newUntitledFile", 13 | "editor.suggestSelection": "first", 14 | "editor.acceptSuggestionOnCommitCharacter": false, 15 | "css.lint.propertyIgnoredDueToDisplay": "ignore", 16 | "editor.quickSuggestions": { 17 | "other": true, 18 | "comments": true, 19 | "strings": true 20 | }, 21 | "files.associations": { 22 | "editor.snippetSuggestions": "top" 23 | }, 24 | "[css]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "editor.codeActionsOnSave": { 28 | "source.fixAll.eslint": true 29 | }, 30 | "iconify.excludes": ["el"] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/TableRender/TLables.ts: -------------------------------------------------------------------------------- 1 | import { ElDescriptions, ElDescriptionsItem } from "element-plus"; 2 | import { defineComponent, h } from "vue"; 3 | 4 | // 封装element-plus的el-col组件 5 | export default defineComponent({ 6 | name: "Labels", 7 | props: { 8 | options: { 9 | type: Array, 10 | default: () => [] 11 | }, 12 | prop: { 13 | type: String, 14 | default: "name" 15 | }, 16 | label: { 17 | type: String, 18 | default: "value" 19 | } 20 | }, 21 | render() { 22 | const options = this.options; 23 | const items = options.map((item: any) => { 24 | return h( 25 | ElDescriptionsItem, 26 | { 27 | label: item[this.prop] 28 | }, 29 | { default: () => item[this.label] } 30 | ); 31 | }); 32 | 33 | return h( 34 | ElDescriptions, 35 | { 36 | column: 1, 37 | border: true 38 | }, 39 | items 40 | ); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mock/login.ts: -------------------------------------------------------------------------------- 1 | // 根据角色动态生成路由 2 | import { MockMethod } from "vite-plugin-mock"; 3 | 4 | export default [ 5 | { 6 | url: "/login", 7 | method: "post", 8 | response: ({ body }) => { 9 | if (body.username === "admin") { 10 | return { 11 | success: true, 12 | data: { 13 | username: "admin", 14 | // 一个用户可能有多个角色 15 | roles: ["admin"], 16 | accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", 17 | refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", 18 | expires: "2023/10/30 00:00:00" 19 | } 20 | }; 21 | } else { 22 | return { 23 | success: true, 24 | data: { 25 | username: "common", 26 | // 一个用户可能有多个角色 27 | roles: ["common"], 28 | accessToken: "eyJhbGciOiJIUzUxMiJ9.common", 29 | refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", 30 | expires: "2023/10/30 00:00:00" 31 | } 32 | }; 33 | } 34 | } 35 | } 36 | ] as MockMethod[]; 37 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type UserResult = { 4 | success: boolean; 5 | data: { 6 | /** 用户名 */ 7 | username: string; 8 | /** 当前登陆用户的角色 */ 9 | roles: Array; 10 | /** `token` */ 11 | accessToken: string; 12 | /** 用于调用刷新`accessToken`的接口时所需的`token` */ 13 | refreshToken: string; 14 | /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ 15 | expires: Date; 16 | }; 17 | }; 18 | 19 | export type RefreshTokenResult = { 20 | success: boolean; 21 | data: { 22 | /** `token` */ 23 | accessToken: string; 24 | /** 用于调用刷新`accessToken`的接口时所需的`token` */ 25 | refreshToken: string; 26 | /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ 27 | expires: Date; 28 | }; 29 | }; 30 | 31 | /** 登录 */ 32 | export const getLogin = (data?: object) => { 33 | return http.request("post", "/login", { data }); 34 | }; 35 | 36 | /** 刷新token */ 37 | export const refreshTokenApi = (data?: object) => { 38 | return http.request("post", "/refreshToken", { data }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/store/modules/settings.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { store } from "@/store"; 3 | import { setType } from "./types"; 4 | import { getConfig } from "@/config"; 5 | 6 | export const useSettingStore = defineStore({ 7 | id: "pure-setting", 8 | state: (): setType => ({ 9 | title: getConfig().Title, 10 | fixedHeader: getConfig().FixedHeader, 11 | hiddenSideBar: getConfig().HiddenSideBar, 12 | sidebar: getConfig().sidebar 13 | }), 14 | getters: { 15 | getTitle(state) { 16 | return state.title; 17 | }, 18 | getFixedHeader(state) { 19 | return state.fixedHeader; 20 | }, 21 | getHiddenSideBar(state) { 22 | return state.hiddenSideBar; 23 | }, 24 | getSidebar(state) { 25 | return state.sidebar; 26 | } 27 | }, 28 | actions: { 29 | CHANGE_SETTING({ key, value }) { 30 | if (Reflect.has(this, key)) { 31 | this[key] = value; 32 | } 33 | }, 34 | changeSetting(data) { 35 | this.CHANGE_SETTING(data); 36 | } 37 | } 38 | }); 39 | 40 | export function useSettingStoreHook() { 41 | return useSettingStore(store); 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present, pure-admin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/ReUpload/src/upload.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 48 | -------------------------------------------------------------------------------- /src/utils/propTypes.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties, VNodeChild } from "vue"; 2 | import { 3 | createTypes, 4 | toValidableType, 5 | VueTypesInterface, 6 | VueTypeValidableDef 7 | } from "vue-types"; 8 | 9 | export type VueNode = VNodeChild | JSX.Element; 10 | 11 | type PropTypes = VueTypesInterface & { 12 | readonly style: VueTypeValidableDef; 13 | readonly VNodeChild: VueTypeValidableDef; 14 | }; 15 | 16 | const newPropTypes = createTypes({ 17 | func: undefined, 18 | bool: undefined, 19 | string: undefined, 20 | number: undefined, 21 | object: undefined, 22 | integer: undefined 23 | }) as PropTypes; 24 | 25 | // 从 vue-types v5.0 开始,extend()方法已经废弃,当前已改为官方推荐的ES6+方法 https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method 26 | export default class propTypes extends newPropTypes { 27 | // a native-like validator that supports the `.validable` method 28 | static get style() { 29 | return toValidableType("style", { 30 | type: [String, Object] 31 | }); 32 | } 33 | 34 | static get VNodeChild() { 35 | return toValidableType("VNodeChild", { 36 | type: undefined 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/style/transition.scss: -------------------------------------------------------------------------------- 1 | /* fade */ 2 | .fade-enter-active, 3 | .fade-leave-active { 4 | transition: opacity 0.28s; 5 | } 6 | 7 | .fade-enter, 8 | .fade-leave-active { 9 | opacity: 0; 10 | } 11 | 12 | /* fade-transform */ 13 | .fade-transform-leave-active, 14 | .fade-transform-enter-active { 15 | transition: all 0.5s; 16 | } 17 | 18 | .fade-transform-enter-from { 19 | opacity: 0; 20 | transform: translateX(-30px); 21 | } 22 | 23 | .fade-transform-leave-to { 24 | opacity: 0; 25 | transform: translateX(30px); 26 | } 27 | 28 | /* breadcrumb transition */ 29 | .breadcrumb-enter-active { 30 | transition: all 0.4s; 31 | } 32 | 33 | .breadcrumb-leave-active { 34 | position: absolute; 35 | transition: all 0.3s; 36 | } 37 | 38 | .breadcrumb-enter-from, 39 | .breadcrumb-leave-active { 40 | opacity: 0; 41 | transform: translateX(20px); 42 | } 43 | 44 | /** 45 | * @description 重置el-menu的展开收起动画时长 46 | */ 47 | .outer-most .el-collapse-transition-leave-active, 48 | .outer-most .el-collapse-transition-enter-active { 49 | transition: 0.2s all ease-in-out !important; 50 | } 51 | 52 | .horizontal-collapse-transition { 53 | transition: var(--pure-transition-duration) all !important; 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/http/types.d.ts: -------------------------------------------------------------------------------- 1 | import Axios, { 2 | Method, 3 | AxiosError, 4 | AxiosResponse, 5 | AxiosRequestConfig 6 | } from "axios"; 7 | 8 | export type resultType = { 9 | accessToken?: string; 10 | }; 11 | 12 | export type RequestMethods = Extract< 13 | Method, 14 | "get" | "post" | "put" | "delete" | "patch" | "option" | "head" 15 | >; 16 | 17 | export interface PureHttpError extends AxiosError { 18 | isCancelRequest?: boolean; 19 | } 20 | 21 | export interface PureHttpResponse extends AxiosResponse { 22 | config: PureHttpRequestConfig; 23 | } 24 | 25 | export interface PureHttpRequestConfig extends AxiosRequestConfig { 26 | beforeRequestCallback?: (request: PureHttpRequestConfig) => void; 27 | beforeResponseCallback?: (response: PureHttpResponse) => void; 28 | } 29 | 30 | export default class PureHttp { 31 | request( 32 | method: RequestMethods, 33 | url: string, 34 | param?: AxiosRequestConfig, 35 | axiosConfig?: PureHttpRequestConfig 36 | ): Promise; 37 | post( 38 | url: string, 39 | params?: T, 40 | config?: PureHttpRequestConfig 41 | ): Promise

; 42 | get( 43 | url: string, 44 | params?: T, 45 | config?: PureHttpRequestConfig 46 | ): Promise

; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Form/XCertForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 48 | -------------------------------------------------------------------------------- /src/plugins/echarts/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import * as echarts from "echarts/core"; 3 | import { CanvasRenderer } from "echarts/renderers"; 4 | import { PieChart, BarChart, LineChart } from "echarts/charts"; 5 | import { 6 | GridComponent, 7 | TitleComponent, 8 | LegendComponent, 9 | GraphicComponent, 10 | ToolboxComponent, 11 | TooltipComponent, 12 | DataZoomComponent, 13 | VisualMapComponent 14 | } from "echarts/components"; 15 | 16 | const { use } = echarts; 17 | 18 | use([ 19 | PieChart, 20 | BarChart, 21 | LineChart, 22 | CanvasRenderer, 23 | GridComponent, 24 | TitleComponent, 25 | LegendComponent, 26 | GraphicComponent, 27 | ToolboxComponent, 28 | TooltipComponent, 29 | DataZoomComponent, 30 | VisualMapComponent 31 | ]); 32 | 33 | /** 34 | * @description 按需引入echarts 35 | * @see {@link https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6} 36 | * @see 温馨提示:必须将 `$echarts` 添加到全局 `globalProperties` ,为了配合 https://pure-admin-utils.netlify.app/hooks/useEcharts/useEcharts.html 使用 37 | */ 38 | export function useEcharts(app: App) { 39 | app.config.globalProperties.$echarts = echarts; 40 | } 41 | 42 | export default echarts; 43 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/iconfont.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | 3 | // 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code) 4 | export default defineComponent({ 5 | name: "FontIcon", 6 | props: { 7 | icon: { 8 | type: String, 9 | default: "" 10 | } 11 | }, 12 | render() { 13 | const attrs = this.$attrs; 14 | if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") { 15 | return h( 16 | "i", 17 | { 18 | class: "iconfont", 19 | ...attrs 20 | }, 21 | this.icon 22 | ); 23 | } else if ( 24 | Object.keys(attrs).includes("svg") || 25 | attrs?.iconType === "svg" 26 | ) { 27 | return h( 28 | "svg", 29 | { 30 | class: "icon-svg", 31 | "aria-hidden": true 32 | }, 33 | { 34 | default: () => [ 35 | h("use", { 36 | "xlink:href": `#${this.icon}` 37 | }) 38 | ] 39 | } 40 | ); 41 | } else { 42 | return h("i", { 43 | class: `iconfont ${this.icon}`, 44 | ...attrs 45 | }); 46 | } 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "Node", 6 | "strict": false, 7 | "jsx": "preserve", 8 | "importHelpers": true, 9 | "experimentalDecorators": true, 10 | "strictFunctionTypes": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "isolatedModules": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "sourceMap": true, 17 | "baseUrl": ".", 18 | "allowJs": false, 19 | "resolveJsonModule": true, 20 | "lib": ["dom", "esnext"], 21 | "paths": { 22 | "@/*": ["src/*"], 23 | "@build/*": ["build/*"] 24 | }, 25 | "types": [ 26 | "node", 27 | "vite/client", 28 | "element-plus/global", 29 | "@pureadmin/table/volar", 30 | "@pureadmin/descriptions/volar" 31 | ], 32 | "typeRoots": ["./types", "./node_modules/@types/"] 33 | }, 34 | "include": [ 35 | "mock/*.ts", 36 | "src/**/*.ts", 37 | "src/**/*.tsx", 38 | "src/**/*.vue", 39 | "types/*.d.ts", 40 | "vite.config.ts" 41 | ], 42 | "exclude": ["dist", "**/*.js", "node_modules"], 43 | "references": [ 44 | { 45 | "path": "./tsconfig.node.json" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/hooks/useCommonEditConfig.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from "vue"; 2 | import { FormInstance } from "element-plus"; 3 | import { useRestAPI } from "@/hooks/useRestAPI"; 4 | 5 | export function useCommonEditConfig(props, emit, api) { 6 | const form = ref({} as any); 7 | const formRef = ref(null); 8 | 9 | watch( 10 | () => props.data, 11 | val => { 12 | form.value = val as any; 13 | } 14 | ); 15 | 16 | const { loading, create, update } = useRestAPI({ 17 | api 18 | }); 19 | 20 | function handleClose() { 21 | formRef.value.resetFields(); 22 | emit("close"); 23 | } 24 | 25 | function handleSubmit() { 26 | formRef.value.validate(async valid => { 27 | if (valid) { 28 | if (props.data.id) { 29 | update(form.value).then(() => { 30 | setTimeout(() => { 31 | handleClose(); 32 | emit("submit"); 33 | }, 200); 34 | }); 35 | } else { 36 | create(form.value).then(() => { 37 | setTimeout(() => { 38 | handleClose(); 39 | emit("submit"); 40 | }, 200); 41 | }); 42 | } 43 | } 44 | }); 45 | } 46 | return { 47 | form, 48 | formRef, 49 | loading, 50 | handleClose, 51 | handleSubmit 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Form/XSelect.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 54 | -------------------------------------------------------------------------------- /src/utils/responsive.ts: -------------------------------------------------------------------------------- 1 | // 响应式storage 2 | import { App } from "vue"; 3 | import Storage from "responsive-storage"; 4 | import { routerArrays } from "@/layout/types"; 5 | import { responsiveStorageNameSpace } from "@/config"; 6 | 7 | export const injectResponsiveStorage = (app: App, config: ServerConfigs) => { 8 | const nameSpace = responsiveStorageNameSpace(); 9 | const configObj = Object.assign( 10 | { 11 | // layout模式以及主题 12 | layout: Storage.getData("layout", nameSpace) ?? { 13 | layout: config.Layout ?? "vertical", 14 | theme: config.Theme ?? "default", 15 | darkMode: config.DarkMode ?? false, 16 | sidebarStatus: config.SidebarStatus ?? true, 17 | epThemeColor: config.EpThemeColor ?? "#409EFF" 18 | }, 19 | configure: Storage.getData("configure", nameSpace) ?? { 20 | grey: config.Grey ?? false, 21 | weak: config.Weak ?? false, 22 | hideTabs: config.HideTabs ?? false, 23 | showLogo: config.ShowLogo ?? true, 24 | showModel: config.ShowModel ?? "smart", 25 | multiTagsCache: config.MultiTagsCache ?? false 26 | } 27 | }, 28 | config.MultiTagsCache 29 | ? { 30 | // 默认显示顶级菜单tag 31 | tags: Storage.getData("tags", nameSpace) ?? routerArrays 32 | } 33 | : {} 34 | ); 35 | 36 | app.use(Storage, { nameSpace, memory: configObj }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/hooks/useSystemConfig.ts: -------------------------------------------------------------------------------- 1 | import { computed, onMounted, ref, watch } from "vue"; 2 | import { http } from "@/utils/http"; 3 | 4 | export function useSystemConfig(name: string, init?: any) { 5 | const loading = ref(false); 6 | const origin = ref(JSON.stringify({})); 7 | const data = ref({}); 8 | const computedUpdated = computed(() => { 9 | return JSON.stringify(data.value) !== origin.value; 10 | }); 11 | 12 | async function update() { 13 | await http.request("put", `/system/configs/` + name, { data: {...data.value, name} }); 14 | await load(); 15 | } 16 | 17 | onMounted(async () => { 18 | await load(); 19 | }); 20 | 21 | async function load() { 22 | loading.value = true; 23 | const r = await http.request("get", `/system/configs/${name}`); 24 | loading.value = false; 25 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 26 | // @ts-ignore 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 28 | if (JSON.stringify(r.data) !== "{}") { 29 | data.value = r.data; 30 | } else { 31 | if (init) { 32 | data.value = init || {}; 33 | await update(); 34 | } 35 | } 36 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 37 | // @ts-ignore 38 | origin.value = JSON.stringify(r.data); 39 | } 40 | 41 | return { 42 | data, 43 | load, 44 | loading, 45 | origin, 46 | update, 47 | computedUpdated 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /.electron-builder.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {() => import('electron-builder').Configuration} 3 | * @see https://www.electron.build/configuration/configuration 4 | */ 5 | module.exports = async function () { 6 | return { 7 | appId: "com.pure.electron", 8 | productName: "electron-pure-admin", 9 | copyright: "Copyright © 2020-present, pure-admin", 10 | publish: { 11 | provider: "github", 12 | releaseType: "release" 13 | }, 14 | directories: { 15 | buildResources: "dist", 16 | output: "release/${version}" 17 | }, 18 | extraMetadata: { 19 | version: process.env.npm_package_version 20 | }, 21 | files: ["dist-electron/**", "dist/**"], 22 | nsis: { 23 | allowToChangeInstallationDirectory: true, 24 | createDesktopShortcut: true, 25 | createStartMenuShortcut: true, 26 | shortcutName: "pure-admin", 27 | perMachine: true, 28 | oneClick: false 29 | }, 30 | mac: { 31 | icon: "dist/icons/mac/icon.icns", 32 | artifactName: "${productName}_${version}.${ext}", 33 | target: ["dmg"] 34 | }, 35 | win: { 36 | icon: "dist/icons/win/icon.ico", 37 | artifactName: "${productName}_${version}.${ext}", 38 | target: [ 39 | { 40 | target: "nsis", 41 | arch: ["x64"] 42 | } 43 | ] 44 | }, 45 | linux: { 46 | icon: "dist/icons/png", 47 | artifactName: "${productName}_${version}.${ext}", 48 | target: ["deb", "AppImage"] 49 | } 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/hooks/useDialog.tsx: -------------------------------------------------------------------------------- 1 | import { h, ref } from "vue"; 2 | import { addDialog } from "@/components/ReDialog/index"; 3 | import { message } from "@/utils/message"; 4 | import { http } from "@/utils/http"; 5 | import editForm from "../views/basic/page/common-form.vue"; 6 | 7 | export function useAddDialog(symbol: string, fn: Function) { 8 | const formRef = ref(); 9 | function openDialog(title = "新增", row: any) { 10 | addDialog({ 11 | title: `${title}`, 12 | props: { 13 | formInline: row || {} 14 | }, 15 | width: "40%", 16 | draggable: true, 17 | fullscreenIcon: true, 18 | closeOnClickModal: false, 19 | contentRenderer: () => 20 | h(editForm, { 21 | ref: formRef, 22 | formInline: row, 23 | symbol: symbol 24 | }), 25 | beforeSure: (done, { options }) => { 26 | const FormRef = formRef.value.getRef(); 27 | const curData = { ...options.props.formInline }; 28 | 29 | function chores() { 30 | message(`操作成功`, { 31 | type: "success" 32 | }); 33 | done(); // 关闭弹框 34 | } 35 | 36 | FormRef.validate(async valid => { 37 | if (valid) { 38 | await http.post(`/${symbol}`, { data: { ...curData } }); 39 | await new Promise(resolve => setTimeout(resolve, 1000)); 40 | fn && fn(); 41 | chores(); 42 | } 43 | }); 44 | } 45 | }); 46 | } 47 | 48 | return { 49 | openDialog 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/style/login.css: -------------------------------------------------------------------------------- 1 | .wave { 2 | position: fixed; 3 | left: 0; 4 | bottom: 0; 5 | z-index: -1; 6 | } 7 | 8 | .login-container { 9 | width: 100vw; 10 | height: 100vh; 11 | display: grid; 12 | grid-template-columns: repeat(2, 1fr); 13 | grid-gap: 18rem; 14 | padding: 0 2rem; 15 | } 16 | 17 | .img { 18 | display: flex; 19 | justify-content: flex-end; 20 | align-items: center; 21 | } 22 | 23 | .img img { 24 | width: 500px; 25 | } 26 | 27 | .login-box { 28 | display: flex; 29 | align-items: center; 30 | text-align: center; 31 | } 32 | 33 | .login-form { 34 | width: 360px; 35 | } 36 | 37 | .avatar { 38 | width: 350px; 39 | height: 80px; 40 | background: white; 41 | } 42 | 43 | .login-form h2 { 44 | text-transform: uppercase; 45 | margin: 15px 0; 46 | color: #999; 47 | font: bold 200% Consolas, Monaco, monospace; 48 | } 49 | 50 | @media screen and (max-width: 1180px) { 51 | .login-container { 52 | grid-gap: 9rem; 53 | } 54 | 55 | .login-form { 56 | width: 290px; 57 | } 58 | 59 | .login-form h2 { 60 | font-size: 2.4rem; 61 | margin: 8px 0; 62 | } 63 | 64 | .img img { 65 | width: 360px; 66 | } 67 | 68 | .avatar { 69 | width: 280px; 70 | height: 80px; 71 | } 72 | } 73 | 74 | @media screen and (max-width: 968px) { 75 | .wave { 76 | display: none; 77 | } 78 | 79 | .img { 80 | display: none; 81 | } 82 | 83 | .login-container { 84 | grid-template-columns: 1fr; 85 | } 86 | 87 | .login-box { 88 | justify-content: center; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 71 | -------------------------------------------------------------------------------- /src/views/error/500.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 71 | -------------------------------------------------------------------------------- /src/views/error/403.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 71 | -------------------------------------------------------------------------------- /src/utils/sso.ts: -------------------------------------------------------------------------------- 1 | import { removeToken, setToken, type DataInfo } from "./auth"; 2 | import { subBefore, getQueryMap } from "@pureadmin/utils"; 3 | 4 | /** 5 | * 简版前端单点登录,根据实际业务自行编写 6 | * 划重点: 7 | * 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理 8 | * 1.清空本地旧信息; 9 | * 2.获取url中的重要参数信息,然后通过 setToken 保存在本地; 10 | * 3.删除不需要显示在 url 的参数 11 | * 4.使用 window.location.replace 跳转正确页面 12 | */ 13 | (function () { 14 | // 获取 url 中的参数 15 | const params = getQueryMap(location.href) as DataInfo; 16 | const must = ["username", "roles", "accessToken"]; 17 | const mustLength = must.length; 18 | if (Object.keys(params).length !== mustLength) return; 19 | 20 | // url 参数满足 must 里的全部值,才判定为单点登录,避免非单点登录时刷新页面无限循环 21 | let sso = []; 22 | let start = 0; 23 | 24 | while (start < mustLength) { 25 | if (Object.keys(params).includes(must[start]) && sso.length <= mustLength) { 26 | sso.push(must[start]); 27 | } else { 28 | sso = []; 29 | } 30 | start++; 31 | } 32 | 33 | if (sso.length === mustLength) { 34 | // 判定为单点登录 35 | 36 | // 清空本地旧信息 37 | removeToken(); 38 | 39 | // 保存新信息到本地 40 | setToken(params); 41 | 42 | // 删除不需要显示在 url 的参数 43 | delete params["roles"]; 44 | delete params["accessToken"]; 45 | 46 | const newUrl = `${location.origin}${location.pathname}${subBefore( 47 | location.hash, 48 | "?" 49 | )}?${JSON.stringify(params) 50 | .replace(/["{}]/g, "") 51 | .replace(/:/g, "=") 52 | .replace(/,/g, "&")}`; 53 | 54 | // 替换历史记录项 55 | window.location.replace(newUrl); 56 | } else { 57 | return; 58 | } 59 | })(); 60 | -------------------------------------------------------------------------------- /src/store/modules/epTheme.ts: -------------------------------------------------------------------------------- 1 | import { store } from "@/store"; 2 | import { defineStore } from "pinia"; 3 | import { storageLocal } from "@pureadmin/utils"; 4 | import { getConfig, responsiveStorageNameSpace } from "@/config"; 5 | 6 | export const useEpThemeStore = defineStore({ 7 | id: "pure-epTheme", 8 | state: () => ({ 9 | epThemeColor: 10 | storageLocal().getItem( 11 | `${responsiveStorageNameSpace()}layout` 12 | )?.epThemeColor ?? getConfig().EpThemeColor, 13 | epTheme: 14 | storageLocal().getItem( 15 | `${responsiveStorageNameSpace()}layout` 16 | )?.theme ?? getConfig().Theme 17 | }), 18 | getters: { 19 | getEpThemeColor(state) { 20 | return state.epThemeColor; 21 | }, 22 | /** 用于mix导航模式下hamburger-svg的fill属性 */ 23 | fill(state) { 24 | if (state.epTheme === "light") { 25 | return "#3650d3"; 26 | } else if (state.epTheme === "yellow") { 27 | return "#d25f00"; 28 | } else { 29 | return "#fff"; 30 | } 31 | } 32 | }, 33 | actions: { 34 | setEpThemeColor(newColor: string): void { 35 | const layout = storageLocal().getItem( 36 | `${responsiveStorageNameSpace()}layout` 37 | ); 38 | this.epTheme = layout?.theme; 39 | this.epThemeColor = newColor; 40 | if (!layout) return; 41 | layout.epThemeColor = newColor; 42 | storageLocal().setItem(`${responsiveStorageNameSpace()}layout`, layout); 43 | } 44 | } 45 | }); 46 | 47 | export function useEpThemeStoreHook() { 48 | return useEpThemeStore(store); 49 | } 50 | -------------------------------------------------------------------------------- /src/hooks/useOSS.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from "vue"; 2 | import OSS from "ali-oss"; 3 | import { http } from "@/utils/http"; 4 | 5 | export function useOSS() { 6 | const STSToken = ref({}); 7 | const fileRef = ref(); 8 | const file = ref(null); 9 | const loading = ref(false); 10 | const url = ref(""); 11 | const completed = ref(false); 12 | 13 | async function getSTSToken() { 14 | const r = await http.get("/oss/token"); 15 | return r && r.data; 16 | } 17 | 18 | async function uploadFile({ file }) { 19 | if (!STSToken.value) { 20 | STSToken.value = await getSTSToken(); 21 | } 22 | if (!file) { 23 | alert("no file"); 24 | return; 25 | } 26 | loading.value = true; 27 | 28 | const { 29 | AccessKeyId: accessKeyId, 30 | AccessKeySecret: accessKeySecret, 31 | SecurityToken: securityToken, 32 | bucket = "bsc-test2022", 33 | region = "oss-cn-hangzhou" 34 | } = STSToken.value; 35 | const client = new OSS({ 36 | accessKeyId, 37 | accessKeySecret, 38 | stsToken: securityToken, 39 | bucket, 40 | region 41 | }); 42 | 43 | // @ts-ignore 44 | const name = "message/" + file.name; 45 | const result = await client.put(name, file); 46 | url.value = result.url; 47 | loading.value = false; 48 | completed.value = true; 49 | } 50 | 51 | onMounted(async () => { 52 | STSToken.value = await getSTSToken(); 53 | }); 54 | 55 | return { 56 | uploadFile, 57 | loading, 58 | start: uploadFile, 59 | url, 60 | file, 61 | fileRef, 62 | completed 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/layout/frameView.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 |