├── .nvmrc ├── .browserslistrc ├── .stylelintignore ├── src ├── components │ ├── ReCountTo │ │ ├── README.md │ │ ├── index.ts │ │ └── src │ │ │ ├── rebound │ │ │ ├── props.ts │ │ │ └── rebound.css │ │ │ └── normal │ │ │ └── props.ts │ ├── ReAuth │ │ ├── index.ts │ │ └── src │ │ │ └── auth.tsx │ ├── ReSplitPane │ │ ├── iconfont │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.woff2 │ │ │ ├── iconfont.json │ │ │ └── iconfont.css │ │ ├── resizer.tsx │ │ ├── index.css │ │ └── resizer.css │ ├── RePlusPage │ │ ├── src │ │ │ ├── assets │ │ │ │ └── defaultFile.png │ │ │ ├── components │ │ │ │ ├── ButtonOperation │ │ │ │ │ └── index.ts │ │ │ │ └── JsonInput.vue │ │ │ └── utils │ │ │ │ ├── constants.ts │ │ │ │ ├── renders.tsx │ │ │ │ └── public.tsx │ │ └── index.ts │ ├── ReCropper │ │ ├── src │ │ │ ├── circled.css │ │ │ └── svg │ │ │ │ ├── arrow-v.svg │ │ │ │ ├── arrow-h.svg │ │ │ │ ├── arrow-up.svg │ │ │ │ ├── arrow-down.svg │ │ │ │ ├── arrow-left.svg │ │ │ │ ├── arrow-right.svg │ │ │ │ ├── download.svg │ │ │ │ ├── upload.svg │ │ │ │ ├── search-minus.svg │ │ │ │ ├── rotate-left.svg │ │ │ │ ├── search-plus.svg │ │ │ │ ├── rotate-right.svg │ │ │ │ ├── reload.svg │ │ │ │ ├── index.ts │ │ │ │ └── change.svg │ │ └── index.ts │ ├── ReQrcode │ │ ├── src │ │ │ └── index.scss │ │ └── index.ts │ ├── RePlusSearch │ │ ├── index.ts │ │ └── src │ │ │ └── types.ts │ ├── ReTypeit │ │ ├── index.ts │ │ └── src │ │ │ └── index.tsx │ ├── ReText │ │ ├── index.ts │ │ └── src │ │ │ └── index.vue │ ├── ReImageVerify │ │ ├── index.ts │ │ └── src │ │ │ ├── index.vue │ │ │ └── hooks.ts │ ├── RePictureUpload │ │ └── index.ts │ ├── ReSeamlessScroll │ │ └── index.ts │ ├── ReSendVerifyCode │ │ ├── index.ts │ │ └── src │ │ │ └── verifyCode.ts │ ├── ReSegmented │ │ ├── index.ts │ │ └── src │ │ │ └── type.ts │ ├── RePureTableBar │ │ ├── src │ │ │ └── types.ts │ │ └── index.ts │ ├── ReAnimateSelector │ │ └── index.ts │ ├── ReIcon │ │ ├── index.ts │ │ └── src │ │ │ ├── types.ts │ │ │ ├── iconifyIconOnline.ts │ │ │ ├── iconfont.ts │ │ │ └── iconifyIconOffline.ts │ ├── ReCol │ │ └── index.ts │ ├── FromQuestion │ │ └── index.vue │ ├── ReFlicker │ │ ├── index.css │ │ └── index.ts │ └── ReDrawer │ │ └── index.ts ├── assets │ ├── avatar.png │ ├── login │ │ ├── bg.png │ │ └── avatar.svg │ ├── iconfont │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.woff2 │ │ ├── iconfont.css │ │ └── iconfont.json │ ├── table-bar │ │ ├── expand.svg │ │ ├── refresh.svg │ │ ├── drag.svg │ │ ├── collapse.svg │ │ └── settings.svg │ └── svg │ │ ├── dark.svg │ │ ├── full_screen.svg │ │ ├── exit_screen.svg │ │ ├── enter_outlined.svg │ │ ├── keyboard_esc.svg │ │ ├── day.svg │ │ ├── back_top.svg │ │ ├── system.svg │ │ └── globalization.svg ├── api │ ├── system │ │ ├── role.ts │ │ ├── online.ts │ │ ├── permission.ts │ │ ├── logs │ │ │ ├── operation.ts │ │ │ └── login.ts │ │ ├── search.ts │ │ ├── dept.ts │ │ ├── config │ │ │ ├── user.ts │ │ │ └── system.ts │ │ ├── field.ts │ │ ├── menu.ts │ │ ├── file.ts │ │ ├── notice.ts │ │ ├── notifications.ts │ │ ├── user.ts │ │ └── dashboard.ts │ ├── user │ │ ├── logs.ts │ │ ├── notifications.ts │ │ ├── notice.ts │ │ └── userinfo.ts │ ├── routes.ts │ ├── common.ts │ ├── config.ts │ └── types.ts ├── layout │ ├── components │ │ ├── lay-search │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ └── SearchHistoryItem.vue │ │ │ ├── types.ts │ │ │ └── index.vue │ │ ├── lay-notice │ │ │ ├── data.ts │ │ │ └── components │ │ │ │ └── noticeList.vue │ │ ├── lay-sidebar │ │ │ └── components │ │ │ │ ├── SidebarExtraIcon.vue │ │ │ │ ├── SidebarLinkItem.vue │ │ │ │ ├── SidebarFullScreen.vue │ │ │ │ ├── SidebarTopCollapse.vue │ │ │ │ └── SidebarLeftCollapse.vue │ │ ├── lay-footer │ │ │ └── index.vue │ │ └── lay-tag │ │ │ └── components │ │ │ └── TagChrome.vue │ ├── hooks │ │ ├── useMultiFrame.ts │ │ ├── useBoolean.ts │ │ └── useTranslationLang.ts │ └── redirect.vue ├── views │ ├── user │ │ ├── info │ │ │ ├── utils │ │ │ │ ├── reset.css │ │ │ │ ├── types.ts │ │ │ │ └── rule.ts │ │ │ └── components │ │ │ │ └── avatar.vue │ │ └── notice │ │ │ └── index.vue │ ├── system │ │ ├── user │ │ │ ├── utils │ │ │ │ └── reset.css │ │ │ ├── svg │ │ │ │ ├── expand.svg │ │ │ │ └── unexpand.svg │ │ │ └── index.vue │ │ ├── menu │ │ │ └── svg │ │ │ │ ├── expand.svg │ │ │ │ └── unexpand.svg │ │ ├── setting │ │ │ ├── index.vue │ │ │ └── utils │ │ │ │ └── hook.tsx │ │ ├── permission │ │ │ ├── components │ │ │ │ └── utils │ │ │ │ │ ├── types.ts │ │ │ │ │ └── rule.ts │ │ │ └── index.vue │ │ ├── online │ │ │ └── index.vue │ │ ├── config │ │ │ ├── system │ │ │ │ ├── index.vue │ │ │ │ └── utils │ │ │ │ │ └── hook.tsx │ │ │ └── user │ │ │ │ └── index.vue │ │ ├── logs │ │ │ ├── operation │ │ │ │ └── index.vue │ │ │ └── login │ │ │ │ └── index.vue │ │ ├── role │ │ │ └── index.vue │ │ ├── notice │ │ │ ├── read │ │ │ │ └── list.vue │ │ │ └── index.vue │ │ ├── field │ │ │ ├── index.vue │ │ │ └── utils │ │ │ │ └── hook.tsx │ │ ├── dept │ │ │ └── index.vue │ │ ├── file │ │ │ └── index.vue │ │ ├── constants.ts │ │ └── components │ │ │ ├── SearchRole.vue │ │ │ ├── SearchDept.vue │ │ │ ├── SearchMenu.vue │ │ │ ├── SearchUser.vue │ │ │ └── SearchDialog.vue │ ├── login │ │ ├── utils │ │ │ ├── static.ts │ │ │ ├── enums.ts │ │ │ ├── motion.ts │ │ │ ├── verifyCode.ts │ │ │ └── rule.ts │ │ └── components │ │ │ └── qrCode.vue │ ├── welcome │ │ └── components │ │ │ ├── index.ts │ │ │ ├── ChartLine.vue │ │ │ └── ChartRound.vue │ ├── settings │ │ ├── components │ │ │ └── settings │ │ │ │ ├── types.ts │ │ │ │ └── index.vue │ │ ├── basic │ │ │ └── index.vue │ │ ├── message │ │ │ └── index.vue │ │ └── sms.vue │ ├── demo │ │ └── book │ │ │ ├── utils │ │ │ └── api.ts │ │ │ └── index.vue │ ├── account │ │ ├── utils │ │ │ └── rule.ts │ │ └── components │ │ │ ├── SecurityLog.vue │ │ │ └── Notifications.vue │ ├── empty │ │ └── index.vue │ └── error │ │ ├── 404.vue │ │ ├── 403.vue │ │ └── 500.vue ├── directives │ ├── index.ts │ ├── auth │ │ └── index.ts │ ├── copy │ │ └── index.ts │ └── ripple │ │ └── index.scss ├── store │ ├── index.ts │ ├── utils.ts │ ├── modules │ │ ├── settings.ts │ │ └── epTheme.ts │ └── types.ts ├── utils │ ├── globalPolyfills.ts │ ├── aes.ts │ ├── progress │ │ └── index.ts │ ├── token.ts │ ├── mitt.ts │ ├── preventDefault.ts │ ├── propTypes.ts │ ├── http │ │ └── types.d.ts │ ├── sso.ts │ └── responsive.ts ├── router │ └── modules │ │ ├── home.ts │ │ ├── error.ts │ │ └── remaining.ts ├── plugins │ └── plusProComponents.ts ├── style │ ├── index.scss │ ├── transition.scss │ ├── tailwind.css │ └── login.css └── constants │ └── constants.ts ├── public ├── favicon.ico ├── logo.svg └── platform-config.json ├── .npmrc ├── .env ├── .husky ├── commit-msg ├── common.sh └── pre-commit ├── postcss.config.js ├── .prettierrc.js ├── Dockerfile-base ├── .markdownlint.json ├── .env.development ├── .editorconfig ├── mock ├── version.ts ├── refreshToken.ts ├── login.ts └── asyncRoutes.ts ├── .dockerignore ├── .gitignore ├── types ├── shims-vue.d.ts ├── shims-tsx.d.ts └── directives.d.ts ├── .vscode ├── vue3.2.code-snippets ├── vue3.3.code-snippets ├── vue3.0.code-snippets └── extensions.json ├── .github ├── workflows │ ├── issue-open.yml │ ├── issue-close.yml │ ├── issue-close-require.yml │ ├── lint-code.yml │ └── build-image.yml └── release-config.yml ├── Dockerfile ├── nginx.conf ├── .env.production ├── web ├── acme.sh │ ├── deploy │ │ ├── nginx.sh │ │ └── apache.sh │ └── notify │ │ └── feishu.sh └── conf │ └── xadmin-api-conf ├── .lintstagedrc ├── .env.staging ├── commitlint.config.js ├── docker-compose.yml ├── Dockerfile-web ├── LICENSE ├── tsconfig.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v23.11.0 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | public/* 4 | src/style/reset.scss -------------------------------------------------------------------------------- /src/components/ReCountTo/README.md: -------------------------------------------------------------------------------- 1 | normal 普通数字动画组件 2 | rebound 回弹式数字动画组件 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/assets/avatar.png -------------------------------------------------------------------------------- /src/assets/login/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/assets/login/bg.png -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/assets/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/components/ReAuth/index.ts: -------------------------------------------------------------------------------- 1 | import auth from "./src/auth"; 2 | 3 | const Auth = auth; 4 | 5 | export { Auth }; 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator=true 2 | shamefully-hoist=true 3 | enable-pre-post-scripts=false 4 | strict-peer-dependencies=false 5 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/assets/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/assets/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /src/api/system/role.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | 3 | export const roleApi = new BaseApi("/api/system/role"); 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 平台本地运行端口号 2 | VITE_PORT = 8848 3 | 4 | # 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置) 5 | VITE_HIDE_HOME = false 6 | -------------------------------------------------------------------------------- /src/layout/components/lay-search/components/index.ts: -------------------------------------------------------------------------------- 1 | import SearchModal from "./SearchModal.vue"; 2 | 3 | export { SearchModal }; 4 | -------------------------------------------------------------------------------- /src/api/system/online.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | 3 | export const userOnlineApi = new BaseApi("/api/system/online"); 4 | -------------------------------------------------------------------------------- /src/api/user/logs.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | 3 | export const userLoginLogApi = new BaseApi("/api/system/user/log"); 4 | -------------------------------------------------------------------------------- /src/api/system/permission.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | 3 | export const dataPermissionApi = new BaseApi("/api/system/permission"); 4 | -------------------------------------------------------------------------------- /src/api/system/logs/operation.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | 3 | export const operationLogApi = new BaseApi("/api/system/logs/operation"); 4 | -------------------------------------------------------------------------------- /src/components/ReSplitPane/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/components/ReSplitPane/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/components/ReSplitPane/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/components/ReSplitPane/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/components/ReSplitPane/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/components/ReSplitPane/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /src/views/user/info/utils/reset.css: -------------------------------------------------------------------------------- 1 | /** 局部重置 ElProgress 的部分样式 */ 2 | .el-progress-bar__outer, 3 | .el-progress-bar__inner { 4 | border-radius: 0; 5 | } 6 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck source=./_/husky.sh 4 | 5 | PATH="/usr/local/bin:$PATH" 6 | 7 | npx --no-install commitlint --edit "$1" 8 | -------------------------------------------------------------------------------- /src/components/RePlusPage/src/assets/defaultFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-client/HEAD/src/components/RePlusPage/src/assets/defaultFile.png -------------------------------------------------------------------------------- /src/views/system/user/utils/reset.css: -------------------------------------------------------------------------------- 1 | /** 局部重置 ElProgress 的部分样式 */ 2 | .el-progress-bar__outer, 3 | .el-progress-bar__inner { 4 | border-radius: 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./copy"; 3 | export * from "./longpress"; 4 | export * from "./optimize"; 5 | export * from "./ripple"; 6 | -------------------------------------------------------------------------------- /src/assets/table-bar/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/circled.css: -------------------------------------------------------------------------------- 1 | @import "cropperjs/dist/cropper.css"; 2 | 3 | .re-circled { 4 | .cropper-view-box, 5 | .cropper-face { 6 | border-radius: 50%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/views/system/menu/svg/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/system/user/svg/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/system/menu/svg/unexpand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/system/user/svg/unexpand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReQrcode/src/index.scss: -------------------------------------------------------------------------------- 1 | .qrcode { 2 | &--disabled { 3 | background: rgb(255 255 255 / 95%); 4 | 5 | & > div { 6 | transform: translate(-50%, -50%); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('postcss-load-config').Config} */ 4 | export default { 5 | plugins: { 6 | ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}) 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import("prettier").Config} */ 4 | export default { 5 | bracketSpacing: true, 6 | singleQuote: false, 7 | arrowParens: "avoid", 8 | trailingComma: "none" 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/ReCropper/index.ts: -------------------------------------------------------------------------------- 1 | import reCropper from "./src"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 图片裁剪组件 */ 5 | export const ReCropper = withInstall(reCropper); 6 | 7 | export default ReCropper; 8 | -------------------------------------------------------------------------------- /src/components/ReQrcode/index.ts: -------------------------------------------------------------------------------- 1 | import reQrcode from "./src/index"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 二维码组件 */ 5 | export const ReQrcode = withInstall(reQrcode); 6 | 7 | export default ReQrcode; 8 | -------------------------------------------------------------------------------- /src/api/user/notifications.ts: -------------------------------------------------------------------------------- 1 | import { SystemMsgSubscriptionApi } from "@/api/system/notifications"; 2 | 3 | export const userMsgSubscriptionApi = new SystemMsgSubscriptionApi( 4 | "/api/notifications/user-msg-subscription" 5 | ); 6 | -------------------------------------------------------------------------------- /src/components/RePlusSearch/index.ts: -------------------------------------------------------------------------------- 1 | import rePlusSearch from "./src/index.vue"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | export const RePlusSearch = withInstall(rePlusSearch); 5 | 6 | export default RePlusSearch; 7 | -------------------------------------------------------------------------------- /src/components/ReTypeit/index.ts: -------------------------------------------------------------------------------- 1 | import typeIt from "./src/index"; 2 | import type { Options as TypeItOptions } from "typeit"; 3 | 4 | const TypeIt = typeIt; 5 | 6 | export { TypeIt, TypeItOptions }; 7 | 8 | export default TypeIt; 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 | -------------------------------------------------------------------------------- /Dockerfile-base: -------------------------------------------------------------------------------- 1 | FROM node:23.11.0-slim 2 | 3 | WORKDIR /app 4 | RUN corepack enable 5 | RUN corepack prepare pnpm@10.10.0 --activate 6 | 7 | COPY .npmrc package.json pnpm-lock.yaml ./ 8 | 9 | RUN pnpm install --frozen-lockfile 10 | -------------------------------------------------------------------------------- /src/components/ReText/index.ts: -------------------------------------------------------------------------------- 1 | import reText from "./src/index.vue"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 支持`Tooltip`提示的文本省略组件 */ 5 | export const ReText = withInstall(reText); 6 | 7 | export default ReText; 8 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import { createPinia } from "pinia"; 3 | 4 | const store = createPinia(); 5 | 6 | export function setupStore(app: App) { 7 | app.use(store); 8 | } 9 | 10 | export { store }; 11 | -------------------------------------------------------------------------------- /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/components/ReImageVerify/index.ts: -------------------------------------------------------------------------------- 1 | import reImageVerify from "./src/index.vue"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 图形验证码组件 */ 5 | export const ReImageVerify = withInstall(reImageVerify); 6 | 7 | export default ReImageVerify; 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/common.sh" 3 | 4 | [ -n "$CI" ] && exit 0 5 | 6 | PATH="/usr/local/bin:$PATH" 7 | 8 | # Perform lint check on files in the staging area through .lintstagedrc configuration 9 | pnpm exec lint-staged 10 | -------------------------------------------------------------------------------- /src/assets/table-bar/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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": { 10 | "indent": 4 11 | }, 12 | "no-hard-tabs": false 13 | } 14 | -------------------------------------------------------------------------------- /src/components/RePictureUpload/index.ts: -------------------------------------------------------------------------------- 1 | import rePictureUpload from "./src/index.vue"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 图片裁剪预览组件 */ 5 | export const RePictureUpload = withInstall(rePictureUpload); 6 | 7 | export default RePictureUpload; 8 | -------------------------------------------------------------------------------- /src/components/ReSeamlessScroll/index.ts: -------------------------------------------------------------------------------- 1 | import reSeamlessScroll from "./src/index.vue"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 无缝滚动组件 */ 5 | export const ReSeamlessScroll = withInstall(reSeamlessScroll); 6 | 7 | export default ReSeamlessScroll; 8 | -------------------------------------------------------------------------------- /src/components/ReSendVerifyCode/index.ts: -------------------------------------------------------------------------------- 1 | import reSendVerifyCode from "./src/index.vue"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 图形验证码组件 */ 5 | export const ReSendVerifyCode = withInstall(reSendVerifyCode); 6 | 7 | export default ReSendVerifyCode; 8 | -------------------------------------------------------------------------------- /src/views/welcome/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ChartBar } from "./ChartBar.vue"; 2 | export { default as ChartLine } from "./ChartLine.vue"; 3 | export { default as ChartRound } from "./ChartRound.vue"; 4 | export { default as ChartClock } from "./ChartClock.vue"; 5 | -------------------------------------------------------------------------------- /src/assets/svg/dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/routes.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | type Result = { 4 | success: boolean; 5 | data: Array; 6 | auths: Array; 7 | }; 8 | 9 | export const getAsyncRoutes = () => { 10 | return http.request("get", "/api/system/routes"); 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/ReSegmented/index.ts: -------------------------------------------------------------------------------- 1 | import reSegmented from "./src/index"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 分段控制器组件 */ 5 | export const ReSegmented = withInstall(reSegmented); 6 | 7 | export default ReSegmented; 8 | export type { OptionsType } from "./src/type"; 9 | -------------------------------------------------------------------------------- /.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 | 10 | VITE_API_DOMAIN="" 11 | VITE_WSS_DOMAIN="" 12 | 13 | -------------------------------------------------------------------------------- /src/assets/svg/full_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/types.ts: -------------------------------------------------------------------------------- 1 | interface PureTableBarProps { 2 | title?: string; 3 | tableRef?: PropType; 4 | columns: Array>; 5 | isExpandAll?: boolean; 6 | tableKey?: PropType; 7 | } 8 | 9 | export type { PureTableBarProps }; 10 | -------------------------------------------------------------------------------- /src/components/ReAnimateSelector/index.ts: -------------------------------------------------------------------------------- 1 | import reAnimateSelector from "./src/index.vue"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** [animate.css](https://animate.style/) 选择器组件 */ 5 | export const ReAnimateSelector = withInstall(reAnimateSelector); 6 | 7 | export default ReAnimateSelector; 8 | -------------------------------------------------------------------------------- /src/components/RePlusPage/src/components/ButtonOperation/index.ts: -------------------------------------------------------------------------------- 1 | import buttonOperation from "./src/index.vue"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | export const ButtonOperation = withInstall(buttonOperation); 5 | 6 | export * from "./src/types"; 7 | 8 | export default ButtonOperation; 9 | -------------------------------------------------------------------------------- /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 | export * from "./src/types"; 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mock/version.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 2 | 3 | // 模拟刷新token接口 4 | export default defineFakeRoute([ 5 | { 6 | url: "/version.json", 7 | method: "get", 8 | response: () => { 9 | return { version: "4.1.0", external: "" }; 10 | } 11 | } 12 | ]); 13 | -------------------------------------------------------------------------------- /src/assets/svg/exit_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache 7 | report.html 8 | 9 | yarn.lock 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 | -------------------------------------------------------------------------------- /src/views/system/setting/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/api/system/search.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | 3 | export const searchDeptApi = new BaseApi("/api/system/search/dept"); 4 | export const searchUserApi = new BaseApi("/api/system/search/user"); 5 | export const searchRoleApi = new BaseApi("/api/system/search/role"); 6 | export const searchMenuApi = new BaseApi("/api/system/search/menu"); 7 | -------------------------------------------------------------------------------- /src/assets/svg/enter_outlined.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/arrow-v.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/arrow-h.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache 7 | report.html 8 | vite.config.*.timestamp* 9 | 10 | yarn.lock 11 | npm-debug.log* 12 | .pnpm-error.log* 13 | .pnpm-debug.log 14 | tests/**/coverage/ 15 | 16 | # Editor directories and files 17 | .idea 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | tsconfig.tsbuildinfo 23 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/keyboard_esc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/aes.ts: -------------------------------------------------------------------------------- 1 | import AES from "crypto-js/aes"; 2 | import Utf8 from "crypto-js/enc-utf8"; 3 | 4 | export function AesEncrypted(key: string, msg: string): string { 5 | return AES.encrypt(msg, key).toString(); 6 | } 7 | 8 | export function AesDecrypted(key: string, encryptedMessage: string): string { 9 | return AES.decrypt(encryptedMessage, key).toString(Utf8); 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import type { DefineComponent } from "vue"; 3 | // eslint-disable-next-line 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | 8 | declare module "*.scss" { 9 | const scss: Record; 10 | export default scss; 11 | } 12 | 13 | declare module "vue-virtual-scroller"; 14 | -------------------------------------------------------------------------------- /src/assets/table-bar/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReCountTo/index.ts: -------------------------------------------------------------------------------- 1 | import reNormalCountTo from "./src/normal"; 2 | import reboundCountTo from "./src/rebound"; 3 | import { withInstall } from "@pureadmin/utils"; 4 | 5 | /** 普通数字动画组件 */ 6 | const ReNormalCountTo = withInstall(reNormalCountTo); 7 | 8 | /** 回弹式数字动画组件 */ 9 | const ReboundCountTo = withInstall(reboundCountTo); 10 | 11 | export { ReNormalCountTo, ReboundCountTo }; 12 | -------------------------------------------------------------------------------- /.vscode/vue3.2.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.2+快速生成模板": { 3 | "scope": "vue", 4 | "prefix": "Vue3.2+", 5 | "body": [ 6 | "\n", 8 | "", 9 | "\ttest", 10 | "\n", 11 | "", 13 | "$2" 14 | ], 15 | "description": "Vue3.2+" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/RePlusPage/index.ts: -------------------------------------------------------------------------------- 1 | import rePlusPage from "./src/index.vue"; 2 | 3 | export const RePlusPage = rePlusPage; 4 | 5 | export * from "./src/utils/index"; 6 | export * from "./src/utils/columns"; 7 | export * from "./src/utils/renders"; 8 | export * from "./src/utils/handle"; 9 | export * from "./src/components/ButtonOperation"; 10 | export * from "./src/utils/types"; 11 | export * from "./src/utils/public"; 12 | -------------------------------------------------------------------------------- /.github/workflows/issue-open.yml: -------------------------------------------------------------------------------- 1 | name: Issue Open Check 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | issue-open-add-labels: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Add labels 12 | uses: actions-cool/issues-helper@v3 13 | if: ${{ !github.event.issue.pull_request }} 14 | with: 15 | actions: 'add-labels' 16 | labels: '🔔 Pending processing' -------------------------------------------------------------------------------- /src/layout/components/lay-notice/data.ts: -------------------------------------------------------------------------------- 1 | export interface ListItem { 2 | pk: number; 3 | avatar?: string; 4 | title: string; 5 | created_time?: string; 6 | notice_type: number; 7 | message: string; 8 | level?: { value: "success" | "warning" | "info" | "danger" | "primary" | "" }; 9 | extra?: string; 10 | } 11 | 12 | export interface TabItem { 13 | key: string; 14 | name: string; 15 | list: ListItem[]; 16 | } 17 | -------------------------------------------------------------------------------- /src/views/system/permission/components/utils/types.ts: -------------------------------------------------------------------------------- 1 | interface FormItemProps { 2 | name?: string[]; 3 | match?: string; 4 | exclude?: boolean; 5 | type?: string; 6 | value?: string; 7 | } 8 | 9 | interface FormProps { 10 | formInline?: FormItemProps; 11 | fieldLookupsData?: any[]; 12 | ruleList?: any[]; 13 | dataList?: any[]; 14 | valuesData?: any[]; 15 | } 16 | 17 | export type { FormItemProps, FormProps }; 18 | -------------------------------------------------------------------------------- /src/api/system/dept.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult } from "@/api/types"; 3 | 4 | class DeptApi extends BaseApi { 5 | empower = (pk: number | string, data?: object) => { 6 | return this.request( 7 | "post", 8 | {}, 9 | data, 10 | `${this.baseApi}/${pk}/empower` 11 | ); 12 | }; 13 | } 14 | 15 | export const deptApi = new DeptApi("/api/system/dept"); 16 | -------------------------------------------------------------------------------- /src/layout/components/lay-search/types.ts: -------------------------------------------------------------------------------- 1 | interface optionsItem { 2 | path: string; 3 | type: "history" | "collect"; 4 | meta: { 5 | icon?: string; 6 | title?: string; 7 | }; 8 | } 9 | 10 | interface dragItem { 11 | oldIndex: number; 12 | newIndex: number; 13 | } 14 | 15 | interface Props { 16 | value: string; 17 | options: Array; 18 | } 19 | 20 | export type { optionsItem, dragItem, Props }; 21 | -------------------------------------------------------------------------------- /src/views/settings/components/settings/types.ts: -------------------------------------------------------------------------------- 1 | import type { ViewBaseApi } from "@/api/base"; 2 | 3 | export interface settingItemProps { 4 | api: ViewBaseApi; 5 | title?: string; 6 | label?: string; 7 | localeName?: string; 8 | autoSubmit?: boolean; 9 | formProps?: object; 10 | queryParams?: object; 11 | auth?: { 12 | partialUpdate?: boolean; 13 | retrieve: boolean; 14 | test?: boolean; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/svg/day.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/demo/book/utils/api.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult } from "@/api/types"; 3 | 4 | class BookApi extends BaseApi { 5 | push = (pk: number | string) => { 6 | return this.request( 7 | "post", 8 | {}, 9 | {}, 10 | `${this.baseApi}/${pk}/push` 11 | ); 12 | }; 13 | } 14 | 15 | const bookApi = new BookApi("/api/demo/book"); 16 | export { bookApi }; 17 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/system/config/user.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult } from "@/api/types"; 3 | 4 | class UserConfigApi extends BaseApi { 5 | invalid = (pk: number | string) => { 6 | return this.request( 7 | "post", 8 | {}, 9 | {}, 10 | `${this.baseApi}/${pk}/invalid` 11 | ); 12 | }; 13 | } 14 | 15 | export const userConfigApi = new UserConfigApi("/api/system/config/user"); 16 | -------------------------------------------------------------------------------- /src/api/system/logs/login.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult } from "@/api/types"; 3 | 4 | class LoginLogApi extends BaseApi { 5 | logout = (pk: number | string, data?: object) => { 6 | return this.request( 7 | "post", 8 | {}, 9 | data, 10 | `${this.baseApi}/${pk}/logout` 11 | ); 12 | }; 13 | } 14 | export const loginLogApi = new LoginLogApi("/api/system/logs/login"); 15 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/table-bar/collapse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/issue-close.yml: -------------------------------------------------------------------------------- 1 | name: Issue Close Check 2 | 3 | on: 4 | issues: 5 | types: [closed] 6 | 7 | jobs: 8 | issue-close-remove-labels: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Remove labels 12 | uses: actions-cool/issues-helper@v3 13 | if: ${{ !github.event.issue.pull_request }} 14 | with: 15 | actions: 'remove-labels' 16 | labels: '🔔 Pending processing,⏳ Pending feedback' -------------------------------------------------------------------------------- /src/api/system/config/system.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult } from "@/api/types"; 3 | 4 | class SystemConfigApi extends BaseApi { 5 | invalid = (pk: number | string) => { 6 | return this.request( 7 | "post", 8 | {}, 9 | {}, 10 | `${this.baseApi}/${pk}/invalid` 11 | ); 12 | }; 13 | } 14 | 15 | export const systemConfigApi = new SystemConfigApi("/api/system/config/system"); 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nineaiyu/xadmin-client-base:20251224_141231 AS stage-build 2 | 3 | ARG VERSION 4 | 5 | COPY . . 6 | 7 | RUN sed -i "s@\"Version\": .*@\"Version\": \"${VERSION}\",@" public/platform-config.json \ 8 | && sed -i "s@\"version\": .*@\"version\": \"${VERSION}\",@" package.json 9 | 10 | RUN pnpm build 11 | 12 | FROM nginx:1.24-bullseye 13 | COPY --from=stage-build /app/dist /usr/share/nginx/html 14 | COPY nginx.conf /etc/nginx/conf.d/default.conf 15 | -------------------------------------------------------------------------------- /src/components/ReCountTo/src/rebound/props.ts: -------------------------------------------------------------------------------- 1 | import type { PropType } from "vue"; 2 | import propTypes from "@/utils/propTypes"; 3 | 4 | export const reboundProps = { 5 | delay: propTypes.number.def(1), 6 | blur: propTypes.number.def(2), 7 | i: { 8 | type: Number as PropType, 9 | required: false, 10 | default: 0, 11 | validator(value: number) { 12 | return value < 10 && value >= 0 && Number.isInteger(value); 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/RePlusPage/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { $t, transformI18n } from "@/plugins/i18n"; 2 | 3 | export const ExportImportFormatOptions = [ 4 | { label: "CSV", value: "csv" }, 5 | { label: "Excel", value: "xlsx" } 6 | ]; 7 | 8 | export const selectBooleanOptions = [ 9 | { 10 | label: transformI18n($t("labels.enable")), 11 | value: true 12 | }, 13 | { 14 | label: transformI18n($t("labels.disable")), 15 | value: false 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /src/layout/hooks/useMultiFrame.ts: -------------------------------------------------------------------------------- 1 | const MAP = new Map(); 2 | 3 | export const useMultiFrame = () => { 4 | function setMap(path, Comp) { 5 | MAP.set(path, Comp); 6 | } 7 | 8 | function getMap(path?) { 9 | if (path) { 10 | return MAP.get(path); 11 | } 12 | return [...MAP.entries()]; 13 | } 14 | 15 | function delMap(path) { 16 | MAP.delete(path); 17 | } 18 | 19 | return { 20 | setMap, 21 | getMap, 22 | delMap, 23 | MAP 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | gzip on; 5 | gzip_min_length 1k; 6 | gzip_buffers 4 16k; 7 | gzip_comp_level 8; 8 | gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; 9 | gzip_vary off; 10 | gzip_static on; 11 | gzip_disable "MSIE [1-6]."; 12 | 13 | location / { 14 | try_files $uri $uri/ /index.html; 15 | alias /usr/share/nginx/html; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/vue3.3.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.3+defineOptions快速生成模板": { 3 | "scope": "vue", 4 | "prefix": "Vue3.3+", 5 | "body": [ 6 | "\n", 11 | "", 12 | "\ttest", 13 | "\n", 14 | "", 16 | "$2" 17 | ], 18 | "description": "Vue3.3+defineOptions快速生成模板" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/vue3.0.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.0快速生成模板": { 3 | "scope": "vue", 4 | "prefix": "Vue3.0", 5 | "body": [ 6 | "", 7 | "\ttest", 8 | "\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 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { getRefreshToken, getToken, setToken } from "@/utils/auth"; 2 | import { refreshTokenApi } from "@/api/auth"; 3 | 4 | export async function getUsedAccessToken() { 5 | const accessToken = getToken(); 6 | if (accessToken) { 7 | return accessToken; 8 | } else { 9 | const RefreshToken = getRefreshToken(); 10 | if (RefreshToken) { 11 | const res = await refreshTokenApi({ refresh: RefreshToken }); 12 | setToken(res.data); 13 | return getToken(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | import type { Emitter } from "mitt"; 2 | import mitt from "mitt"; 3 | 4 | /** 全局公共事件需要在此处添加类型 */ 5 | type Events = { 6 | openPanel: string; 7 | tagOnClick: string; 8 | logoChange: boolean; 9 | tagViewsChange: string; 10 | changLayoutRoute: string; 11 | tagViewsShowModel: string; 12 | imageInfo: { 13 | img: HTMLImageElement; 14 | height: number; 15 | width: number; 16 | x: number; 17 | y: number; 18 | }; 19 | }; 20 | 21 | export const emitter: Emitter = mitt(); 22 | -------------------------------------------------------------------------------- /src/views/system/permission/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/svg/back_top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/lay-sidebar/components/SidebarExtraIcon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/system/online/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 20 | 21 | -------------------------------------------------------------------------------- /src/directives/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { hasAuth } from "@/router/utils"; 2 | import type { Directive, DirectiveBinding } from "vue"; 3 | 4 | export const auth: Directive = { 5 | mounted(el: HTMLElement, binding: DirectiveBinding) { 6 | const { value } = binding; 7 | if (value) { 8 | if (!hasAuth(value)) { 9 | el.parentNode?.removeChild(el); 10 | } 11 | } else { 12 | throw new Error( 13 | "[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\"" 14 | ); 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /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 | 10 | function setTrue() { 11 | setBool(true); 12 | } 13 | 14 | function setFalse() { 15 | setBool(false); 16 | } 17 | 18 | function toggle() { 19 | setBool(!bool.value); 20 | } 21 | 22 | return { 23 | bool, 24 | setBool, 25 | setTrue, 26 | setFalse, 27 | toggle 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/search-minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/system/config/system/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /src/views/system/logs/operation/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /src/views/system/setting/utils/hook.tsx: -------------------------------------------------------------------------------- 1 | import { settingsApi } from "@/api/system/settings"; 2 | import { useI18n } from "vue-i18n"; 3 | import { getDefaultAuths } from "@/router/utils"; 4 | import { getCurrentInstance, reactive } from "vue"; 5 | 6 | export function useSystemSetting() { 7 | const { t } = useI18n(); 8 | 9 | const api = reactive(settingsApi); 10 | 11 | const auth = reactive({ 12 | ...getDefaultAuths(getCurrentInstance()), 13 | partialUpdate: false 14 | }); 15 | 16 | return { 17 | t, 18 | api, 19 | auth 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ReImageVerify/src/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/ReIcon/index.ts: -------------------------------------------------------------------------------- 1 | import iconifyIconOffline from "./src/iconifyIconOffline"; 2 | import iconifyIconOnline from "./src/iconifyIconOnline"; 3 | import iconSelect from "./src/Select.vue"; 4 | import fontIcon from "./src/iconfont"; 5 | 6 | /** 本地图标组件 */ 7 | const IconifyIconOffline = iconifyIconOffline; 8 | /** 在线图标组件 */ 9 | const IconifyIconOnline = iconifyIconOnline; 10 | /** `IconSelect`图标选择器组件 */ 11 | const IconSelect = iconSelect; 12 | /** `iconfont`组件 */ 13 | const FontIcon = fontIcon; 14 | 15 | export { IconifyIconOffline, IconifyIconOnline, IconSelect, FontIcon }; 16 | -------------------------------------------------------------------------------- /src/components/ReSplitPane/iconfont/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3268330", 3 | "name": "split", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "22378774", 10 | "name": "拖拽", 11 | "font_class": "tuozhuai1", 12 | "unicode": "e647", 13 | "unicode_decimal": 58951 14 | }, 15 | { 16 | "icon_id": "23570521", 17 | "name": "拖拽", 18 | "font_class": "tuozhuai1-copy", 19 | "unicode": "eda3", 20 | "unicode_decimal": 60835 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/api/system/field.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult, DataListResult } from "@/api/types"; 3 | 4 | class ModelLabelFieldApi extends BaseApi { 5 | lookups = (params?: object) => { 6 | return this.request( 7 | "get", 8 | params, 9 | {}, 10 | `${this.baseApi}/lookups` 11 | ); 12 | }; 13 | 14 | sync = (params?: object) => { 15 | return this.request("get", params, {}, `${this.baseApi}/sync`); 16 | }; 17 | } 18 | 19 | export const modelLabelFieldApi = new ModelLabelFieldApi("/api/system/field"); 20 | -------------------------------------------------------------------------------- /src/views/system/role/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /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?: () => void; 15 | includes?: () => void; 16 | // svg 需要什么SVG属性自行添加 17 | fill?: string; 18 | // all icon 19 | style?: object; 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/svg/system.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReSplitPane/resizer.tsx: -------------------------------------------------------------------------------- 1 | import "./resizer.css"; 2 | import { computed, unref, defineComponent } from "vue"; 3 | 4 | export default defineComponent({ 5 | name: "Resizer", 6 | props: { 7 | split: { 8 | type: String, 9 | required: true 10 | }, 11 | className: { 12 | type: String, 13 | default: "" 14 | } 15 | }, 16 | setup(props) { 17 | const classes = computed(() => { 18 | return ["splitter-pane-resizer", props.split, props.className].join(" "); 19 | }); 20 | 21 | return () => ; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "christian-kohler.path-intellisense", 4 | "warmthsea.vscode-custom-code-color", 5 | "vscode-icons-team.vscode-icons", 6 | "davidanson.vscode-markdownlint", 7 | "ms-azuretools.vscode-docker", 8 | "stylelint.vscode-stylelint", 9 | "bradlc.vscode-tailwindcss", 10 | "dbaeumer.vscode-eslint", 11 | "esbenp.prettier-vscode", 12 | "lokalise.i18n-ally", 13 | "redhat.vscode-yaml", 14 | "csstools.postcss", 15 | "mikestead.dotenv", 16 | "eamodio.gitlens", 17 | "antfu.iconify", 18 | "Vue.volar" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/views/system/notice/read/list.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /.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 = "gzip" 14 | 15 | # api接口地址 16 | VITE_API_DOMAIN="" 17 | # ws 接口地址,由于建立socket需要token授权,则需要保证前端域名和ws域名一致 18 | VITE_WSS_DOMAIN="" 19 | -------------------------------------------------------------------------------- /src/views/system/logs/login/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/system/field/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 23 | 24 | -------------------------------------------------------------------------------- /src/layout/components/lay-search/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /web/acme.sh/deploy/nginx.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | #Here is a script to deploy cert to nginx server. 4 | 5 | #returns 0 means success, otherwise error. 6 | 7 | ######## Public functions ##################### 8 | 9 | #domain keyfile certfile cafile fullchain 10 | nginx_deploy() { 11 | _cdomain="$1" 12 | _ckey="$2" 13 | _ccert="$3" 14 | _cca="$4" 15 | _cfullchain="$5" 16 | 17 | _debug _cdomain "$_cdomain" 18 | _debug _ckey "$_ckey" 19 | _debug _ccert "$_ccert" 20 | _debug _cca "$_cca" 21 | _debug _cfullchain "$_cfullchain" 22 | 23 | _err "deploy cert to nginx server, Not implemented yet" 24 | return 1 25 | 26 | } 27 | -------------------------------------------------------------------------------- /web/acme.sh/deploy/apache.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | #Here is a script to deploy cert to apache server. 4 | 5 | #returns 0 means success, otherwise error. 6 | 7 | ######## Public functions ##################### 8 | 9 | #domain keyfile certfile cafile fullchain 10 | apache_deploy() { 11 | _cdomain="$1" 12 | _ckey="$2" 13 | _ccert="$3" 14 | _cca="$4" 15 | _cfullchain="$5" 16 | 17 | _debug _cdomain "$_cdomain" 18 | _debug _ckey "$_ckey" 19 | _debug _ccert "$_ccert" 20 | _debug _cca "$_cca" 21 | _debug _cfullchain "$_cfullchain" 22 | 23 | _err "Deploy cert to apache server, Not implemented yet" 24 | return 1 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/rotate-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/search-plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import type 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 | // eslint-disable-next-line 11 | interface Element extends VNode {} 12 | // eslint-disable-next-line 13 | interface ElementClass extends Vue {} 14 | 15 | interface ElementAttributesProperty { 16 | $props: any; 17 | } 18 | 19 | interface IntrinsicElements { 20 | [elem: string]: any; 21 | } 22 | 23 | interface IntrinsicAttributes { 24 | [elem: string]: any; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx}": [ 3 | "prettier --cache --ignore-unknown --write", 4 | "eslint --cache --fix" 5 | ], 6 | "{!(package)*.json,*.code-snippets,.!({browserslist,npm,nvm})*rc}": [ 7 | "prettier --cache --write--parser json" 8 | ], 9 | "package.json": ["prettier --cache --write"], 10 | "*.vue": [ 11 | "prettier --write", 12 | "eslint --cache --fix", 13 | "stylelint --fix --allow-empty-input" 14 | ], 15 | "*.{css,scss,html}": [ 16 | "prettier --cache --ignore-unknown --write", 17 | "stylelint --fix --allow-empty-input" 18 | ], 19 | "*.md": ["prettier --cache --ignore-unknown --write"] 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/rotate-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReCol/index.ts: -------------------------------------------------------------------------------- 1 | import { ElCol } from "element-plus"; 2 | import { defineComponent, h } 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/components/ReSplitPane/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 3268330 */ 3 | src: 4 | url("iconfont.woff2?t=1647939915215") format("woff2"), 5 | url("iconfont.woff?t=1647939915215") format("woff"), 6 | url("iconfont.ttf?t=1647939915215") format("truetype"); 7 | } 8 | 9 | .iconfont { 10 | font-family: "iconfont" !important; 11 | font-size: 16px; 12 | font-style: normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .icon-tuozhuai1:before { 18 | content: "\e647"; 19 | } 20 | 21 | .icon-tuozhuai1-copy:before { 22 | content: "\eda3"; 23 | } 24 | -------------------------------------------------------------------------------- /src/views/login/utils/enums.ts: -------------------------------------------------------------------------------- 1 | import { $t } from "@/plugins/i18n"; 2 | 3 | const operates = [ 4 | { 5 | title: $t("login.verifyCodeLogin") 6 | }, 7 | { 8 | title: $t("login.qRCodeLogin") 9 | }, 10 | { 11 | title: $t("login.register") 12 | } 13 | ]; 14 | 15 | const thirdParty = [ 16 | { 17 | title: $t("login.weChatLogin"), 18 | icon: "wechat" 19 | }, 20 | { 21 | title: $t("login.alipayLogin"), 22 | icon: "alipay" 23 | }, 24 | { 25 | title: $t("login.qqLogin"), 26 | icon: "qq" 27 | }, 28 | { 29 | title: $t("login.weiboLogin"), 30 | icon: "weibo" 31 | } 32 | ]; 33 | 34 | export { operates, thirdParty }; 35 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | # 预发布也需要生产环境的行为 2 | # https://cn.vitejs.dev/guide/env-and-mode.html#modes 3 | # NODE_ENV = development 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 | 18 | # api接口地址 19 | VITE_API_DOMAIN="" 20 | # ws 接口地址 21 | VITE_WSS_DOMAIN="" 22 | -------------------------------------------------------------------------------- /src/components/ReSegmented/src/type.ts: -------------------------------------------------------------------------------- 1 | import type { Component, VNode } from "vue"; 2 | import type { iconType } from "@/components/ReIcon/src/types.ts"; 3 | 4 | export interface OptionsType { 5 | /** 文字 */ 6 | label?: string | (() => VNode | Component); 7 | /** 8 | * @description 图标,采用平台内置的 `useRenderIcon` 函数渲染 9 | * @see {@link 用法参考 https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks } */ 10 | icon?: string | Component; 11 | /** 图标属性、样式配置 */ 12 | iconAttrs?: iconType; 13 | /** 值 */ 14 | value?: any; 15 | /** 是否禁用 */ 16 | disabled?: boolean; 17 | /** `tooltip` 提示 */ 18 | tip?: string; 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/issue-close-require.yml: -------------------------------------------------------------------------------- 1 | name: Issue Close Require 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | issue-close-require: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: need reproduce 12 | uses: actions-cool/issues-helper@v3 13 | with: 14 | actions: 'close-issues' 15 | labels: '⏳ Pending feedback' 16 | inactive-day: 30 17 | body: | 18 | You haven't provided feedback for over 30 days. 19 | We will close this issue. If you have any further needs, you can reopen it or submit a new issue. 20 | 您超过 30 天未反馈信息,我们将关闭该 issue,如有需求您可以重新打开或者提交新的 issue。 21 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/utils.ts: -------------------------------------------------------------------------------- 1 | export { store } from "@/store"; 2 | export { routerArrays } from "@/layout/types"; 3 | export { router, resetRouter, constantMenus } from "@/router"; 4 | export { getConfig, responsiveStorageNameSpace } from "@/config"; 5 | export { 6 | ascending, 7 | filterTree, 8 | filterNoPermissionTree, 9 | formatFlatteningRoutes 10 | } from "@/router/utils"; 11 | export { 12 | isUrl, 13 | isEqual, 14 | isNumber, 15 | debounce, 16 | isBoolean, 17 | getKeyList, 18 | storageLocal, 19 | deviceDetection 20 | } from "@pureadmin/utils"; 21 | export type { 22 | setType, 23 | appType, 24 | userType, 25 | multiType, 26 | cacheType, 27 | positionType 28 | } from "./types"; 29 | -------------------------------------------------------------------------------- /src/assets/login/avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/router/modules/home.ts: -------------------------------------------------------------------------------- 1 | import { $t } from "@/plugins/i18n"; 2 | 3 | const { VITE_HIDE_HOME } = import.meta.env; 4 | const Layout = () => import("@/layout/index.vue"); 5 | 6 | export default { 7 | path: "/", 8 | name: "Home", 9 | component: Layout, 10 | redirect: "/welcome", 11 | meta: { 12 | icon: "ep/home-filled", 13 | title: $t("menus.home"), 14 | rank: 0 15 | }, 16 | children: [ 17 | { 18 | path: "/welcome", 19 | name: "Welcome", 20 | component: () => import("@/views/welcome/index.vue"), 21 | meta: { 22 | title: $t("menus.home"), 23 | showLink: VITE_HIDE_HOME !== "true" 24 | } 25 | } 26 | ] 27 | } satisfies RouteConfigsTable; 28 | -------------------------------------------------------------------------------- /src/views/system/config/user/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 30 | 31 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2208059 */ 3 | src: 4 | url("iconfont.woff2?t=1671895108120") format("woff2"), 5 | url("iconfont.woff?t=1671895108120") format("woff"), 6 | url("iconfont.ttf?t=1671895108120") format("truetype"); 7 | } 8 | 9 | .iconfont { 10 | font-family: "iconfont" !important; 11 | font-size: 16px; 12 | font-style: normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .pure-iconfont-tabs:before { 18 | content: "\e63e"; 19 | } 20 | 21 | .pure-iconfont-logo:before { 22 | content: "\e620"; 23 | } 24 | 25 | .pure-iconfont-new:before { 26 | content: "\e615"; 27 | } 28 | -------------------------------------------------------------------------------- /src/views/account/utils/rule.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | import { $t, transformI18n } from "@/plugins/i18n"; 4 | 5 | /** 自定义表单规则校验 */ 6 | export const formRules = reactive({ 7 | username: [ 8 | { 9 | required: true, 10 | message: transformI18n($t("userinfo.username")), 11 | trigger: "blur" 12 | } 13 | ], 14 | nickname: [ 15 | { 16 | required: true, 17 | message: transformI18n($t("userinfo.nickname")), 18 | trigger: "blur" 19 | } 20 | ], 21 | gender: [ 22 | { 23 | required: true, 24 | message: transformI18n($t("userinfo.gender")), 25 | trigger: "blur" 26 | } 27 | ] 28 | }); 29 | -------------------------------------------------------------------------------- /src/api/system/menu.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult, DataListResult } from "@/api/types"; 3 | 4 | class MenuApi extends BaseApi { 5 | permissions = (pk: string, data: object) => { 6 | return this.request( 7 | "post", 8 | {}, 9 | data, 10 | `${this.baseApi}/${pk}/permissions` 11 | ); 12 | }; 13 | rank = (data?: object) => { 14 | return this.request("post", {}, data, `${this.baseApi}/rank`); 15 | }; 16 | apiUrl = () => { 17 | return this.request( 18 | "get", 19 | {}, 20 | {}, 21 | `${this.baseApi}/api-url` 22 | ); 23 | }; 24 | } 25 | 26 | export const menuApi = new MenuApi("/api/system/menu"); 27 | -------------------------------------------------------------------------------- /public/platform-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "4.2.5", 3 | "Title": "xAdmin", 4 | "FixedHeader": true, 5 | "HiddenSideBar": false, 6 | "MultiTagsCache": false, 7 | "KeepAlive": true, 8 | "Locale": "zh", 9 | "Layout": "vertical", 10 | "Theme": "light", 11 | "DarkMode": false, 12 | "ThemeMode": "light", 13 | "Grey": false, 14 | "Weak": false, 15 | "HideTabs": false, 16 | "HideFooter": false, 17 | "Stretch": false, 18 | "SidebarStatus": true, 19 | "EpThemeColor": "#409EFF", 20 | "ShowLogo": true, 21 | "ShowModel": "smart", 22 | "MenuArrowIconNoTransition": false, 23 | "CachingAsyncRoutes": false, 24 | "TooltipEffect": "light", 25 | "ResponsiveStorageNameSpace": "responsive-", 26 | "MenuSearchHistory": 6 27 | } 28 | -------------------------------------------------------------------------------- /src/views/settings/components/settings/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/layout/components/lay-footer/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 20 | 21 | 22 | 32 | -------------------------------------------------------------------------------- /src/views/system/notice/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 31 | 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/conf/xadmin-api-conf: -------------------------------------------------------------------------------- 1 | proxy_pass http://server:8896; 2 | proxy_buffering off; 3 | proxy_request_buffering off; 4 | proxy_http_version 1.1; 5 | proxy_set_header Host $host; 6 | proxy_set_header Upgrade $http_upgrade; 7 | proxy_set_header Connection $http_connection; 8 | proxy_set_header X-Forwarded-For $remote_addr; 9 | #proxy_set_header X-Forwarded-Host $host:$server_port; # 非默认的80,443端口,则需要打开该配置 10 | #proxy_set_header X-Forwarded-Proto $scheme; 11 | proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 如果上层还有其他 slb 需要使用 $proxy_add_x_forwarded_for 获取真实 ip 13 | 14 | proxy_ignore_client_abort on; 15 | proxy_connect_timeout 600; 16 | proxy_send_timeout 600; 17 | proxy_read_timeout 600; 18 | send_timeout 6000; -------------------------------------------------------------------------------- /src/api/common.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type IDCacheResult = { 4 | detail: string; 5 | code: number; 6 | spm: string; 7 | }; 8 | 9 | export type CountriesResult = { 10 | detail: string; 11 | code: number; 12 | data: Array<{ 13 | name: string; 14 | phone_code: string; 15 | flag: string | any; 16 | code: string; 17 | }>; 18 | }; 19 | 20 | /** 资源缓存ID, 300秒自动过期 */ 21 | export const resourcesIDCacheApi = (resources?: Array) => { 22 | return http.request("post", "/api/common/resources/cache", { 23 | data: { resources } 24 | }); 25 | }; 26 | 27 | /** 获取城市code */ 28 | export const countriesApi = () => { 29 | return http.request("get", "/api/common/countries"); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/FromQuestion/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | {{ label }} 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/layout/components/lay-notice/components/noticeList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/views/system/dept/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 33 | 34 | -------------------------------------------------------------------------------- /src/layout/components/lay-sidebar/components/SidebarLinkItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/views/login/components/qrCode.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ t("login.tip") }} 17 | 18 | 19 | 20 | 24 | {{ t("login.back") }} 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/assets/svg/globalization.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/system/file/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 32 | 33 | -------------------------------------------------------------------------------- /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 | "aria-hidden": false, 21 | style: attrs?.style 22 | ? Object.assign(attrs.style, { outline: "none" }) 23 | : { outline: "none" }, 24 | ...attrs 25 | }, 26 | { 27 | default: () => [] 28 | } 29 | ); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /src/views/settings/basic/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/views/system/constants.ts: -------------------------------------------------------------------------------- 1 | export const NoticeChoices = { 2 | SYSTEM: 0, 3 | NOTICE: 1, 4 | USER: 2, 5 | DEPT: 3, 6 | ROLE: 4 7 | }; 8 | 9 | export const MethodChoices = { 10 | GET: "GET", 11 | POST: "POST", 12 | PUT: "PUT", 13 | DELETE: "DELETE" 14 | }; 15 | 16 | export const MenuChoices = { 17 | DIRECTORY: 0, 18 | MENU: 1, 19 | PERMISSION: 2 20 | }; 21 | export const ModeChoices = { 22 | OR: 0, 23 | AND: 1 24 | }; 25 | 26 | export const FieldKeyChoices = { 27 | DATETIME: "value.datetime", 28 | DATETIME_RANGE: "value.datetime.range", 29 | TABLE_USER: "value.table.user.ids", 30 | TABLE_MENU: "value.table.menu.ids", 31 | TABLE_ROLE: "value.table.role.ids", 32 | TABLE_DEPT: "value.table.dept.ids" 33 | }; 34 | 35 | export const FieldChoices = { 36 | ROLE: 0, 37 | DATA: 1 38 | }; 39 | -------------------------------------------------------------------------------- /src/assets/table-bar/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/lay-sidebar/components/SidebarFullScreen.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/user/notice/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/ReFlicker/index.css: -------------------------------------------------------------------------------- 1 | .point { 2 | width: var(--point-width); 3 | height: var(--point-height); 4 | background: var(--point-background); 5 | position: relative; 6 | border-radius: var(--point-border-radius); 7 | } 8 | 9 | .point-flicker:after { 10 | background: var(--point-background); 11 | } 12 | 13 | .point-flicker:before, 14 | .point-flicker:after { 15 | content: ""; 16 | width: 100%; 17 | height: 100%; 18 | top: 0; 19 | left: 0; 20 | position: absolute; 21 | border-radius: var(--point-border-radius); 22 | animation: flicker 1.2s ease-out infinite; 23 | } 24 | 25 | @keyframes flicker { 26 | 0% { 27 | transform: scale(0.5); 28 | opacity: 1; 29 | } 30 | 31 | 30% { 32 | opacity: 1; 33 | } 34 | 35 | 100% { 36 | transform: scale(var(--point-scale)); 37 | opacity: 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /mock/refreshToken.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 2 | 3 | // 模拟刷新token接口 4 | export default defineFakeRoute([ 5 | { 6 | url: "/refresh-token", 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 | ]); 28 | -------------------------------------------------------------------------------- /src/api/user/notice.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult, DetailResult } from "@/api/types"; 3 | 4 | class UserNoticeReadApi extends BaseApi { 5 | unread = (params?: object) => { 6 | return this.request( 7 | "get", 8 | params, 9 | {}, 10 | `${this.baseApi}/unread` 11 | ); 12 | }; 13 | allRead = (params?: object) => { 14 | return this.request( 15 | "patch", 16 | params, 17 | {}, 18 | `${this.baseApi}/all-read` 19 | ); 20 | }; 21 | batchRead = (data?: object) => { 22 | return this.request( 23 | "patch", 24 | {}, 25 | data, 26 | `${this.baseApi}/batch-read` 27 | ); 28 | }; 29 | } 30 | 31 | export const userNoticeReadApi = new UserNoticeReadApi( 32 | "/api/notifications/site-messages" 33 | ); 34 | -------------------------------------------------------------------------------- /src/plugins/plusProComponents.ts: -------------------------------------------------------------------------------- 1 | // 按需引入plus-pro-components(该方法稳定且明确。当然也支持:https://plus-pro-components.github.io/guide/quickstart.html#%E8%87%AA%E5%8A%A8%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5-%E6%8E%A8%E8%8D%90) 2 | import type { App, Component } from "vue"; 3 | import { PlusForm, PlusSearch } from "plus-pro-components"; 4 | 5 | const components = [ 6 | // PlusLayout, 7 | // PlusPage, 8 | // PlusTable, 9 | PlusSearch, 10 | PlusForm 11 | // PlusDialogForm, 12 | // PlusDrawerForm, 13 | // PlusDescriptions 14 | ]; 15 | const plugins = []; 16 | 17 | /** 按需引入`plus-pro-components` */ 18 | export function usePlusProComponents(app: App) { 19 | // 全局注册组件 20 | components.forEach((component: Component) => { 21 | app.component(component.name, component); 22 | }); 23 | // 全局注册插件 24 | plugins.forEach(plugin => { 25 | app.use(plugin); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/views/demo/book/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/reload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import("@commitlint/types").UserConfig} */ 4 | export default { 5 | ignores: [commit => commit.includes("init")], 6 | extends: ["@commitlint/config-conventional"], 7 | rules: { 8 | "body-leading-blank": [2, "always"], 9 | "footer-leading-blank": [1, "always"], 10 | "header-max-length": [2, "always", 108], 11 | "subject-empty": [2, "never"], 12 | "type-empty": [2, "never"], 13 | "type-enum": [ 14 | 2, 15 | "always", 16 | [ 17 | "feat", 18 | "fix", 19 | "perf", 20 | "style", 21 | "docs", 22 | "test", 23 | "refactor", 24 | "build", 25 | "ci", 26 | "chore", 27 | "revert", 28 | "wip", 29 | "workflow", 30 | "types", 31 | "release" 32 | ] 33 | ] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/views/account/components/SecurityLog.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 19 | {{ t("account.securityLog") }} 20 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/layout/components/lay-sidebar/components/SidebarTopCollapse.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 27 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- 1 | @use "theme"; 2 | @use "transition"; 3 | @use "element-plus"; 4 | @use "sidebar"; 5 | @use "dark"; 6 | 7 | /* 自定义全局 CssVar */ 8 | :root { 9 | /* 左侧菜单展开、收起动画时长 */ 10 | --pure-transition-duration: 0.3s; 11 | 12 | /* 常用border-color 需要时可取用 */ 13 | --pure-border-color: rgb(5 5 5 / 6%); 14 | 15 | /* switch关闭状态下的color 需要时可取用 */ 16 | --pure-switch-off-color: #a6a6a6; 17 | 18 | /** 主题色 */ 19 | --pure-theme-sub-menu-active-text: initial; 20 | --pure-theme-menu-bg: none; 21 | --pure-theme-menu-hover: none; 22 | --pure-theme-sub-menu-bg: transparent; 23 | --pure-theme-menu-text: initial; 24 | --pure-theme-sidebar-logo: none; 25 | --pure-theme-menu-title-hover: initial; 26 | --pure-theme-menu-active-before: transparent; 27 | } 28 | 29 | /* 灰色模式 */ 30 | .html-grey { 31 | filter: grayscale(100%); 32 | } 33 | 34 | /* 色弱模式 */ 35 | .html-weakness { 36 | filter: invert(80%); 37 | } 38 | -------------------------------------------------------------------------------- /src/views/system/permission/components/utils/rule.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | import { $t, transformI18n } from "@/plugins/i18n"; 4 | 5 | /** 自定义表单规则校验 */ 6 | export const formRules = reactive({ 7 | name: [ 8 | { 9 | required: true, 10 | message: transformI18n($t("systemPermission.addName")), 11 | trigger: "blur" 12 | } 13 | ], 14 | match: [ 15 | { 16 | required: true, 17 | message: transformI18n($t("systemPermission.addMatch")), 18 | trigger: "blur" 19 | } 20 | ], 21 | type: [ 22 | { 23 | required: true, 24 | message: transformI18n($t("systemPermission.addType")), 25 | trigger: "blur" 26 | } 27 | ], 28 | value: [ 29 | { 30 | required: true, 31 | message: transformI18n($t("systemPermission.addValue")), 32 | trigger: "blur" 33 | } 34 | ] 35 | }); 36 | -------------------------------------------------------------------------------- /src/views/login/utils/motion.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, resolveDirective, withDirectives } 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 | -------------------------------------------------------------------------------- /src/store/modules/settings.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { getConfig, type setType, store } from "../utils"; 3 | 4 | export const useSettingStore = defineStore("pure-setting", { 5 | state: (): setType => ({ 6 | title: getConfig().Title, 7 | fixedHeader: getConfig().FixedHeader, 8 | hiddenSideBar: getConfig().HiddenSideBar 9 | }), 10 | getters: { 11 | getTitle(state) { 12 | return state.title; 13 | }, 14 | getFixedHeader(state) { 15 | return state.fixedHeader; 16 | }, 17 | getHiddenSideBar(state) { 18 | return state.hiddenSideBar; 19 | } 20 | }, 21 | actions: { 22 | CHANGE_SETTING({ key, value }) { 23 | if (Reflect.has(this, key)) { 24 | this[key] = value; 25 | } 26 | }, 27 | changeSetting(data) { 28 | this.CHANGE_SETTING(data); 29 | } 30 | } 31 | }); 32 | 33 | export function useSettingStoreHook() { 34 | return useSettingStore(store); 35 | } 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | net: 3 | name: xadmin-server_net # 使用 xadmin-server-api 的网络 4 | external: true 5 | 6 | 7 | services: 8 | nginx-web: 9 | image: xadmin-web 10 | container_name: xadmin-web 11 | hostname: xadmin-web 12 | build: 13 | context: . 14 | dockerfile: Dockerfile-web 15 | restart: always 16 | environment: 17 | TZ: ${TZ:-Asia/Shanghai} 18 | # DOMAIN: ${DOMAIN:-xadmin.dvcloud.xin} # 如果要使用ssl,并且使用acme.sh自动申请证书,请取消注释,并将 xadmin.dvcloud.xin 填写自己的域名!!! 19 | # EMAIL: ${EMAIL:-xadmin@dvcloud.xin} # 如果要使用ssl,并且使用acme.sh自动申请证书,请取消注释,并将 xadmin@dvcloud.xin 填写自己的邮件!!! 20 | volumes: 21 | - ./web/acme.sh:/web/acme.sh 22 | - ./web/data/dist:/web/dist 23 | - ./web/data/.acme.sh:/root/.acme.sh 24 | - ./web/data/cert:/web/cert 25 | - ./web/data/logs:/var/log/nginx 26 | ports: 27 | # - "443:443" # 如果要使用ssl,请取消注释 28 | - "80:80" 29 | networks: 30 | - net 31 | -------------------------------------------------------------------------------- /src/router/modules/error.ts: -------------------------------------------------------------------------------- 1 | import { $t } from "@/plugins/i18n"; 2 | 3 | export default { 4 | path: "/error", 5 | redirect: "/error/403", 6 | meta: { 7 | icon: "informationLine", 8 | title: $t("menus.abnormal"), 9 | showLink: false, 10 | rank: 9 11 | }, 12 | children: [ 13 | { 14 | path: "/error/403", 15 | name: "403", 16 | component: () => import("@/views/error/403.vue"), 17 | meta: { 18 | title: $t("menus.fourZeroThree") 19 | } 20 | }, 21 | { 22 | path: "/error/404", 23 | name: "404", 24 | component: () => import("@/views/error/404.vue"), 25 | meta: { 26 | title: $t("menus.fourZeroFour") 27 | } 28 | }, 29 | { 30 | path: "/error/500", 31 | name: "500", 32 | component: () => import("@/views/error/500.vue"), 33 | meta: { 34 | title: $t("menus.FiveZeroZero") 35 | } 36 | } 37 | ] 38 | } satisfies RouteConfigsTable; 39 | -------------------------------------------------------------------------------- /src/views/user/info/utils/types.ts: -------------------------------------------------------------------------------- 1 | type ChoicesLabel = { label?: string }; 2 | 3 | interface FormItemProps { 4 | title?: string; 5 | /** ID */ 6 | pk?: number; 7 | /** 用户名 */ 8 | username: string; 9 | /** 昵称 */ 10 | nickname: string; 11 | /** 头像 */ 12 | avatar: string; 13 | /** 手机号码 */ 14 | phone?: string; 15 | /** 邮箱 */ 16 | email?: string; 17 | dept?: { 18 | name?: string; 19 | pk?: number; 20 | }; 21 | /** 性别 */ 22 | gender?: number | ChoicesLabel; 23 | /** 角色 */ 24 | roles?: any[]; 25 | /** 密码 */ 26 | password?: string; 27 | /** 注册时间 */ 28 | date_joined?: string; 29 | /** 最近登录时间 */ 30 | last_login?: string; 31 | } 32 | 33 | interface FormProps { 34 | formInline: FormItemProps; 35 | genderChoices: any[]; 36 | } 37 | 38 | interface FormPasswordProps { 39 | old_password: string; 40 | new_password: string; 41 | sure_password?: string; 42 | } 43 | 44 | export type { FormItemProps, FormProps, FormPasswordProps }; 45 | -------------------------------------------------------------------------------- /.github/release-config.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🌱 新功能 Features' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - 'feat' 9 | - '新功能' 10 | - title: '🚀 性能优化 Optimization' 11 | labels: 12 | - 'perf' 13 | - 'opt' 14 | - 'refactor' 15 | - 'Optimization' 16 | - '优化' 17 | - title: '🐛 Bug修复 Bug Fixes' 18 | labels: 19 | - 'fix' 20 | - 'bugfix' 21 | - 'bug' 22 | - title: '🧰 其它 Maintenance' 23 | labels: 24 | - 'chore' 25 | - 'docs' 26 | exclude-labels: 27 | - 'no' 28 | - '无需处理' 29 | - 'wontfix' 30 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 31 | version-resolver: 32 | major: 33 | labels: 34 | - 'major' 35 | minor: 36 | labels: 37 | - 'minor' 38 | patch: 39 | labels: 40 | - 'patch' 41 | default: patch 42 | template: | 43 | ## 版本变化 What’s Changed 44 | 45 | $CHANGES 46 | -------------------------------------------------------------------------------- /src/views/system/components/SearchRole.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | { 32 | emit('change', value); 33 | } 34 | " 35 | /> 36 | 37 | -------------------------------------------------------------------------------- /src/components/ReSplitPane/index.css: -------------------------------------------------------------------------------- 1 | .clearfix::after { 2 | visibility: hidden; 3 | display: block; 4 | font-size: 0; 5 | content: " "; 6 | clear: both; 7 | height: 0; 8 | } 9 | 10 | .vue-splitter-container { 11 | height: 100%; 12 | position: relative; 13 | } 14 | 15 | .vue-splitter-container-mask { 16 | z-index: 9999; 17 | width: 100%; 18 | height: 100%; 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | } 23 | 24 | .splitter-pane.vertical.splitter-paneL { 25 | position: absolute; 26 | left: 0; 27 | height: 100%; 28 | padding-right: 3px; 29 | } 30 | 31 | .splitter-pane.vertical.splitter-paneR { 32 | position: absolute; 33 | right: 0; 34 | height: 100%; 35 | padding-left: 3px; 36 | } 37 | 38 | .splitter-pane.horizontal.splitter-paneL { 39 | position: absolute; 40 | top: 0; 41 | width: 100%; 42 | } 43 | 44 | .splitter-pane.horizontal.splitter-paneR { 45 | position: absolute; 46 | bottom: 0; 47 | width: 100%; 48 | padding-top: 3px; 49 | } 50 | -------------------------------------------------------------------------------- /src/constants/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 选项 3 | */ 4 | export interface SelectOption { 5 | label: string; 6 | value: T; 7 | } 8 | 9 | export interface SelectOptionMap { 10 | [key: string | number]: T; 11 | } 12 | 13 | /** 14 | * 状态 15 | */ 16 | export const enableOptions: SelectOption[] = [ 17 | { label: "启用", value: 1 }, 18 | { label: "禁用", value: 0 } 19 | ]; 20 | export const enableMap: SelectOptionMap = { 21 | 1: "启用", 22 | 0: "禁用" 23 | }; 24 | export const enableBooleanMap: SelectOptionMap = { 25 | 1: true, 26 | 0: false 27 | }; 28 | 29 | export const ifEnableOptions: SelectOption[] = [ 30 | { label: "启用", value: true }, 31 | { label: "禁用", value: false } 32 | ]; 33 | 34 | export const ifNumberOptions: SelectOption[] = [ 35 | { label: "是", value: 1 }, 36 | { label: "否", value: 0 } 37 | ]; 38 | 39 | export const ifOptions: SelectOption[] = [ 40 | { label: "是", value: true }, 41 | { label: "否", value: false } 42 | ]; 43 | -------------------------------------------------------------------------------- /types/directives.d.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from "vue"; 2 | import type { CopyEl, OptimizeOptions, RippleOptions } from "@/directives"; 3 | 4 | declare module "vue" { 5 | export interface ComponentCustomProperties { 6 | /** `Loading` 动画加载指令,具体看:https://element-plus.org/zh-CN/component/loading.html#%E6%8C%87%E4%BB%A4 */ 7 | vLoading: Directive; 8 | /** 按钮权限指令 */ 9 | vAuth: Directive>; 10 | /** 文本复制指令(默认双击复制) */ 11 | vCopy: Directive; 12 | /** 长按指令 */ 13 | vLongpress: Directive; 14 | /** 防抖、节流指令 */ 15 | vOptimize: Directive; 16 | /** 17 | * `v-ripple`指令,用法如下: 18 | * 1. `v-ripple`代表启用基本的`ripple`功能 19 | * 2. `v-ripple="{ class: 'text-red' }"`代表自定义`ripple`颜色,支持`tailwindcss`,生效样式是`color` 20 | * 3. `v-ripple.center`代表从中心扩散 21 | */ 22 | vRipple: Directive; 23 | } 24 | } 25 | 26 | export {}; 27 | -------------------------------------------------------------------------------- /src/api/system/file.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import { http } from "@/utils/http"; 3 | import type { DetailResult } from "@/api/types"; 4 | import type { PureHttpRequestConfig } from "@/utils/http/types"; 5 | 6 | type UploadFileResult = { 7 | code: number; 8 | data?: Array<{ 9 | pk: number | string; 10 | filename: string; 11 | access_url: string; 12 | filesize: number; 13 | }>; 14 | detail?: string; 15 | }; 16 | 17 | class SystemUploadFileApi extends BaseApi { 18 | upload = (data?: object, config?: PureHttpRequestConfig) => { 19 | return http.upload( 20 | `${this.baseApi}/upload`, 21 | {}, 22 | data, 23 | config 24 | ); 25 | }; 26 | config = (params?: object) => { 27 | return this.request( 28 | "get", 29 | params, 30 | {}, 31 | `${this.baseApi}/config` 32 | ); 33 | }; 34 | } 35 | 36 | export const systemUploadFileApi = new SystemUploadFileApi("/api/system/file"); 37 | -------------------------------------------------------------------------------- /src/components/RePlusPage/src/utils/renders.tsx: -------------------------------------------------------------------------------- 1 | import Segmented from "@/components/ReSegmented"; 2 | import { selectBooleanOptions } from "./constants"; 3 | 4 | export const renderBooleanSegmentedOption = (options = null) => { 5 | return (value, onChange) => { 6 | return ( 7 | { 11 | onChange(option?.value); 12 | }} 13 | /> 14 | ); 15 | }; 16 | }; 17 | 18 | /** 19 | * 格式化后端选择列表,如果是obj的数据,isObjValue为true 20 | */ 21 | export const formatAddOrEditOptions = ( 22 | data: Array, 23 | isObjValue = false 24 | ) => { 25 | const result = []; 26 | data?.forEach(item => { 27 | item.pk = item.value; 28 | result.push({ 29 | label: item?.label, 30 | value: isObjValue ? item : item.value, 31 | fieldItemProps: { 32 | disabled: item?.disabled 33 | } 34 | }); 35 | }); 36 | return result; 37 | }; 38 | -------------------------------------------------------------------------------- /src/layout/components/lay-tag/components/TagChrome.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/ReImageVerify/src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from "vue"; 2 | import { getCaptchaApi } from "@/api/auth"; 3 | import { useUserStoreHook } from "@/store/modules/user"; 4 | import { delay } from "@pureadmin/utils"; 5 | 6 | export const useImageVerify = imgCode => { 7 | // const imgCode = ref(""); 8 | const imgUrl = ref(""); 9 | const loading = ref(false); 10 | 11 | function getImgCode() { 12 | loading.value = true; 13 | getCaptchaApi() 14 | .then(res => { 15 | if (res.code === 1000) { 16 | imgUrl.value = res.captcha_image; 17 | imgCode.value = res.captcha_key; 18 | useUserStoreHook().SET_VERIFY_CODE_LENGTH(res.length); 19 | } 20 | }) 21 | .finally(() => { 22 | delay(100).then(() => { 23 | loading.value = false; 24 | }); 25 | }); 26 | } 27 | 28 | onMounted(() => { 29 | getImgCode(); 30 | }); 31 | 32 | return { 33 | imgUrl, 34 | loading, 35 | imgCode, 36 | getImgCode 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/api/system/notice.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult } from "@/api/types"; 3 | 4 | class NoticeApi extends BaseApi { 5 | announcement = (data?: object) => { 6 | return this.request( 7 | "post", 8 | {}, 9 | data, 10 | `${this.baseApi}/announcement` 11 | ); 12 | }; 13 | publish = (pk: number | string, data?: object) => { 14 | return this.request( 15 | "patch", 16 | {}, 17 | data, 18 | `${this.baseApi}/${pk}/publish` 19 | ); 20 | }; 21 | } 22 | 23 | export const noticeApi = new NoticeApi("/api/notifications/notice-messages"); 24 | 25 | // 用户已读公告查询 26 | class NoticeReadApi extends BaseApi { 27 | state = (pk: number | string, data?: object) => { 28 | return this.request( 29 | "patch", 30 | {}, 31 | data, 32 | `${this.baseApi}/${pk}/state` 33 | ); 34 | }; 35 | } 36 | 37 | export const noticeReadApi = new NoticeReadApi( 38 | "/api/notifications/user-read-messages" 39 | ); 40 | -------------------------------------------------------------------------------- /src/api/system/notifications.ts: -------------------------------------------------------------------------------- 1 | import type { DetailResult } from "@/api/types"; 2 | import { BaseRequest } from "@/api/base"; 3 | import type { DataListResult } from "@/api/types"; 4 | 5 | export class SystemMsgSubscriptionApi extends BaseRequest { 6 | backends = () => { 7 | return this.request( 8 | "get", 9 | {}, 10 | {}, 11 | `${this.baseApi}/backends` 12 | ); 13 | }; 14 | 15 | list = (params?: object) => { 16 | return this.request("get", params, {}); 17 | }; 18 | 19 | update = (pk: number | string, data?: object) => { 20 | return this.request("put", {}, data, `${this.baseApi}/${pk}`); 21 | }; 22 | partialUpdate = (pk: number | string, data?: object) => { 23 | return this.request( 24 | "patch", 25 | {}, 26 | data, 27 | `${this.baseApi}/${pk}` 28 | ); 29 | }; 30 | } 31 | 32 | export const systemMsgSubscriptionApi = new SystemMsgSubscriptionApi( 33 | "/api/notifications/system-msg-subscription" 34 | ); 35 | -------------------------------------------------------------------------------- /src/views/empty/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 业务内容模块 15 | 使用场景:需要外嵌平台某个页面,不需要展示菜单导航以及额外模块 16 | 17 | 18 | 19 | 20 | 21 | 22 | 43 | -------------------------------------------------------------------------------- /src/views/account/components/Notifications.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 29 | {{ t("account.notifications") }} 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Dockerfile-web: -------------------------------------------------------------------------------- 1 | FROM registry.cn-beijing.aliyuncs.com/nineaiyu/nginx:1.24-bullseye 2 | 3 | ARG APT_MIRROR=http://mirrors.tuna.tsinghua.edu.cn 4 | 5 | # Install APT dependencies 6 | ARG DEPENDENCIES=" \ 7 | curl \ 8 | socat \ 9 | cron \ 10 | ca-certificates" 11 | 12 | RUN set -ex \ 13 | && rm -f /etc/apt/apt.conf.d/docker-clean \ 14 | && sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \ 15 | && apt-get update > /dev/null \ 16 | && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ 17 | && echo "no" | dpkg-reconfigure dash 18 | 19 | 20 | RUN mkdir -p /var/www/acme-challenge 21 | 22 | COPY web/conf/nginx.conf /etc/nginx/nginx.conf 23 | COPY web/conf/xadmin-api-conf /etc/nginx/conf.d/xadmin-api-conf 24 | 25 | COPY entrypoint.sh /entrypoint.sh 26 | 27 | RUN chmod +x /entrypoint.sh 28 | 29 | EXPOSE 80 443 30 | 31 | ENTRYPOINT ["/bin/bash", "entrypoint.sh"] 32 | 33 | STOPSIGNAL SIGQUIT 34 | -------------------------------------------------------------------------------- /src/api/config.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { RequestMethods } from "@/utils/http/types"; 3 | 4 | type ConfigResult = { 5 | detail: string; 6 | code: number; 7 | config: object | number | boolean | string | any; 8 | auth: string; 9 | }; 10 | 11 | class ConfigApi extends BaseApi { 12 | getConfig = (name: string) => { 13 | return this.request("get", {}, {}, `${this.baseApi}/${name}`); 14 | }; 15 | setConfig = (name: string, data: object, method: RequestMethods) => { 16 | return this.request( 17 | method, 18 | {}, 19 | data, 20 | `${this.baseApi}/${name}` 21 | ); 22 | }; 23 | getSiteConfig = () => { 24 | return this.getConfig("WEB_SITE_CONFIG"); 25 | }; 26 | setSiteConfig = (data: object) => { 27 | return this.setConfig("WEB_SITE_CONFIG", data, "patch"); 28 | }; 29 | resetSiteConfig = () => { 30 | return this.setConfig("WEB_SITE_CONFIG", {}, "delete"); 31 | }; 32 | } 33 | 34 | export const configApi = new ConfigApi("/api/system/configs"); 35 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/index.ts: -------------------------------------------------------------------------------- 1 | import Reload from "./reload.svg?component"; 2 | import Upload from "./upload.svg?component"; 3 | import ArrowH from "./arrow-h.svg?component"; 4 | import ArrowV from "./arrow-v.svg?component"; 5 | import ArrowUp from "./arrow-up.svg?component"; 6 | import ChangeIcon from "./change.svg?component"; 7 | import ArrowDown from "./arrow-down.svg?component"; 8 | import ArrowLeft from "./arrow-left.svg?component"; 9 | import DownloadIcon from "./download.svg?component"; 10 | import ArrowRight from "./arrow-right.svg?component"; 11 | import RotateLeft from "./rotate-left.svg?component"; 12 | import SearchPlus from "./search-plus.svg?component"; 13 | import RotateRight from "./rotate-right.svg?component"; 14 | import SearchMinus from "./search-minus.svg?component"; 15 | 16 | export { 17 | Reload, 18 | Upload, 19 | ArrowH, 20 | ArrowV, 21 | ArrowUp, 22 | ArrowDown, 23 | ArrowLeft, 24 | ChangeIcon, 25 | ArrowRight, 26 | RotateLeft, 27 | SearchPlus, 28 | RotateRight, 29 | SearchMinus, 30 | DownloadIcon 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/preventDefault.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from "@vueuse/core"; 2 | 3 | /** 是否为`img`标签 */ 4 | function isImgElement(element) { 5 | return typeof HTMLImageElement !== "undefined" 6 | ? element instanceof HTMLImageElement 7 | : element.tagName.toLowerCase() === "img"; 8 | } 9 | 10 | // 在 src/main.ts 引入并调用即可 import { addPreventDefault } from "@/utils/preventDefault"; addPreventDefault(); 11 | export const addPreventDefault = () => { 12 | // 阻止通过键盘F12快捷键打开浏览器开发者工具面板 13 | useEventListener( 14 | window.document, 15 | "keydown", 16 | ev => ev.key === "F12" && ev.preventDefault() 17 | ); 18 | // 阻止浏览器默认的右键菜单弹出(不会影响自定义右键事件) 19 | useEventListener(window.document, "contextmenu", ev => ev.preventDefault()); 20 | // 阻止页面元素选中 21 | // useEventListener(window.document, "selectstart", ev => ev.preventDefault()); 22 | // 浏览器中图片通常默认是可拖动的,并且可以在新标签页或窗口中打开,或者将其拖动到其他应用程序中,此处将其禁用,使其默认不可拖动 23 | useEventListener( 24 | window.document, 25 | "dragstart", 26 | ev => isImgElement(ev?.target) && ev.preventDefault() 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /mock/login.ts: -------------------------------------------------------------------------------- 1 | // 根据角色动态生成路由 2 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 3 | 4 | export default defineFakeRoute([ 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 | ]); 37 | -------------------------------------------------------------------------------- /src/components/ReCountTo/src/normal/props.ts: -------------------------------------------------------------------------------- 1 | import type { PropType } from "vue"; 2 | import propTypes from "@/utils/propTypes"; 3 | 4 | export const countToProps = { 5 | startVal: propTypes.number.def(0), 6 | endVal: propTypes.number.def(2020), 7 | duration: propTypes.number.def(1300), 8 | autoplay: propTypes.bool.def(true), 9 | decimals: { 10 | type: Number as PropType, 11 | required: false, 12 | default: 0, 13 | validator(value: number) { 14 | return value >= 0; 15 | } 16 | }, 17 | color: propTypes.string.def(), 18 | fontSize: propTypes.string.def(), 19 | decimal: propTypes.string.def("."), 20 | separator: propTypes.string.def(","), 21 | prefix: propTypes.string.def(""), 22 | suffix: propTypes.string.def(""), 23 | useEasing: propTypes.bool.def(true), 24 | easingFn: { 25 | type: Function as PropType< 26 | (t: number, b: number, c: number, d: number) => number 27 | >, 28 | default(t: number, b: number, c: number, d: number) { 29 | return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b; 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/ReSplitPane/resizer.css: -------------------------------------------------------------------------------- 1 | @import "./iconfont/iconfont.css"; 2 | 3 | .splitter-pane-resizer { 4 | box-sizing: border-box; 5 | background: #000; 6 | position: absolute; 7 | opacity: 0.2; 8 | z-index: 1; 9 | background-clip: padding; 10 | background-clip: padding-box; 11 | } 12 | 13 | .splitter-pane-resizer.horizontal { 14 | height: 6px; 15 | width: 100%; 16 | background: #e5e6eb; 17 | cursor: row-resize; 18 | } 19 | 20 | .splitter-pane-resizer.horizontal:before { 21 | content: "\eda3"; 22 | font-family: "iconfont"; 23 | font-size: 13px; 24 | color: #000; 25 | position: absolute; 26 | top: 50%; 27 | left: 50%; 28 | transform: translate(-50%, -50%); 29 | } 30 | 31 | .splitter-pane-resizer.vertical { 32 | width: 6px; 33 | height: 100%; 34 | background: #e5e6eb; 35 | cursor: col-resize; 36 | } 37 | 38 | .splitter-pane-resizer.vertical:before { 39 | content: "\e647"; 40 | font-family: "iconfont"; 41 | font-size: 13px; 42 | color: #000; 43 | position: absolute; 44 | top: 50%; 45 | left: 50%; 46 | transform: translate(-50%, -50%); 47 | } 48 | -------------------------------------------------------------------------------- /src/api/user/userinfo.ts: -------------------------------------------------------------------------------- 1 | import { ViewBaseApi } from "@/api/base"; 2 | import type { BaseResult, ChoicesResult } from "@/api/types"; 3 | import { http } from "@/utils/http"; 4 | import type { UserInfoResult } from "@/api/auth"; 5 | 6 | class UserInfoApi extends ViewBaseApi { 7 | upload = (data?: object) => { 8 | return http.upload(`${this.baseApi}/upload`, {}, data); 9 | }; 10 | choices = () => { 11 | return this.request( 12 | "get", 13 | {}, 14 | {}, 15 | `${this.baseApi}/choices` 16 | ); 17 | }; 18 | resetPassword = (data?: object) => { 19 | return this.request( 20 | "post", 21 | {}, 22 | data, 23 | `${this.baseApi}/reset-password` 24 | ); 25 | }; 26 | 27 | bind = (data?: object) => { 28 | return this.request("post", {}, data, `${this.baseApi}/bind`); 29 | }; 30 | 31 | retrieve = (params?: object) => { 32 | return this.request("get", params, {}, `${this.baseApi}`); 33 | }; 34 | } 35 | 36 | export const userInfoApi = new UserInfoApi("/api/system/userinfo"); 37 | -------------------------------------------------------------------------------- /src/layout/hooks/useTranslationLang.ts: -------------------------------------------------------------------------------- 1 | import { useNav } from "./useNav"; 2 | import { useI18n } from "vue-i18n"; 3 | import { useRoute } from "vue-router"; 4 | import { onBeforeMount, type Ref, watch } from "vue"; 5 | 6 | export function useTranslationLang(ref?: Ref) { 7 | const { $storage, changeTitle, handleResize } = useNav(); 8 | const { locale, t } = useI18n(); 9 | const route = useRoute(); 10 | 11 | function translationCh() { 12 | $storage.locale = { locale: "zh" }; 13 | locale.value = "zh"; 14 | if (ref) { 15 | handleResize(ref.value); 16 | } 17 | } 18 | 19 | function translationEn() { 20 | $storage.locale = { locale: "en" }; 21 | locale.value = "en"; 22 | if (ref) { 23 | handleResize(ref.value); 24 | } 25 | } 26 | 27 | watch( 28 | () => locale.value, 29 | () => { 30 | changeTitle(route.meta); 31 | } 32 | ); 33 | 34 | onBeforeMount(() => { 35 | locale.value = $storage.locale?.locale ?? "zh"; 36 | }); 37 | 38 | return { 39 | t, 40 | route, 41 | locale, 42 | translationCh, 43 | translationEn 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/ReFlicker/index.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { type Component, defineComponent, h } from "vue"; 3 | 4 | export interface attrsType { 5 | width?: string; 6 | height?: string; 7 | borderRadius?: number | string; 8 | background?: string; 9 | scale?: number | string; 10 | } 11 | 12 | /** 13 | * 圆点、方形闪烁动画组件 14 | * @returns Component 15 | * @param attrs 16 | */ 17 | export function useRenderFlicker(attrs?: attrsType): Component { 18 | return defineComponent({ 19 | name: "ReFlicker", 20 | render() { 21 | return h( 22 | "div", 23 | { 24 | class: "point point-flicker", 25 | style: { 26 | "--point-width": attrs?.width ?? "12px", 27 | "--point-height": attrs?.height ?? "12px", 28 | "--point-background": 29 | attrs?.background ?? "var(--el-color-primary)", 30 | "--point-border-radius": attrs?.borderRadius ?? "50%", 31 | "--point-scale": attrs?.scale ?? "2" 32 | } 33 | }, 34 | { 35 | default: () => [] 36 | } 37 | ); 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 nineven 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 | -------------------------------------------------------------------------------- /mock/asyncRoutes.ts: -------------------------------------------------------------------------------- 1 | // 模拟后端动态生成路由 2 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 3 | 4 | /** 5 | * roles:页面级别权限,这里模拟二种 "admin"、"common" 6 | * admin:管理员角色 7 | * common:普通角色 8 | */ 9 | 10 | const permissionRouter = { 11 | path: "/permission", 12 | meta: { 13 | title: "menus.permission", 14 | icon: "lollipop", 15 | rank: 10 16 | }, 17 | children: [ 18 | { 19 | path: "/permission/page/index", 20 | name: "PermissionPage", 21 | meta: { 22 | title: "menus.permissionPage", 23 | roles: ["admin", "common"] 24 | } 25 | }, 26 | { 27 | path: "/permission/button/index", 28 | name: "PermissionButton", 29 | meta: { 30 | title: "menus.permissionButton", 31 | roles: ["admin", "common"], 32 | auths: ["btn_add", "btn_edit", "btn_delete"] 33 | } 34 | } 35 | ] 36 | }; 37 | 38 | export default defineFakeRoute([ 39 | { 40 | url: "/get-async-routes", 41 | method: "get", 42 | response: () => { 43 | return { 44 | success: true, 45 | data: [permissionRouter] 46 | }; 47 | } 48 | } 49 | ]); 50 | -------------------------------------------------------------------------------- /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/propTypes.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties, VNodeChild } from "vue"; 2 | import { 3 | createTypes, 4 | toValidableType, 5 | type VueTypesInterface, 6 | type 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/directives/copy/index.ts: -------------------------------------------------------------------------------- 1 | import { message } from "@/utils/message"; 2 | import { useEventListener } from "@vueuse/core"; 3 | import { copyTextToClipboard } from "@pureadmin/utils"; 4 | import type { Directive, DirectiveBinding } from "vue"; 5 | 6 | export interface CopyEl extends HTMLElement { 7 | copyValue: string; 8 | } 9 | 10 | /** 文本复制指令(默认双击复制) */ 11 | export const copy: Directive = { 12 | mounted(el: CopyEl, binding: DirectiveBinding) { 13 | const { value } = binding; 14 | if (value) { 15 | el.copyValue = value; 16 | const arg = binding.arg ?? "dblclick"; 17 | // Register using addEventListener on mounted, and removeEventListener automatically on unmounted 18 | useEventListener(el, arg, () => { 19 | if (copyTextToClipboard(el.copyValue)) { 20 | message("复制成功", { type: "success" }); 21 | } else { 22 | message("复制失败", { type: "error" }); 23 | } 24 | }); 25 | } else { 26 | // throw new Error( 27 | // '[Directive: copy]: need value! Like v-copy="modelValue"' 28 | // ); 29 | } 30 | }, 31 | updated(el: CopyEl, binding: DirectiveBinding) { 32 | el.copyValue = binding.value; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/RePlusPage/src/utils/public.tsx: -------------------------------------------------------------------------------- 1 | import { computed } from "vue"; 2 | import { useDark } from "@pureadmin/utils"; 3 | 4 | export const usePublicHooks = () => { 5 | const { isDark } = useDark(); 6 | 7 | const switchStyle = computed(() => { 8 | return { 9 | "--el-switch-on-color": "#6abe39", 10 | "--el-switch-off-color": "#e84749" 11 | }; 12 | }); 13 | 14 | const tagStyle = computed(() => { 15 | return (status: boolean) => { 16 | return status 17 | ? { 18 | "--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d", 19 | "--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed", 20 | "--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f" 21 | } 22 | : { 23 | "--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322", 24 | "--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0", 25 | "--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e" 26 | }; 27 | }; 28 | }); 29 | 30 | return { 31 | /** 当前网页是否为`dark`模式 */ 32 | isDark, 33 | /** 表现更鲜明的`el-switch`组件 */ 34 | switchStyle, 35 | /** 表现更鲜明的`el-tag`组件 */ 36 | tagStyle 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/RePlusSearch/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { BaseApi } from "@/api/base"; 2 | import type { RecordType } from "plus-pro-components"; 3 | import type { PaginationProps } from "@pureadmin/table"; 4 | import type { ComputedRef, Ref } from "vue"; 5 | import type { PageTableColumn } from "@/components/RePlusPage"; 6 | import type { PageColumn } from "@/components/RePlusPage"; 7 | 8 | interface PlusSearchProps { 9 | api: Partial; 10 | isTree?: boolean; 11 | multiple?: boolean; 12 | localeName?: string; 13 | searchColumnsFormat?: (columns: PageColumn[]) => PageColumn[]; 14 | listColumnsFormat?: (columns: PageTableColumn[]) => PageTableColumn[]; 15 | baseColumnsFormat?: ({ 16 | listColumns, 17 | detailColumns, 18 | searchColumns, 19 | addOrEditRules, 20 | addOrEditColumns, 21 | searchDefaultValue, 22 | addOrEditDefaultValue 23 | }) => void; 24 | pagination?: Partial & { 25 | size?: string; 26 | }; 27 | valueProps?: { 28 | label: 29 | | string 30 | | Ref 31 | | ComputedRef 32 | | ((row: RecordType) => string | Ref | ComputedRef); 33 | value?: string; 34 | }; 35 | } 36 | 37 | export type { PlusSearchProps }; 38 | -------------------------------------------------------------------------------- /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 | }, 32 | { 33 | default: () => [ 34 | h("use", { 35 | "xlink:href": `#${this.icon}` 36 | }) 37 | ] 38 | } 39 | ); 40 | } else { 41 | return h("i", { 42 | class: `iconfont ${this.icon}`, 43 | ...attrs 44 | }); 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/utils/http/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AxiosError, 3 | AxiosRequestConfig, 4 | AxiosResponse, 5 | Method 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 | 38 | post( 39 | url: string, 40 | params?: P, 41 | config?: PureHttpRequestConfig 42 | ): Promise; 43 | 44 | get( 45 | url: string, 46 | params?: P, 47 | config?: PureHttpRequestConfig 48 | ): Promise; 49 | } 50 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordName } from "vue-router"; 2 | import type { WS } from "@/utils/websocket"; 3 | 4 | export type cacheType = { 5 | mode: string; 6 | name?: RouteRecordName; 7 | }; 8 | 9 | export type positionType = { 10 | startIndex?: number; 11 | length?: number; 12 | }; 13 | 14 | export type appType = { 15 | sidebar: { 16 | opened: boolean; 17 | withoutAnimation: boolean; 18 | // 判断是否手动点击Collapse 19 | isClickCollapse: boolean; 20 | }; 21 | layout: string; 22 | device: string; 23 | viewportSize: { width: number; height: number }; 24 | sortSwap: boolean; 25 | }; 26 | 27 | export type multiType = { 28 | path: string; 29 | name: string; 30 | meta: any; 31 | query?: object; 32 | params?: object; 33 | }; 34 | 35 | export type setType = { 36 | title: string; 37 | fixedHeader: boolean; 38 | hiddenSideBar: boolean; 39 | }; 40 | 41 | export type userType = { 42 | avatar?: string; 43 | username?: string; 44 | nickname?: string; 45 | email?: string; 46 | phone?: string; 47 | roles?: Array; 48 | verifyCodeLength?: number; 49 | currentPage?: number; 50 | isRemembered?: boolean; 51 | loginDay?: number; 52 | noticeCount?: number; 53 | websocket?: WS | null; 54 | clear?: any; 55 | }; 56 | -------------------------------------------------------------------------------- /src/directives/ripple/index.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable-next-line scss/dollar-variable-colon-space-after */ 2 | $ripple-animation-transition-in: 3 | transform 0.4s cubic-bezier(0, 0, 0.2, 1), 4 | opacity 0.2s cubic-bezier(0, 0, 0.2, 1) !default; 5 | $ripple-animation-transition-out: opacity 0.5s cubic-bezier(0, 0, 0.2, 1) !default; 6 | $ripple-animation-visible-opacity: 0.25 !default; 7 | 8 | .v-ripple { 9 | &__container { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | z-index: 0; 14 | width: 100%; 15 | height: 100%; 16 | contain: strict; 17 | overflow: hidden; 18 | pointer-events: none; 19 | border-radius: inherit; 20 | } 21 | 22 | &__animation { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | overflow: hidden; 27 | pointer-events: none; 28 | background: currentcolor; 29 | border-radius: 50%; 30 | opacity: 0; 31 | will-change: transform, opacity; 32 | 33 | &--enter { 34 | opacity: 0; 35 | transition: none; 36 | } 37 | 38 | &--in { 39 | opacity: $ripple-animation-visible-opacity; 40 | transition: $ripple-animation-transition-in; 41 | } 42 | 43 | &--out { 44 | opacity: 0; 45 | transition: $ripple-animation-transition-out; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "strict": false, 7 | "strictFunctionTypes": false, 8 | "noImplicitThis": true, 9 | "jsx": "preserve", 10 | "importHelpers": true, 11 | "experimentalDecorators": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "isolatedModules": true, 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "sourceMap": true, 18 | "baseUrl": ".", 19 | "allowJs": false, 20 | "resolveJsonModule": true, 21 | "lib": [ 22 | "ESNext", 23 | "DOM" 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "src/*" 28 | ], 29 | "@build/*": [ 30 | "build/*" 31 | ] 32 | }, 33 | "types": [ 34 | "node", 35 | "vite/client", 36 | "element-plus/global", 37 | "@pureadmin/table/volar", 38 | "unplugin-icons/types/vue" 39 | ] 40 | }, 41 | "include": [ 42 | "mock/*.ts", 43 | "src/**/*.ts", 44 | "src/**/*.tsx", 45 | "src/**/*.vue", 46 | "types/*.d.ts", 47 | "vite.config.ts" 48 | ], 49 | "exclude": [ 50 | "dist", 51 | "**/*.js", 52 | "node_modules" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/components/RePlusPage/src/components/JsonInput.vue: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 46 | -------------------------------------------------------------------------------- /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里本地使用(用于内网环境) 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 | if (typeof this.icon === "string") { 17 | return h( 18 | IconifyIcon, 19 | { 20 | icon: this.icon, 21 | "aria-hidden": false, 22 | style: attrs?.style 23 | ? Object.assign(attrs.style, { outline: "none" }) 24 | : { outline: "none" }, 25 | ...attrs 26 | }, 27 | { 28 | default: () => [] 29 | } 30 | ); 31 | } else { 32 | return h( 33 | this.icon, 34 | { 35 | "aria-hidden": false, 36 | style: attrs?.style 37 | ? Object.assign(attrs.style, { outline: "none" }) 38 | : { outline: "none" }, 39 | ...attrs 40 | }, 41 | { 42 | default: () => [] 43 | } 44 | ); 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/style/tailwind.css: -------------------------------------------------------------------------------- 1 | @layer theme, base, components, utilities; 2 | @import "tailwindcss/theme.css" layer(theme); 3 | @import "tailwindcss/utilities.css" layer(utilities); 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | @theme { 8 | --color-bg_color: var(--el-bg-color); 9 | --color-primary: var(--el-color-primary); 10 | --color-text_color_primary: var(--el-text-color-primary); 11 | --color-text_color_regular: var(--el-text-color-regular); 12 | } 13 | 14 | /* 15 | The default border color has changed to `currentColor` in Tailwind CSS v4, 16 | so we've added these compatibility styles to make sure everything still 17 | looks the same as it did with Tailwind CSS v3. 18 | 19 | If we ever want to remove these styles, we need to add an explicit border 20 | color utility to any element that depends on these defaults. 21 | */ 22 | @layer base { 23 | *, 24 | ::after, 25 | ::before, 26 | ::backdrop, 27 | ::file-selector-button { 28 | border-color: var(--color-gray-200, currentColor); 29 | } 30 | } 31 | 32 | @utility flex-c { 33 | @apply flex justify-center items-center; 34 | } 35 | 36 | @utility flex-ac { 37 | @apply flex justify-around items-center; 38 | } 39 | 40 | @utility flex-bc { 41 | @apply flex justify-between items-center; 42 | } 43 | 44 | @utility navbar-bg-hover { 45 | @apply dark:text-white dark:hover:bg-[#242424]!; 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xadmin-client 2 | 3 | xadmin-基于 Django+vue3 的 rbac 权限管理系统 4 | 5 | 基于 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) 二次开发 6 | 7 | Django 做后端服务 8 | [xadmin-server](https://github.com/nineaiyu/xadmin-server) 9 | 10 | ### 在线预览 11 | 12 | [https://xadmin.dvcloud.xin/](https://xadmin.dvcloud.xin/) 13 | 账号密码:admin/admin123 14 | 15 | ## 开发文档 16 | 17 | [https://docs.dvcloud.xin/](https://docs.dvcloud.xin/) 18 | 19 | ## docker 构建 20 | 21 | 修改 api 配置文件`.env.production`,将 api 域名修改为自己服务器,然后进行构建 22 | 23 | ```shell 24 | docker compose up xadmin-client-build 25 | ``` 26 | 27 | ### docker 启动 28 | 29 | ```shell 30 | docker compose up xadmin-client-prod 31 | ``` 32 | 33 | 然后浏览器 http://localhost:8891 进行访问 34 | 35 | # 本地开发运行,记得在`vite.config.ts` 添加proxy代理,要不然无法访问api服务 36 | 37 | ```ts 38 | proxy: { 39 | "/api": { 40 | target: "http://127.0.0.1:8896", 41 | changeOrigin: true, 42 | rewrite: path => path 43 | }, 44 | "/media": { 45 | target: "http://127.0.0.1:8896", 46 | changeOrigin: true, 47 | rewrite: path => path 48 | }, 49 | "/ws": { 50 | target: "ws://127.0.0.1:8896" 51 | }, 52 | "/api-docs": { 53 | target: "http://127.0.0.1:8896", 54 | changeOrigin: true, 55 | rewrite: path => path 56 | } 57 | }, 58 | ``` 59 | -------------------------------------------------------------------------------- /src/views/welcome/components/ChartLine.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/components/ReCropper/src/svg/change.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/router/modules/remaining.ts: -------------------------------------------------------------------------------- 1 | import { $t } from "@/plugins/i18n"; 2 | 3 | const Layout = () => import("@/layout/index.vue"); 4 | 5 | export default [ 6 | { 7 | path: "/login", 8 | name: "Login", 9 | component: () => import("@/views/login/index.vue"), 10 | meta: { 11 | title: $t("menus.login"), 12 | showLink: false, 13 | rank: 10101 14 | } 15 | }, 16 | { 17 | path: "/redirect", 18 | component: Layout, 19 | meta: { 20 | title: $t("status.hsLoad"), 21 | showLink: false, 22 | rank: 10102 23 | }, 24 | children: [ 25 | { 26 | path: "/redirect/:path(.*)", 27 | name: "Redirect", 28 | component: () => import("@/layout/redirect.vue") 29 | } 30 | ] 31 | }, 32 | // 下面是一个无layout菜单的例子(一个全屏空白页面),因为这种情况极少发生,所以只需要在前端配置即可(配置路径:src/router/modules/remaining.ts) 33 | { 34 | path: "/empty", 35 | name: "Empty", 36 | component: () => import("@/views/empty/index.vue"), 37 | meta: { 38 | title: $t("menus.empty"), 39 | showLink: false, 40 | rank: 10103 41 | } 42 | }, 43 | { 44 | path: "/account-settings", 45 | name: "AccountSettings", 46 | component: () => import("@/views/account/index.vue"), 47 | meta: { 48 | title: $t("menus.accountSettings"), 49 | showLink: false, 50 | rank: 104 51 | } 52 | } 53 | ] satisfies Array; 54 | -------------------------------------------------------------------------------- /src/layout/components/lay-search/components/SearchHistoryItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 | 29 | {{ transformI18n(item.meta?.title) }} 30 | 31 | 37 | 42 | 43 | 44 | 51 | -------------------------------------------------------------------------------- /src/views/system/components/SearchDept.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | { 48 | emit('change', value); 49 | } 50 | " 51 | /> 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/lint-code.yml: -------------------------------------------------------------------------------- 1 | name: Lint Code 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | name: Lint Code 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Install Nodejs 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 23 23 | 24 | - uses: pnpm/action-setup@v3 25 | name: Install pnpm 26 | id: pnpm-install 27 | with: 28 | version: 10 29 | run_install: false 30 | 31 | - name: Get pnpm store directory 32 | id: pnpm-cache 33 | shell: bash 34 | run: | 35 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 36 | 37 | - uses: actions/cache@v4 38 | name: Setup pnpm cache 39 | with: 40 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 41 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 42 | restore-keys: | 43 | ${{ runner.os }}-pnpm-store- 44 | 45 | - name: Start Lint Code 46 | run: | 47 | pnpm install --no-frozen-lockfile 48 | pnpm lint 49 | pnpm typecheck 50 | env: 51 | VALIDATE_ALL_CODEBASE: false 52 | DEFAULT_BRANCH: main 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /src/api/types.ts: -------------------------------------------------------------------------------- 1 | export type ChoicesResult = { 2 | detail: string; 3 | code: number; 4 | choices_dict: object | any; 5 | }; 6 | export type DataListResult = { 7 | detail: string; 8 | code: number; 9 | data: Array; 10 | }; 11 | 12 | export type SearchFieldsResult = { 13 | detail: string; 14 | code: number; 15 | data: Array<{ 16 | key: string; 17 | label: string; 18 | input_type: string | any; 19 | help_text?: string; 20 | default?: string | number | any; 21 | choices?: Array; 22 | }>; 23 | }; 24 | 25 | export type SearchColumnsResult = { 26 | detail: string; 27 | code: number; 28 | data: Array<{ 29 | key: string; 30 | label: string; 31 | input_type: string | any; 32 | required: boolean; 33 | read_only: boolean; 34 | write_only: boolean; 35 | max_length?: number | any; 36 | multiple?: boolean; 37 | table_show?: number; 38 | help_text?: string; 39 | default?: string | number | any; 40 | choices?: Array; 41 | }>; 42 | }; 43 | 44 | export type ListResult = { 45 | detail: string; 46 | code: number; 47 | data: { 48 | /** 列表数据 */ 49 | results: Array; 50 | /** 总条目数 */ 51 | total?: number; 52 | }; 53 | }; 54 | 55 | export type DetailResult = { 56 | detail: string; 57 | code: number; 58 | data: object | any; 59 | }; 60 | 61 | export type BaseResult = { 62 | detail: string; 63 | code: number; 64 | }; 65 | -------------------------------------------------------------------------------- /.github/workflows/build-image.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Image 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build-and-push: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v3 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | 23 | - name: Login to DockerHub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | 29 | - name: Extract repository name 30 | id: repo 31 | run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV 32 | 33 | - name: Extract metadata (tags, labels) for Docker 34 | id: meta 35 | uses: docker/metadata-action@v5 36 | with: 37 | images: nineaiyu/${{ env.REPO }} 38 | tags: | 39 | type=semver,pattern={{version}} 40 | 41 | - name: Build and push multi-arch image 42 | uses: docker/build-push-action@v6 43 | with: 44 | build-args: VERSION=${{ steps.meta.outputs.version }} 45 | platforms: linux/amd64,linux/arm64 46 | push: true 47 | file: Dockerfile 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | -------------------------------------------------------------------------------- /src/store/modules/epTheme.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { 3 | getConfig, 4 | responsiveStorageNameSpace, 5 | storageLocal, 6 | store 7 | } from "../utils"; 8 | 9 | export const useEpThemeStore = defineStore("pure-epTheme", { 10 | state: () => ({ 11 | epThemeColor: 12 | storageLocal().getItem( 13 | `${responsiveStorageNameSpace()}layout` 14 | )?.epThemeColor ?? getConfig().EpThemeColor, 15 | epTheme: 16 | storageLocal().getItem( 17 | `${responsiveStorageNameSpace()}layout` 18 | )?.theme ?? getConfig().Theme 19 | }), 20 | getters: { 21 | getEpThemeColor(state) { 22 | return state.epThemeColor; 23 | }, 24 | /** 用于mix导航模式下hamburger-svg的fill属性 */ 25 | fill(state) { 26 | if (state.epTheme === "light") { 27 | return "#409eff"; 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/api/system/user.ts: -------------------------------------------------------------------------------- 1 | import { BaseApi } from "@/api/base"; 2 | import type { BaseResult } from "@/api/types"; 3 | import type { PureHttpRequestConfig } from "@/utils/http/types"; 4 | import { http } from "@/utils/http"; 5 | 6 | class UserApi extends BaseApi { 7 | upload = ( 8 | pk?: number | string, 9 | data?: object, 10 | action?: string, 11 | config?: PureHttpRequestConfig 12 | ) => { 13 | return http.upload( 14 | `${this.baseApi}/${pk}/${action ?? "upload"}`, 15 | {}, 16 | data, 17 | config 18 | ); 19 | }; 20 | resetPassword = (pk: number | string, data?: object) => { 21 | return this.request( 22 | "post", 23 | {}, 24 | data, 25 | `${this.baseApi}/${pk}/reset-password` 26 | ); 27 | }; 28 | 29 | empower = (pk: number | string, data?: object) => { 30 | return this.request( 31 | "post", 32 | {}, 33 | data, 34 | `${this.baseApi}/${pk}/empower` 35 | ); 36 | }; 37 | unblock = (pk: number | string, data?: object) => { 38 | return this.request( 39 | "post", 40 | {}, 41 | data, 42 | `${this.baseApi}/${pk}/unblock` 43 | ); 44 | }; 45 | logout = (pk: number | string, data?: object) => { 46 | return this.request( 47 | "post", 48 | {}, 49 | data, 50 | `${this.baseApi}/${pk}/logout` 51 | ); 52 | }; 53 | } 54 | 55 | export const userApi = new UserApi("/api/system/user"); 56 | -------------------------------------------------------------------------------- /src/views/login/utils/verifyCode.ts: -------------------------------------------------------------------------------- 1 | import type { FormInstance, FormItemProp } from "element-plus"; 2 | import { clone } from "@pureadmin/utils"; 3 | import { ref } from "vue"; 4 | 5 | const isDisabled = ref(false); 6 | const timer = ref(null); 7 | const text = ref(""); 8 | 9 | export const useVerifyCode = () => { 10 | const start = async ( 11 | formEl: FormInstance | undefined, 12 | props: FormItemProp, 13 | time = 60, 14 | callback = null 15 | ) => { 16 | if (!formEl) return; 17 | await formEl.validateField(props, isValid => { 18 | if (isValid) { 19 | if (callback) { 20 | callback(interval); 21 | } else { 22 | interval(time); 23 | } 24 | } 25 | }); 26 | }; 27 | 28 | const interval = (time: number) => { 29 | clearInterval(timer.value); 30 | const initTime = clone(time, true); 31 | isDisabled.value = true; 32 | text.value = `${time}`; 33 | timer.value = setInterval(() => { 34 | if (time > 0) { 35 | time -= 1; 36 | text.value = `${time}`; 37 | } else { 38 | text.value = ""; 39 | isDisabled.value = false; 40 | clearInterval(timer.value); 41 | time = initTime; 42 | } 43 | }, 1000); 44 | }; 45 | 46 | const end = () => { 47 | text.value = ""; 48 | isDisabled.value = false; 49 | clearInterval(timer.value); 50 | }; 51 | 52 | return { 53 | isDisabled, 54 | timer, 55 | text, 56 | start, 57 | end 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/ReSendVerifyCode/src/verifyCode.ts: -------------------------------------------------------------------------------- 1 | import type { FormInstance, FormItemProp } from "element-plus"; 2 | import { clone } from "@pureadmin/utils"; 3 | import { ref } from "vue"; 4 | 5 | const isDisabled = ref(false); 6 | const timer = ref(null); 7 | const text = ref(""); 8 | 9 | export const useVerifyCode = () => { 10 | const start = async ( 11 | formEl: FormInstance | undefined, 12 | props: FormItemProp, 13 | time = 60, 14 | callback = null 15 | ) => { 16 | if (!formEl) return; 17 | await formEl.validateField(props, isValid => { 18 | if (isValid) { 19 | if (callback) { 20 | callback(interval); 21 | } else { 22 | interval(time); 23 | } 24 | } 25 | }); 26 | }; 27 | 28 | const interval = (time: number) => { 29 | clearInterval(timer.value); 30 | const initTime = clone(time, true); 31 | isDisabled.value = true; 32 | text.value = `${time}`; 33 | timer.value = setInterval(() => { 34 | if (time > 0) { 35 | time -= 1; 36 | text.value = `${time}`; 37 | } else { 38 | text.value = ""; 39 | isDisabled.value = false; 40 | clearInterval(timer.value); 41 | time = initTime; 42 | } 43 | }, 1000); 44 | }; 45 | 46 | const end = () => { 47 | text.value = ""; 48 | isDisabled.value = false; 49 | clearInterval(timer.value); 50 | }; 51 | 52 | return { 53 | isDisabled, 54 | timer, 55 | text, 56 | start, 57 | end 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/views/login/utils/rule.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | import { $t, transformI18n } from "@/plugins/i18n"; 4 | import { useUserStoreHook } from "@/store/modules/user"; 5 | 6 | /** 6位数字验证码正则 */ 7 | export const REGEXP_SIX = /^\d{6}$/; 8 | 9 | /** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */ 10 | export const REGEXP_PWD = 11 | /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/; 12 | 13 | /** 登录校验 */ 14 | const loginRules = reactive({ 15 | username: [ 16 | { 17 | required: true, 18 | message: transformI18n($t("login.usernameReg")), 19 | trigger: "blur" 20 | } 21 | ], 22 | password: [ 23 | { 24 | validator: (rule, value, callback) => { 25 | if (value === "") { 26 | callback(new Error(transformI18n($t("login.passwordReg")))); 27 | } else { 28 | callback(); 29 | } 30 | }, 31 | trigger: "blur" 32 | } 33 | ], 34 | captcha_code: [ 35 | { 36 | validator: (rule, value, callback) => { 37 | if (value === "") { 38 | callback(new Error(transformI18n($t("login.verifyCodeReg")))); 39 | } else if (useUserStoreHook().verifyCodeLength !== value.length) { 40 | callback(new Error(transformI18n($t("login.verifyCodeCorrectReg")))); 41 | } else { 42 | callback(); 43 | } 44 | }, 45 | trigger: "blur" 46 | } 47 | ] 48 | }); 49 | 50 | export { loginRules }; 51 | -------------------------------------------------------------------------------- /src/views/system/field/utils/hook.tsx: -------------------------------------------------------------------------------- 1 | import { useI18n } from "vue-i18n"; 2 | import Money from "~icons/ep/money"; 3 | import { getDefaultAuths } from "@/router/utils"; 4 | import { modelLabelFieldApi } from "@/api/system/field"; 5 | import { useRenderIcon } from "@/components/ReIcon/src/hooks"; 6 | import { getCurrentInstance, reactive, type Ref, shallowRef } from "vue"; 7 | import { handleOperation, type OperationProps } from "@/components/RePlusPage"; 8 | 9 | export function useModelField(tableRef: Ref) { 10 | const { t } = useI18n(); 11 | 12 | const api = reactive(modelLabelFieldApi); 13 | 14 | const auth = reactive({ 15 | sync: false, 16 | ...getDefaultAuths(getCurrentInstance(), ["sync"]) 17 | }); 18 | 19 | const tableBarButtonsProps = shallowRef({ 20 | buttons: [ 21 | { 22 | text: t("modelFieldManagement.makeData"), 23 | code: "sync", 24 | props: { 25 | type: "primary", 26 | plain: true, 27 | icon: useRenderIcon(Money) 28 | }, 29 | onClick: ({ loading }) => { 30 | loading.value = true; 31 | handleOperation({ 32 | t, 33 | apiReq: api.sync(), 34 | success() { 35 | tableRef.value.handleGetData(); 36 | }, 37 | requestEnd() { 38 | loading.value = false; 39 | } 40 | }); 41 | }, 42 | show: auth.sync 43 | } 44 | ] 45 | }); 46 | 47 | return { 48 | t, 49 | api, 50 | auth, 51 | tableBarButtonsProps 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /web/acme.sh/notify/feishu.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | #Support feishu webhooks api 4 | 5 | #required 6 | #FEISHU_WEBHOOK="xxxx" 7 | 8 | #optional 9 | #FEISHU_KEYWORD="yyyy" 10 | 11 | # subject content statusCode 12 | feishu_send() { 13 | _subject="$1" 14 | _content="$2" 15 | _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped 16 | _debug "_subject" "$_subject" 17 | _debug "_content" "$_content" 18 | _debug "_statusCode" "$_statusCode" 19 | 20 | FEISHU_WEBHOOK="${FEISHU_WEBHOOK:-$(_readaccountconf_mutable FEISHU_WEBHOOK)}" 21 | if [ -z "$FEISHU_WEBHOOK" ]; then 22 | FEISHU_WEBHOOK="" 23 | _err "You didn't specify a feishu webhooks FEISHU_WEBHOOK yet." 24 | _err "You can get yours from https://www.feishu.cn" 25 | return 1 26 | fi 27 | _saveaccountconf_mutable FEISHU_WEBHOOK "$FEISHU_WEBHOOK" 28 | 29 | FEISHU_KEYWORD="${FEISHU_KEYWORD:-$(_readaccountconf_mutable FEISHU_KEYWORD)}" 30 | if [ "$FEISHU_KEYWORD" ]; then 31 | _saveaccountconf_mutable FEISHU_KEYWORD "$FEISHU_KEYWORD" 32 | fi 33 | 34 | _content=$(echo "$_content" | _json_encode) 35 | _subject=$(echo "$_subject" | _json_encode) 36 | _data="{\"msg_type\": \"text\", \"content\": {\"text\": \"[$FEISHU_KEYWORD]\n$_subject\n$_content\"}}" 37 | 38 | response="$(_post "$_data" "$FEISHU_WEBHOOK" "" "POST" "application/json")" 39 | 40 | if [ "$?" = "0" ] && _contains "$response" "StatusCode\":0"; then 41 | _info "feishu webhooks event fired success." 42 | return 0 43 | fi 44 | 45 | _err "feishu webhooks event fired error." 46 | _err "$response" 47 | return 1 48 | } 49 | -------------------------------------------------------------------------------- /src/style/login.css: -------------------------------------------------------------------------------- 1 | .wave { 2 | position: fixed; 3 | height: 100%; 4 | width: 80%; 5 | left: 0; 6 | bottom: 0; 7 | z-index: -1; 8 | } 9 | 10 | .login-container { 11 | width: 100vw; 12 | height: 100vh; 13 | max-width: 100%; 14 | display: grid; 15 | grid-template-columns: repeat(2, 1fr); 16 | grid-gap: 18rem; 17 | padding: 0 2rem; 18 | } 19 | 20 | .img { 21 | display: flex; 22 | justify-content: flex-end; 23 | align-items: center; 24 | } 25 | 26 | .img img { 27 | width: 500px; 28 | } 29 | 30 | .login-box { 31 | display: flex; 32 | align-items: center; 33 | text-align: center; 34 | overflow: hidden; 35 | } 36 | 37 | .login-form { 38 | width: 360px; 39 | } 40 | 41 | .avatar { 42 | width: 350px; 43 | height: 80px; 44 | } 45 | 46 | .login-form h2 { 47 | text-transform: uppercase; 48 | margin: 15px 0; 49 | color: #999; 50 | font: 51 | bold 200% Consolas, 52 | Monaco, 53 | monospace; 54 | } 55 | 56 | @media screen and (max-width: 1180px) { 57 | .login-container { 58 | grid-gap: 9rem; 59 | } 60 | 61 | .login-form { 62 | width: 290px; 63 | } 64 | 65 | .login-form h2 { 66 | font-size: 2.4rem; 67 | margin: 8px 0; 68 | } 69 | 70 | .img img { 71 | width: 360px; 72 | } 73 | 74 | .avatar { 75 | width: 280px; 76 | height: 80px; 77 | } 78 | } 79 | 80 | @media screen and (max-width: 968px) { 81 | .wave { 82 | display: none; 83 | } 84 | 85 | .img { 86 | display: none; 87 | } 88 | 89 | .login-container { 90 | grid-template-columns: 1fr; 91 | } 92 | 93 | .login-box { 94 | justify-content: center; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/sso.ts: -------------------------------------------------------------------------------- 1 | import { removeToken, setToken } from "./auth"; 2 | import { getQueryMap, subBefore } from "@pureadmin/utils"; 3 | import type { TokenInfo } from "@/api/auth"; 4 | 5 | /** 6 | * 简版前端单点登录,根据实际业务自行编写 7 | * 划重点: 8 | * 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理 9 | * 1.清空本地旧信息; 10 | * 2.获取url中的重要参数信息,然后通过 setToken 保存在本地; 11 | * 3.删除不需要显示在 url 的参数 12 | * 4.使用 window.location.replace 跳转正确页面 13 | */ 14 | (function () { 15 | // 获取 url 中的参数 16 | const params = getQueryMap(location.href) as TokenInfo; 17 | const must = ["username", "roles", "accessToken"]; 18 | const mustLength = must.length; 19 | if (Object.keys(params).length !== mustLength) return; 20 | 21 | // url 参数满足 must 里的全部值,才判定为单点登录,避免非单点登录时刷新页面无限循环 22 | let sso = []; 23 | let start = 0; 24 | 25 | while (start < mustLength) { 26 | if (Object.keys(params).includes(must[start]) && sso.length <= mustLength) { 27 | sso.push(must[start]); 28 | } else { 29 | sso = []; 30 | } 31 | start++; 32 | } 33 | 34 | if (sso.length === mustLength) { 35 | // 判定为单点登录 36 | 37 | // 清空本地旧信息 38 | removeToken(); 39 | 40 | // 保存新信息到本地 41 | setToken(params); 42 | 43 | // 删除不需要显示在 url 的参数 44 | delete params.refresh; 45 | delete params.access; 46 | 47 | const newUrl = `${location.origin}${location.pathname}${subBefore( 48 | location.hash, 49 | "?" 50 | )}?${JSON.stringify(params) 51 | .replace(/["{}]/g, "") 52 | .replace(/:/g, "=") 53 | .replace(/,/g, "&")}`; 54 | 55 | // 替换历史记录项 56 | window.location.replace(newUrl); 57 | } else { 58 | return; 59 | } 60 | })(); 61 | -------------------------------------------------------------------------------- /src/components/ReText/src/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 58 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/components/ReTypeit/src/index.tsx: -------------------------------------------------------------------------------- 1 | import type { El } from "typeit/dist/types"; 2 | import TypeIt, { type Options as TypeItOptions } from "typeit"; 3 | import { defineComponent, onMounted, type PropType, ref } from "vue"; 4 | 5 | // 打字机效果组件(配置项详情请查阅 https://www.typeitjs.com/docs/vanilla/usage#options) 6 | export default defineComponent({ 7 | name: "TypeIt", 8 | props: { 9 | options: { 10 | type: Object as PropType, 11 | default: () => ({}) as TypeItOptions 12 | } 13 | }, 14 | setup(props, { slots, expose }) { 15 | /** 16 | * 输出错误信息 17 | * @param message 错误信息 18 | */ 19 | function throwError(message: string) { 20 | throw new TypeError(message); 21 | } 22 | 23 | /** 24 | * 获取浏览器默认语言 25 | */ 26 | function getBrowserLanguage() { 27 | return navigator.language; 28 | } 29 | 30 | const typedItRef = ref(null); 31 | 32 | onMounted(() => { 33 | const $typed = typedItRef.value!.querySelector(".type-it") as El; 34 | 35 | if (!$typed) { 36 | const errorMsg = 37 | getBrowserLanguage() === "zh-CN" 38 | ? "请确保有且只有一个具有class属性为 'type-it' 的元素" 39 | : "Please make sure that there is only one element with a Class attribute with 'type-it'"; 40 | throwError(errorMsg); 41 | } 42 | 43 | const typeIt = new TypeIt($typed, props.options).go(); 44 | 45 | expose({ 46 | typeIt 47 | }); 48 | }); 49 | 50 | return () => ( 51 | 52 | {slots.default?.() ?? } 53 | 54 | ); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /src/views/system/components/SearchMenu.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | { 51 | emit('change', value); 52 | } 53 | " 54 | /> 55 | 56 | -------------------------------------------------------------------------------- /src/views/welcome/components/ChartRound.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/views/user/info/components/avatar.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 44 | 45 | 46 | 47 | 48 | 53 | 54 | {{ t("buttons.save") }} 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 32 | 404 33 | 34 | 49 | {{ t("error.error404") }} 50 | 51 | 67 | {{ t("error.goBack") }} 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/views/error/403.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 33 | 403 34 | 35 | 50 | {{ t("error.error403") }} 51 | 52 | 68 | {{ t("error.goBack") }} 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/views/error/500.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 33 | 500 34 | 35 | 50 | {{ t("error.error500") }} 51 | 52 | 68 | {{ t("error.goBack") }} 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/views/user/info/utils/rule.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | import { $t, transformI18n } from "@/plugins/i18n"; 4 | import { isEmail, isPhone } from "@pureadmin/utils"; 5 | 6 | /** 自定义表单规则校验 */ 7 | export const formRules = reactive({ 8 | username: [ 9 | { 10 | required: true, 11 | message: transformI18n($t("userinfo.username")), 12 | trigger: "blur" 13 | } 14 | ], 15 | password: [ 16 | { 17 | required: true, 18 | message: transformI18n($t("userinfo.verifyPassword")), 19 | trigger: "blur" 20 | } 21 | ], 22 | nickname: [ 23 | { 24 | required: true, 25 | message: transformI18n($t("userinfo.nickname")), 26 | trigger: "blur" 27 | } 28 | ], 29 | gender: [ 30 | { 31 | required: true, 32 | message: transformI18n($t("userinfo.gender")), 33 | trigger: "blur" 34 | } 35 | ], 36 | phone: [ 37 | { 38 | validator: (rule, value, callback) => { 39 | if (value === "") { 40 | callback(); 41 | } else if (!isPhone(value)) { 42 | callback(new Error(transformI18n($t("login.phoneCorrectReg")))); 43 | } else { 44 | callback(); 45 | } 46 | }, 47 | trigger: "blur" 48 | } 49 | ], 50 | email: [ 51 | { 52 | validator: (rule, value, callback) => { 53 | if (value === "") { 54 | callback(); 55 | } else if (!isEmail(value)) { 56 | callback(new Error(transformI18n($t("login.emailCorrectReg")))); 57 | } else { 58 | callback(); 59 | } 60 | }, 61 | trigger: "blur" 62 | } 63 | ] 64 | }); 65 | -------------------------------------------------------------------------------- /src/views/settings/message/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 38 | 43 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/ReCountTo/src/rebound/rebound.css: -------------------------------------------------------------------------------- 1 | .scroll-num { 2 | width: var(--width, 20px); 3 | height: var(--height, calc(var(--width, 20px) * 1.8)); 4 | color: var(--color, #333); 5 | font-size: var(--height, calc(var(--width, 20px) * 1.1)); 6 | line-height: var(--height, calc(var(--width, 20px) * 1.8)); 7 | text-align: center; 8 | overflow: hidden; 9 | animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards; 10 | } 11 | 12 | ul { 13 | animation: 14 | move 0.3s linear infinite, 15 | bounce-in-down 1s calc(var(--delay) * 1s) forwards; 16 | } 17 | 18 | @keyframes move { 19 | from { 20 | transform: translateY(-90%); 21 | filter: url(#blur); 22 | } 23 | 24 | to { 25 | transform: translateY(1%); 26 | filter: url(#blur); 27 | } 28 | } 29 | 30 | @keyframes bounce-in-down { 31 | from { 32 | transform: translateY(calc(var(--i) * -9.09% - 7%)); 33 | filter: none; 34 | } 35 | 36 | 25% { 37 | transform: translateY(calc(var(--i) * -9.09% + 3%)); 38 | } 39 | 40 | 50% { 41 | transform: translateY(calc(var(--i) * -9.09% - 1%)); 42 | } 43 | 44 | 70% { 45 | transform: translateY(calc(var(--i) * -9.09% + 0.6%)); 46 | } 47 | 48 | 85% { 49 | transform: translateY(calc(var(--i) * -9.09% - 0.3%)); 50 | } 51 | 52 | to { 53 | transform: translateY(calc(var(--i) * -9.09%)); 54 | } 55 | } 56 | 57 | @keyframes enhance-bounce-in-down { 58 | 25% { 59 | transform: translateY(8%); 60 | } 61 | 62 | 50% { 63 | transform: translateY(-4%); 64 | } 65 | 66 | 70% { 67 | transform: translateY(2%); 68 | } 69 | 70 | 85% { 71 | transform: translateY(-1%); 72 | } 73 | 74 | to { 75 | transform: translateY(0); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/responsive.ts: -------------------------------------------------------------------------------- 1 | // 响应式storage 2 | import type { 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: PlatformConfigs) => { 8 | const nameSpace = responsiveStorageNameSpace(); 9 | const configObj = Object.assign( 10 | { 11 | // 国际化 默认中文zh 12 | locale: { 13 | locale: config.Locale ?? "zh" 14 | }, 15 | // layout模式以及主题 16 | layout: { 17 | layout: config.Layout ?? "vertical", 18 | theme: config.Theme ?? "light", 19 | darkMode: config.DarkMode ?? false, 20 | sidebarStatus: config.SidebarStatus ?? true, 21 | epThemeColor: config.EpThemeColor ?? "#409EFF", 22 | themeColor: config.Theme ?? "light", // 主题色(对应项目配置中的主题色,与theme不同的是它不会受到浅色、深色整体风格切换的影响,只会在手动点击主题色时改变) 23 | themeMode: config.ThemeMode ?? "light" // 整体风格(浅色:light、深色:dark、自动:system) 24 | }, 25 | // 项目配置-界面显示 26 | configure: { 27 | grey: config.Grey ?? false, 28 | weak: config.Weak ?? false, 29 | hideTabs: config.HideTabs ?? false, 30 | hideFooter: config.HideFooter ?? true, 31 | showLogo: config.ShowLogo ?? true, 32 | showModel: config.ShowModel ?? "smart", 33 | multiTagsCache: config.MultiTagsCache ?? false, 34 | stretch: config.Stretch ?? false 35 | } 36 | }, 37 | config.MultiTagsCache 38 | ? { 39 | // 默认显示顶级菜单tag 40 | tags: Storage.getData("tags", nameSpace) ?? routerArrays 41 | } 42 | : {} 43 | ); 44 | 45 | app.use(Storage, { nameSpace, memory: configObj }); 46 | }; 47 | -------------------------------------------------------------------------------- /src/views/settings/sms.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/api/system/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | import type { DataListResult } from "@/api/types"; 3 | 4 | type DashBoardResult = { 5 | code: number; 6 | detail: string; 7 | percent: number; 8 | count: number; 9 | results?: Array; 10 | }; 11 | 12 | export const getDashBoardUserLoginTotalApi = (params?: object) => { 13 | return http.request( 14 | "get", 15 | "/api/system/dashboard/user-login-total", 16 | { 17 | params: params 18 | } 19 | ); 20 | }; 21 | 22 | export const getDashBoardUserTotalApi = (params?: object) => { 23 | return http.request( 24 | "get", 25 | "/api/system/dashboard/user-total", 26 | { 27 | params: params 28 | } 29 | ); 30 | }; 31 | 32 | export const getDashBoardUserRegisterTrendApi = (params?: object) => { 33 | return http.request( 34 | "get", 35 | "/api/system/dashboard/user-registered-trend", 36 | { 37 | params: params 38 | } 39 | ); 40 | }; 41 | 42 | export const getDashBoardUserLoginTrendApi = (params?: object) => { 43 | return http.request( 44 | "get", 45 | "/api/system/dashboard/user-login-trend", 46 | { 47 | params: params 48 | } 49 | ); 50 | }; 51 | 52 | export const getDashBoardUserActiveApi = (params?: object) => { 53 | return http.request( 54 | "get", 55 | "/api/system/dashboard/user-active", 56 | { 57 | params: params 58 | } 59 | ); 60 | }; 61 | 62 | export const getDashBoardTodayOperateTotalApi = (params?: object) => { 63 | return http.request( 64 | "get", 65 | "/api/system/dashboard/today-operate-total", 66 | { 67 | params: params 68 | } 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/views/system/components/SearchUser.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | { 55 | emit('change', value); 56 | } 57 | " 58 | /> 59 | 60 | -------------------------------------------------------------------------------- /src/views/system/user/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 38 | 46 | 59 | 60 | 61 | 62 | 67 | -------------------------------------------------------------------------------- /src/components/ReDrawer/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import reDrawer from "./index.vue"; 3 | import { useTimeoutFn } from "@vueuse/core"; 4 | import { withInstall } from "@pureadmin/utils"; 5 | import type { 6 | EventType, 7 | ArgsType, 8 | DrawerProps, 9 | DrawerOptions, 10 | ButtonProps 11 | } from "./type"; 12 | 13 | const drawerStore = ref>([]); 14 | 15 | /** 打开抽屉 */ 16 | const addDrawer = (options: DrawerOptions) => { 17 | const open = () => 18 | drawerStore.value.push(Object.assign(options, { visible: true })); 19 | if (options?.openDelay) { 20 | useTimeoutFn(() => { 21 | open(); 22 | }, options.openDelay); 23 | } else { 24 | open(); 25 | } 26 | }; 27 | 28 | /** 关闭抽屉 */ 29 | const closeDrawer = (options: DrawerOptions, index: number, args?: any) => { 30 | drawerStore.value[index].visible = false; 31 | if (options.closeCallBack) { 32 | options.closeCallBack({ options, index, args }); 33 | } 34 | const closeDelay = options?.closeDelay ?? 200; 35 | useTimeoutFn(() => { 36 | drawerStore.value.splice(index, 1); 37 | }, closeDelay); 38 | }; 39 | 40 | /** 41 | * @description 更改抽屉自身属性值 42 | * @param value 属性值 43 | * @param key 属性,默认`title` 44 | * @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`) 45 | */ 46 | const updateDrawer = (value: any, key = "title", index = 0) => { 47 | drawerStore.value[index][key] = value; 48 | }; 49 | 50 | /** 关闭所有弹框 */ 51 | const closeAllDrawer = () => { 52 | drawerStore.value = []; 53 | }; 54 | 55 | const ReDrawer = withInstall(reDrawer); 56 | 57 | export type { EventType, ArgsType, DrawerOptions, DrawerProps, ButtonProps }; 58 | export { 59 | ReDrawer, 60 | drawerStore, 61 | addDrawer, 62 | closeDrawer, 63 | updateDrawer, 64 | closeAllDrawer 65 | }; 66 | -------------------------------------------------------------------------------- /src/views/system/components/SearchDialog.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 41 | 42 | 43 | (newFormInline.data = data)" 47 | /> 48 | (newFormInline.data = data)" 52 | /> 53 | (newFormInline.data = data)" 57 | /> 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/views/system/config/system/utils/hook.tsx: -------------------------------------------------------------------------------- 1 | import { useI18n } from "vue-i18n"; 2 | import { systemConfigApi } from "@/api/system/config/system"; 3 | import { getDefaultAuths } from "@/router/utils"; 4 | import { getCurrentInstance, reactive, type Ref, shallowRef } from "vue"; 5 | import { handleOperation, type OperationProps } from "@/components/RePlusPage"; 6 | import { useRenderIcon } from "@/components/ReIcon/src/hooks"; 7 | import CircleClose from "~icons/ep/circle-close"; 8 | 9 | export function useSystemConfig(tableRef: Ref) { 10 | const { t } = useI18n(); 11 | 12 | const api = reactive(systemConfigApi); 13 | 14 | const auth = reactive({ 15 | invalid: false, 16 | ...getDefaultAuths(getCurrentInstance(), ["invalid"]) 17 | }); 18 | 19 | const operationButtonsProps = shallowRef({ 20 | width: 250, 21 | buttons: [ 22 | { 23 | text: t("configSystem.invalidCache"), 24 | code: "invalid", 25 | confirm: { title: t("configSystem.confirmInvalid") }, 26 | props: { 27 | type: "danger", 28 | icon: useRenderIcon(CircleClose), 29 | link: true 30 | }, 31 | onClick: ({ row, loading }) => { 32 | loading.value = true; 33 | handleOperation({ 34 | t, 35 | apiReq: api.invalid(row?.pk ?? row?.id), 36 | success() { 37 | tableRef.value.handleGetData(); 38 | }, 39 | requestEnd() { 40 | loading.value = false; 41 | } 42 | }); 43 | }, 44 | show: auth.invalid && 3 45 | }, 46 | { 47 | code: "detail", 48 | show: false 49 | } 50 | ] 51 | }); 52 | 53 | return { 54 | api, 55 | auth, 56 | operationButtonsProps 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/layout/components/lay-sidebar/components/SidebarLeftCollapse.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | 43 | 55 | 56 | 57 | 58 | 68 | --------------------------------------------------------------------------------
{{ t("login.tip") }}
使用场景:需要外嵌平台某个页面,不需要展示菜单导航以及额外模块
32 | 404 33 |
49 | {{ t("error.error404") }} 50 |
33 | 403 34 |
50 | {{ t("error.error403") }} 51 |
33 | 500 34 |
50 | {{ t("error.error500") }} 51 |