├── .env ├── .husky ├── README.md ├── .gitignore ├── commit-msg └── pre-commit ├── .env.staging ├── .env.development ├── .env.production ├── tsconfig.tsbuildinfo ├── .stylelintignore ├── src ├── styles │ ├── tailwind.css │ ├── var │ │ ├── index.scss │ │ ├── element │ │ │ └── theme │ │ │ │ ├── index.scss │ │ │ │ ├── dark.scss │ │ │ │ └── light.scss │ │ └── site-bar.scss │ ├── mixin.scss │ ├── intro.scss │ ├── transition.scss │ ├── theme.scss │ ├── nprogress.scss │ └── index.scss ├── assets │ ├── logo.png │ ├── login │ │ ├── bg.png │ │ └── logo.png │ └── icons │ │ ├── fold.svg │ │ ├── daosanjiao.svg │ │ ├── full_screen_close.svg │ │ ├── full_screen.svg │ │ ├── Vue.svg │ │ ├── link.svg │ │ ├── about.svg │ │ ├── password.svg │ │ ├── guide.svg │ │ ├── locales.svg │ │ ├── components.svg │ │ ├── moon.svg │ │ ├── editor.svg │ │ ├── echarts.svg │ │ └── full_screen_page.svg ├── enum │ ├── locales.ts │ └── role.ts ├── views │ ├── permissions │ │ ├── test-permissions-a │ │ │ └── index.vue │ │ ├── test-permissions-b │ │ │ └── index.vue │ │ └── page │ │ │ └── index.vue │ ├── nested │ │ ├── menu2 │ │ │ └── index.vue │ │ └── menu1 │ │ │ ├── menu1-1 │ │ │ └── index.vue │ │ │ ├── menu1-3 │ │ │ └── index.vue │ │ │ ├── menu1-2 │ │ │ ├── menu1-2-1 │ │ │ │ └── index.vue │ │ │ ├── menu1-2-2 │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ │ └── index.vue │ ├── echarts │ │ └── bar │ │ │ ├── index.vue │ │ │ └── components │ │ │ └── BarRace.vue │ ├── components │ │ ├── count-to │ │ │ └── index.vue │ │ ├── calendar │ │ │ └── index.vue │ │ ├── date │ │ │ └── index.vue │ │ └── seamless-scroll │ │ │ └── index.vue │ ├── functions │ │ ├── guide │ │ │ └── index.vue │ │ ├── docx │ │ │ └── index.vue │ │ └── water-mark │ │ │ ├── hooks │ │ │ └── useWaterMark.ts │ │ │ └── index.vue │ ├── details-page │ │ ├── datails-params │ │ │ └── index.vue │ │ ├── datails-info │ │ │ └── index.vue │ │ ├── index.vue │ │ └── hooks │ │ │ └── useDatailsInfo.ts │ ├── external-link │ │ └── embedded-page │ │ │ └── index.vue │ ├── editor │ │ ├── markdown │ │ │ └── index.vue │ │ ├── rich-text │ │ │ └── index.vue │ │ └── logic-flow │ │ │ ├── index.vue │ │ │ └── adpter-for-turbo.ts │ ├── error │ │ ├── 403.vue │ │ ├── 404.vue │ │ └── 500.vue │ ├── index │ │ └── components │ │ │ ├── PieChart.vue │ │ │ ├── Comment.vue │ │ │ ├── AnalysisChart.vue │ │ │ └── WordCloud.vue │ ├── about │ │ └── index.vue │ └── login │ │ ├── index.vue │ │ └── compoontne │ │ └── form.vue ├── store │ ├── index.ts │ ├── modules │ │ ├── app.ts │ │ └── user.ts │ └── types.ts ├── server │ ├── config.ts │ ├── route.ts │ └── useInfo.ts ├── utils │ ├── axios │ │ ├── errorConfig.ts │ │ ├── axiosConfig.ts │ │ └── axiosStatus.ts │ ├── mitt.ts │ ├── plugin │ │ ├── progress.ts │ │ └── echarts │ │ │ └── index.ts │ ├── slotsHelper.ts │ ├── propTypes.ts │ ├── index.ts │ ├── operate │ │ └── index.ts │ ├── theme │ │ └── transformTheme.ts │ └── waterMark.ts ├── layouts │ ├── empty-layouts │ │ └── index.vue │ ├── redirect │ │ └── index.vue │ └── page-layouts │ │ ├── index.vue │ │ └── components │ │ ├── AppFold │ │ └── index.vue │ │ ├── Sidebar │ │ ├── Link.vue │ │ ├── MinSidebar.vue │ │ ├── Item.vue │ │ └── index.vue │ │ ├── AppMain │ │ └── index.vue │ │ ├── VerticalSidebar │ │ └── index.vue │ │ ├── AppLogo │ │ └── index.vue │ │ ├── SideNavigationBar │ │ └── index.vue │ │ ├── Seting │ │ └── pageSettings │ │ │ └── index.vue │ │ └── AppTabs │ │ └── hooks │ │ └── useTabsChange.ts ├── App.vue ├── components │ ├── Application │ │ ├── index.ts │ │ ├── AppLocale.vue │ │ ├── AppAccount.vue │ │ └── AppTheme.vue │ ├── TsxComponents.tsx │ ├── CountTo │ │ ├── src │ │ │ ├── rebound │ │ │ │ └── props.ts │ │ │ └── normal │ │ │ │ └── props.ts │ │ └── index.vue │ ├── Table │ │ ├── index.vue │ │ ├── types │ │ │ └── table.ts │ │ └── src │ │ │ └── components │ │ │ └── TableChild.tsx │ ├── Form │ │ ├── componentMap.ts │ │ ├── types │ │ │ └── from.ts │ │ ├── src │ │ │ └── components │ │ │ │ └── FormItem.vue │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ └── PreviewDocx │ │ └── index.vue ├── hooks │ ├── useCurrentInstance.ts │ ├── setting │ │ └── useRootSetting.ts │ ├── web │ │ ├── useTheme.ts │ │ ├── useMessage.tsx │ │ ├── useDetailsNavigation.ts │ │ ├── useI18n.ts │ │ ├── useSortable.ts │ │ ├── useIntro.ts │ │ └── useECharts.ts │ └── event │ │ └── useEventListener.ts ├── locales │ ├── types.ts │ └── index.ts ├── router │ ├── modules │ │ ├── home │ │ │ └── index.ts │ │ ├── about │ │ │ └── index.ts │ │ ├── echarts │ │ │ └── index.ts │ │ ├── root │ │ │ └── index.ts │ │ ├── externa-link │ │ │ └── index.ts │ │ ├── editor │ │ │ └── index.ts │ │ ├── error │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── details-page │ │ │ └── index.ts │ │ ├── permissions │ │ │ └── index.ts │ │ ├── functions │ │ │ └── index.ts │ │ ├── nested │ │ │ └── index.ts │ │ └── components │ │ │ └── index.ts │ ├── type.ts │ └── index.ts ├── main.ts ├── config │ └── index.ts └── instruct │ └── waterMark.ts ├── k8s └── base │ ├── namespace.yaml │ ├── serviceAccount.yaml │ ├── secret.yaml │ ├── kustomization.yaml │ ├── service.yaml │ ├── hap.yaml │ └── deployment.yaml ├── public ├── pwa │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── browserconfig.xml │ └── site.webmanifest └── serverConfig.json ├── .npmrc ├── .gitignore ├── .prettierignore ├── postcss.config.js ├── prettier.config.js ├── types ├── plugin.d.ts ├── env.d.ts ├── global.d.ts └── axios.d.ts ├── jsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── release.yml │ ├── lint-and-type-checking.yml │ ├── auto-merge.yml │ └── git-pages.yml ├── Dockerfile ├── .vscode ├── extensions.json └── settings.json ├── tests └── TsxComponents.spec.ts ├── .editorconfig ├── tsconfig.node.json ├── eslint.config.js ├── vitest.config.ts ├── default.conf ├── LICENSE ├── tsconfig.json ├── commitlint.config.js ├── vite.config.ts ├── stylelint.config.js └── mock └── account.mock.ts /.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | VITE_ENV = staging -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_ENV = development -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_ENV = production -------------------------------------------------------------------------------- /tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"version":"4.9.4"} -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | dist 2 | public 3 | dist_electron -------------------------------------------------------------------------------- /src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /src/styles/var/index.scss: -------------------------------------------------------------------------------- 1 | @forward './site-bar'; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npx commitlint --edit $1 4 | -------------------------------------------------------------------------------- /src/styles/var/element/theme/index.scss: -------------------------------------------------------------------------------- 1 | @use './light'; 2 | @use './dark'; 3 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/enum/locales.ts: -------------------------------------------------------------------------------- 1 | export enum LocalesEnum { 2 | EN = 'en', 3 | ZHCN = 'zh-CN', 4 | } 5 | -------------------------------------------------------------------------------- /k8s/base/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: vue-xs-admin 5 | -------------------------------------------------------------------------------- /public/pwa/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/public/pwa/favicon.ico -------------------------------------------------------------------------------- /src/assets/login/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/src/assets/login/bg.png -------------------------------------------------------------------------------- /src/assets/login/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/src/assets/login/logo.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # # 防止npm用不支持的node版本 2 | # engine-strict = true 3 | 4 | # # 偏平化node_modules结构 5 | # shamefully-hoist=true -------------------------------------------------------------------------------- /public/pwa/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/public/pwa/favicon-16x16.png -------------------------------------------------------------------------------- /public/pwa/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/public/pwa/favicon-32x32.png -------------------------------------------------------------------------------- /public/pwa/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/public/pwa/mstile-150x150.png -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin relative { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /public/pwa/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/public/pwa/apple-touch-icon.png -------------------------------------------------------------------------------- /public/pwa/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/public/pwa/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/pwa/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/vue-xs-admin/HEAD/public/pwa/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/styles/var/element/theme/dark.scss: -------------------------------------------------------------------------------- 1 | @forward 'element-plus/theme-chalk/src/dark/var.scss' with ( 2 | $text-color: () 3 | ); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.local 2 | 3 | .DS_Store 4 | package-lock.json 5 | yarn-error.log 6 | 7 | bin 8 | dist 9 | dist_electron 10 | node_modules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .local 2 | .output.js 3 | .npmrc 4 | 5 | **/*.svg 6 | **/*.sh 7 | 8 | dist 9 | public 10 | node_modules 11 | dist_electron -------------------------------------------------------------------------------- /src/views/permissions/test-permissions-a/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | admin 可见 5 | 6 | -------------------------------------------------------------------------------- /src/views/permissions/test-permissions-b/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | test 可见 5 | 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'postcss-import': {}, 4 | '@tailwindcss/postcss': {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | import prettierConfig from '@jsxiaosi/eslint-config-prettier'; 2 | 3 | export default prettierConfig({ 4 | tailwindcss: true, 5 | }); 6 | -------------------------------------------------------------------------------- /src/views/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /types/plugin.d.ts: -------------------------------------------------------------------------------- 1 | // vite-plugin-theme-preprocessor不支持ts,默认导出一个d.ts解决报错 2 | declare module '@zougt/*'; 3 | 4 | declare module 'editor.md/*'; 5 | 6 | declare module '*.js'; 7 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /k8s/base/serviceAccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: vue-xs-admin-sa 5 | namespace: vue-xs-admin 6 | imagePullSecrets: 7 | - name: jsxiaosi-docker 8 | -------------------------------------------------------------------------------- /src/enum/role.ts: -------------------------------------------------------------------------------- 1 | export enum RoleEnum { 2 | ADMIN = 'admin', 3 | TEST = 'test', 4 | } 5 | 6 | export enum PermissionMode { 7 | REAREND = 'REAREND', 8 | ROLE = 'ROLE', 9 | } 10 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | LABEL description="Nginx vue-xs-admin" 4 | 5 | COPY dist /usr/share/nginx/html 6 | 7 | COPY default.conf /etc/nginx/conf.d/ 8 | 9 | CMD [ "nginx", "-g", "daemon off;" ] 10 | 11 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /k8s/base/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | .dockerconfigjson: DOKER_CONFIG_JSON_BASE64_ENCODED_CONTENT 4 | kind: Secret 5 | metadata: 6 | name: jsxiaosi-docker 7 | namespace: vue-xs-admin 8 | type: kubernetes.io/dockerconfigjson 9 | -------------------------------------------------------------------------------- /src/assets/icons/fold.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import type { App } from 'vue'; 3 | 4 | const store = createPinia(); 5 | 6 | export const configMainStore = (app: App) => { 7 | app.use(createPinia()); 8 | }; 9 | 10 | export { store }; 11 | -------------------------------------------------------------------------------- /src/views/echarts/bar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/server/config.ts: -------------------------------------------------------------------------------- 1 | import type { AppConfig } from '@/store/types'; 2 | import { deffHttp } from '@/utils/axios'; 3 | 4 | enum Api { 5 | ROUTE_CONFIG_INFO = '/serverConfig.json', 6 | } 7 | 8 | export const getConfigInfo = () => deffHttp.get({ url: Api.ROUTE_CONFIG_INFO }); 9 | -------------------------------------------------------------------------------- /k8s/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: vue-xs-admin 4 | resources: 5 | - ./namespace.yaml 6 | - ./secret.yaml 7 | - ./serviceAccount.yaml 8 | - ./deployment.yaml 9 | - ./hap.yaml 10 | - ./service.yaml 11 | -------------------------------------------------------------------------------- /types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare interface ViteEnv { 4 | readonly VITE_ENV: string; 5 | } 6 | 7 | interface ImportMetaEnv extends ViteEnv { 8 | _: unknown; 9 | } 10 | 11 | interface ImportMeta { 12 | readonly env: ImportMetaEnv; 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "bradlc.vscode-tailwindcss", 5 | "syler.sass-indented", 6 | "esbenp.prettier-vscode", 7 | "lokalise.i18n-ally", 8 | "dbaeumer.vscode-eslint", 9 | "donjayamanne.githistory" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue/jsx'; 2 | 3 | declare global { 4 | interface Window {} 5 | 6 | type RefType = T | null; 7 | 8 | type Recordable = Record; 9 | 10 | interface Fn { 11 | (...arg: T[]): R; 12 | } 13 | } 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /tests/TsxComponents.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import { expect, test } from 'vitest'; 3 | import TsxComponents from '../src/components/TsxComponents'; 4 | 5 | test('HelloWorld', () => { 6 | const wrapper = mount(TsxComponents); 7 | expect(wrapper.text()).toContain('admin'); 8 | }); 9 | -------------------------------------------------------------------------------- /public/pwa/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2d89ef 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/utils/axios/errorConfig.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from '#/axios'; 2 | import type { AxiosResponse } from 'axios'; 3 | 4 | export const errorData = (res: AxiosResponse>) => { 5 | return { 6 | data: null, 7 | message: res.data.message, 8 | code: res.data.code, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /k8s/base/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: vue-xs-admin-service 5 | namespace: vue-xs-admin 6 | labels: 7 | app: vue-xs-admin-service 8 | spec: 9 | type: NodePort 10 | ports: 11 | - port: 80 12 | name: vue-xs-admin 13 | selector: 14 | app: vue-xs-admin-pod 15 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "baseUrl": "./", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "types": ["vite/client"], 8 | "allowSyntheticDefaultImports": true 9 | }, 10 | "include": ["vite.config.ts", "build", "types", "mock"] 11 | } 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # 已存在文件只需添加以下命令 2 | # npx eslint --fix --ext src/**/*.{vue} 3 | # npx prettier --write --loglevel warn src/**/*.{js,scss,vue} 4 | # npx stylelint --fix src/**/*.{vue,scss} --cache --cache-location node_modules/.cache/stylelint/ 5 | # npm run lint:eslint 6 | # npm run lint:prettier 7 | # npm run lint:stylelint 8 | npm run lint:staged -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import jsxiaosiConfig from '@jsxiaosi/eslint-config'; 2 | 3 | export default jsxiaosiConfig( 4 | { 5 | vue: true, 6 | prettier: { 7 | usePrettierrc: true, 8 | }, 9 | ignores: ['src/**/china.json'], 10 | }, 11 | { 12 | rules: { 13 | 'no-console': 'off', 14 | }, 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /src/layouts/empty-layouts/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/styles/var/element/theme/light.scss: -------------------------------------------------------------------------------- 1 | /* 修改element颜色变量 */ 2 | @forward 'element-plus/theme-chalk/src/common/var.scss' with ( 3 | $colors: ( 4 | 'primary': ( 5 | 'base': #409eff, 6 | ), 7 | ), 8 | $text-color: ( 9 | 'primary': #909399, 10 | 'regular': #606266, 11 | 'placeholder': #ebeef5, 12 | ) 13 | ); 14 | -------------------------------------------------------------------------------- /src/assets/icons/daosanjiao.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | import type { Emitter } from 'mitt'; 3 | import type { AppRouteRecordRaw } from '@/router/type'; 4 | 5 | interface Events { 6 | siteBarChange: { 7 | routeRaw: AppRouteRecordRaw; 8 | }; 9 | [key: string | symbol]: Recordable; 10 | } 11 | 12 | export const emitter: Emitter = mitt(); 13 | -------------------------------------------------------------------------------- /src/components/Application/index.ts: -------------------------------------------------------------------------------- 1 | import { withInstall } from '@/utils'; 2 | import appAccount from './AppAccount.vue'; 3 | import appLocale from './AppLocale.vue'; 4 | import appTheme from './AppTheme.vue'; 5 | 6 | export const AppLocale = withInstall(appLocale); 7 | export const AppTheme = withInstall(appTheme); 8 | export const AppAccount = withInstall(appAccount); 9 | -------------------------------------------------------------------------------- /src/views/components/count-to/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/hooks/useCurrentInstance.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from 'vue'; 2 | import type { ComponentInternalInstance } from 'vue'; 3 | 4 | export function useCurrentInstance() { 5 | const { appContext } = getCurrentInstance() as ComponentInternalInstance; 6 | const globalProperties = appContext.config.globalProperties; 7 | return { 8 | ...globalProperties, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/views/functions/guide/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 引导页 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/TsxComponents.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref } from 'vue'; 2 | 3 | export default defineComponent({ 4 | name: 'TsxComponents', 5 | setup(_props) { 6 | const dataname = ref('admin'); 7 | return () => ( 8 | <> 9 | TSX:{dataname.value} 10 | 11 | 12 | 13 | > 14 | ); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/plugin/progress.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/rstacruz/nprogress 2 | import NProgress from 'nprogress'; 3 | import '@/styles/nprogress.scss'; 4 | 5 | NProgress.configure({ 6 | easing: 'ease', 7 | // 递增进度条的速度 8 | speed: 400, 9 | // 是否显示加载ico 10 | showSpinner: true, 11 | // 自动递增间隔 12 | trickleSpeed: 300, 13 | // 初始化时的最小百分比 14 | minimum: 0.2, 15 | }); 16 | 17 | export default NProgress; 18 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config'; 2 | import viteConfig from './vite.config'; 3 | 4 | export default defineConfig(configEnv => 5 | mergeConfig( 6 | viteConfig(configEnv), 7 | defineConfig({ 8 | test: { 9 | environment: 'jsdom', 10 | testTransformMode: { 11 | web: ['*.tsx'], 12 | }, 13 | }, 14 | }), 15 | ), 16 | ); 17 | -------------------------------------------------------------------------------- /src/locales/types.ts: -------------------------------------------------------------------------------- 1 | import type { LocalesEnum } from '@/enum/locales'; 2 | import type en from './modules/en.json'; 3 | 4 | export type MessageSchema = typeof en; 5 | 6 | export type localeKey = (typeof LocalesEnum)[keyof typeof LocalesEnum]; 7 | 8 | export interface LocalesType { 9 | name: string; 10 | locale: localeKey; 11 | } 12 | 13 | export type LocaleMessages = { 14 | [K in localeKey]: MessageSchema; 15 | }; 16 | -------------------------------------------------------------------------------- /src/server/route.ts: -------------------------------------------------------------------------------- 1 | import { deffHttp } from '@/utils/axios'; 2 | 3 | enum Api { 4 | ROUTE_LIST = '/mock_api/getRoute', 5 | } 6 | 7 | interface Param { 8 | name: string; 9 | } 10 | 11 | export interface RouteDataItemType { 12 | path: string; 13 | name: string; 14 | children: RouteDataItemType[]; 15 | } 16 | 17 | export const getRouteApi = (data: Param) => deffHttp.post({ url: Api.ROUTE_LIST, data }); 18 | -------------------------------------------------------------------------------- /src/assets/icons/full_screen_close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/CountTo/src/rebound/props.ts: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue'; 2 | import { propTypes } from '@/utils/propTypes'; 3 | export const reboundProps = { 4 | delay: propTypes.number.def(1), 5 | blur: propTypes.number.def(2), 6 | i: { 7 | type: Number as PropType, 8 | required: false, 9 | default: 0, 10 | validator(value: number) { 11 | return value < 10 && value >= 0 && Number.isInteger(value); 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/assets/icons/full_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /types/axios.d.ts: -------------------------------------------------------------------------------- 1 | export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined; 2 | export interface RequestOptions { 3 | // 网址前缀 留空使用默认 4 | urlPrefix?: string; 5 | // 设置token 6 | specialToken?: string; 7 | // 是否开启自定义请求报错提示 8 | errorMassge?: boolean; 9 | // 是否携带token 10 | withToken?: boolean; 11 | // 错误消息提示类型 12 | errorMessageMode?: ErrorMessageMode; 13 | } 14 | export interface Result { 15 | code: number; 16 | message: string; 17 | data: T; 18 | } 19 | -------------------------------------------------------------------------------- /src/layouts/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/utils/slotsHelper.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@jsxiaosi/utils'; 2 | import type { Slots } from 'vue'; 3 | 4 | export function getSlot(slots: Slots, slot = 'default', data?: T) { 5 | if (!slots || !Reflect.has(slots, slot)) { 6 | return null; 7 | } 8 | if (!isFunction(slots[slot])) { 9 | console.error(`${slot} is not a function!`); 10 | return null; 11 | } 12 | const slotFn = slots[slot]; 13 | if (!slotFn) return null; 14 | return slotFn(data); 15 | } 16 | -------------------------------------------------------------------------------- /public/serverConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "XsAdmin", 3 | "collapseMenu": false, 4 | "sidebarMode": "vertical", 5 | "themeMode": "dark", 6 | "locale": "zh-CN", 7 | "primaryColor": "#409eff", 8 | "greyMode": false, 9 | "colorWeaknessMode": false, 10 | "hideNavbart": false, 11 | "hideTabs": false, 12 | "closeTabDrag": false, 13 | "tabPersistent": true, 14 | "sidebarFold": "top", 15 | "permissionMode": "REAREND", 16 | "StorageConfig": { 17 | "expire": 0, 18 | "isEncrypt": false 19 | } 20 | } -------------------------------------------------------------------------------- /public/pwa/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-xs-admin", 3 | "short_name": "vue-xs-admin", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/intro.scss: -------------------------------------------------------------------------------- 1 | .custom-intro-tooltip * { 2 | color: black; 3 | 4 | a { 5 | border: 1px solid var(--text-color-primary); 6 | background-color: #{--main-bg-color}; 7 | text-shadow: none; 8 | } 9 | } 10 | 11 | .custom-intro-tooltip { 12 | width: 300px; 13 | border: 1px solid var(--text-color-primary); 14 | background-color: #{--main-bg-color}; 15 | 16 | .introjs-skipbutton { 17 | font-size: var(--font-size-base); 18 | } 19 | } 20 | 21 | .custom-intro-highlight { 22 | border: 1px solid var(--text-color-primary); 23 | } 24 | -------------------------------------------------------------------------------- /src/hooks/setting/useRootSetting.ts: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash-es'; 2 | import { storeToRefs } from 'pinia'; 3 | import { useAppStoreHook } from '@/store/modules/app'; 4 | import type { AppConfig } from '@/store/types'; 5 | 6 | export function useRootSetting() { 7 | const appStore = useAppStoreHook(); 8 | const { appConfigMode } = storeToRefs(appStore); 9 | 10 | function setAppConfigMode(config: Partial) { 11 | appStore.setAppConfigMode(merge(appStore.appConfigMode, config)); 12 | } 13 | 14 | return { appConfig: appConfigMode, setAppConfigMode }; 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/icons/Vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /src/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/details-page/datails-params/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | page-params --- {{ params.id }} 22 | 23 | 24 | -------------------------------------------------------------------------------- /k8s/base/hap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: vue-xs-admin-hpa 5 | namespace: vue-xs-admin 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: vue-xs-admin-deploy 11 | minReplicas: 1 12 | maxReplicas: 5 13 | metrics: 14 | - type: Resource 15 | resource: 16 | name: cpu 17 | target: 18 | type: Utilization 19 | averageUtilization: 80 20 | - type: Resource 21 | resource: 22 | name: memory 23 | target: 24 | type: Utilization 25 | averageUtilization: 80 26 | -------------------------------------------------------------------------------- /src/router/modules/home/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | /* Layout */ 3 | // import Layout from '@/layouts/index.vue' 4 | // import AppMain from '@/layouts/components/AppMain/index.vue' 5 | import type { AppRouteRecordRaw } from '@/router/type'; 6 | 7 | const safeManagerRoutes: Array = [ 8 | { 9 | path: '/welcome', 10 | name: 'RtWelcome', 11 | component: () => import('@/views/index/index.vue'), 12 | meta: { 13 | title: t('route.pathName.index'), 14 | icon: 'iEL-home-filled', 15 | position: 1, 16 | keepAlive: true, 17 | }, 18 | }, 19 | ]; 20 | 21 | export default safeManagerRoutes; 22 | -------------------------------------------------------------------------------- /src/utils/propTypes.ts: -------------------------------------------------------------------------------- 1 | import { createTypes } from 'vue-types'; 2 | import type { CSSProperties, VNodeChild } from 'vue'; 3 | import type { VueTypesInterface, VueTypeValidableDef } from 'vue-types'; 4 | 5 | export type VueNode = VNodeChild | JSX.Element; 6 | 7 | type PropTypes = VueTypesInterface & { 8 | readonly style: VueTypeValidableDef; 9 | readonly VNodeChild: VueTypeValidableDef; 10 | }; 11 | 12 | const propTypes = createTypes({ 13 | func: undefined, 14 | bool: undefined, 15 | string: undefined, 16 | number: undefined, 17 | object: undefined, 18 | integer: undefined, 19 | }) as PropTypes; 20 | 21 | export { propTypes }; 22 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/icons/about.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## PR 标题 2 | 3 | 简要描述本次 Pull Request 的目的或变更内容。 4 | 5 | ## 变更类型 6 | 7 | 请勾选本次 PR 属于以下哪种类型(可多选): 8 | 9 | - [ ] Bug 修复(修复现有问题) 10 | - [ ] 新功能(添加新功能或特性) 11 | - [ ] 文档更新(仅更改文档) 12 | - [ ] 性能优化(提升性能或代码质量) 13 | - [ ] 测试添加(增加或完善测试用例) 14 | - [ ] 重构代码(不影响功能的代码优化) 15 | 16 | ## 相关问题 17 | 18 | 请关联相关的 Issue 编号(如适用): 19 | 20 | - 关联问题:#123 21 | 22 | ## 变更内容描述 23 | 24 | 详细描述本次变更的内容: 25 | 26 | 1. 添加/更新的功能或逻辑。 27 | 2. 影响的模块或文件。 28 | 3. 其他说明信息。 29 | 30 | ## Checklist 31 | 32 | 在提交前,请确保已完成以下事项: 33 | 34 | - [ ] 我的代码遵循项目的编码规范。 35 | - [ ] 我已对代码进行了自测,并确保功能正常。 36 | - [ ] 我的变更不影响现有功能或测试通过。 37 | - [ ] 文档已更新(如适用)。 38 | 39 | ## 其他说明 40 | 41 | 其他需要告知审阅者的信息: 42 | 43 | - 示例:兼容性注意事项、已知问题等。 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | 3 | name: Release 4 | 5 | permissions: 6 | contents: write 7 | 8 | on: 9 | push: 10 | tags: 11 | - 'v*' 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v4 23 | with: 24 | registry-url: https://registry.npmjs.org/ 25 | node-version: lts/* 26 | 27 | - run: npx changelogithub # or changelogithub@0.12 if ensure the stable result 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /src/views/details-page/datails-info/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | page --- {{ query.id }} 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/router/modules/about/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const about: AppRouteRecordRaw[] = [ 5 | { 6 | path: '/about', 7 | redirect: '/about/index', 8 | name: 'RtAdminInfo', 9 | meta: { 10 | title: t('route.pathName.about'), 11 | icon: 'about', 12 | alwaysShow: false, 13 | position: 11, 14 | }, 15 | children: [ 16 | { 17 | path: 'index', 18 | name: 'RtAbout', 19 | component: () => import('@/views/about/index.vue'), 20 | meta: { title: t('route.pathName.about') }, 21 | }, 22 | ], 23 | }, 24 | ]; 25 | 26 | export default about; 27 | -------------------------------------------------------------------------------- /src/server/useInfo.ts: -------------------------------------------------------------------------------- 1 | import type { RoleEnum } from '@/enum/role'; 2 | import { deffHttp } from '@/utils/axios'; 3 | 4 | export interface UseInfoType { 5 | name: string; 6 | userid: string; 7 | email: string; 8 | signature: string; 9 | introduction: string; 10 | title: string; 11 | token: string; 12 | role: RoleEnum; 13 | } 14 | 15 | export interface UserParams { 16 | username: string; 17 | password: string; 18 | } 19 | 20 | export const getUserInfo = (user: string, pwd: string) => 21 | deffHttp.post( 22 | { 23 | url: '/mock_api/login', 24 | data: { username: user, password: pwd }, 25 | }, 26 | { errorMessageMode: 'modal', withToken: false }, 27 | ); 28 | -------------------------------------------------------------------------------- /src/views/details-page/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 详情页{{ item }} 16 | 17 | 18 | 详情页-params-{{ item }} 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "source.fixAll.stylelint": "explicit" 5 | }, 6 | "files.associations": { 7 | "tailwind.css": "tailwindcss" 8 | }, 9 | "css.validate": false, 10 | "less.validate": false, 11 | "scss.validate": false, 12 | "eslint.validate": ["json", "javascript", "typescript", "typescriptreact", "javascriptreact"], 13 | "stylelint.enable": true, 14 | "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"], 15 | "i18n-ally.localesPaths": ["src/locales/modules"], 16 | "i18n-ally.displayLanguage": "zh-CN", 17 | "i18n-ally.enabledFrameworks": ["vue"], 18 | "i18n-ally.sourceLanguage": "zh-CN", 19 | "i18n-ally.keystyle": "nested" 20 | } 21 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { getConfig } from '@/config'; 3 | import { store } from '@/store'; 4 | import type { AppConfig, AppState } from '../types'; 5 | 6 | const useAppStore = defineStore('app', { 7 | state: (): AppState => ({ 8 | appConfigMode: getConfig(), 9 | }), 10 | getters: { 11 | getAppConfigMode(): AppConfig { 12 | return this.appConfigMode; 13 | }, 14 | }, 15 | actions: { 16 | setAppConfigMode(data: AppConfig): void { 17 | const newData = data; 18 | localStorage.setItem('appConfigMode', JSON.stringify(newData)); 19 | this.appConfigMode = newData; 20 | }, 21 | }, 22 | }); 23 | 24 | export function useAppStoreHook() { 25 | return useAppStore(store); 26 | } 27 | -------------------------------------------------------------------------------- /src/views/permissions/page/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 切换权限: 21 | 22 | {{ userInfoStore.roles }} 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/hooks/web/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useColorMode } from '@vueuse/core'; 2 | import { watch } from 'vue'; 3 | import type { AppConfig } from '@/store/types'; 4 | import { updateColor } from '@/utils/theme/transformTheme'; 5 | import { useRootSetting } from '../setting/useRootSetting'; 6 | 7 | export const useTheme = () => { 8 | const color = useColorMode({ disableTransition: false }); 9 | 10 | const { appConfig, setAppConfigMode } = useRootSetting(); 11 | 12 | const toggleDarkMode = () => { 13 | setAppConfigMode({ themeMode: color.value as AppConfig['themeMode'] }); 14 | }; 15 | 16 | watch(color, () => { 17 | toggleDarkMode(); 18 | updateColor(appConfig.value.primaryColor, color.value as AppConfig['themeMode']); 19 | }); 20 | 21 | return { color }; 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-type-checking.yml: -------------------------------------------------------------------------------- 1 | name: Lint and type checking 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | lint-and-type-checking: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Setup pnpm 17 | uses: pnpm/action-setup@v4 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '22' 23 | cache: pnpm 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Run lint check 29 | run: npm run lint:eslint 30 | 31 | - name: Run type check 32 | run: npm run type:check 33 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/AppFold/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 21 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /src/views/functions/docx/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 点击上传 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/router/modules/echarts/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const echarts: AppRouteRecordRaw[] = [ 5 | { 6 | path: '/echarts', 7 | redirect: '/echarts/bar', 8 | name: 'RtEcharts', 9 | meta: { title: t('route.pathName.echarts'), icon: 'echarts', position: 3 }, 10 | children: [ 11 | { 12 | path: 'bar', 13 | name: 'RtBar', 14 | component: () => import('@/views/echarts/bar/index.vue'), 15 | meta: { title: t('route.pathName.echarts_bar') }, 16 | }, 17 | { 18 | path: 'map', 19 | name: 'RtMap', 20 | component: () => import('@/views/echarts/map/index.vue'), 21 | meta: { title: t('route.pathName.echarts_map') }, 22 | }, 23 | ], 24 | }, 25 | ]; 26 | 27 | export default echarts; 28 | -------------------------------------------------------------------------------- /src/assets/icons/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/web/useMessage.tsx: -------------------------------------------------------------------------------- 1 | import { ElMessage, ElMessageBox } from 'element-plus'; 2 | import type { ElMessageBoxOptions } from 'element-plus'; 3 | import { useI18n } from '@/hooks/web/useI18n'; 4 | 5 | const { t } = useI18n(); 6 | 7 | function createElMessageBox(message: string, title: string, options: ElMessageBoxOptions) { 8 | ElMessageBox.confirm(message, title, options) 9 | .then(() => {}) 10 | .catch(() => {}); 11 | } 12 | 13 | function createErrorModal(message: string) { 14 | createElMessageBox(message, t('sys.errorTip'), { 15 | confirmButtonText: t('sys.okText'), 16 | cancelButtonText: t('sys.closeText'), 17 | type: 'error', 18 | }); 19 | } 20 | 21 | function createErrorMsg(message: string) { 22 | ElMessage.error(message); 23 | } 24 | 25 | export function useMessage() { 26 | return { 27 | createErrorModal, 28 | createErrorMsg, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/views/details-page/hooks/useDatailsInfo.ts: -------------------------------------------------------------------------------- 1 | import { useDetailsNavigation } from '@/hooks/web/useDetailsNavigation'; 2 | 3 | export function useDatailsInfo() { 4 | const { openDetails } = useDetailsNavigation(); 5 | 6 | function toDatailsInfo(params: string, model: string) { 7 | if (model === 'query') { 8 | // query 参数详情页 9 | openDetails({ 10 | path: `/details_page/details_info`, 11 | name: `RtDetailsInfo`, 12 | query: { id: `${params}` }, 13 | title: { 'zh-CN': `详情页-${params}`, en: `pageDatails-${params}` }, 14 | }); 15 | } else { 16 | // params 参数详情页 17 | openDetails({ 18 | path: `/details_page/details_params/${params}`, 19 | name: `RtDetailsParams`, 20 | title: { 'zh-CN': `详情页-params-${params}`, en: `pageDatails-${params}` }, 21 | }); 22 | } 23 | } 24 | 25 | return { toDatailsInfo }; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Table/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import messages from '@intlify/unplugin-vue-i18n/messages'; 2 | import { createI18n } from 'vue-i18n'; 3 | import type { App } from 'vue'; 4 | import { LocalesEnum } from '@/enum/locales'; 5 | import type { localeKey, LocaleMessages, LocalesType } from './types'; 6 | 7 | const localesList: LocalesType[] = [ 8 | { 9 | name: 'English', 10 | locale: LocalesEnum.EN, 11 | }, 12 | { 13 | name: '中文简体', 14 | locale: LocalesEnum.ZHCN, 15 | }, 16 | ]; 17 | 18 | const i18n = createI18n({ 19 | legacy: false, 20 | locale: LocalesEnum.ZHCN, 21 | fallbackLocale: LocalesEnum.ZHCN, 22 | messages: messages as LocaleMessages, 23 | }); 24 | 25 | export const configMainI18n = (app: App, locale: localeKey) => { 26 | i18n.global.locale.value = locale; 27 | app.use(i18n); 28 | }; 29 | 30 | export const availableLocales: LocalesType[] = localesList; 31 | 32 | export default i18n; 33 | -------------------------------------------------------------------------------- /src/components/Form/componentMap.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from 'vue'; 2 | // import { ElInput, ElDatePicker, ElCascader, ElSelect, ElCheckbox, ElRadio } from 'element-plus'; 3 | 4 | const componentMap = new Map(); 5 | 6 | // componentMap.set('ElInput', ElInput); 7 | // componentMap.set('ElDatePicker', ElDatePicker); 8 | // componentMap.set('ElCascader', ElCascader); 9 | 10 | // componentMap.set('ElSelect', ElSelect); 11 | // componentMap.set('ElOption', ElSelect.Option); 12 | // componentMap.set('ElCheckbox', ElCheckbox); 13 | // componentMap.set('ElCheckboxGroup', ElCheckbox.CheckboxGroup); 14 | // componentMap.set('ElRadio', ElRadio); 15 | // componentMap.set('ElRadioGroup', ElRadio.RadioGroup); 16 | 17 | const elComponentItem: Recordable = { 18 | ElSelect: 'ElOption', 19 | ElCheckboxGroup: 'ElCheckbox', 20 | ElRadioGroup: 'ElRadio', 21 | }; 22 | 23 | export { componentMap, elComponentItem }; 24 | -------------------------------------------------------------------------------- /src/components/Table/types/table.ts: -------------------------------------------------------------------------------- 1 | import type { RenderRowData, TableColumnCtx } from 'element-plus'; 2 | 3 | export interface TableColumnProps 4 | extends Partial, 'prop' | 'children'>> { 5 | isSlots?: boolean; 6 | prop?: keyof T; 7 | render?: (row: any) => JSX.Element; 8 | render_header?: (row: any) => JSX.Element; 9 | children?: TableColumnProps[]; 10 | } 11 | 12 | // export type TableSlotType = { 13 | // [key in keyof T as `${string & key}_header`]: (props: any) => void; 14 | // }; 15 | 16 | export type TableSlotType = { 17 | [key in keyof T]: (props: RenderRowData) => void; 18 | } & { 19 | [key in keyof T as `${string & key}_header`]: ( 20 | props: Omit, 'row' | 'treeNode' | 'expanded'> & { 21 | customItem: TableColumnProps; 22 | }, 23 | ) => void; 24 | }; 25 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/hooks/web/useDetailsNavigation.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'vue-router'; 2 | import type { LocationQuery } from 'vue-router'; 3 | import type { Meta } from '@/router/type'; 4 | import { usePermissionStoreHook } from '@/store/modules/permission'; 5 | 6 | export interface DetailsNavigationOption { 7 | path: string; 8 | name: string; 9 | title: Meta['title']; 10 | query?: LocationQuery; 11 | } 12 | 13 | export const useDetailsNavigation = () => { 14 | const router = useRouter(); 15 | 16 | const openDetails = (options: DetailsNavigationOption) => { 17 | const { title, ...res } = options; 18 | usePermissionStoreHook().handleMultiTabs('add', { 19 | ...res, 20 | meta: { 21 | title, 22 | }, 23 | }); 24 | try { 25 | router.push({ path: res.path, query: res.query }); 26 | } catch (e) { 27 | console.log(e); 28 | } 29 | }; 30 | 31 | return { 32 | openDetails, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | // fade 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter-from, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | // fade-transform 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all 0.5s; 18 | } 19 | 20 | .fade-transform-enter-from { 21 | transform: translateX(-30px); 22 | opacity: 0; 23 | } 24 | 25 | .fade-transform-leave-to { 26 | transform: translateX(30px); 27 | opacity: 0; 28 | } 29 | 30 | // breadcrumb transition 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all 0.5s; 34 | } 35 | 36 | .breadcrumb-enter-from, 37 | .breadcrumb-leave-active { 38 | transform: translateX(20px); 39 | opacity: 0; 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all 0.5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { App, Plugin } from 'vue'; 2 | import 'virtual:svg-icons-register'; 3 | // import { setWindowAppConfig } from '@/store/modules/app'; 4 | 5 | // 定义全局钩子 6 | export const configMainGlobalProperties = (app: App): void => { 7 | // 全局定义属性 8 | app.config.globalProperties.foo = 'bar'; 9 | /** 10 | * 页面使用方法: 11 | * import { getCurrentInstance } from 'vue'; 12 | * const { proxy } = getCurrentInstance() 13 | * proxy.foo 14 | */ 15 | }; 16 | 17 | export const withInstall = (component: T, alias?: string) => { 18 | const comp = component as Recordable; 19 | comp.install = (app: App) => { 20 | app.component(comp.name || comp.displayName, comp); 21 | if (alias) { 22 | app.config.globalProperties[alias] = component; 23 | } 24 | }; 25 | return component as T & Plugin; 26 | }; 27 | 28 | export const converToArray = (number: number): Array => [...`${number}`].map(el => parseInt(el)); 29 | -------------------------------------------------------------------------------- /src/views/external-link/embedded-page/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 36 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | gzip on; 2 | gzip_static on; 3 | gzip_comp_level 5; # 压缩等级,范围 1-9,越高越费 CPU 4 | gzip_min_length 1024; # 只压缩大于 1KB 的响应体 5 | gzip_buffers 16 8k; # 用于压缩的缓冲区配置 6 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 7 | gzip_vary on; # 添加 Vary: Accept-Encoding 响应头,支持代理缓存区分压缩和未压缩 8 | gzip_disable "msie6"; # 禁用对 IE6 的 gzip,兼容性问题 9 | 10 | server { 11 | listen 80; 12 | server_name localhost; 13 | 14 | root /usr/share/nginx/html; 15 | index index.html index.htm; 16 | 17 | 18 | location ~* \.mjs$ { 19 | add_header Content-Type application/javascript; 20 | } 21 | 22 | location / { 23 | try_files $uri $uri/ /index.html =404; 24 | } 25 | 26 | error_page 500 502 503 504 /500.html; 27 | client_max_body_size 20M; 28 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import { getServerConfig } from './config'; 4 | import { configMainI18n } from './locales'; 5 | import { configMainRouter } from './router'; 6 | import { configMainStore } from './store'; 7 | import { configMainGlobalProperties } from './utils'; 8 | import { useElementPlus } from './utils/plugin/element'; 9 | 10 | // tailwind css 11 | import '@/styles/tailwind.css'; 12 | // element-plus dark style 13 | import 'element-plus/theme-chalk/src/dark/css-vars.scss'; 14 | // 公共样式 15 | import '@/styles/index.scss'; 16 | 17 | const app = createApp(App); 18 | 19 | getServerConfig(app).then(async config => { 20 | // 路由 21 | await configMainRouter(app); 22 | 23 | // 全局钩子 24 | configMainGlobalProperties(app); 25 | 26 | // Pinia 27 | configMainStore(app); 28 | 29 | // 国际化 30 | configMainI18n(app, config.locale); 31 | 32 | // ElementPlus 33 | useElementPlus(app); 34 | 35 | app.mount('#app'); 36 | }); 37 | -------------------------------------------------------------------------------- /src/router/modules/root/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const Layout = () => import('@/layouts/page-layouts/index.vue'); 5 | 6 | const root: Array = [ 7 | { 8 | path: '/', 9 | component: Layout, 10 | name: 'RtRoot', 11 | meta: { 12 | title: t('route.pathName.index'), 13 | icon: 'iEL-home-filled', 14 | whiteRoute: true, 15 | }, 16 | }, 17 | { 18 | path: '/login', 19 | component: () => import('@/views/login/index.vue'), 20 | name: 'login', 21 | }, 22 | { 23 | path: '/redirect', 24 | component: Layout, 25 | meta: { title: '', icon: 'home-filled' }, 26 | children: [ 27 | { 28 | path: '/redirect/:path(.*)', 29 | name: 'Redirect', 30 | component: () => import('@/layouts/redirect/index.vue'), 31 | }, 32 | ], 33 | }, 34 | { path: '/:path(.*)', redirect: '/error/404' }, 35 | ]; 36 | 37 | export default root; 38 | -------------------------------------------------------------------------------- /src/router/type.ts: -------------------------------------------------------------------------------- 1 | import type { RouteMeta, RouteRecordRaw } from 'vue-router'; 2 | import type { RoleEnum } from '@/enum/role'; 3 | import type { localeKey } from '@/locales/types'; 4 | 5 | export type localeTitle = { [key in localeKey]: string }; 6 | export interface Meta extends RouteMeta { 7 | // 菜单标题 8 | title: string | localeTitle; 9 | // 设置菜单图标 10 | icon?: string; 11 | //排序位置 (子路由无效) 12 | position?: number; 13 | // 单个路由的时候是否开启折叠 14 | alwaysShow?: boolean; 15 | // 不显示侧边栏 16 | hideSidebar?: boolean; 17 | // 是否显示面包屑 18 | breadcrumb?: boolean; 19 | // 是否开启缓存 20 | keepAlive?: boolean; 21 | // 权限路由白名单(只有后端路由模式才生效) 22 | whiteRoute?: boolean; 23 | // 路由层级(扁平化路由时自动添加) 24 | pathList?: number[]; 25 | // 角色权限 26 | roles?: RoleEnum[]; 27 | // 外部页面地址 28 | externalUrl?: string; 29 | // 不显示标签 30 | hideTabs?: boolean; 31 | } 32 | 33 | export interface AppRouteRecordRaw extends Omit { 34 | children?: AppRouteRecordRaw[]; 35 | meta?: Meta; 36 | basePath?: string; 37 | } 38 | -------------------------------------------------------------------------------- /src/router/modules/externa-link/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const externalLink: AppRouteRecordRaw[] = [ 5 | { 6 | path: '/external-link', 7 | // component: Layout, 8 | name: 'RtExternal', 9 | meta: { 10 | title: t('route.pathName.externalLink'), 11 | icon: 'link', 12 | alwaysShow: true, 13 | position: 9, 14 | }, 15 | children: [ 16 | { 17 | path: 'https://jsxiaosi.github.io/vue-xs-admin-docs/', 18 | name: 'RtGitLink', 19 | meta: { title: t('route.pathName.externalDocument') }, 20 | }, 21 | { 22 | path: 'embedded-page', 23 | component: () => import('@/views/external-link/embedded-page/index.vue'), 24 | name: 'RtGitLink', 25 | meta: { 26 | title: t('route.pathName.embeddedDocument'), 27 | externalUrl: 'https://jsxiaosi.github.io/vue-xs-admin-docs/', 28 | }, 29 | }, 30 | ], 31 | }, 32 | ]; 33 | 34 | export default externalLink; 35 | -------------------------------------------------------------------------------- /src/components/CountTo/src/normal/props.ts: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue'; 2 | import { propTypes } from '@/utils/propTypes'; 3 | export const countToProps = { 4 | startVal: propTypes.number.def(0), 5 | endVal: propTypes.number.def(2020), 6 | duration: propTypes.number.def(1300), 7 | autoplay: propTypes.bool.def(true), 8 | decimals: { 9 | type: Number as PropType, 10 | required: false, 11 | default: 0, 12 | validator(value: number) { 13 | return value >= 0; 14 | }, 15 | }, 16 | color: propTypes.string.def(), 17 | fontSize: propTypes.string.def(), 18 | decimal: propTypes.string.def('.'), 19 | separator: propTypes.string.def(','), 20 | prefix: propTypes.string.def(''), 21 | suffix: propTypes.string.def(''), 22 | useEasing: propTypes.bool.def(true), 23 | easingFn: { 24 | type: Function as PropType<(t: number, b: number, c: number, d: number) => number>, 25 | default(t: number, b: number, c: number, d: number) { 26 | return (c * (-(2 ** ((-10 * t) / d)) + 1) * 1024) / 1023 + b; 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/plugin/echarts/index.ts: -------------------------------------------------------------------------------- 1 | import { BarChart, LineChart, MapChart, PieChart } from 'echarts/charts'; 2 | import { 3 | GeoComponent, 4 | GridComponent, 5 | LegendComponent, 6 | TitleComponent, 7 | ToolboxComponent, 8 | TooltipComponent, 9 | VisualMapComponent, 10 | } from 'echarts/components'; 11 | import * as echarts from 'echarts/core'; 12 | import { UniversalTransition } from 'echarts/features'; 13 | import { CanvasRenderer } from 'echarts/renderers'; 14 | import EchartsDarkTheme from './theme/dark.json'; 15 | import EchartsLightTheme from './theme/light.json'; 16 | import 'echarts-wordcloud'; 17 | 18 | echarts.registerTheme('light', EchartsLightTheme); 19 | echarts.registerTheme('dark', EchartsDarkTheme); 20 | 21 | echarts.use([ 22 | GridComponent, 23 | LineChart, 24 | TooltipComponent, 25 | TitleComponent, 26 | ToolboxComponent, 27 | LegendComponent, 28 | CanvasRenderer, 29 | UniversalTransition, 30 | BarChart, 31 | VisualMapComponent, 32 | GeoComponent, 33 | MapChart, 34 | PieChart, 35 | ]); 36 | 37 | export default echarts; 38 | -------------------------------------------------------------------------------- /src/views/editor/markdown/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 42 | -------------------------------------------------------------------------------- /src/assets/icons/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/web/useI18n.ts: -------------------------------------------------------------------------------- 1 | import en from 'element-plus/es/locale/lang/en'; 2 | import zh_Cn from 'element-plus/es/locale/lang/zh-cn'; 3 | import { computed } from 'vue'; 4 | import i18n, { availableLocales } from '@/locales/index'; 5 | 6 | export const useI18n = () => i18n.global; 7 | 8 | export const t: typeof i18n.global.t = (key: string) => key; 9 | 10 | export const localesList = availableLocales; 11 | 12 | export const deffElementLocale = () => { 13 | const { locale } = useI18n(); 14 | 15 | const tolocale = computed(() => { 16 | if (locale.value === 'en') return en; 17 | else return zh_Cn; 18 | }); 19 | 20 | return { tolocale }; 21 | }; 22 | 23 | // 转换国际化,适用于不在i18n配置的国际化语言 24 | export function translateI18n(message: any = '') { 25 | if (!message) { 26 | return ''; 27 | } 28 | const locale = i18n.global.locale.value; 29 | if (typeof message === 'object') { 30 | return message[locale]; 31 | } 32 | const key = message.split('.')[0]; 33 | if (key && Object.keys(i18n.global.messages.value[locale]).includes(key)) { 34 | return i18n.global.t(message); 35 | } 36 | return message; 37 | } 38 | -------------------------------------------------------------------------------- /src/router/modules/editor/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const editor: AppRouteRecordRaw[] = [ 5 | { 6 | path: '/editor', 7 | redirect: '/editor/rich-text', 8 | name: 'RtEditor', 9 | meta: { title: t('route.pathName.editor'), icon: 'editor', position: 4 }, 10 | children: [ 11 | { 12 | path: 'rich-text', 13 | name: 'RtRichText', 14 | component: () => import('@/views/editor/rich-text/index.vue'), 15 | meta: { title: t('route.pathName.editor_richText') }, 16 | }, 17 | { 18 | path: 'markdown', 19 | name: 'RtMarkdown', 20 | component: () => import('@/views/editor/markdown/index.vue'), 21 | meta: { title: t('route.pathName.editor_markdown') }, 22 | }, 23 | { 24 | path: 'logic-flow', 25 | name: 'RtLogicFlow', 26 | component: () => import('@/views/editor/logic-flow/index.vue'), 27 | meta: { title: t('route.pathName.editor_logicFlow') }, 28 | }, 29 | ], 30 | }, 31 | ]; 32 | 33 | export default editor; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 小斯(xiaosi) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/Application/AppLocale.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{ item.name }} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | -------------------------------------------------------------------------------- /src/components/Application/AppAccount.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 退出登录 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "preserve", 5 | "jsxImportSource": "vue", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "useDefineForClassFields": true, 8 | "baseUrl": "./", 9 | "module": "ESNext", 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | 14 | "paths": { 15 | "@/*": ["src/*"], 16 | "#/*": ["types/*"] 17 | }, 18 | "resolveJsonModule": true, 19 | "types": [ 20 | "node", 21 | "vite/client", 22 | "element-plus/global", 23 | "element-plus/lib/locale", 24 | "@intlify/unplugin-vue-i18n/messages" 25 | ], 26 | "allowImportingTsExtensions": true, 27 | /* Linting */ 28 | "strict": true, 29 | "noFallthroughCasesInSwitch": true, 30 | "noUnusedLocals": true, 31 | "noUnusedParameters": true, 32 | "noEmit": true, 33 | "allowSyntheticDefaultImports": true, 34 | "isolatedModules": true, 35 | "skipLibCheck": true 36 | }, 37 | 38 | "references": [{ "path": "./tsconfig.node.json" }], 39 | "include": ["src", "types", "tests"], 40 | "exclude": ["node_modules", "dist", "**/*.js", "discard/"] 41 | } 42 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { _storage } from '@jsxiaosi/utils'; 2 | import type { App } from 'vue'; 3 | import { getConfigInfo } from '@/server/config'; 4 | import type { AppConfig } from '@/store/types'; 5 | import { configTheme } from '@/utils/theme/transformTheme'; 6 | 7 | let config: AppConfig = {} as AppConfig; 8 | 9 | export function getConfig(): AppConfig { 10 | return config; 11 | } 12 | 13 | // 延迟进入vue,显示loding页 14 | export async function getServerConfig(app: App): Promise { 15 | const appConfigMode = localStorage.getItem('appConfigMode'); 16 | if (appConfigMode) { 17 | config = JSON.parse(appConfigMode); 18 | } else { 19 | const res = await getConfigInfo(); 20 | if (res) { 21 | config = res.data; 22 | localStorage.setItem('appConfigMode', JSON.stringify(config)); 23 | } else { 24 | throw new Error( 25 | `\npublic文件夹下无法查找到serverConfig配置文件\nUnable to find serverconfig configuration file under public folder`, 26 | ); 27 | } 28 | } 29 | configTheme(config); 30 | _storage.setStorageConfig({ ...config.StorageConfig, prefix: config.title }); 31 | app.config.globalProperties.$config = getConfig(); 32 | return config; 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/var/site-bar.scss: -------------------------------------------------------------------------------- 1 | // 左侧导航栏(此地方颜色全部改成自定义) 2 | $side-bar-width: 210px; 3 | $side-hide-bar-width: 55px; 4 | $nav-bar-height: 50px; 5 | $tabs-page-height: 38px; // 面包屑高度 38px 6 | 7 | $menu-bg: var(--main-bg-color); // 侧边栏背景颜色 8 | $menu-hover: var(--sub-color-9); // 焦点背景色 9 | $menu-hover-text: var(--main-color); // 焦点文字颜色 10 | $menu-text: var(--text-color-primary); // 文字颜色 11 | $menu-active-text: var(--main-color); // 选中的文字颜色 12 | 13 | $menu-item-bg: var(--main-bg-color); // 二级背景颜色 14 | $menu-item-border-left: var(--main-bg-color); // 二级未选中的背景颜色 15 | $menu-item-text: var(--text-color-primary); // 二级文字颜色 16 | $menu-item-hover: var(--sub-color-9); // 二级焦点背景颜色 17 | $menu-item-hover-text: var(--main-color); // 二级焦点文字颜色 18 | $menu-item-active-text: var(--main-color); // 二级选中的背景颜色 19 | $menu-item-active-bg: var(--sub-color-8); // 二级选中背景颜色 20 | $menu-item-active-border-left: var(--main-color); // 二级左描边背景颜色 21 | 22 | $app-main-bg-color: var(--sub-main-bg-content); // app-mian背景颜色 23 | $nav-bar-color: var(--main-bg-color); // 顶部导航栏背景颜色 24 | $nav-bar-border-bottom-color: var(--text-color-placeholder); 25 | $tabs-page-color: var(--main-bg-color); // 面包屑背景颜色 26 | $breadcrumb-text: var(--text-color-primary); // 面包屑文字颜色 27 | -------------------------------------------------------------------------------- /src/utils/operate/index.ts: -------------------------------------------------------------------------------- 1 | export const hasClass = (ele: RefType, cls: string): any => { 2 | return !!ele.className.match(new RegExp(`(\\s|^)${cls}(\\s|$)`)); 3 | }; 4 | 5 | export const addClass = (ele: RefType, cls: string, extracls?: string): any => { 6 | if (!hasClass(ele, cls)) ele.className += ` ${cls}`; 7 | if (extracls) { 8 | if (!hasClass(ele, extracls)) ele.className += ` ${extracls}`; 9 | } 10 | }; 11 | 12 | export const removeClass = (ele: RefType, cls: string, extracls?: string): any => { 13 | if (hasClass(ele, cls)) { 14 | const reg = new RegExp(`(\\s|^)${cls}(\\s|$)`); 15 | ele.className = ele.className.replace(reg, ' ').trim(); 16 | } 17 | if (extracls) { 18 | if (hasClass(ele, extracls)) { 19 | const regs = new RegExp(`(\\s|^)${extracls}(\\s|$)`); 20 | ele.className = ele.className.replace(regs, ' ').trim(); 21 | } 22 | } 23 | }; 24 | 25 | export const toggleClass = (flag: boolean, clsName: string, target?: RefType): any => { 26 | const targetEl = target || document.body; 27 | let { className } = targetEl; 28 | className = className.replace(clsName, ''); 29 | targetEl.className = flag ? `${className} ${clsName} ` : className; 30 | }; 31 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/AppMain/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 39 | -------------------------------------------------------------------------------- /src/router/modules/error/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const Layout = () => import('@/layouts/page-layouts/index.vue'); 5 | 6 | const error: AppRouteRecordRaw[] = [ 7 | { 8 | path: '/error', 9 | redirect: '/error/404', 10 | name: 'error', 11 | component: Layout, 12 | meta: { 13 | title: t('route.pathName.error'), 14 | icon: 'iEL-remove-filled', 15 | position: 8, 16 | whiteRoute: true, 17 | alwaysShow: true, 18 | }, 19 | children: [ 20 | { 21 | path: '403', 22 | name: '403', 23 | component: () => import('@/views/error/403.vue'), 24 | meta: { title: t('route.pathName.error403') }, 25 | }, 26 | { 27 | path: '404', 28 | name: '404', 29 | component: () => import('@/views/error/404.vue'), 30 | meta: { title: t('route.pathName.error404') }, 31 | }, 32 | { 33 | path: '500', 34 | name: '500', 35 | component: () => import('@/views/error/500.vue'), 36 | meta: { title: t('route.pathName.error500') }, 37 | }, 38 | ], 39 | }, 40 | ]; 41 | 42 | export default error; 43 | -------------------------------------------------------------------------------- /src/instruct/waterMark.ts: -------------------------------------------------------------------------------- 1 | import type { DirectiveBinding, VNode } from 'vue'; 2 | import { createBase64, createWaterMarkDom } from '@/utils/waterMark'; 3 | 4 | // 设置水印 5 | export function setWaterMark(el: HTMLElement, binding: string) { 6 | // 创建 waterMark 父元素 7 | const waterMark = createWaterMarkDom(); 8 | waterMark.style.background = `url(${createBase64(binding)}) left top repeat`; 9 | // 将对应图片的父容器作为定位元素 10 | if (el) { 11 | if (!el.style.position) { 12 | el.style.position = 'relative'; 13 | } 14 | 15 | // 将图片元素移动到 waterMark 中 16 | el.appendChild(waterMark); 17 | } 18 | } 19 | 20 | export default { 21 | // 在绑定元素的父组件挂载之后调用 22 | mounted(el: HTMLElement, binding: DirectiveBinding, _vnode: VNode) { 23 | setWaterMark(el, binding.value); 24 | }, 25 | // 在包含组件的 VNode 及其子组件的 VNode 更新之后调用 26 | updated(el: HTMLElement, binding: DirectiveBinding, _vnode: VNode, _prevVNode: VNode) { 27 | const { oldValue, value } = binding; 28 | if (oldValue !== value) { 29 | const waterMarkEl = el.querySelector('.water-mark') as HTMLElement; 30 | if (waterMarkEl) { 31 | waterMarkEl.style.background = `url(${createBase64(value)}) left top repeat`; 32 | } 33 | } 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/VerticalSidebar/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 39 | -------------------------------------------------------------------------------- /src/assets/icons/locales.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/router/modules/index.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouteRecordRaw } from '@/router/type'; 2 | import { pathNamekeyCheck, setUpRoutePath } from '../utils'; 3 | 4 | export function configRouteList() { 5 | // 白名单目录/文件,白名单目录 = 不渲染到菜单,不显示标签页 6 | const whiteCatalogue = ['root', 'whiteList']; 7 | 8 | let routeModulesList: AppRouteRecordRaw[] = []; //菜单路由 9 | const whiteRouteModulesList: AppRouteRecordRaw[] = []; // 不参与菜单处理的路由 10 | 11 | // 自动查找路由配置文件 12 | const modules: Recordable = import.meta.glob('./**/*.ts', { eager: true }); 13 | Object.keys(modules).forEach(key => { 14 | const mod = modules[key].default; 15 | if (!mod) return; 16 | const modList = Array.isArray(mod) ? [...mod] : [mod]; 17 | if (pathNamekeyCheck(key, whiteCatalogue)) { 18 | whiteRouteModulesList.push(...modList); 19 | } else { 20 | routeModulesList.push(...modList); 21 | } 22 | }); 23 | 24 | // 菜单路由 根据父级重新处理子路由的path路径 25 | routeModulesList = setUpRoutePath(routeModulesList); 26 | 27 | /** 28 | * 先把菜单路由插入根路径 '/' 防止route 初始化警告查找不到路由 29 | */ 30 | const whIndex = whiteRouteModulesList.findIndex(i => i.path === '/'); 31 | if (whiteRouteModulesList[whIndex]) whiteRouteModulesList[whIndex].children = routeModulesList; 32 | return { whiteRouteModulesList, routeModulesList }; 33 | } 34 | -------------------------------------------------------------------------------- /src/router/modules/details-page/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const detailsPage: AppRouteRecordRaw[] = [ 5 | { 6 | path: '/details_page', 7 | name: 'RtDetailsPage', 8 | meta: { 9 | title: t('route.pathName.detailsPage'), 10 | icon: 'iEL-management', 11 | position: 10, 12 | }, 13 | component: () => import('@/views/details-page/index.vue'), 14 | children: [ 15 | { 16 | path: 'details_info', 17 | name: 'RtDetailsInfo', 18 | meta: { 19 | title: '', 20 | icon: 'iEL-management', 21 | whiteRoute: true, 22 | keepAlive: true, 23 | hideSidebar: true, 24 | }, 25 | component: () => import('@/views/details-page/datails-info/index.vue'), 26 | }, 27 | { 28 | path: 'details_params/:id', 29 | name: 'RtDetailsParams', 30 | meta: { 31 | title: '', 32 | icon: 'iEL-management', 33 | whiteRoute: true, 34 | keepAlive: true, 35 | hideSidebar: true, 36 | }, 37 | component: () => import('@/views/details-page/datails-params/index.vue'), 38 | }, 39 | ], 40 | }, 41 | ]; 42 | 43 | export default detailsPage; 44 | -------------------------------------------------------------------------------- /src/assets/icons/components.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const scopes = fs 7 | .readdirSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'src')) 8 | .map(i => i.toLowerCase()); 9 | 10 | const gitStatus = execSync('git status --porcelain || true').toString().trim().split('\n'); 11 | 12 | const scopeComplete = gitStatus 13 | .find(r => ~r.indexOf('M src')) 14 | ?.replace(/(\/)/g, '%%') 15 | ?.match(/src%%((\w|-)*)/)?.[1]; 16 | 17 | const subjectComplete = gitStatus 18 | .find(r => ~r.indexOf('M src')) 19 | ?.replace(/\//g, '%%') 20 | ?.match(/src%%((\w|-)*)/)?.[1]; 21 | 22 | const Configuration = { 23 | extends: ['@jsxiaosi/commitlint-config'], 24 | prompt: { 25 | // 范围设置 26 | scopes: [...scopes, 'mock'], 27 | // 范围是否可以多选 28 | enableMultipleScopes: true, 29 | // 多选范围后用标识符隔开 30 | scopeEnumSeparator: ',', 31 | // 设置 选择范围 中 为空选项(empty) 和 自定义选项(custom) 的 位置 32 | customScopesAlign: !scopeComplete ? 'top' : 'bottom', 33 | // 如果 defaultScope 与在选择范围列表项中的 value 相匹配就会进行星标置顶操作。 34 | defaultScope: scopeComplete, 35 | // 描述预设值 36 | defaultSubject: subjectComplete && `[${subjectComplete}] `, 37 | }, 38 | }; 39 | 40 | export default Configuration; 41 | -------------------------------------------------------------------------------- /src/assets/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 10 | 11 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/router/modules/permissions/index.ts: -------------------------------------------------------------------------------- 1 | import { RoleEnum } from '@/enum/role'; 2 | import { t } from '@/hooks/web/useI18n'; 3 | import type { AppRouteRecordRaw } from '@/router/type'; 4 | 5 | const permissions: AppRouteRecordRaw[] = [ 6 | { 7 | path: '/permissions', 8 | redirect: '/permissions/page', 9 | name: 'RtPermissions', 10 | meta: { title: 'route.pathName.permissions', icon: 'guide', position: 7 }, 11 | children: [ 12 | { 13 | path: 'page', 14 | name: 'RtPermissionsPage', 15 | component: () => import('@/views/permissions/page/index.vue'), 16 | meta: { title: t('route.pathName.permissionsPage') }, 17 | }, 18 | { 19 | path: 'test-page-admin', 20 | name: 'RtPermissionsTestPageAdmin', 21 | component: () => import('@/views/permissions/test-permissions-a/index.vue'), 22 | meta: { title: t('route.pathName.testPermissionsPage1'), roles: [RoleEnum.ADMIN] }, 23 | }, 24 | { 25 | path: 'test-page-test', 26 | name: 'RtPermissionsTestPageTest', 27 | component: () => import('@/views/permissions/test-permissions-b/index.vue'), 28 | meta: { title: t('route.pathName.testPermissionsPage2'), roles: [RoleEnum.TEST] }, 29 | }, 30 | ], 31 | }, 32 | ]; 33 | 34 | export default permissions; 35 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { _storage } from '@jsxiaosi/utils'; 2 | import { defineStore } from 'pinia'; 3 | import type { RoleEnum } from '@/enum/role'; 4 | import type { UseInfoType } from '@/server/useInfo'; 5 | import { store } from '..'; 6 | 7 | export interface UserState { 8 | userInfo: UseInfoType | null; 9 | roles: RoleEnum | null; 10 | } 11 | 12 | const getStorageUserInfo = (): UserState => { 13 | const userInfo = _storage.getStorage('userInfo'); 14 | 15 | if (userInfo) { 16 | return { 17 | userInfo, 18 | roles: userInfo.role, 19 | }; 20 | } 21 | return { 22 | userInfo: null, 23 | roles: null, 24 | }; 25 | }; 26 | 27 | const useUserInfoStore = defineStore('userInfo', { 28 | state: (): UserState => getStorageUserInfo(), 29 | actions: { 30 | setUserInfo(value: UseInfoType) { 31 | _storage.setStorage('userInfo', value); 32 | this.userInfo = value; 33 | this.roles = value.role; 34 | }, 35 | setRoles(value: RoleEnum) { 36 | this.roles = value; 37 | }, 38 | removeUserInfo() { 39 | this.userInfo = null; 40 | this.roles = null; 41 | _storage.removeStorage('userInfo'); 42 | }, 43 | }, 44 | }); 45 | 46 | export const useUserInfoStoreHook = () => { 47 | return useUserInfoStore(store); 48 | }; 49 | -------------------------------------------------------------------------------- /src/views/components/calendar/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 48 | -------------------------------------------------------------------------------- /src/assets/icons/editor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/theme.scss: -------------------------------------------------------------------------------- 1 | /* 根据element 主题统一配色 不在页面内直接使用element css 变量是为了更好的统一管理 */ 2 | :root { 3 | // 统一颜色主题 4 | --color-white: #fff; 5 | --main-color: var(--el-color-primary); // 主色 6 | --sub-color: var(--el-color-primary-light-5); // 浅蓝1 7 | --sub-color-8: var(--el-color-primary-light-8); // 浅蓝2 8 | --sub-color-9: var(--el-color-primary-light-9); // 浅蓝2 9 | 10 | // 背景颜色 11 | --main-bg-color: #fff; // 首选背景颜色 12 | --sub-main-bg-content: #f5f6fa; // 次选背景颜色 13 | 14 | // 主题文字颜色 15 | --text-color-primary: var(--el-text-color-primary); // 首选文字颜色 16 | --text-color-regular: var(--el-text-color-regular); // 次选文字颜色 17 | --text-color-placeholder: var(--el-text-color-placeholder); // 分割线/备注 18 | --text-color-disabled: var(--el-text-color-disabled); 19 | 20 | // border-color 21 | --border-color-light: var(--el-border-color-light); 22 | 23 | // 文字大小 24 | --font-size-extra-large: var(--el-font-size-extra-large); // 超大号字体 25 | --font-size-large: var(--el-font-size-large); // 大号字体 26 | --font-size-medium: var(--el-font-size-medium); // 中等字体 27 | --font-size-base: var(--el-font-size-base); // 基本字体 28 | --font-size-small: var(--el-font-size-small); // 小号字体 29 | --font-size-extra-small: var(--el-font-size-extra-small); // 超小号字体 30 | } 31 | 32 | html.dark { 33 | --main-bg-color: #212121; // 首选背景颜色 34 | --sub-main-bg-content: #151515; // 次选背景颜色 35 | } 36 | -------------------------------------------------------------------------------- /src/views/editor/rich-text/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 45 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type { ConfigEnv, UserConfig } from 'vite'; 4 | 5 | import { createViteBuild } from './build/vite/build'; 6 | import { createViteCSS } from './build/vite/css'; 7 | import { createViteEsbuild } from './build/vite/esbuild'; 8 | import { createViteOptimizeDeps } from './build/vite/optimizeDeps'; 9 | import { createVitePlugins } from './build/vite/plugin'; 10 | import { createViteResolve } from './build/vite/resolve'; 11 | import { createViteServer } from './build/vite/server'; 12 | 13 | // https://vitejs.dev/config/ 14 | export default (configEnv: ConfigEnv): UserConfig => { 15 | const { mode, command } = configEnv; 16 | // const root = process.cwd(); 17 | 18 | // const env = loadEnv(mode, root); 19 | 20 | const isBuild = command === 'build'; 21 | 22 | return { 23 | // 设为 false 可以避免 Vite 清屏而错过在终端中打印某些关键信息。命令行模式下请通过 --clearScreen false 设置。 24 | clearScreen: true, 25 | logLevel: 'info', 26 | // esbuild 27 | esbuild: createViteEsbuild(isBuild), 28 | // 解析配置 29 | resolve: createViteResolve(mode, __dirname), 30 | // 插件配置 31 | plugins: createVitePlugins(isBuild, configEnv), 32 | // 服务配置 33 | server: createViteServer(), 34 | // 打包配置 35 | build: createViteBuild(), 36 | // 依赖优化配置 37 | optimizeDeps: createViteOptimizeDeps(), 38 | // css预处理配置 39 | css: createViteCSS(), 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: true, 3 | extends: ['stylelint-config-standard', 'stylelint-config-property-sort-order-smacss'], 4 | plugins: ['stylelint-order'], 5 | overrides: [ 6 | { 7 | files: ['**/*.{css,html,vue}'], 8 | customSyntax: 'postcss-html', 9 | }, 10 | { 11 | files: ['*.vue'], 12 | rules: { 13 | // TODO: https://github.com/stylelint/stylelint/issues/8695 14 | 'no-invalid-position-declaration': null, 15 | }, 16 | }, 17 | { 18 | files: ['*.scss', '**/*.scss'], 19 | extends: ['stylelint-config-standard-scss', 'stylelint-config-recommended-vue/scss'], 20 | customSyntax: 'postcss-scss', 21 | rule: { 22 | 'scss/percent-placeholder-pattern': null, 23 | }, 24 | }, 25 | ], 26 | rules: { 27 | 'keyframes-name-pattern': null, 28 | 'selector-pseudo-class-no-unknown': [ 29 | true, 30 | { 31 | ignorePseudoClasses: ['deep', 'global'], 32 | }, 33 | ], 34 | 'selector-pseudo-element-no-unknown': [ 35 | true, 36 | { 37 | ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'], 38 | }, 39 | ], 40 | // BEM 命名规范 41 | 'selector-class-pattern': '^.[a-z]([a-z0-9-]+)?(__([a-z0-9]+-?)+)?(--([a-z0-9]+-?)+){0,2}$', 42 | 'no-descending-specificity': null, 43 | }, 44 | ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'], 45 | }; 46 | -------------------------------------------------------------------------------- /k8s/base/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: vue-xs-admin-deploy 5 | namespace: vue-xs-admin 6 | labels: 7 | app: vue-xs-admin-deploy 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: vue-xs-admin-pod 13 | template: 14 | metadata: 15 | labels: 16 | app: vue-xs-admin-pod 17 | spec: 18 | serviceAccountName: vue-xs-admin-sa 19 | containers: 20 | - name: vue-xs-admin 21 | image: YOUR_DOCKER_REGISTRY/vue-xs-admin:latest 22 | imagePullPolicy: IfNotPresent 23 | ports: 24 | - containerPort: 80 25 | protocol: TCP 26 | livenessProbe: 27 | initialDelaySeconds: 15 28 | periodSeconds: 10 29 | successThreshold: 1 30 | failureThreshold: 3 31 | timeoutSeconds: 3 32 | tcpSocket: 33 | port: 80 34 | readinessProbe: 35 | initialDelaySeconds: 15 36 | periodSeconds: 10 37 | successThreshold: 1 38 | failureThreshold: 3 39 | timeoutSeconds: 3 40 | tcpSocket: 41 | port: 80 42 | resources: 43 | limits: 44 | memory: 2Gi 45 | cpu: '1' 46 | requests: 47 | memory: 256Mi 48 | cpu: 250m 49 | restartPolicy: Always 50 | -------------------------------------------------------------------------------- /src/router/modules/functions/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const functions: AppRouteRecordRaw[] = [ 5 | { 6 | path: '/functions', 7 | redirect: '/functions/preview-pdf', 8 | name: 'Functions', 9 | meta: { 10 | title: 'route.pathName.functions', 11 | icon: 'iEL-briefcase', 12 | position: 2, 13 | whiteRoute: true, 14 | alwaysShow: true, 15 | }, 16 | children: [ 17 | { 18 | path: 'water_mark', 19 | name: 'RtWaterMark', 20 | component: () => import('@/views/functions/water-mark/index.vue'), 21 | meta: { title: t('route.pathName.waterMark') }, 22 | }, 23 | { 24 | path: 'preview-pdf', 25 | name: 'RtPreviewPdf', 26 | component: () => import('@/views/functions/pdf/index.vue'), 27 | meta: { title: t('route.pathName.pdf') }, 28 | }, 29 | { 30 | path: 'preview-docx', 31 | name: 'RtPreviewDocx', 32 | component: () => import('@/views/functions/docx/index.vue'), 33 | meta: { title: t('route.pathName.docx') }, 34 | }, 35 | { 36 | path: 'guide', 37 | name: 'RtGuide', 38 | component: () => import('@/views/functions/guide/index.vue'), 39 | meta: { title: t('route.pathName.guide') }, 40 | }, 41 | ], 42 | }, 43 | ]; 44 | 45 | export default functions; 46 | -------------------------------------------------------------------------------- /src/utils/theme/transformTheme.ts: -------------------------------------------------------------------------------- 1 | import { colorPalette } from '@jsxiaosi/utils'; 2 | import type { AppConfig } from '@/store/types'; 3 | 4 | const body = document.documentElement as HTMLElement; 5 | 6 | export function updateColor(primaryColor: string, themeMode: 'light' | 'dark') { 7 | if (!primaryColor) return; 8 | 9 | const style = document.getElementById('admin-style-root-color'); 10 | 11 | const mixColor = themeMode === 'dark' ? '#141414' : '#ffffff'; 12 | let innerHTML = `html${themeMode === 'dark' ? '.dark' : ''}:root{ --el-color-primary: ${primaryColor};\n`; 13 | 14 | for (let i = 1; i <= 9; i++) { 15 | innerHTML += `--el-color-primary-light-${i}: ${colorPalette(primaryColor, mixColor, i * 0.1)};\n`; 16 | } 17 | 18 | if (style) style.innerHTML = `${innerHTML}}`; 19 | } 20 | 21 | export function themeHtmlClassName(className: string, isShow: boolean) { 22 | if (isShow) { 23 | body.classList.add(className); 24 | } else { 25 | body.classList.remove(className); 26 | } 27 | } 28 | 29 | export function configTheme(appConfig: AppConfig) { 30 | if (!appConfig) return; 31 | const { primaryColor, themeMode, colorWeaknessMode, greyMode } = appConfig; 32 | 33 | updateColor(primaryColor, themeMode); 34 | if (greyMode || colorWeaknessMode) { 35 | if (greyMode) themeHtmlClassName('html-grey', greyMode); 36 | else if (colorWeaknessMode) themeHtmlClassName('html-weakness', colorWeaknessMode); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 61 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Auto merge main into git-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [closed] 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | sync: 15 | if: github.event.pull_request.merged == true || github.event_name == 'push' 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout main branch 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Configure Git 25 | run: | 26 | git config user.name "github-actions[bot]" 27 | git config user.email "github-actions[bot]@users.noreply.github.com" 28 | 29 | - name: Add git-pages remote 30 | run: | 31 | git remote add git-pages https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git 32 | 33 | - name: Fetch git-pages branch 34 | run: git fetch origin git-pages:git-pages 35 | 36 | - name: Checkout git-page branch 37 | run: git checkout git-pages 38 | 39 | - name: Merge main into git-pages 40 | run: | 41 | git merge main --no-ff -m "feat: ✨ Auto-merge main into git-pages" 42 | 43 | - name: Push changes to git-pages 44 | run: git push origin git-pages 45 | 46 | - name: Repository Dispatch 47 | uses: peter-evans/repository-dispatch@v3 48 | with: 49 | event-type: git-pages-deploy 50 | -------------------------------------------------------------------------------- /.github/workflows/git-pages.yml: -------------------------------------------------------------------------------- 1 | name: Automatic deployment to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - git-pages 7 | repository_dispatch: 8 | types: [git-pages-deploy] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | ref: ${{ github.event_name == 'push' && github.ref || 'git-pages' }} 18 | fetch-depth: 0 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: '22' 27 | cache: pnpm 28 | 29 | - name: Setup Pages 30 | uses: actions/configure-pages@v5 31 | 32 | - name: Install dependencies 33 | run: pnpm install 34 | 35 | - name: Build 36 | run: pnpm build 37 | 38 | - name: Upload artifact 39 | uses: actions/upload-pages-artifact@v3 40 | with: 41 | path: ./dist 42 | 43 | deploy: 44 | environment: 45 | name: github-pages 46 | url: ${{ steps.deployment.outputs.page_url }} 47 | permissions: 48 | contents: read 49 | pages: write 50 | id-token: write 51 | needs: build 52 | runs-on: ubuntu-latest 53 | name: Deploy 54 | steps: 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 58 | -------------------------------------------------------------------------------- /src/components/Application/AppTheme.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | { 14 | color = color === 'dark' ? 'light' : 'dark'; 15 | } 16 | " 17 | > 18 | 19 | 20 | 21 | 22 | 23 | 24 | 62 | -------------------------------------------------------------------------------- /src/utils/axios/axiosConfig.ts: -------------------------------------------------------------------------------- 1 | import type { RequestOptions, Result } from '#/axios'; 2 | import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; 3 | /** 4 | * axios 数据处理类 5 | */ 6 | 7 | export interface CreateAxiosOptions extends AxiosRequestConfig { 8 | requestOptions?: RequestOptions; 9 | interceptor?: AxiosInterceptor; 10 | } 11 | 12 | export type InternalAxiosOptions = Pick & 13 | InternalAxiosRequestConfig; 14 | 15 | export abstract class AxiosInterceptor { 16 | /** 17 | * @description: 请求前的配置 18 | */ 19 | beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig; 20 | 21 | /** 22 | * @description: 请求成功的处理 23 | */ 24 | requestHook?: (res: AxiosResponse, options: RequestOptions) => any; 25 | 26 | /** 27 | * @description: 请求失败处理 28 | */ 29 | requestCatchHook?: (e: Error, options: RequestOptions) => Promise; 30 | 31 | /** 32 | * @description: 请求之前的拦截器 33 | */ 34 | requestInterceptors?: (config: InternalAxiosOptions) => InternalAxiosOptions; 35 | 36 | /** 37 | * @description: 请求之前的拦截器错误处理 38 | */ 39 | requestInterceptorsCatch?: (error: Error) => void; 40 | 41 | /** 42 | * @description: 请求之后的拦截器 43 | */ 44 | responseInterceptors?: (res: AxiosResponse) => AxiosResponse; 45 | 46 | /** 47 | * @description: 请求之后的拦截器错误处理 48 | */ 49 | responseInterceptorsCatch?: (error: Error) => void; 50 | } 51 | -------------------------------------------------------------------------------- /src/hooks/web/useSortable.ts: -------------------------------------------------------------------------------- 1 | import { tryOnMounted } from '@vueuse/core'; 2 | import Sortable from 'sortablejs'; 3 | import { isRef, ref, unref } from 'vue'; 4 | import type { Options } from 'sortablejs'; 5 | import type { Ref } from 'vue'; 6 | 7 | /** 8 | * 9 | * @param options sortableJs配置项 10 | * @param elRef Ref 11 | * @returns 12 | * - initSortable(el: Ref | (HTMLElement | null)): 初始化排序功能。 13 | * - destroy: 销毁排序实例,并完全删除可排序功能。 14 | * - sortableJs: SortableJS 实例。 15 | */ 16 | 17 | function useSortable(options?: Options, elRef?: Ref) { 18 | const sortableJs = ref(null); 19 | 20 | const createSortable = (el: HTMLElement) => { 21 | sortableJs.value = new Sortable(el, { 22 | animation: 300, 23 | delay: 400, 24 | delayOnTouchOnly: true, 25 | ...options, 26 | }); 27 | }; 28 | 29 | const initSortable = (el: Ref | (HTMLElement | null)) => { 30 | if (sortableJs.value) return; 31 | 32 | let element: HTMLElement | null; 33 | if (isRef(el)) { 34 | element = unref(el); 35 | } else { 36 | element = el; 37 | } 38 | 39 | if (element) createSortable(element); 40 | }; 41 | 42 | tryOnMounted(() => { 43 | if (elRef) initSortable(elRef); 44 | }); 45 | 46 | const destroy = () => { 47 | sortableJs.value?.destroy(); 48 | sortableJs.value = null; 49 | }; 50 | 51 | return { initSortable, destroy, sortableJs }; 52 | } 53 | 54 | export default useSortable; 55 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/AppLogo/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | {{ config.title }} 21 | 22 | 23 | 24 | 62 | -------------------------------------------------------------------------------- /src/utils/axios/axiosStatus.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorMessageMode } from '#/axios'; 2 | import { useMessage } from '@/hooks/web/useMessage'; 3 | import i18n from '@/locales'; 4 | const { createErrorModal, createErrorMsg } = useMessage(); 5 | 6 | export function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode = 'message'): void { 7 | const { t } = i18n.global; 8 | let errMessage = ''; 9 | 10 | switch (status) { 11 | case 400: 12 | errMessage = `${msg}`; 13 | break; 14 | case 401: 15 | errMessage = t('api.errMsg401'); 16 | break; 17 | case 403: 18 | errMessage = t('api.errMsg403'); 19 | break; 20 | case 404: 21 | errMessage = t('api.errMsg404'); 22 | break; 23 | case 405: 24 | errMessage = t('api.errMsg405'); 25 | break; 26 | case 408: 27 | errMessage = t('api.errMsg408'); 28 | break; 29 | case 500: 30 | errMessage = t('api.errMsg500'); 31 | break; 32 | case 501: 33 | errMessage = t('api.errMsg501'); 34 | break; 35 | case 502: 36 | errMessage = t('api.errMsg502'); 37 | break; 38 | case 503: 39 | errMessage = t('api.errMsg503'); 40 | break; 41 | case 504: 42 | errMessage = t('api.errMsg504'); 43 | break; 44 | case 505: 45 | errMessage = t('api.errMsg505'); 46 | break; 47 | default: 48 | } 49 | if (errMessage) { 50 | if (errorMessageMode === 'modal') { 51 | createErrorModal(msg); 52 | } else if (errorMessageMode === 'message') { 53 | createErrorMsg(errMessage); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/views/functions/water-mark/hooks/useWaterMark.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeUnmount, ref, shallowRef, unref } from 'vue'; 2 | import type { Ref } from 'vue'; 3 | import { createBase64, createWaterMarkDom } from '@/utils/waterMark'; 4 | import type { WaterMarkOptions } from '@/utils/waterMark'; 5 | 6 | export const useWaterMark = ( 7 | appendEl: Ref = ref(document.body) as Ref, 8 | options?: WaterMarkOptions, 9 | ) => { 10 | const watermarkEl = shallowRef(); 11 | 12 | const updateWatermark = (text: string) => { 13 | const el = unref(watermarkEl); 14 | if (!el) return; 15 | el.style.background = `url(${createBase64(text, options)}) left top repeat`; 16 | }; 17 | 18 | const createWatermark = (text: string) => { 19 | if (unref(watermarkEl)) { 20 | updateWatermark(text); 21 | return; 22 | } 23 | 24 | // 创建 waterMark 父元素 25 | const waterMark = createWaterMarkDom(); 26 | watermarkEl.value = waterMark; 27 | const el = unref(appendEl); 28 | if (!el) return; 29 | updateWatermark(text); 30 | el.style.position = 'relative'; 31 | el.appendChild(waterMark); 32 | }; 33 | 34 | const setWaterMark = (text: string) => { 35 | createWatermark(text); 36 | }; 37 | 38 | const close = () => { 39 | const el = unref(appendEl); 40 | if (!el) return; 41 | const waterMark = unref(watermarkEl); 42 | if (!waterMark) return; 43 | watermarkEl.value = undefined; 44 | el.removeChild(waterMark); 45 | }; 46 | 47 | onBeforeUnmount(() => { 48 | close(); 49 | }); 50 | 51 | return { setWaterMark, close }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/PreviewDocx/index.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | -------------------------------------------------------------------------------- /src/styles/nprogress.scss: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | position: fixed; 8 | z-index: 1031; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 2px; 13 | background: var(--main-color); 14 | } 15 | 16 | /* Fancy blur effect */ 17 | #nprogress .peg { 18 | display: block; 19 | position: absolute; 20 | right: 0; 21 | width: 100px; 22 | height: 100%; 23 | transform: rotate(3deg) translate(0, -4px); 24 | opacity: 1; 25 | box-shadow: 26 | 0 0 10px var(--main-color), 27 | 0 0 5px var(--main-color); 28 | } 29 | 30 | /* Remove these to get rid of the spinner */ 31 | #nprogress .spinner { 32 | display: block; 33 | position: fixed; 34 | z-index: 1031; 35 | top: 15px; 36 | right: 15px; 37 | } 38 | 39 | #nprogress .spinner-icon { 40 | box-sizing: border-box; 41 | width: 18px; 42 | height: 18px; 43 | animation: nprogress-spinner 400ms linear infinite; 44 | border: solid 2px transparent; 45 | border-radius: 50%; 46 | border-top-color: var(--main-color); 47 | border-left-color: var(--main-color); 48 | } 49 | 50 | .nprogress-custom-parent { 51 | position: relative; 52 | overflow: hidden; 53 | } 54 | 55 | .nprogress-custom-parent #nprogress .spinner, 56 | .nprogress-custom-parent #nprogress .bar { 57 | position: absolute; 58 | } 59 | 60 | @keyframes nprogress-spinner { 61 | 0% { 62 | transform: rotate(0deg); 63 | } 64 | 65 | 100% { 66 | transform: rotate(360deg); 67 | } 68 | } 69 | 70 | @keyframes nprogress-spinner { 71 | 0% { 72 | transform: rotate(0deg); 73 | } 74 | 75 | 100% { 76 | transform: rotate(360deg); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Form/types/from.ts: -------------------------------------------------------------------------------- 1 | import type { Arrayable } from '@vueuse/core'; 2 | import type { FormItemRule } from 'element-plus'; 3 | import type { VNode } from 'vue'; 4 | 5 | export interface FormProps { 6 | labelPosition: 'top' | 'right' | 'left'; 7 | formItem: Array>; 8 | } 9 | 10 | export interface FormItemProps { 11 | gutter: number; 12 | xs?: number; 13 | sm?: number; 14 | md?: number; 15 | lg?: number; 16 | xl?: number; 17 | itemList: Array>; 18 | } 19 | 20 | /** 21 | * @param(component) 组件名称,ElInput,ElSelect 22 | * @param(label) 标签名 23 | * @param(prop) form表单双向绑定字段 24 | * @param(props) 组件属性 25 | * @param(rules) 表单校验 26 | * @param(childrenComponent) 子组件属性 类似ElSelect、ElCheckboxGroup、ElRadioGroup等组件 27 | */ 28 | export interface FormItemListProps { 29 | component: string; 30 | label: string; 31 | prop: P; 32 | props?: object; 33 | rules?: Arrayable; 34 | childrenComponent?: { 35 | props?: object; 36 | options?: Array; 37 | }; 38 | render?: (data: { formModel: T; formItem: FormItemListProps }) => VNode | VNode[] | string; 39 | } 40 | 41 | export interface FormSelectOptProps { 42 | label: string; 43 | value: string | number | boolean; 44 | } 45 | 46 | // 暴露:{ formItem: FormItemListProps; formModel: T } 类型 47 | export interface FormItemRenderProps { 48 | formItem: FormItemListProps; 49 | formModel: T; 50 | } 51 | 52 | // slottYPE 53 | export type FormSlotType = { 54 | [key in keyof T]: (props: FormItemRenderProps) => any; 55 | }; 56 | -------------------------------------------------------------------------------- /src/views/error/403.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ $t('error-page.403.title') }} 19 | 20 | 21 | {{ $t('error-page.403.description') }} 22 | 23 | 24 | 25 | {{ $t('error-page.back') }} 26 | 27 | 28 | 29 | 30 | 70 | -------------------------------------------------------------------------------- /src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ $t('error-page.404.title') }} 19 | 20 | 21 | {{ $t('error-page.404.description') }} 22 | 23 | 24 | 25 | {{ $t('error-page.back') }} 26 | 27 | 28 | 29 | 30 | 70 | -------------------------------------------------------------------------------- /src/views/error/500.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ $t('error-page.500.title') }} 19 | 20 | 21 | {{ $t('error-page.500.description') }} 22 | 23 | 24 | 25 | {{ $t('error-page.back') }} 26 | 27 | 28 | 29 | 30 | 70 | -------------------------------------------------------------------------------- /src/utils/waterMark.ts: -------------------------------------------------------------------------------- 1 | // 全局保存 canvas 和 div ,避免重复创建(单例模式) 2 | const globalCanvas = null; 3 | const globalWaterMark = null; 4 | 5 | export interface WaterMarkOptions { 6 | width?: number; 7 | height?: number; 8 | rotate?: number; 9 | fontSize?: string; 10 | fillStyle?: string; 11 | textAlign?: CanvasTextAlign; 12 | } 13 | 14 | export const waterMarkStyle = ` 15 | display: inline-block; 16 | overflow: hidden; 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | width: 100%; 21 | height: 100%; 22 | pointer-events: none; 23 | background-repeat: repeat; 24 | z-index: 1000; 25 | `; 26 | 27 | const domSymbol = Symbol('watermark-dom'); 28 | 29 | export function createBase64(text: string, options?: WaterMarkOptions) { 30 | const { width, height, rotate, fontSize, fillStyle, textAlign } = options || {}; 31 | const canvas = globalCanvas || document.createElement('canvas'); 32 | canvas.width = width || 240; 33 | canvas.height = height || 100; 34 | const ctx = canvas.getContext('2d'); // 获取画布上下文 35 | 36 | if (ctx && text) { 37 | ctx.rotate(rotate || (-20 * Math.PI) / 180); 38 | ctx.font = fontSize || '14px normal'; 39 | ctx.fillStyle = fillStyle || 'rgba(180, 180, 180, 0.3)'; 40 | ctx.textAlign = textAlign || 'left'; 41 | ctx.textBaseline = 'middle'; 42 | ctx.fillText(text, canvas.width / 20, canvas.height / 1.5); 43 | } 44 | 45 | return canvas.toDataURL('image/png'); 46 | } 47 | 48 | // 设置水印 49 | export function createWaterMarkDom() { 50 | // 创建 waterMark 父元素 51 | const waterMark = globalWaterMark || document.createElement('div'); 52 | waterMark.id = domSymbol.toString(); 53 | waterMark.className = `water-mark`; 54 | waterMark.setAttribute('style', `${waterMarkStyle}`); 55 | 56 | return waterMark; 57 | } 58 | -------------------------------------------------------------------------------- /src/views/index/components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | -------------------------------------------------------------------------------- /src/assets/icons/echarts.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/event/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import { useDebounceFn, useThrottleFn } from '@vueuse/core'; 2 | import { ref, unref, watch } from 'vue'; 3 | import type { Ref } from 'vue'; 4 | 5 | export type RemoveEventFn = () => void; 6 | export interface UseEventParams { 7 | el?: Element | Ref | Window | any; 8 | name: string; 9 | listener: EventListener; 10 | options?: boolean | AddEventListenerOptions; 11 | autoRemove?: boolean; 12 | isDebounce?: boolean; 13 | wait?: number; 14 | } 15 | export function useEventListener({ 16 | el = window, 17 | name, 18 | listener, 19 | options, 20 | autoRemove = true, 21 | isDebounce = true, 22 | wait = 80, 23 | }: UseEventParams): { removeEvent: RemoveEventFn } { 24 | let remove: RemoveEventFn = () => {}; 25 | const isAddRef = ref(false); 26 | 27 | if (el) { 28 | const element = ref(el as Element) as Ref; 29 | const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait); 30 | const realHandler = wait ? handler : listener; 31 | const removeEventListener = (e: Element) => { 32 | isAddRef.value = true; 33 | e.removeEventListener(name, realHandler, options); 34 | }; 35 | const addEventListener = (e: Element) => e.addEventListener(name, realHandler, options); 36 | 37 | const removeWatch = watch( 38 | element, 39 | (v, _ov, cleanUp) => { 40 | if (v) { 41 | !unref(isAddRef) && addEventListener(v); 42 | cleanUp(() => { 43 | autoRemove && removeEventListener(v); 44 | }); 45 | } 46 | }, 47 | { immediate: true }, 48 | ); 49 | 50 | remove = () => { 51 | removeEventListener(element.value); 52 | removeWatch(); 53 | }; 54 | } 55 | return { removeEvent: remove }; 56 | } 57 | -------------------------------------------------------------------------------- /src/views/index/components/Comment.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 某某某 11 | 2021-12-31 12 | 13 | 据我观察,你今天吃意大利面遇到不良商家,没有拌42号混凝土,以及使用劣质螺丝钉,严重影响你的扭矩,从而影响UFO的产量,吸收不足引发内蜂蜜失调,导致排放系统紊乱,对整个太平洋以及充电器造成严重的核污染。我个人认为,这个意大利面就应该拌42号混凝土,因为这个螺丝钉的长度,它很容易会直接影响到挖掘机的扭矩你知道吧,你往里砸的时候,一瞬间它就会产生大量的高能蛋白。 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 64 | -------------------------------------------------------------------------------- /src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {{ toName[index] }} 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{ cindex }} 37 | 38 | {{ i }} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 58 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | import type { StorageConfig } from '@jsxiaosi/utils/es/window/storage/types'; 2 | import type { _RouteLocationBase, LocationQuery, RouteParams, RouteRecordName } from 'vue-router'; 3 | import type { PermissionMode } from '@/enum/role'; 4 | import type { localeKey } from '@/locales/types'; 5 | import type { AppRouteRecordRaw, Meta } from '@/router/type'; 6 | 7 | export interface AppState { 8 | appConfigMode: AppConfig; 9 | } 10 | 11 | export type SidebarMode = 'vertical' | 'horizontal' | 'blend'; 12 | 13 | export interface AppConfig { 14 | // 标题 15 | title: string; 16 | // 折叠菜单 17 | collapseMenu: boolean; 18 | // 菜单显示模式: 'vertical':左侧模式 | 'horizontal':顶部模式 | 'blend':混合模式 19 | sidebarMode: SidebarMode; 20 | // 主题模式:白天主题、夜间主题 21 | themeMode: 'light' | 'dark'; 22 | // 国际化 23 | locale: localeKey; 24 | // storage配置 25 | StorageConfig: StorageConfig; 26 | // 移动端菜单 27 | drawerSidebar?: boolean; 28 | // 主题颜色 29 | primaryColor: string; 30 | // 灰色模式 31 | greyMode: boolean; 32 | // 色弱模式 33 | colorWeaknessMode: boolean; 34 | // 隐藏侧边菜单栏 35 | hideSidebar: boolean; 36 | // 隐藏顶部 37 | hideNavbart: boolean; 38 | // 隐藏标签栏 39 | hideTabs: boolean; 40 | // 关闭标签页拖拽 41 | closeTabDrag: boolean; 42 | // 隐藏标签栏操作按钮 43 | hideTabsConfig: boolean; 44 | // 标签持久化 45 | tabPersistent: boolean; 46 | // 侧边栏按钮 47 | sidebarFold: 'none' | 'top' | 'bottom'; 48 | // 路由模式 REAREND后端路由、ROLE角色权限控制路由 49 | permissionMode: keyof typeof PermissionMode; 50 | } 51 | 52 | export type MultiTabsType = Omit< 53 | _RouteLocationBase, 54 | 'fullPath' | 'hash' | 'params' | 'query' | 'redirectedFrom' | 'meta' 55 | > & { 56 | query?: LocationQuery; 57 | params?: RouteParams; 58 | meta?: Meta; 59 | }; 60 | export interface PermissionState { 61 | wholeMenus: AppRouteRecordRaw[]; 62 | cachePageList: RouteRecordName[]; 63 | multiTabs: MultiTabsType[]; 64 | } 65 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/Sidebar/MinSidebar.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 35 | 40 | 41 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/hooks/web/useIntro.ts: -------------------------------------------------------------------------------- 1 | // https://introjs.com/docs 2 | 3 | import intro from 'intro.js'; 4 | import type { TourOptions } from 'intro.js/src/packages/tour/option'; 5 | import 'intro.js/minified/introjs.min.css'; 6 | 7 | async function useIntro(options: Partial = {}) { 8 | intro() 9 | .setOptions({ 10 | ...{ 11 | prevLabel: '上一步', 12 | nextLabel: '下一步', 13 | skipLabel: '跳过', 14 | doneLabel: '结束', 15 | tooltipPosition: 'bottom' /* 引导说明框相对高亮说明区域的位置 */, 16 | tooltipClass: 'custom-intro-tooltip' /* 引导说明文本框的样式 */, 17 | highlightClass: 'custom-intro-highlight' /* 说明高亮区域的样式 */, 18 | exitOnEsc: true /* 是否使用键盘Esc退出 */, 19 | exitOnOverlayClick: false /* 是否允许点击空白处退出 */, 20 | showStepNumbers: false /* 是否显示说明的数据步骤*/, 21 | keyboardNavigation: true /* 是否允许键盘来操作 */, 22 | showButtons: true /* 是否按键来操作 */, 23 | showBullets: true /* 是否使用点点点显示进度 */, 24 | showProgress: false /* 是否显示进度条 */, 25 | scrollToElement: true /* 是否滑动到高亮的区域 */, 26 | overlayOpacity: 0.8 /* 遮罩层的透明度 */, 27 | positionPrecedence: ['bottom', 'top', 'right', 'left'] /* 当位置选择自动的时候,位置排列的优先级 */, 28 | disableInteraction: true /* 是否禁止与元素的相互关联 */, 29 | hintPosition: 'top-middle' /* 默认提示位置 */, 30 | hintButtonLabel: '默认提示内容' /* 默认提示内容 */, 31 | steps: [ 32 | { 33 | title: '欢迎', 34 | intro: '懒得做中英翻译,自行解决 👋', 35 | }, 36 | { 37 | title: '导航栏', 38 | element: '.sidebar-container', 39 | intro: '这是导航栏', 40 | }, 41 | { 42 | title: '折叠按钮', 43 | element: '.breadcrumb-fold', 44 | intro: '这是折叠按钮', 45 | }, 46 | { 47 | title: '操作', 48 | element: '.navbar-right', 49 | intro: '功能区', 50 | }, 51 | ], 52 | }, 53 | ...options, 54 | }) 55 | .start(); 56 | } 57 | 58 | export default useIntro; 59 | -------------------------------------------------------------------------------- /src/assets/icons/full_screen_page.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/CountTo/index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 67 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | {{ translateI18n(props.title) }} 47 | 48 | 49 | 50 | 51 | 52 | 66 | -------------------------------------------------------------------------------- /mock/account.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from 'vite-plugin-fake-server/client'; 2 | 3 | const userInfo = { 4 | name: '爱喝蜂蜜绿的小斯斯', 5 | userid: '00000001', 6 | email: '1531733886@qq.com', 7 | signature: '甜甜的蜂蜜,甘甜的绿茶,蜂蜜中和了绿茶的苦涩保留了绿茶回甘,绝妙啊', 8 | introduction: '微笑着,努力着,欣赏着', 9 | title: '小斯斯', 10 | token: '', 11 | role: 'admin', 12 | }; 13 | 14 | const userInfo2 = { 15 | name: 'test', 16 | userid: '00000002', 17 | email: '12312311223@qq.com', 18 | signature: '小啊小啊浪', 19 | introduction: '一个只会喝蜂蜜绿的小前端', 20 | title: '咪咪咪', 21 | token: '', 22 | role: 'test', 23 | }; 24 | 25 | export default defineFakeRoute([ 26 | { 27 | url: '/mock_api/login', 28 | timeout: 1000, 29 | method: 'post', 30 | response: ({ body }: { body: Recordable }) => { 31 | const { username, password } = body; 32 | if (username === 'admin' && password === 'admin123') { 33 | userInfo.token = genID(16); 34 | return { 35 | data: userInfo, 36 | code: 1, 37 | message: 'ok', 38 | }; 39 | } else if (username === 'test' && password === 'test123') { 40 | userInfo2.token = genID(16); 41 | return { 42 | data: userInfo2, 43 | code: 1, 44 | message: 'ok', 45 | }; 46 | } else { 47 | return { 48 | data: null, 49 | code: -1, 50 | message: '账号密码错误', 51 | }; 52 | } 53 | }, 54 | // rawResponse: async (req, res) => { 55 | // console.log(req, res); 56 | // let reqbody = {}; 57 | // res.setHeader('Content-Type', 'application/json'); 58 | // reqbody = { data: userInfo }; 59 | // res.statusCode = 500; 60 | // res.end(JSON.stringify(reqbody)); 61 | // }, 62 | }, 63 | { 64 | url: '/mock_api/getUserInfo', 65 | timeout: 1000, 66 | method: 'get', 67 | response: () => { 68 | return userInfo; 69 | }, 70 | }, 71 | ]); 72 | 73 | function genID(length: number) { 74 | return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36); 75 | } 76 | -------------------------------------------------------------------------------- /src/views/editor/logic-flow/index.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 63 | 64 | 65 | 66 | 71 | -------------------------------------------------------------------------------- /src/router/modules/nested/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const nested: AppRouteRecordRaw[] = [ 5 | { 6 | path: '/nested', 7 | redirect: '/nested/menu1', 8 | name: 'RtNested', 9 | meta: { 10 | title: t('route.pathName.nested'), 11 | icon: 'iEL-grid', 12 | position: 6, 13 | }, 14 | children: [ 15 | { 16 | path: 'menu1', 17 | name: 'RtMenu1', 18 | redirect: '/nested/menu1/menu1-1', 19 | meta: { title: t('route.pathName.nested1') }, 20 | children: [ 21 | { 22 | path: 'menu1-1', 23 | component: () => import('@/views/nested/menu1/menu1-1/index.vue'), 24 | name: 'RtMenu1-1', 25 | meta: { title: t('route.pathName.nested1_1') }, 26 | }, 27 | { 28 | path: 'menu1-2', 29 | name: 'RtMenu1-2', 30 | redirect: '/nested/menu1/menu1-2/menu1-2-1', 31 | meta: { title: t('route.pathName.nested1_2') }, 32 | children: [ 33 | { 34 | path: 'menu1-2-1', 35 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1/index.vue'), 36 | name: 'RtMenu1-2-1', 37 | meta: { title: t('route.pathName.nested1_2_1') }, 38 | }, 39 | { 40 | path: 'menu1-2-2', 41 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2/index.vue'), 42 | name: 'RtMenu1-2-2', 43 | meta: { title: t('route.pathName.nested1_2_2') }, 44 | }, 45 | ], 46 | }, 47 | { 48 | path: 'menu1-3', 49 | component: () => import('@/views/nested/menu1/menu1-3/index.vue'), 50 | name: 'RtMenu1-3', 51 | meta: { title: t('route.pathName.nested1_3') }, 52 | }, 53 | ], 54 | }, 55 | { 56 | path: 'menu2', 57 | component: () => import('@/views/nested/menu2/index.vue'), 58 | name: 'RtMenu2', 59 | meta: { title: t('route.pathName.nested2') }, 60 | }, 61 | ], 62 | }, 63 | ]; 64 | 65 | export default nested; 66 | -------------------------------------------------------------------------------- /src/views/editor/logic-flow/adpter-for-turbo.ts: -------------------------------------------------------------------------------- 1 | const TurboType = { 2 | SEQUENCE_FLOW: 1, 3 | START_EVENT: 2, 4 | END_EVENT: 3, 5 | USER_TASK: 4, 6 | SERVICE_TASK: 5, 7 | EXCLUSIVE_GATEWAY: 6, 8 | }; 9 | 10 | function convertFlowElementToEdge(element: Recordable) { 11 | const { incoming, outgoing, properties, key } = element; 12 | const { text, startPoint, endPoint, pointsList, logicFlowType } = properties; 13 | const edge: Recordable = { 14 | id: key, 15 | type: logicFlowType, 16 | sourceNodeId: incoming[0], 17 | targetNodeId: outgoing[0], 18 | text, 19 | startPoint, 20 | endPoint, 21 | pointsList, 22 | properties: {}, 23 | }; 24 | const excludeProperties = ['startPoint', 'endPoint', 'pointsList', 'text', 'logicFlowType']; 25 | Object.keys(element.properties).forEach(property => { 26 | if (excludeProperties.indexOf(property) === -1) { 27 | edge.properties[property] = element.properties[property]; 28 | } 29 | }); 30 | return edge; 31 | } 32 | 33 | function convertFlowElementToNode(element: Recordable) { 34 | const { properties, key } = element; 35 | const { x, y, text, logicFlowType } = properties; 36 | const node: Recordable = { 37 | id: key, 38 | type: logicFlowType, 39 | x, 40 | y, 41 | text, 42 | properties: {}, 43 | }; 44 | const excludeProperties = ['x', 'y', 'text', 'logicFlowType']; 45 | Object.keys(element.properties).forEach(property => { 46 | if (excludeProperties.indexOf(property) === -1) { 47 | node.properties[property] = element.properties[property]; 48 | } 49 | }); 50 | return node; 51 | } 52 | 53 | export function toLogicFlowData(data: Recordable) { 54 | const lfData: { 55 | // TODO type 56 | nodes: any[]; 57 | edges: any[]; 58 | } = { 59 | nodes: [], 60 | edges: [], 61 | }; 62 | const list = data.flowElementList; 63 | list && 64 | list.length > 0 && 65 | list.forEach((element: Recordable) => { 66 | if (element.type === TurboType.SEQUENCE_FLOW) { 67 | const edge = convertFlowElementToEdge(element); 68 | lfData.edges.push(edge); 69 | } else { 70 | const node = convertFlowElementToNode(element); 71 | lfData.nodes.push(node); 72 | } 73 | }); 74 | return lfData; 75 | } 76 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/SideNavigationBar/index.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 52 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 83 | -------------------------------------------------------------------------------- /src/components/Form/src/components/FormItem.vue: -------------------------------------------------------------------------------- 1 | 62 | -------------------------------------------------------------------------------- /src/components/Form/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 47 | 48 | 49 | 50 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Create 68 | Reset 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/router/modules/components/index.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@/hooks/web/useI18n'; 2 | import type { AppRouteRecordRaw } from '@/router/type'; 3 | 4 | const component: AppRouteRecordRaw[] = [ 5 | { 6 | path: '/components', 7 | // component: Layout, 8 | redirect: '/components/form', 9 | name: 'RtComponents', 10 | meta: { 11 | title: t('route.pathName.components'), 12 | icon: 'components', 13 | position: 2, 14 | }, 15 | children: [ 16 | { 17 | path: 'form', 18 | name: 'RtForm', 19 | component: () => import('@/views/components/form/index.vue'), 20 | meta: { title: t('route.pathName.form'), keepAlive: true }, 21 | }, 22 | { 23 | path: 'table', 24 | name: 'RtTable', 25 | component: () => import('@/views/components/table-page/index.vue'), 26 | meta: { title: t('route.pathName.table') }, 27 | }, 28 | { 29 | path: 'drag', 30 | name: 'RtDrag', 31 | component: () => import('@/views/components/drag/index.vue'), 32 | meta: { title: t('route.pathName.dragCpts') }, 33 | }, 34 | { 35 | path: 'count-to', 36 | name: 'RtCountTo', 37 | component: () => import('@/views/components/count-to/index.vue'), 38 | meta: { title: t('route.pathName.countTo') }, 39 | }, 40 | { 41 | path: 'seamless-scroll', 42 | name: 'RtSeamlessScroll', 43 | component: () => import('@/views/components/seamless-scroll/index.vue'), 44 | meta: { title: t('route.pathName.seamlessScroll') }, 45 | }, 46 | { 47 | path: 'date-time', 48 | // Parent router-view 49 | name: 'DateTime', 50 | redirect: '/components/date-time/date', 51 | meta: { title: t('route.pathName.date') }, 52 | children: [ 53 | { 54 | path: 'date-select', 55 | name: 'RtDate', 56 | component: () => import('@/views/components/date/index.vue'), 57 | meta: { title: t('route.pathName.dateSelect'), keepAlive: true }, 58 | }, 59 | { 60 | path: 'calendar', 61 | name: 'RtCalendar', 62 | component: () => import('@/views/components/calendar/index.vue'), 63 | meta: { title: t('route.pathName.calendar') }, 64 | }, 65 | ], 66 | }, 67 | ], 68 | }, 69 | ]; 70 | 71 | export default component; 72 | -------------------------------------------------------------------------------- /src/views/index/components/AnalysisChart.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | -------------------------------------------------------------------------------- /src/views/index/components/WordCloud.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 72 | 73 | 74 | 75 | 81 | -------------------------------------------------------------------------------- /src/hooks/web/useECharts.ts: -------------------------------------------------------------------------------- 1 | import { tryOnUnmounted, useDebounceFn } from '@vueuse/core'; 2 | import { storeToRefs } from 'pinia'; 3 | import { nextTick, ref, unref, watch } from 'vue'; 4 | import type { EChartsOption } from 'echarts'; 5 | import type { Ref } from 'vue'; 6 | import { useEventListener } from '@/hooks/event/useEventListener'; 7 | import { useAppStoreHook } from '@/store/modules/app'; 8 | import echarts from '@/utils/plugin/echarts'; 9 | 10 | export type createEChartsOption = EChartsOption; 11 | 12 | export function useECharts(elRef: Ref) { 13 | let chartInstance: echarts.ECharts | null = null; 14 | const cacheOptions = ref({}) as Ref; 15 | let resizeFn: Fn = resize; 16 | let removeResizeFn: Fn = () => {}; 17 | resizeFn = useDebounceFn(resize, 200); 18 | 19 | const appStore = useAppStoreHook(); 20 | 21 | const { appConfigMode } = storeToRefs(appStore); 22 | 23 | watch( 24 | () => appStore.appConfigMode.themeMode, 25 | () => { 26 | if (chartInstance) { 27 | chartInstance.dispose(); 28 | initCharts(); 29 | setOptions(unref(cacheOptions), false); 30 | } 31 | }, 32 | ); 33 | 34 | // 创建echarts 35 | function initCharts() { 36 | // 获取ref demo 37 | const el = unref(elRef); 38 | if (!el || !unref(el)) { 39 | return; 40 | } 41 | chartInstance = echarts.init(el, appConfigMode.value.themeMode); 42 | 43 | // 监听window宽度变化重新渲染echarts 44 | const { removeEvent } = useEventListener({ 45 | el: window, 46 | name: 'resize', 47 | listener: resizeFn, 48 | }); 49 | removeResizeFn = removeEvent; 50 | } 51 | 52 | // 配置echarts属性,如果chartInstance为空就创建echarts实例; 53 | function setOptions(options: EChartsOption, clear = true) { 54 | cacheOptions.value = options; 55 | nextTick(() => { 56 | // 判断是否创建echarts实例 57 | if (!chartInstance) { 58 | initCharts(); 59 | if (!chartInstance) return; 60 | } 61 | 62 | // 清空当前实例,会移除实例中所有的组件和图表 63 | clear && chartInstance?.clear(); 64 | chartInstance?.setOption(unref(cacheOptions), true); 65 | }); 66 | } 67 | 68 | // 改变图表大小 69 | function resize() { 70 | chartInstance?.resize(); 71 | } 72 | 73 | tryOnUnmounted(() => { 74 | if (!chartInstance) return; 75 | removeResizeFn(); 76 | chartInstance.dispose(); 77 | chartInstance = null; 78 | }); 79 | 80 | return { 81 | echarts, 82 | setOptions, 83 | resize, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/views/echarts/bar/components/BarRace.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 83 | 84 | 85 | 86 | 动态柱状图 87 | 88 | 89 | 90 | 91 | 97 | -------------------------------------------------------------------------------- /src/views/functions/water-mark/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | 36 | 37 | 修改水印名称: 38 | 39 | 40 | 确定 41 | 42 | 43 | (全局) 44 | 清除 45 | 46 | 47 | 48 | 修改水印名称: 49 | 50 | 51 | 确定 52 | 53 | 54 | (hooks) 55 | 清除 56 | 57 | 58 | 59 | 60 | 修改水印名称: 61 | 62 | 63 | 确定 64 | 65 | 66 | (指令) 67 | 68 | 69 | 70 | 71 | 72 | 87 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { _storage, isUrl } from '@jsxiaosi/utils'; 2 | import { createRouter, createWebHistory } from 'vue-router'; 3 | import type { App } from 'vue'; 4 | import type { RouteRecordRaw } from 'vue-router'; 5 | import { getConfig } from '@/config'; 6 | import { translateI18n } from '@/hooks/web/useI18n'; 7 | import { usePermissionStoreHook } from '@/store/modules/permission'; 8 | import { useUserInfoStoreHook } from '@/store/modules/user'; 9 | import NProgress from '@/utils/plugin/progress'; 10 | import { configRouteList } from './modules'; 11 | import { handleAliveRoute, initRoute } from './utils'; 12 | 13 | const { whiteRouteModulesList, routeModulesList } = configRouteList(); 14 | 15 | // 在导航栏上的路由 16 | export const sidebarRouteList = routeModulesList; 17 | 18 | export const router = createRouter({ 19 | history: createWebHistory(''), 20 | routes: whiteRouteModulesList as unknown as RouteRecordRaw[], 21 | }); 22 | 23 | export const configMainRouter = async (app: App) => { 24 | app.use(router); 25 | await router.isReady(); 26 | }; 27 | 28 | // 路由守卫 29 | router.beforeEach((to, from, next) => { 30 | NProgress.start(); 31 | if (to.meta?.keepAlive) { 32 | const newMatched = to.matched; 33 | handleAliveRoute(newMatched, 'add'); 34 | // 页面整体刷新 35 | if (from.name === undefined || from.name === 'Redirect') { 36 | handleAliveRoute(newMatched); 37 | } 38 | } 39 | 40 | if (!isUrl(to.path) && to.meta.title) { 41 | const Title = getConfig().title; 42 | if (Title) document.title = `${translateI18n(to.meta.title)} | ${Title}`; 43 | else document.title = translateI18n(to.meta.title); 44 | } 45 | 46 | const userInfoStore = useUserInfoStoreHook(); 47 | 48 | if (userInfoStore.userInfo) { 49 | // 已登陆状态不允许去登录页 50 | if (to.path === '/login') { 51 | next({ 52 | path: from.path, 53 | query: from.query, 54 | }); 55 | return; 56 | } 57 | 58 | if (from.name) { 59 | next(); 60 | } else { 61 | if (usePermissionStoreHook().wholeMenus.length === 0) { 62 | initRoute(userInfoStore.roles).then(res => { 63 | if (res.length) { 64 | router.push({ 65 | path: to.path, 66 | query: to.query, 67 | }); 68 | } else { 69 | userInfoStore.removeUserInfo(); 70 | router.push('/login'); 71 | } 72 | }); 73 | } else { 74 | next(); 75 | } 76 | } 77 | } else { 78 | if (to.path !== '/login') { 79 | next({ path: '/login' }); 80 | } else { 81 | next(); 82 | } 83 | } 84 | }); 85 | 86 | router.afterEach(() => { 87 | NProgress.done(); 88 | }); 89 | -------------------------------------------------------------------------------- /src/views/components/date/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 44 | 45 | 日期: 46 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 95 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 66 | 67 | 74 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/components/Table/src/components/TableChild.tsx: -------------------------------------------------------------------------------- 1 | import { ElTableColumn } from 'element-plus'; 2 | import { defineComponent } from 'vue'; 3 | import type { TableColumnInstance } from 'element-plus'; 4 | import type { SetupContext, VNode } from 'vue'; 5 | import { getSlot } from '@/utils/slotsHelper'; 6 | import type { TableColumnProps } from '../../types/table'; 7 | 8 | interface RenderType { 9 | default?: (scope: Recordable) => VNode[] | VNode | null; 10 | header?: (scope: Recordable) => VNode[] | VNode | null; 11 | } 12 | 13 | // TODO: header 存在children时,slots 无法渲染 14 | const TableChild = defineComponent( 15 | (props: { item: TableColumnProps }, { slots }: SetupContext) => { 16 | const { item } = props; 17 | 18 | function eLComponent(childrenRender: RenderType | null | undefined = null) { 19 | const { children, ...reItem } = item; 20 | if (childrenRender?.default) { 21 | return {childrenRender}; 22 | } else { 23 | return ( 24 | 25 | {childrenRender?.header} 26 | {children?.map(child => ( 27 | 28 | ))} 29 | 30 | ); 31 | } 32 | } 33 | 34 | function slotsComponent() { 35 | const slotContent: RenderType = { 36 | default: (scope: Recordable) => getSlot(slots, String(item.prop), scope), 37 | header: (scope: Recordable) => 38 | getSlot(slots, `${String(item.prop)}_header`, { 39 | ...scope, 40 | customItem: item, 41 | }), 42 | }; 43 | if (!slots[`${String(item.prop)}_header`]) delete slotContent.header; 44 | 45 | if (!slots[String(item.prop)]) delete slotContent.default; 46 | 47 | return eLComponent(slotContent); 48 | } 49 | 50 | function renderComponent() { 51 | const { render, render_header } = item; 52 | 53 | const renderContent: RenderType = {}; 54 | 55 | if (render_header) { 56 | renderContent.header = (scope: Recordable) => render_header({ ...scope, customItem: item }); 57 | } 58 | 59 | if (render) { 60 | renderContent.default = (scope: Recordable) => { 61 | // console.log(scope, 'scope', item.type); 62 | return render({ ...scope, customItem: item }); 63 | }; 64 | } 65 | 66 | return eLComponent(renderContent); 67 | } 68 | 69 | return () => { 70 | const { isSlots, render, render_header } = item; 71 | const getContent = isSlots ? slotsComponent() : render || render_header ? renderComponent() : eLComponent(); 72 | return getContent; 73 | }; 74 | }, 75 | { 76 | name: 'TableChild', 77 | props: ['item'], 78 | }, 79 | ); 80 | 81 | export default TableChild; 82 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Vue Xs Admin 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 109 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/Seting/pageSettings/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 25 | 26 | {{ $t('layout.hideSidebar') }} 27 | 28 | 29 | 30 | {{ $t('layout.hideNavBart') }} 31 | 32 | 33 | 34 | {{ $t('layout.hideTabs') }} 35 | 36 | 37 | 38 | {{ $t('layout.hideTabsConfig') }} 39 | 40 | 41 | 42 | {{ $t('layout.closeTabDrag') }} 43 | 44 | 45 | 46 | {{ $t('layout.labelPersistent') }} 47 | 48 | 49 | 50 | {{ $t('layout.sidebarFold') }} 51 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 93 | -------------------------------------------------------------------------------- /src/views/components/seamless-scroll/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 48 | 49 | 50 | 51 | 无缝滚动示例 52 | 53 | 向上滚动 54 | 55 | 56 | 61 | 向下滚动 62 | 63 | 64 | 65 | 向左滚动 66 | 67 | 68 | 向右滚动 69 | 70 | 71 | 72 | 73 | 74 | 75 | {{ item.title }} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 105 | -------------------------------------------------------------------------------- /src/layouts/page-layouts/components/AppTabs/hooks/useTabsChange.ts: -------------------------------------------------------------------------------- 1 | import { addClass, removeClass } from '@jsxiaosi/utils'; 2 | import qs from 'qs'; 3 | import { unref } from 'vue'; 4 | import { useRoute, useRouter } from 'vue-router'; 5 | import type { Ref } from 'vue'; 6 | import type { RouteLocationNormalizedLoaded } from 'vue-router'; 7 | import { usePermissionStoreHook } from '@/store/modules/permission'; 8 | import type { MultiTabsType } from '@/store/types'; 9 | 10 | export const useTabsChange = (multiTabs: Ref) => { 11 | const route = useRoute(); 12 | const router = useRouter(); 13 | 14 | const setTabPaneKey = (item: MultiTabsType | RouteLocationNormalizedLoaded): string => { 15 | return `${item.path}${item.query && Object.keys(item.query).length ? `?${qs.stringify(item.query)}` : ''}`; 16 | }; 17 | 18 | // 添加标签 19 | const addRouteTabs = (routeRaw: MultiTabsType) => { 20 | const { path, name, meta } = routeRaw; 21 | if (!meta?.hideTabs && !meta?.hideSidebar) { 22 | const currentRoute = { path, meta, name }; 23 | usePermissionStoreHook().handleMultiTabs('add', currentRoute); 24 | } 25 | }; 26 | 27 | // 关闭标签 28 | const closeTabsRoute = (e: MultiTabsType, type: 'other' | 'left' | 'right') => { 29 | const item = multiTabs.value.findIndex(i => setTabPaneKey(i) === setTabPaneKey(e)); 30 | const mapList = multiTabs.value.filter((i, index) => { 31 | if (i.path !== e.path && type === 'other') return true; 32 | else if (index < item && type === 'left') return true; 33 | else if (index > item && type === 'right') return true; 34 | return false; 35 | }); 36 | if (mapList.find(i => i.path === route.path)) { 37 | const { path, query } = multiTabs.value[item]; 38 | router.push({ 39 | path, 40 | query, 41 | }); 42 | } 43 | mapList.forEach(i => usePermissionStoreHook().handleMultiTabs('delete', i)); 44 | }; 45 | 46 | // 关闭当前导航 47 | const removeTab = (e: MultiTabsType) => { 48 | const item = multiTabs.value.findIndex(i => setTabPaneKey(i) === setTabPaneKey(e)); 49 | const tabsLength = multiTabs.value.length; 50 | let value, toRoute; 51 | if (multiTabs.value[item].name === route.name) { 52 | if (item === tabsLength - 1) { 53 | value = multiTabs.value[item - 1]; 54 | } else { 55 | value = multiTabs.value[tabsLength - 1]; 56 | } 57 | toRoute = { 58 | path: value.path, 59 | query: value.query, 60 | }; 61 | router.push(toRoute); 62 | } 63 | 64 | usePermissionStoreHook().handleMultiTabs('delete', multiTabs.value[item]); 65 | }; 66 | 67 | // 重新加载 68 | function onFresh(item?: MultiTabsType) { 69 | const refreshButton = 'refresh-button'; 70 | addClass(document.querySelector('.rotate') as HTMLElement, refreshButton); 71 | const { path, query } = unref(item || route); 72 | router.replace({ 73 | path: `/redirect${path}`, 74 | query, 75 | }); 76 | setTimeout(() => { 77 | removeClass(document.querySelector('.rotate') as HTMLElement, refreshButton); 78 | }, 600); 79 | } 80 | 81 | return { setTabPaneKey, addRouteTabs, onFresh, closeTabsRoute, removeTab }; 82 | }; 83 | -------------------------------------------------------------------------------- /src/views/login/compoontne/form.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {{ $t('sys.login.forgotPassword') }} 74 | 75 | 76 | 77 | 78 | 79 | 80 | {{ $t('sys.login.loginButton') }} 81 | 82 | 83 | 84 | 85 | 86 | 99 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './theme'; 2 | @use './intro'; 3 | @use './transition'; 4 | @use './sidebar'; 5 | @use './mixin'; 6 | 7 | body { 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif; 12 | font-size: var(--font-size-base); 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-font-smoothing: antialiased; 15 | text-rendering: optimizelegibility; 16 | } 17 | 18 | label { 19 | font-weight: 700; 20 | } 21 | 22 | html { 23 | box-sizing: border-box; 24 | height: 100%; 25 | } 26 | 27 | #app { 28 | // height: 100%; 29 | color: var(--text-color-primary); 30 | } 31 | 32 | *, 33 | *::before, 34 | *::after { 35 | box-sizing: inherit; 36 | } 37 | 38 | div:focus { 39 | outline: none; 40 | } 41 | 42 | .clearfix { 43 | &::after { 44 | content: ' '; 45 | display: block; 46 | visibility: hidden; 47 | height: 0; 48 | clear: both; 49 | font-size: 0; 50 | } 51 | } 52 | 53 | .title { 54 | display: inline-block; 55 | font-size: var(--font-size-large); 56 | font-weight: 700; 57 | } 58 | 59 | .page-container { 60 | width: 100%; 61 | min-height: 100%; 62 | padding: 20px; 63 | border-radius: 10px; 64 | background-color: var(--main-bg-color); 65 | } 66 | 67 | ul { 68 | margin: 0; 69 | padding: 0; 70 | list-style-type: none; 71 | } 72 | 73 | .cursor { 74 | cursor: pointer; 75 | } 76 | 77 | .disabled { 78 | border-color: var(--text-color-disabled); 79 | color: var(--text-color-disabled); 80 | 81 | &:hover { 82 | border-color: var(--text-color-disabled) !important; 83 | background-color: var(--main-bg-color) !important; 84 | color: var(--text-color-disabled) !important; 85 | cursor: not-allowed; 86 | } 87 | } 88 | 89 | // 灰色 90 | .html-grey { 91 | filter: grayscale(100%); 92 | } 93 | 94 | // 色弱 95 | .html-weakness { 96 | filter: invert(80%); 97 | } 98 | 99 | @for $i from 1 to 6 { 100 | * > .enter-x:nth-child(#{$i}) { 101 | transform: translateX(50px); 102 | } 103 | * > .-enter-x:nth-child(#{$i}) { 104 | transform: translateX(-50px); 105 | } 106 | * > .enter-x:nth-child(#{$i}), 107 | * > .-enter-x:nth-child(#{$i}) { 108 | z-index: 10 - $i; 109 | animation: enter-x-animation 0.4s ease-in-out 0.3s; 110 | animation-delay: calc(($i * 1s) / 10); 111 | opacity: 0; 112 | animation-fill-mode: forwards; 113 | } 114 | 115 | * > .enter-y:nth-child(#{$i}) { 116 | transform: translateY(50px); 117 | } 118 | * > .-enter-y:nth-child(#{$i}) { 119 | transform: translateY(-50px); 120 | } 121 | * > .enter-y:nth-child(#{$i}), 122 | * > .-enter-y:nth-child(#{$i}) { 123 | z-index: 10 - $i; 124 | animation: enter-x-animation 0.4s ease-in-out 0.3s; 125 | animation-delay: calc(($i * 1s) / 5); 126 | opacity: 0; 127 | animation-fill-mode: forwards; 128 | } 129 | } 130 | 131 | @keyframes enter-x-animation { 132 | to { 133 | transform: translateX(0); 134 | opacity: 1; 135 | } 136 | } 137 | 138 | @keyframes enter-y-animation { 139 | to { 140 | transform: translateY(0); 141 | opacity: 1; 142 | } 143 | } 144 | --------------------------------------------------------------------------------
6 |-
7 |
8 |
9 |
10 | 某某某
11 | 2021-12-31
12 |
13 | 据我观察,你今天吃意大利面遇到不良商家,没有拌42号混凝土,以及使用劣质螺丝钉,严重影响你的扭矩,从而影响UFO的产量,吸收不足引发内蜂蜜失调,导致排放系统紊乱,对整个太平洋以及充电器造成严重的核污染。我个人认为,这个意大利面就应该拌42号混凝土,因为这个螺丝钉的长度,它很容易会直接影响到挖掘机的扭矩你知道吧,你往里砸的时候,一瞬间它就会产生大量的高能蛋白。
14 |
15 |
16 |
17 |
18 |
19 |
20 |