├── .dockerignore ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── components.d.ts ├── config ├── plugin │ ├── arcoResolver.ts │ ├── arcoStyleImport.ts │ ├── compress.ts │ ├── imagemin.ts │ └── visualizer.ts ├── utils │ └── index.ts ├── vite.config.base.ts ├── vite.config.dev.ts └── vite.config.prod.ts ├── deploy ├── README.md ├── docker-compose.yml └── nginx.conf ├── docs └── images │ ├── api_manage.jpg │ ├── code_generator.jpg │ ├── dept_manage.jpg │ ├── login.jpg │ ├── login_log.jpg │ ├── menu_manage.jpg │ ├── redis_monitor.jpg │ ├── role_manage.jpg │ ├── server_monitor.jpg │ └── user_manage.jpg ├── index.html ├── package.json ├── src ├── App.vue ├── api │ ├── api.ts │ ├── auth.ts │ ├── automatiion.ts │ ├── casbin.ts │ ├── data-rule.ts │ ├── dept.ts │ ├── interceptor.ts │ ├── log.ts │ ├── menu.ts │ ├── monitor.ts │ ├── oauth.ts │ ├── role.ts │ └── user.ts ├── assets │ ├── images │ │ └── login-banner.png │ ├── logo.svg │ ├── style │ │ ├── breakpoint.less │ │ └── global.less │ └── world.json ├── components │ ├── breadcrumb │ │ └── index.vue │ ├── chart │ │ └── index.vue │ ├── footer │ │ └── index.vue │ ├── global-setting │ │ ├── block.vue │ │ ├── form-wrapper.vue │ │ └── index.vue │ ├── icon-picker │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── index.ts │ ├── menu │ │ ├── index.vue │ │ └── use-menu-tree.ts │ ├── navbar │ │ └── index.vue │ └── tab-bar │ │ ├── index.vue │ │ ├── readme.md │ │ └── tab-item.vue ├── config │ └── settings.json ├── directive │ ├── index.ts │ └── permission │ │ └── index.ts ├── env.d.ts ├── hooks │ ├── chart-option.ts │ ├── loading.ts │ ├── locale.ts │ ├── permission.ts │ ├── request.ts │ ├── responsive.ts │ ├── user.ts │ └── visible.ts ├── layout │ ├── default-layout.vue │ └── page-layout.vue ├── locale │ ├── en-US.ts │ ├── en-US │ │ └── settings.ts │ ├── index.ts │ ├── zh-CN.ts │ └── zh-CN │ │ └── settings.ts ├── main.ts ├── router │ ├── app-menus │ │ └── index.ts │ ├── constants.ts │ ├── guard │ │ ├── index.ts │ │ ├── permission.ts │ │ └── userLoginInfo.ts │ ├── index.ts │ ├── routes │ │ ├── base.ts │ │ ├── index.ts │ │ ├── modules │ │ │ ├── admin.ts │ │ │ ├── automation.ts │ │ │ ├── dashboard.ts │ │ │ ├── log.ts │ │ │ └── monitor.ts │ │ └── types.ts │ └── typings.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app │ │ ├── index.ts │ │ └── types.ts │ │ ├── tab-bar │ │ ├── index.ts │ │ └── types.ts │ │ └── user │ │ ├── index.ts │ │ └── types.ts ├── types │ ├── echarts.ts │ └── global.ts ├── utils │ ├── auth.ts │ ├── color.ts │ ├── env.ts │ ├── event.ts │ ├── index.ts │ ├── is.ts │ ├── list.ts │ ├── route-listener.ts │ └── string.ts └── views │ ├── admin │ ├── api │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── data-rule │ │ ├── index.vue │ │ └── local │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── dept │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── menu │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ ├── role │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ └── user │ │ ├── index.vue │ │ └── locale │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── automation │ └── code-generator │ │ ├── index.vue │ │ └── local │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── dashboard │ └── workplace │ │ ├── components │ │ ├── banner.vue │ │ └── data-panel.vue │ │ ├── index.vue │ │ └── locale │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── log │ ├── login │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ └── opera │ │ ├── index.vue │ │ └── locale │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── login │ ├── components │ │ ├── banner.vue │ │ ├── login-form.vue │ │ └── oauth_callback.vue │ ├── index.vue │ └── locale │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── monitor │ ├── redis │ │ ├── components │ │ │ ├── active-series.vue │ │ │ └── commands-series.vue │ │ ├── index.vue │ │ └── locale │ │ │ ├── en-US.ts │ │ │ └── zh-CN.ts │ └── server │ │ ├── index.vue │ │ └── locale │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── not-found │ └── index.vue │ └── redirect │ └── index.vue ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL='http://localhost:8000' 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL='https://xxx.com' 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /*.json 2 | /*.js 3 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | root: true, 6 | parser: "vue-eslint-parser", 7 | parserOptions: { 8 | // Parser that checks the content of the 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fba-ui", 3 | "description": "FastAPI Best Architecture UI", 4 | "version": "0.0.1", 5 | "private": false, 6 | "author": "FastAPI Practices", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "vite --config ./config/vite.config.dev.ts", 10 | "build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts", 11 | "report": "cross-env REPORT=true npm run build", 12 | "preview": "npm run build && vite preview --host", 13 | "type:check": "vue-tsc --noEmit --skipLibCheck", 14 | "lint-staged": "npx lint-staged" 15 | }, 16 | "lint-staged": { 17 | "*.{js,ts,jsx,tsx}": [ 18 | "prettier --write", 19 | "eslint --fix" 20 | ], 21 | "*.vue": [ 22 | "stylelint --fix", 23 | "prettier --write", 24 | "eslint --fix" 25 | ], 26 | "*.{less,css}": [ 27 | "stylelint --fix", 28 | "prettier --write" 29 | ] 30 | }, 31 | "dependencies": { 32 | "@arco-design/web-vue": "^2.56.0", 33 | "@types/codemirror": "^5.60.15", 34 | "@vueuse/core": "^9.3.0", 35 | "axios": "^0.24.0", 36 | "codemirror": ">=5.64.0 <6", 37 | "codemirror-editor-vue3": "^2.7.0", 38 | "dayjs": "^1.11.5", 39 | "echarts": "^5.4.0", 40 | "lodash": "^4.17.21", 41 | "mitt": "^3.0.0", 42 | "nprogress": "^0.2.0", 43 | "pinia": "^2.0.23", 44 | "query-string": "^8.0.3", 45 | "sortablejs": "^1.15.0", 46 | "vue": "^3.2.40", 47 | "vue-echarts": "^6.2.3", 48 | "vue-i18n": "^9.2.2", 49 | "vue-router": "^4.0.14" 50 | }, 51 | "devDependencies": { 52 | "@arco-plugins/vite-vue": "^1.4.5", 53 | "@commitlint/cli": "^17.1.2", 54 | "@commitlint/config-conventional": "^17.1.0", 55 | "@types/lodash": "^4.14.186", 56 | "@types/nprogress": "^0.2.0", 57 | "@types/sortablejs": "^1.15.0", 58 | "@typescript-eslint/eslint-plugin": "^5.40.0", 59 | "@typescript-eslint/parser": "^5.40.0", 60 | "@vitejs/plugin-vue": "^3.1.2", 61 | "@vitejs/plugin-vue-jsx": "^2.0.1", 62 | "@vue/babel-plugin-jsx": "^1.1.1", 63 | "consola": "^2.15.3", 64 | "cross-env": "^7.0.3", 65 | "eslint": "^8.25.0", 66 | "eslint-config-airbnb-base": "^15.0.0", 67 | "eslint-config-prettier": "^8.5.0", 68 | "eslint-import-resolver-typescript": "^3.5.1", 69 | "eslint-plugin-import": "^2.26.0", 70 | "eslint-plugin-prettier": "^4.2.1", 71 | "eslint-plugin-vue": "^9.6.0", 72 | "less": "^4.1.3", 73 | "lint-staged": "^13.0.3", 74 | "mockjs": "^1.1.0", 75 | "postcss-html": "^1.5.0", 76 | "prettier": "^2.7.1", 77 | "rollup": "^3.9.1", 78 | "rollup-plugin-visualizer": "^5.8.2", 79 | "stylelint": "^14.13.0", 80 | "stylelint-config-prettier": "^9.0.3", 81 | "stylelint-config-rational-order": "^0.1.2", 82 | "stylelint-config-recommended-vue": "^1.4.0", 83 | "stylelint-config-standard": "^29.0.0", 84 | "stylelint-order": "^5.0.0", 85 | "typescript": "^4.8.4", 86 | "unplugin-vue-components": "^0.24.1", 87 | "vite": "^3.2.5", 88 | "vite-plugin-compression": "^0.5.1", 89 | "vite-plugin-eslint": "^1.8.1", 90 | "vite-plugin-imagemin": "^0.6.1", 91 | "vite-svg-loader": "^3.6.0", 92 | "vue-tsc": "^1.0.14" 93 | }, 94 | "engines": { 95 | "node": ">=18.0.0" 96 | }, 97 | "resolutions": { 98 | "bin-wrapper": "npm:bin-wrapper-china", 99 | "rollup": "^2.56.3", 100 | "gifsicle": "5.2.0" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | -------------------------------------------------------------------------------- /src/api/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'query-string'; 3 | 4 | export interface SysApiReq { 5 | name: string; 6 | method: string; 7 | path: string; 8 | remark?: string; 9 | } 10 | 11 | export interface SysApiRes extends SysApiReq { 12 | id: number; 13 | } 14 | 15 | export interface SysApiParams { 16 | name?: string; 17 | method?: string; 18 | path?: string; 19 | page?: number; 20 | size?: number; 21 | } 22 | 23 | export interface SysApiListRes { 24 | items: SysApiRes[]; 25 | total: number; 26 | } 27 | 28 | export interface SysApiDeleteParams { 29 | pk: number[]; 30 | } 31 | 32 | export function querySysApiList(params: SysApiParams): Promise { 33 | return axios.get('/api/v1/sys/apis', { 34 | params, 35 | paramsSerializer: (obj) => { 36 | return qs.stringify(obj); 37 | }, 38 | }); 39 | } 40 | 41 | export function querySysApiAll(): Promise { 42 | return axios.get('/api/v1/sys/apis/all'); 43 | } 44 | 45 | export function querySysApiDetail(pk: number): Promise { 46 | return axios.get(`/api/v1/sys/apis/${pk}`); 47 | } 48 | 49 | export function createSysApi(data: SysApiReq) { 50 | return axios.post('/api/v1/sys/apis', data); 51 | } 52 | 53 | export function updateSysApi(pk: number, data: SysApiReq) { 54 | return axios.put(`/api/v1/sys/apis/${pk}`, data); 55 | } 56 | 57 | export function deleteSysApi(params: SysApiDeleteParams) { 58 | return axios.delete(`/api/v1/sys/apis`, { 59 | params, 60 | paramsSerializer: (obj) => { 61 | return qs.stringify(obj); 62 | }, 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export interface LoginData { 4 | username: string; 5 | password: string; 6 | captcha: string; 7 | } 8 | 9 | export interface LoginRes { 10 | access_token: string; 11 | } 12 | 13 | export interface CaptchaRes { 14 | image: string; 15 | image_type: string; 16 | } 17 | 18 | export function getCaptcha(): Promise { 19 | return axios.get('/api/v1/auth/captcha'); 20 | } 21 | 22 | export function login(data: LoginData): Promise { 23 | return axios.post('/api/v1/auth/login', data); 24 | } 25 | 26 | export function logout() { 27 | return axios.post('/api/v1/auth/logout'); 28 | } 29 | -------------------------------------------------------------------------------- /src/api/automatiion.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | import qs from 'query-string'; 3 | 4 | export interface BusinessReq { 5 | app_name: string; 6 | table_name_en: string; 7 | table_name_zh: string; 8 | table_simple_name_zh: string; 9 | table_comment?: string; 10 | schema_name?: string; 11 | default_datetime_column: boolean; 12 | api_version: string; 13 | gen_path?: string; 14 | remark?: string; 15 | } 16 | 17 | export interface ModelReq { 18 | name: string; 19 | comment?: string; 20 | type: string; 21 | default?: string; 22 | sort: number; 23 | length: number; 24 | is_pk: boolean; 25 | is_nullable: boolean; 26 | gen_business_id?: number; 27 | } 28 | 29 | export interface BusinessRes extends BusinessReq { 30 | id: number; 31 | } 32 | 33 | export interface ModelRes extends ModelReq { 34 | id: number; 35 | pd_type: string; 36 | } 37 | 38 | export interface DBTableParams { 39 | table_schema: string; 40 | } 41 | 42 | export interface ImportReq { 43 | app: string; 44 | table_name: string; 45 | table_schema: string; 46 | } 47 | 48 | export const TemplateBackendDirName = 'py'; 49 | export const ZipFilename = 'fba_generator'; 50 | 51 | export function queryBusinessAll(): Promise { 52 | return axios.get('/api/v1/gen/businesses/all'); 53 | } 54 | 55 | export function queryBusinessDetail(pk: number): Promise { 56 | return axios.get(`/api/v1/gen/businesses/${pk}`); 57 | } 58 | 59 | export function queryBusinessModels(pk: number): Promise { 60 | return axios.get(`/api/v1/gen/businesses/${pk}/models`); 61 | } 62 | 63 | export function createBusiness(data: BusinessReq) { 64 | return axios.post('/api/v1/gen/businesses', data); 65 | } 66 | 67 | export function updateBusiness(pk: number, data: BusinessReq) { 68 | return axios.put(`/api/v1/gen/businesses/${pk}`, data); 69 | } 70 | 71 | export function deleteBusiness(id: number) { 72 | return axios.delete(`/api/v1/gen/businesses/${id}`); 73 | } 74 | 75 | export function queryModelDetail(pk: number): Promise { 76 | return axios.get(`/api/v1/gen/models/${pk}`); 77 | } 78 | 79 | export function queryModelType(): Promise { 80 | return axios.get('/api/v1/gen/models/types'); 81 | } 82 | 83 | export function createModel(data: ModelReq) { 84 | return axios.post('/api/v1/gen/models', data); 85 | } 86 | 87 | export function updateModel(pk: number, data: ModelReq) { 88 | return axios.put(`/api/v1/gen/models/${pk}`, data); 89 | } 90 | 91 | export function deleteModel(id: number) { 92 | return axios.delete(`/api/v1/gen/models/${id}`); 93 | } 94 | 95 | export function queryDBTables(params: DBTableParams): Promise { 96 | return axios.get('/api/v1/gen/tables', { 97 | params, 98 | paramsSerializer: (obj) => { 99 | return qs.stringify(obj); 100 | }, 101 | }); 102 | } 103 | 104 | export function importTable(data: ImportReq) { 105 | return axios.post('/api/v1/gen/import', data); 106 | } 107 | 108 | export function previewCode(pk: number): Promise<{ [key: string]: string }> { 109 | return axios.get(`/api/v1/gen/preview/${pk}`); 110 | } 111 | 112 | export function queryGeneratePath(pk: number): Promise { 113 | return axios.get(`/api/v1/gen/generate/${pk}/path`); 114 | } 115 | 116 | export function generateCode(pk: number) { 117 | return axios.post(`/api/v1/gen/generate/${pk}`); 118 | } 119 | 120 | export function downloadCode(pk: number): Promise> { 121 | return axios.get(`/api/v1/gen/download/${pk}`, { responseType: 'blob' }); 122 | } 123 | -------------------------------------------------------------------------------- /src/api/casbin.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export interface CasbinPolicyReq { 4 | sub: string; 5 | path: string; 6 | method: string; 7 | } 8 | 9 | export interface CasbinPoliciesReq { 10 | ps: CasbinPolicyReq[]; 11 | } 12 | 13 | export interface CasbinPoliciesDel { 14 | uuid?: string; 15 | role: string; 16 | } 17 | 18 | export interface CasbinGroupReq { 19 | uuid: string; 20 | role: string; 21 | } 22 | 23 | export interface CasbinGroupsReq { 24 | gs: CasbinGroupReq[]; 25 | } 26 | 27 | export interface CasbinGroupDel { 28 | uuid: string; 29 | } 30 | 31 | export function queryCasbinPoliciesByRole(role?: number) { 32 | return axios.get(`/api/v1/sys/casbin/policies`, { 33 | params: { role }, 34 | }); 35 | } 36 | 37 | export function createCasbinPolicy(data: CasbinPolicyReq) { 38 | return axios.post('/api/v1/sys/casbin/policy', data); 39 | } 40 | 41 | export function createCasbinPolicies(data: CasbinPoliciesReq) { 42 | return axios.post('/api/v1/sys/casbin/policies', data.ps); 43 | } 44 | 45 | export function deleteCasbinPolicies(data: CasbinPoliciesDel) { 46 | return axios.delete('/api/v1/sys/casbin/policies/all', { data }); 47 | } 48 | 49 | export function createCasbinGroup(data: CasbinGroupReq) { 50 | return axios.post('/api/v1/sys/casbin/group', data); 51 | } 52 | 53 | export function createCasbinGroups(data: CasbinGroupsReq) { 54 | return axios.post('/api/v1/sys/casbin/groups', data.gs); 55 | } 56 | 57 | export function deleteCasbinAllGroups(data: CasbinGroupDel) { 58 | return axios.delete('/api/v1/sys/casbin/groups/all', { 59 | params: data, 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/api/data-rule.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'query-string'; 3 | 4 | export interface SysDataRuleReq { 5 | name: string; 6 | model: string; 7 | column: string; 8 | operator: number; 9 | expression: number; 10 | value: string; 11 | } 12 | 13 | export interface SysDataRuleRes extends SysDataRuleReq { 14 | id: number; 15 | } 16 | 17 | export interface SysDataRuleParams { 18 | name?: string; 19 | page?: number; 20 | size?: number; 21 | } 22 | 23 | export interface SysDataRuleListRes { 24 | items: SysDataRuleRes[]; 25 | total: number; 26 | } 27 | 28 | export interface SysDataRuleDeleteParams { 29 | pk: number[]; 30 | } 31 | 32 | export function querySysDataRuleModels(): Promise { 33 | return axios.get('/api/v1/sys/data-rules/models'); 34 | } 35 | 36 | export function querySysDataRuleColumns(model: string): Promise { 37 | return axios.get(`/api/v1/sys/data-rules/model/${model}/columns`); 38 | } 39 | 40 | export function querySysDataRuleAll(): Promise { 41 | return axios.get('/api/v1/sys/data-rules/all'); 42 | } 43 | 44 | export function querySysDataRuleDetail(pk: number): Promise { 45 | return axios.get(`/api/v1/sys/data-rules/${pk}`); 46 | } 47 | 48 | export function querySysDataRuleList( 49 | params: SysDataRuleParams 50 | ): Promise { 51 | return axios.get('/api/v1/sys/data-rules', { 52 | params, 53 | paramsSerializer: (obj) => { 54 | return qs.stringify(obj); 55 | }, 56 | }); 57 | } 58 | 59 | export function createSysDataRule(data: SysDataRuleReq) { 60 | return axios.post('/api/v1/sys/data-rules', data); 61 | } 62 | 63 | export function updateSysDataRule(pk: number, data: SysDataRuleReq) { 64 | return axios.put(`/api/v1/sys/data-rules/${pk}`, data); 65 | } 66 | 67 | export function deleteSysDataRule(params: SysDataRuleDeleteParams) { 68 | return axios.delete('/api/v1/sys/data-rules', { 69 | params, 70 | paramsSerializer: (obj) => { 71 | return qs.stringify(obj); 72 | }, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /src/api/dept.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'query-string'; 3 | 4 | export interface SysDeptRes { 5 | id: number; 6 | name: string; 7 | sort: number; 8 | leader?: string; 9 | phone?: string; 10 | email?: string; 11 | status: 0 | 1; 12 | created_time: string; 13 | } 14 | 15 | export interface SysDeptTreeRes extends SysDeptRes { 16 | children?: SysDeptTreeRes[]; 17 | } 18 | 19 | export interface SysDeptReq { 20 | name: string; 21 | parent_id?: number; 22 | sort?: number; 23 | leader?: string; 24 | phone?: string; 25 | email?: string; 26 | status: 0 | 1; 27 | } 28 | 29 | export interface SysDeptTreeParams { 30 | name?: string; 31 | leader?: string; 32 | phone?: string; 33 | email?: string; 34 | } 35 | 36 | export function querySysDeptTree( 37 | params: SysDeptTreeParams 38 | ): Promise { 39 | return axios.get('/api/v1/sys/depts', { 40 | params, 41 | paramsSerializer: (obj) => { 42 | return qs.stringify(obj); 43 | }, 44 | }); 45 | } 46 | 47 | export function querySysDeptDetail(pk: number): Promise { 48 | return axios.get(`/api/v1/sys/depts/${pk}`); 49 | } 50 | 51 | export function createSysDept(data: SysDeptReq) { 52 | return axios.post('/api/v1/sys/depts', data); 53 | } 54 | 55 | export function updateSysDept(pk: number, data: SysDeptReq) { 56 | return axios.put(`/api/v1/sys/depts/${pk}`, data); 57 | } 58 | 59 | export function deleteSysDept(pk: number) { 60 | return axios.delete(`/api/v1/sys/depts/${pk}`); 61 | } 62 | -------------------------------------------------------------------------------- /src/api/interceptor.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, AxiosResponse } from 'axios'; 2 | import axios from 'axios'; 3 | import { Message } from '@arco-design/web-vue'; 4 | import { getToken } from '@/utils/auth'; 5 | 6 | export interface HttpResponse { 7 | msg: string; 8 | code: number; 9 | data: T; 10 | } 11 | 12 | export interface HttpError { 13 | msg: string; 14 | code: number; 15 | } 16 | 17 | // 设置全局 baseURL 和 withCredentials 18 | axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL || ''; 19 | axios.defaults.withCredentials = true; 20 | 21 | // 封装错误提示逻辑 22 | const showError = (message: string) => { 23 | Message.error({ 24 | content: message, 25 | duration: 5000, // 5秒 26 | }); 27 | }; 28 | 29 | // 请求拦截器 30 | axios.interceptors.request.use( 31 | (config: AxiosRequestConfig) => { 32 | const token = getToken(); 33 | if (token) { 34 | config.headers = { 35 | ...config.headers, 36 | Authorization: `Bearer ${token}`, 37 | }; 38 | } 39 | return config; 40 | }, 41 | (error) => Promise.reject(error) 42 | ); 43 | 44 | // 响应拦截器 45 | axios.interceptors.response.use( 46 | (response: AxiosResponse) => { 47 | // 直接返回 Blob 类型的响应 48 | if (response.config.responseType === 'blob') { 49 | return response; 50 | } 51 | 52 | const { code, data }: HttpResponse = response.data; 53 | 54 | if (code === 401) { 55 | // TODO: 处理 token 过期,自动刷新 token 或跳转登录界面 56 | } 57 | 58 | return data; 59 | }, 60 | (error: any) => { 61 | let res: HttpError = { 62 | msg: '服务器响应异常,请稍后重试', 63 | code: 500, 64 | }; 65 | 66 | if (error.response) { 67 | res = error.response.data; 68 | } 69 | 70 | if (error.message === 'Network Error') { 71 | res.msg = '服务器连接异常,请稍后重试'; 72 | } 73 | 74 | if (error.code === 'ECONNABORTED') { 75 | res.msg = '请求超时,请稍后重试'; 76 | } 77 | 78 | showError(res.msg); 79 | 80 | return Promise.reject(res); 81 | } 82 | ); 83 | -------------------------------------------------------------------------------- /src/api/log.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'query-string'; 3 | 4 | export interface LoginLogRes { 5 | id: number; 6 | username: string; 7 | ip: string; 8 | browser: string; 9 | device: string; 10 | city: string; 11 | status: 0 | 1; 12 | msg: string; 13 | login_time: string; 14 | } 15 | 16 | export interface OperaLogRes { 17 | id: number; 18 | trace_id: string; 19 | username?: string; 20 | method: string; 21 | title: string; 22 | path: string; 23 | ip: string; 24 | city: string; 25 | browser: string; 26 | device: string; 27 | args?: string; 28 | status: 0 | 1; 29 | code: string; 30 | msg: string; 31 | cost_time: number; 32 | opera_time: string; 33 | } 34 | 35 | export interface LoginLogParams { 36 | page?: number; 37 | size?: number; 38 | } 39 | 40 | export type OperaLogParams = LoginLogParams; 41 | 42 | export interface LoginLogListRes { 43 | items: LoginLogRes[]; 44 | total: number; 45 | } 46 | 47 | export interface OperaLogListRes { 48 | items: OperaLogRes[]; 49 | total: number; 50 | } 51 | 52 | export function queryLoginLogList( 53 | params: LoginLogParams 54 | ): Promise { 55 | return axios.get('/api/v1/logs/login', { 56 | params, 57 | paramsSerializer: (obj) => { 58 | return qs.stringify(obj); 59 | }, 60 | }); 61 | } 62 | 63 | export function queryOperaLogList( 64 | params: OperaLogParams 65 | ): Promise { 66 | return axios.get('/api/v1/logs/opera', { 67 | params, 68 | paramsSerializer: (obj) => { 69 | return qs.stringify(obj); 70 | }, 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/api/menu.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'query-string'; 3 | 4 | export interface SysMenuRes { 5 | id: number; 6 | title: string; 7 | name: string; 8 | parent_id?: number; 9 | sort: number; 10 | icon?: string; 11 | path?: string; 12 | menu_type: number; 13 | component?: string; 14 | perms?: string; 15 | status: 0 | 1; 16 | display: 0 | 1; 17 | cache: 0 | 1; 18 | remark?: string; 19 | created_time: string; 20 | } 21 | 22 | export interface SysMenuTreeRes extends SysMenuRes { 23 | children?: SysMenuTreeRes[]; 24 | } 25 | 26 | export interface SysMenuReq { 27 | title: string; 28 | name: string; 29 | parent_id?: number; 30 | sort?: number; 31 | icon?: string; 32 | path?: string; 33 | menu_type?: number; 34 | component?: string; 35 | perms?: string; 36 | status: 0 | 1; 37 | display: 0 | 1; 38 | cache: 0 | 1; 39 | remark?: string; 40 | } 41 | 42 | export interface SysMenuTreeParams { 43 | title?: string; 44 | status?: number; 45 | } 46 | 47 | export function querySysMenuTree( 48 | params: SysMenuTreeParams 49 | ): Promise { 50 | return axios.get('/api/v1/sys/menus', { 51 | params, 52 | paramsSerializer: (obj) => { 53 | return qs.stringify(obj); 54 | }, 55 | }); 56 | } 57 | 58 | export function querySysMenuDetail(pk: number): Promise { 59 | return axios.get(`/api/v1/sys/menus/${pk}`); 60 | } 61 | 62 | export function createSysMenu(data: SysMenuReq) { 63 | return axios.post('/api/v1/sys/menus', data); 64 | } 65 | 66 | export function updateSysMenu(pk: number, data: SysMenuReq) { 67 | return axios.put(`/api/v1/sys/menus/${pk}`, data); 68 | } 69 | 70 | export function deleteSysMenu(pk: number) { 71 | return axios.delete(`/api/v1/sys/menus/${pk}`); 72 | } 73 | -------------------------------------------------------------------------------- /src/api/monitor.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export interface ServerMonitorRes { 4 | cpu: Record; 5 | mem: Record; 6 | sys: Record; 7 | disk: Record[]; 8 | service: Record; 9 | } 10 | 11 | export interface RedisMonitorRes { 12 | info: Record; 13 | stats: Record[]; 14 | } 15 | 16 | export function queryServerMonitor(): Promise { 17 | return axios.get('/api/v1/monitors/server'); 18 | } 19 | 20 | export function queryRedisMonitor(): Promise { 21 | return axios.get('/api/v1/monitors/redis'); 22 | } 23 | -------------------------------------------------------------------------------- /src/api/oauth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'query-string'; 3 | import { LoginRes } from '@/api/auth'; 4 | 5 | export interface CallBackReq { 6 | code: string; 7 | state?: string; 8 | code_verifier?: string; 9 | } 10 | 11 | export function getOAuth2LinuxDo(): Promise { 12 | return axios.get('/api/v1/oauth2/linux-do'); 13 | } 14 | 15 | export function OAuth2LinuxDoCallBack(params: CallBackReq): Promise { 16 | return axios.get('/api/v1/oauth2/linux-do/callback', { 17 | params, 18 | paramsSerializer: (obj) => { 19 | return qs.stringify(obj); 20 | }, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/api/role.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'query-string'; 3 | import { SysMenuRes, SysMenuTreeRes } from '@/api/menu'; 4 | 5 | export interface SysRoleReq { 6 | name: string; 7 | data_scope: number; 8 | status: number; 9 | remark?: string; 10 | } 11 | 12 | export interface SysRoleMenuReq { 13 | menus: number[]; 14 | } 15 | 16 | export interface SysRoleDataRuleReq { 17 | rules: number[]; 18 | } 19 | 20 | export interface SysRoleRes { 21 | id: number; 22 | name: string; 23 | data_scope: number; 24 | status: number; 25 | remark?: string; 26 | created_time: string; 27 | menus?: SysMenuRes[]; 28 | } 29 | 30 | export interface SysRoleParams { 31 | name?: string; 32 | status?: number; 33 | page?: number; 34 | size?: number; 35 | } 36 | 37 | export interface SysRoleListRes { 38 | items: SysRoleRes[]; 39 | total: number; 40 | } 41 | 42 | export interface SysRoleDeleteParams { 43 | pk: number[]; 44 | } 45 | 46 | export function querySysRoleAll(): Promise { 47 | return axios.get('/api/v1/sys/roles/all'); 48 | } 49 | 50 | export function querySysRoleAllBySysUser(pk: number): Promise { 51 | return axios.get(`/api/v1/sys/roles/${pk}/all`); 52 | } 53 | 54 | export function querySysMenuTreeBySysRole( 55 | pk: number 56 | ): Promise { 57 | return axios.get(`/api/v1/sys/roles/${pk}/menus`); 58 | } 59 | 60 | export function querySysRoleRuleBySysRole(pk: number): Promise { 61 | return axios.get(`/api/v1/sys/roles/${pk}/rules`); 62 | } 63 | 64 | export function querySysRoleList( 65 | params: SysRoleParams 66 | ): Promise { 67 | return axios.get('/api/v1/sys/roles', { 68 | params, 69 | paramsSerializer: (obj) => { 70 | return qs.stringify(obj); 71 | }, 72 | }); 73 | } 74 | 75 | export function querySysRoleDetail(pk: number): Promise { 76 | return axios.get(`/api/v1/sys/roles/${pk}`); 77 | } 78 | 79 | export function createSysRole(data: SysRoleReq) { 80 | return axios.post('/api/v1/sys/roles', data); 81 | } 82 | 83 | export function updateSysRole(pk: number, data: SysRoleReq) { 84 | return axios.put(`/api/v1/sys/roles/${pk}`, data); 85 | } 86 | 87 | export function deleteSysRole(params: SysRoleDeleteParams) { 88 | return axios.delete(`/api/v1/sys/roles`, { 89 | params, 90 | paramsSerializer: (obj) => { 91 | return qs.stringify(obj); 92 | }, 93 | }); 94 | } 95 | 96 | export function updateSysRoleMenu(pk: number, data: SysRoleMenuReq) { 97 | return axios.put(`/api/v1/sys/roles/${pk}/menu`, data); 98 | } 99 | 100 | export function updateSysRoleDataRule(pk: number, data: SysRoleDataRuleReq) { 101 | return axios.put(`/api/v1/sys/roles/${pk}/rule`, data); 102 | } 103 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'query-string'; 3 | import { UserState } from '@/store/modules/user/types'; 4 | import { MenuItem } from '@/store/modules/app/types'; 5 | import { SysDeptRes } from '@/api/dept'; 6 | import { SysRoleRes } from '@/api/role'; 7 | 8 | export interface SysUserNoRelationRes { 9 | id: number; 10 | uuid: string; 11 | avatar?: string; 12 | username: string; 13 | nickname: string; 14 | email: string; 15 | phone?: string; 16 | status: number; 17 | is_superuser: boolean; 18 | is_staff: boolean; 19 | is_multi_login: boolean; 20 | join_time: string; 21 | last_login_time: string; 22 | dept_id?: number; 23 | dept?: SysDeptRes; 24 | } 25 | 26 | export interface SysUserRes extends SysUserNoRelationRes { 27 | roles: SysRoleRes[]; 28 | } 29 | 30 | export interface SysUserParams { 31 | dept?: number; 32 | username?: string; 33 | phone?: string; 34 | status?: number; 35 | page?: number; 36 | size?: number; 37 | } 38 | 39 | export interface SysUserListRes { 40 | items: SysUserRes[]; 41 | total: number; 42 | } 43 | 44 | export interface SysUserRoleReq { 45 | roles: number[]; 46 | } 47 | 48 | export interface SysUserAvatarReq { 49 | url: string; 50 | } 51 | 52 | export interface SysUserInfoReq { 53 | dept_id?: number; 54 | username: string; 55 | nickname: string; 56 | email: string; 57 | phone?: string; 58 | } 59 | 60 | export interface SysUserAddReq { 61 | dept_id?: number; 62 | username: string; 63 | password: string; 64 | email: string; 65 | roles: number[]; 66 | } 67 | 68 | export function getUserInfo(): Promise { 69 | return axios.get('/api/v1/sys/users/me'); 70 | } 71 | 72 | export function getUserMenuList(): Promise { 73 | return axios.get('/api/v1/sys/menus/sidebar'); 74 | } 75 | 76 | export function getUserList(params: SysUserParams): Promise { 77 | return axios.get('/api/v1/sys/users', { 78 | params, 79 | paramsSerializer: (obj) => { 80 | return qs.stringify(obj); 81 | }, 82 | }); 83 | } 84 | 85 | export function getUser(username: string): Promise { 86 | return axios.get(`/api/v1/sys/users/${username}`); 87 | } 88 | 89 | export function updateUserRole(username: string, data: SysUserRoleReq) { 90 | return axios.put(`/api/v1/sys/users/${username}/role`, data); 91 | } 92 | 93 | export function changeUserStatus(pk: number) { 94 | return axios.put(`/api/v1/sys/users/${pk}/status`); 95 | } 96 | 97 | export function changeUserSuper(pk: number) { 98 | return axios.put(`/api/v1/sys/users/${pk}/super`); 99 | } 100 | 101 | export function changeUserStaff(pk: number) { 102 | return axios.put(`/api/v1/sys/users/${pk}/staff`); 103 | } 104 | 105 | export function changeUserMulti(pk: number) { 106 | return axios.put(`/api/v1/sys/users/${pk}/multi`); 107 | } 108 | 109 | export function updateUserAvatar(username: string, data: SysUserAvatarReq) { 110 | return axios.put(`/api/v1/sys/users/${username}/avatar`, data); 111 | } 112 | 113 | export function updateUser(username: string, data: SysUserInfoReq) { 114 | return axios.put(`/api/v1/sys/users/${username}`, data); 115 | } 116 | 117 | export function addUser(data: SysUserAddReq): Promise { 118 | return axios.post('/api/v1/sys/users/add', data); 119 | } 120 | export function deleteUser(username: string) { 121 | return axios.delete(`/api/v1/sys/users/${username}`); 122 | } 123 | -------------------------------------------------------------------------------- /src/assets/images/login-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-practices/fastapi_best_architecture_ui_arco/a1509207a683e39bb0b35e425a90001a50a030e4/src/assets/images/login-banner.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 编组 5 2 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/style/breakpoint.less: -------------------------------------------------------------------------------- 1 | // ==============breakpoint============ 2 | 3 | // Extra small screen / phone 4 | @screen-xs: 480px; 5 | 6 | // Small screen / tablet 7 | @screen-sm: 576px; 8 | 9 | // Medium screen / desktop 10 | @screen-md: 768px; 11 | 12 | // Large screen / wide desktop 13 | @screen-lg: 992px; 14 | 15 | // Extra large screen / full hd 16 | @screen-xl: 1200px; 17 | 18 | // Extra extra large screen / large desktop 19 | @screen-xxl: 1600px; 20 | -------------------------------------------------------------------------------- /src/assets/style/global.less: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | width: 100%; 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | font-size: 14px; 12 | background-color: var(--color-bg-1); 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-font-smoothing: antialiased; 15 | } 16 | 17 | ::-webkit-scrollbar { 18 | display: none; 19 | } 20 | 21 | .echarts-tooltip-diy { 22 | background: linear-gradient(304.17deg, 23 | rgba(253, 254, 255, 0.6) -6.04%, 24 | rgba(244, 247, 252, 0.6) 85.2%) !important; 25 | border: none !important; 26 | backdrop-filter: blur(10px) !important; 27 | /* Note: backdrop-filter has minimal browser support */ 28 | 29 | border-radius: 6px !important; 30 | 31 | .content-panel { 32 | display: flex; 33 | justify-content: space-between; 34 | padding: 0 9px; 35 | background: rgba(255, 255, 255, 0.8); 36 | width: 164px; 37 | height: 32px; 38 | line-height: 32px; 39 | box-shadow: 6px 0px 20px rgba(34, 87, 188, 0.1); 40 | border-radius: 4px; 41 | margin-bottom: 4px; 42 | } 43 | 44 | .tooltip-title { 45 | margin: 0 0 10px 0; 46 | } 47 | 48 | p { 49 | margin: 0; 50 | } 51 | 52 | .tooltip-title, 53 | .tooltip-value { 54 | font-size: 13px; 55 | line-height: 15px; 56 | display: flex; 57 | align-items: center; 58 | text-align: right; 59 | color: #1d2129; 60 | font-weight: bold; 61 | } 62 | 63 | .tooltip-item-icon { 64 | display: inline-block; 65 | margin-right: 8px; 66 | width: 10px; 67 | height: 10px; 68 | border-radius: 50%; 69 | } 70 | } 71 | 72 | .general-card { 73 | border-radius: 4px; 74 | border: none; 75 | 76 | & > .arco-card-header { 77 | height: auto; 78 | padding: 20px; 79 | border: none; 80 | } 81 | 82 | & > .arco-card-body { 83 | padding: 0 20px 20px 20px; 84 | } 85 | } 86 | 87 | .split-line { 88 | border-color: rgb(var(--gray-2)); 89 | } 90 | 91 | .arco-table-cell { 92 | .circle { 93 | display: inline-block; 94 | margin-right: 4px; 95 | width: 6px; 96 | height: 6px; 97 | border-radius: 50%; 98 | background-color: rgb(var(--blue-6)); 99 | 100 | &.pass { 101 | background-color: rgb(var(--green-6)); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 38 | -------------------------------------------------------------------------------- /src/components/chart/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/footer/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/components/global-setting/block.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 62 | 63 | 81 | -------------------------------------------------------------------------------- /src/components/global-setting/form-wrapper.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | -------------------------------------------------------------------------------- /src/components/global-setting/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 86 | 87 | 99 | -------------------------------------------------------------------------------- /src/components/icon-picker/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 116 | 117 | 142 | -------------------------------------------------------------------------------- /src/components/icon-picker/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'icon.picker.input.placeholder': 'Click to select an icon', 3 | 'icon.picker.search.placeholder': 'Search for the icon name', 4 | 'icon.picker.input.copy': 'Selected and copied the icon to clipboard', 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/icon-picker/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'icon.picker.input.placeholder': '点击选择图标', 3 | 'icon.picker.search.placeholder': '搜索图标名称', 4 | 'icon.picker.input.copy': '已选择并且复制图标至剪贴板', 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import { use } from 'echarts/core'; 3 | import { CanvasRenderer } from 'echarts/renderers'; 4 | import { 5 | BarChart, 6 | GaugeChart, 7 | LineChart, 8 | PieChart, 9 | RadarChart, 10 | } from 'echarts/charts'; 11 | import { 12 | DataZoomComponent, 13 | GraphicComponent, 14 | GridComponent, 15 | LegendComponent, 16 | TooltipComponent, 17 | } from 'echarts/components'; 18 | import Chart from './chart/index.vue'; 19 | import Breadcrumb from './breadcrumb/index.vue'; 20 | 21 | // Manually introduce ECharts modules to reduce packing size 22 | 23 | use([ 24 | CanvasRenderer, 25 | GaugeChart, 26 | BarChart, 27 | LineChart, 28 | PieChart, 29 | RadarChart, 30 | GridComponent, 31 | TooltipComponent, 32 | LegendComponent, 33 | DataZoomComponent, 34 | GraphicComponent, 35 | ]); 36 | 37 | export default { 38 | install(Vue: App) { 39 | Vue.component('Chart', Chart); 40 | Vue.component('Breadcrumb', Breadcrumb); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/menu/index.vue: -------------------------------------------------------------------------------- 1 | 151 | 152 | 166 | -------------------------------------------------------------------------------- /src/components/menu/use-menu-tree.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { cloneDeep } from 'lodash'; 3 | import { RouteRecordNormalized, RouteRecordRaw } from 'vue-router'; 4 | import usePermission from '@/hooks/permission'; 5 | import { useAppStore } from '@/store'; 6 | import appClientMenus from '@/router/app-menus'; 7 | 8 | export default function useMenuTree() { 9 | const permission = usePermission(); 10 | const appStore = useAppStore(); 11 | const appRoute = computed(() => { 12 | if (appStore.menuFromServer) { 13 | return appStore.appAsyncMenus; 14 | } 15 | return appClientMenus; 16 | }); 17 | const menuTree = computed(() => { 18 | const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[]; 19 | copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => { 20 | return (a.meta.order || 0) - (b.meta.order || 0); 21 | }); 22 | 23 | function travel(_routes: RouteRecordRaw[], layer: number) { 24 | if (!_routes) return null; 25 | 26 | const collector: any = _routes.map((element) => { 27 | // no access 28 | if (!permission.accessRouter(element)) { 29 | return null; 30 | } 31 | 32 | // leaf node 33 | if (element.meta?.hideInMenu || !element.children) { 34 | element.children = []; 35 | if (appStore.menuFromServer) { 36 | return null; 37 | } 38 | return element; 39 | } 40 | 41 | // route filter hideInMenu true 42 | element.children = element.children.filter( 43 | (x) => x.meta?.hideInMenu !== true 44 | ); 45 | 46 | // Associated child node 47 | const subItem = travel(element.children, layer + 1); 48 | if (subItem.length) { 49 | element.children = subItem; 50 | return element; 51 | } 52 | 53 | // the else logic 54 | if (layer > 1) { 55 | element.children = subItem; 56 | return element; 57 | } 58 | 59 | if (element.meta?.hideInMenu === false) { 60 | return element; 61 | } 62 | 63 | return null; 64 | }); 65 | 66 | return collector.filter(Boolean); 67 | } 68 | 69 | return travel(copyRouter, 0); 70 | }); 71 | 72 | return { 73 | menuTree, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/components/tab-bar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 61 | 62 | 106 | -------------------------------------------------------------------------------- /src/components/tab-bar/readme.md: -------------------------------------------------------------------------------- 1 | ## 组件说明 2 | 3 | 该组件非官方最终设计规范,以单独组件存在。 4 | 5 | 同时仅仅提供最基本的功能,后续进行优化及更改。 6 | 7 | ## Component description 8 | 9 | The component unofficial final design specification exists as a separate component. 10 | 11 | At the same time, only the most basic functions are provided, and subsequent optimizations and changes will be made. 12 | -------------------------------------------------------------------------------- /src/components/tab-bar/tab-item.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 169 | 170 | 208 | -------------------------------------------------------------------------------- /src/config/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": "light", 3 | "colorWeak": false, 4 | "navbar": true, 5 | "menu": true, 6 | "topMenu": false, 7 | "hideMenu": false, 8 | "menuCollapse": false, 9 | "footer": true, 10 | "themeColor": "#165DFF", 11 | "menuWidth": 220, 12 | "globalSettings": false, 13 | "device": "desktop", 14 | "tabBar": false, 15 | "menuFromServer": true, 16 | "serverMenu": [] 17 | } 18 | -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import permission from './permission'; 3 | 4 | export default { 5 | install(Vue: App) { 6 | Vue.directive('permission', permission); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/directive/permission/index.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue'; 2 | import { useUserStore } from '@/store'; 3 | 4 | function checkPermission(el: HTMLElement, binding: DirectiveBinding) { 5 | const { value } = binding; 6 | const userStore = useUserStore(); 7 | const { roles } = userStore; 8 | 9 | if (Array.isArray(value)) { 10 | if (value.length > 0) { 11 | const permissionValues = value; 12 | 13 | const hasPermission = permissionValues.includes(roles); 14 | if (!hasPermission && el.parentNode) { 15 | el.parentNode.removeChild(el); 16 | } 17 | } 18 | } else { 19 | throw new Error(`need roles! Like v-permission="['admin','user']"`); 20 | } 21 | } 22 | 23 | export default { 24 | mounted(el: HTMLElement, binding: DirectiveBinding) { 25 | checkPermission(el, binding); 26 | }, 27 | updated(el: HTMLElement, binding: DirectiveBinding) { 28 | checkPermission(el, binding); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | declare namespace JSX { 11 | interface IntrinsicElements { 12 | [elem: string]: any; 13 | } 14 | } 15 | 16 | interface ImportMetaEnv { 17 | readonly VITE_API_BASE_URL: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/chart-option.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { EChartsOption } from 'echarts'; 3 | import { useAppStore } from '@/store'; 4 | 5 | // for code hints 6 | // import { SeriesOption } from 'echarts'; 7 | // Because there are so many configuration items, this provides a relatively convenient code hint. 8 | // When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, TypeScript does not report errors, and code writing is convenient. 9 | interface optionsFn { 10 | (isDark: boolean): EChartsOption; 11 | } 12 | 13 | export default function useChartOption(sourceOption: optionsFn) { 14 | const appStore = useAppStore(); 15 | const isDark = computed(() => { 16 | return appStore.theme === 'dark'; 17 | }); 18 | // echarts support https://echarts.apache.org/zh/theme-builder.html 19 | // It's not used here 20 | // TODO echarts themes 21 | const chartOption = computed(() => { 22 | return sourceOption(isDark.value); 23 | }); 24 | return { 25 | chartOption, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/loading.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | export default function useLoading(initValue = false) { 4 | const loading = ref(initValue); 5 | const setLoading = (value: boolean) => { 6 | loading.value = value; 7 | }; 8 | const toggle = () => { 9 | loading.value = !loading.value; 10 | }; 11 | return { 12 | loading, 13 | setLoading, 14 | toggle, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/locale.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { useI18n } from 'vue-i18n'; 3 | import { Message } from '@arco-design/web-vue'; 4 | 5 | export default function useLocale() { 6 | const i18 = useI18n(); 7 | const currentLocale = computed(() => { 8 | return i18.locale.value; 9 | }); 10 | const changeLocale = (value: string) => { 11 | if (i18.locale.value === value) { 12 | return; 13 | } 14 | i18.locale.value = value; 15 | localStorage.setItem('arco-locale', value); 16 | Message.success(i18.t('navbar.action.locale')); 17 | }; 18 | return { 19 | currentLocale, 20 | changeLocale, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/permission.ts: -------------------------------------------------------------------------------- 1 | import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'; 2 | import { useUserStore } from '@/store'; 3 | 4 | export default function usePermission() { 5 | const userStore = useUserStore(); 6 | return { 7 | accessRouter(route: RouteLocationNormalized | RouteRecordRaw) { 8 | return ( 9 | !route.meta?.requiresAuth || 10 | !route.meta?.roles || 11 | route.meta?.roles?.includes('*') || 12 | route.meta?.roles?.includes(userStore.roles) 13 | ); 14 | }, 15 | findFirstPermissionRoute(_routers: any, role = 'admin') { 16 | const cloneRouters = [..._routers]; 17 | while (cloneRouters.length) { 18 | const firstElement = cloneRouters.shift(); 19 | if ( 20 | firstElement?.meta?.roles?.find((el: string[]) => { 21 | return el.includes('*') || el.includes(role); 22 | }) 23 | ) 24 | return { name: firstElement.name }; 25 | if (firstElement?.children) { 26 | cloneRouters.push(...firstElement.children); 27 | } 28 | } 29 | return null; 30 | }, 31 | // You can add any rules you want 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/hooks/request.ts: -------------------------------------------------------------------------------- 1 | import { ref, UnwrapRef } from 'vue'; 2 | import { AxiosResponse } from 'axios'; 3 | import { HttpResponse } from '@/api/interceptor'; 4 | import useLoading from './loading'; 5 | 6 | // use to fetch list 7 | // Don't use async function. It doesn't work in async function. 8 | // Use the bind function to add parameters 9 | // example: useRequest(api.bind(null, {})) 10 | 11 | export default function useRequest( 12 | api: () => Promise>, 13 | defaultValue = [] as unknown as T, 14 | isLoading = true 15 | ) { 16 | const { loading, setLoading } = useLoading(isLoading); 17 | const response = ref(defaultValue); 18 | api() 19 | .then((res) => { 20 | response.value = res.data as unknown as UnwrapRef; 21 | }) 22 | .finally(() => { 23 | setLoading(false); 24 | }); 25 | return { loading, response }; 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/responsive.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeMount, onBeforeUnmount, onMounted } from 'vue'; 2 | import { useDebounceFn } from '@vueuse/core'; 3 | import { useAppStore } from '@/store'; 4 | import { addEventListen, removeEventListen } from '@/utils/event'; 5 | 6 | const WIDTH = 992; // https://arco.design/vue/component/grid#responsivevalue 7 | 8 | function queryDevice() { 9 | const rect = document.body.getBoundingClientRect(); 10 | return rect.width - 1 < WIDTH; 11 | } 12 | 13 | export default function useResponsive(immediate?: boolean) { 14 | const appStore = useAppStore(); 15 | 16 | function resizeHandler() { 17 | if (!document.hidden) { 18 | const isMobile = queryDevice(); 19 | appStore.toggleDevice(isMobile ? 'mobile' : 'desktop'); 20 | appStore.toggleMenu(isMobile); 21 | } 22 | } 23 | 24 | const debounceFn = useDebounceFn(resizeHandler, 100); 25 | onMounted(() => { 26 | if (immediate) debounceFn(); 27 | }); 28 | onBeforeMount(() => { 29 | addEventListen(window, 'resize', debounceFn); 30 | }); 31 | onBeforeUnmount(() => { 32 | removeEventListen(window, 'resize', debounceFn); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/user.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'vue-router'; 2 | import { Message } from '@arco-design/web-vue'; 3 | 4 | import { useUserStore } from '@/store'; 5 | 6 | export default function useUser() { 7 | const router = useRouter(); 8 | const userStore = useUserStore(); 9 | const logout = async (logoutTo?: string) => { 10 | await userStore.logout(); 11 | const currentRoute = router.currentRoute.value; 12 | Message.success('登出成功'); 13 | await router.push({ 14 | name: logoutTo && true ? logoutTo : 'login', 15 | query: { 16 | ...router.currentRoute.value.query, 17 | redirect: currentRoute.name as string, 18 | }, 19 | }); 20 | }; 21 | return { 22 | logout, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/visible.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | export default function useVisible(initValue = false) { 4 | const visible = ref(initValue); 5 | const setVisible = (value: boolean) => { 6 | visible.value = value; 7 | }; 8 | const toggle = () => { 9 | visible.value = !visible.value; 10 | }; 11 | return { 12 | visible, 13 | setVisible, 14 | toggle, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/layout/default-layout.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 107 | 108 | 181 | -------------------------------------------------------------------------------- /src/layout/page-layout.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | import localeLogin from '@/views/login/locale/en-US'; 2 | import localeSysMenu from '@/views/admin/menu/locale/en-US'; 3 | import localeWorkplace from '@/views/dashboard/workplace/locale/en-US'; 4 | import localeLogLogin from '@/views/log/login/locale/en-US'; 5 | import localeIconPicker from '@/components/icon-picker/locale/en-US'; 6 | import localeLogOpera from '@/views/log/opera/locale/en-US'; 7 | import localeSysDept from '@/views/admin/dept/locale/en-US'; 8 | import localeServerMonitor from '@/views/monitor/server/locale/en-US'; 9 | import localeRedisMonitor from '@/views/monitor/redis/locale/en-US'; 10 | import localeSysApi from '@/views/admin/api/locale/en-US'; 11 | import localeSysUser from '@/views/admin/user/locale/en-US'; 12 | import localeSysRole from '@/views/admin/role/locale/en-US'; 13 | import localeGenerator from '@/views/automation/code-generator/local/en-US'; 14 | import localeDataRule from '@/views/admin/data-rule/local/en-US'; 15 | import localeSettings from './en-US/settings'; 16 | 17 | export default { 18 | 'menu.dashboard': 'Dashboard', 19 | 'menu.admin': 'System Manage', 20 | 'menu.automation': 'System Automation', 21 | 'menu.monitor': 'System Monitor', 22 | 'menu.log': 'Log', 23 | 'menu.arcoWebsite': 'Arco Design', 24 | 'menu.site': ' Official Website', 25 | 'menu.github': 'GitHub', 26 | 'menu.sponsor': 'Sponsor', 27 | 'navbar.action.locale': 'Switch to English', 28 | 'modal.title.tips': 'Warm Tips', 29 | 'modal.title.tips.delete': 'Are you sure you want to delete it?', 30 | 'switch.open': 'Enable', 31 | 'switch.close': 'Disable', 32 | 'submit.create.success': 'Created success', 33 | 'submit.update.success': 'Updated success', 34 | 'submit.delete.success': 'Deleted success', 35 | 'copy.success': 'Copy success', 36 | 'copy.error': 'Copy failed', 37 | ...localeSettings, 38 | ...localeLogin, 39 | ...localeWorkplace, 40 | ...localeLogLogin, 41 | ...localeSysMenu, 42 | ...localeIconPicker, 43 | ...localeLogOpera, 44 | ...localeSysDept, 45 | ...localeServerMonitor, 46 | ...localeRedisMonitor, 47 | ...localeSysApi, 48 | ...localeSysUser, 49 | ...localeSysRole, 50 | ...localeGenerator, 51 | ...localeDataRule, 52 | }; 53 | -------------------------------------------------------------------------------- /src/locale/en-US/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'settings.title': 'Settings', 3 | 'settings.themeColor': 'Theme Color', 4 | 'settings.content': 'Content Setting', 5 | 'settings.search': 'Search', 6 | 'settings.language': 'Language', 7 | 'settings.navbar': 'Navbar', 8 | 'settings.menuWidth': 'Menu Width (px)', 9 | 'settings.navbar.theme.toLight': 'Click to use light mode', 10 | 'settings.navbar.theme.toDark': 'Click to use dark mode', 11 | 'settings.navbar.screen.toFull': 'Click to switch to full screen mode', 12 | 'settings.navbar.screen.toExit': 'Click to exit the full screen mode', 13 | 'settings.navbar.alerts': 'alerts', 14 | 'settings.menu': 'Menu', 15 | 'settings.topMenu': 'Top Menu', 16 | 'settings.tabBar': 'Tab Bar', 17 | 'settings.footer': 'Footer', 18 | 'settings.otherSettings': 'Other Settings', 19 | 'settings.colorWeak': 'Color Weak', 20 | 'settings.alertContent': 21 | 'After the configuration is only temporarily effective, if you want to really affect the project, click the "Copy Settings" button below and replace the configuration in settings.json.', 22 | 'settings.copySettings': 'Copy Settings', 23 | 'settings.copySettings.message': 24 | 'Copy succeeded, please paste to file src/settings.json.', 25 | 'settings.close': 'Close', 26 | 'settings.color.tooltip': 27 | '10 gradient colors generated according to the theme color', 28 | 'settings.menuFromServer': 'Menu From Server', 29 | }; 30 | -------------------------------------------------------------------------------- /src/locale/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import en from './en-US'; 3 | import cn from './zh-CN'; 4 | 5 | export const LOCALE_OPTIONS = [ 6 | { label: '中文', value: 'zh-CN' }, 7 | { label: 'English', value: 'en-US' }, 8 | ]; 9 | const defaultLocale = localStorage.getItem('arco-locale') || 'zh-CN'; 10 | 11 | const i18n = createI18n({ 12 | locale: defaultLocale, 13 | fallbackLocale: 'en-US', 14 | legacy: false, 15 | allowComposition: true, 16 | messages: { 17 | 'en-US': en, 18 | 'zh-CN': cn, 19 | }, 20 | }); 21 | 22 | export default i18n; 23 | -------------------------------------------------------------------------------- /src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import localeLogin from '@/views/login/locale/zh-CN'; 2 | import localeSysMenu from '@/views/admin/menu/locale/zh-CN'; 3 | import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN'; 4 | import localeLogLogin from '@/views/log/login/locale/zh-CN'; 5 | import localeIconPicker from '@/components/icon-picker/locale/zh-CN'; 6 | import localeLogOpera from '@/views/log/opera/locale/zh-CN'; 7 | import localeSysDept from '@/views/admin/dept/locale/zh-CN'; 8 | import localeSysApi from '@/views/admin/api/locale/zh-CN'; 9 | import localeServerMonitor from '@/views/monitor/server/locale/zh-CN'; 10 | import localeRedisMonitor from '@/views/monitor/redis/locale/zh-CN'; 11 | import localeSysUser from '@/views/admin/user/locale/zh-CN'; 12 | import localeSysRole from '@/views/admin/role/locale/zh-CN'; 13 | import localeGenerator from '@/views/automation/code-generator/local/zh-CN'; 14 | import localeDataRule from '@/views/admin/data-rule/local/zh-CN'; 15 | import localeSettings from './zh-CN/settings'; 16 | 17 | export default { 18 | 'menu.dashboard': '仪表盘', 19 | 'menu.admin': '系统管理', 20 | 'menu.automation': '系统自动化', 21 | 'menu.monitor': '系统监控', 22 | 'menu.log': '日志', 23 | 'menu.arcoWebsite': 'Arco Design', 24 | 'menu.site': '官网', 25 | 'menu.github': 'GitHub', 26 | 'menu.sponsor': '赞助', 27 | 'navbar.action.locale': '切换为中文', 28 | 'modal.title.tips': '温馨提示', 29 | 'modal.title.tips.delete': '确定要删除吗?', 30 | 'switch.open': '开启', 31 | 'switch.close': '关闭', 32 | 'submit.create.success': '创建成功', 33 | 'submit.update.success': '更新成功', 34 | 'submit.delete.success': '删除成功', 35 | 'copy.success': '复制成功', 36 | 'copy.error': '复制失败', 37 | ...localeSettings, 38 | ...localeLogin, 39 | ...localeWorkplace, 40 | ...localeLogLogin, 41 | ...localeSysMenu, 42 | ...localeIconPicker, 43 | ...localeLogOpera, 44 | ...localeSysDept, 45 | ...localeSysApi, 46 | ...localeServerMonitor, 47 | ...localeRedisMonitor, 48 | ...localeSysUser, 49 | ...localeSysRole, 50 | ...localeGenerator, 51 | ...localeDataRule, 52 | }; 53 | -------------------------------------------------------------------------------- /src/locale/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'settings.title': '页面配置', 3 | 'settings.themeColor': '主题色', 4 | 'settings.content': '内容区域', 5 | 'settings.search': '搜索', 6 | 'settings.language': '语言', 7 | 'settings.navbar': '导航栏', 8 | 'settings.menuWidth': '菜单宽度 (px)', 9 | 'settings.navbar.theme.toLight': '点击切换为亮色模式', 10 | 'settings.navbar.theme.toDark': '点击切换为暗黑模式', 11 | 'settings.navbar.screen.toFull': '点击切换全屏模式', 12 | 'settings.navbar.screen.toExit': '点击退出全屏模式', 13 | 'settings.navbar.alerts': '消息通知', 14 | 'settings.menu': '菜单栏', 15 | 'settings.topMenu': '顶部菜单栏', 16 | 'settings.tabBar': '多页签', 17 | 'settings.footer': '底部', 18 | 'settings.otherSettings': '其他设置', 19 | 'settings.colorWeak': '色弱模式', 20 | 'settings.alertContent': 21 | '配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。', 22 | 'settings.copySettings': '复制配置', 23 | 'settings.copySettings.message': 24 | '复制成功,请粘贴到 src/settings.json 文件中', 25 | 'settings.close': '关闭', 26 | 'settings.color.tooltip': 27 | '根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)', 28 | 'settings.menuFromServer': '菜单来源于后台', 29 | }; 30 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import ArcoVue from '@arco-design/web-vue'; 3 | import ArcoVueIcon from '@arco-design/web-vue/es/icon'; 4 | import { InstallCodeMirror } from 'codemirror-editor-vue3'; 5 | import globalComponents from '@/components'; 6 | import router from './router'; 7 | import store from './store'; 8 | import i18n from './locale'; 9 | import directive from './directive'; 10 | import App from './App.vue'; 11 | import '@/assets/style/global.less'; 12 | import '@/api/interceptor'; 13 | 14 | const app = createApp(App); 15 | app.config.warnHandler = () => null; 16 | 17 | app.use(ArcoVue, {}); 18 | app.use(ArcoVueIcon); 19 | 20 | app.use(router); 21 | app.use(store); 22 | app.use(i18n); 23 | app.use(globalComponents); 24 | app.use(directive); 25 | app.use(InstallCodeMirror); 26 | 27 | app.mount('#app'); 28 | -------------------------------------------------------------------------------- /src/router/app-menus/index.ts: -------------------------------------------------------------------------------- 1 | import appRoutes from '@/router/routes'; 2 | 3 | const mixinRoutes = [...appRoutes]; 4 | 5 | const appClientMenus = mixinRoutes.map((el) => { 6 | const { name, path, meta, redirect, children } = el; 7 | return { 8 | name, 9 | path, 10 | meta, 11 | redirect, 12 | children, 13 | }; 14 | }); 15 | 16 | export default appClientMenus; 17 | -------------------------------------------------------------------------------- /src/router/constants.ts: -------------------------------------------------------------------------------- 1 | export const WHITE_LIST = [ 2 | { name: 'notFound', children: [] }, 3 | { name: 'login', children: [] }, 4 | ]; 5 | 6 | export const NOT_FOUND = { 7 | name: 'notFound', 8 | }; 9 | 10 | export const REDIRECT_ROUTE_NAME = 'Redirect'; 11 | 12 | export const DEFAULT_ROUTE_NAME = 'Workplace'; 13 | 14 | export const DEFAULT_ROUTE = { 15 | title: 'menu.dashboard.workplace', 16 | name: DEFAULT_ROUTE_NAME, 17 | fullPath: '/dashboard/workplace', 18 | }; 19 | -------------------------------------------------------------------------------- /src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router'; 2 | import { setRouteEmitter } from '@/utils/route-listener'; 3 | import setupUserLoginInfoGuard from './userLoginInfo'; 4 | import setupPermissionGuard from './permission'; 5 | 6 | function setupPageGuard(router: Router) { 7 | router.beforeEach(async (to) => { 8 | // emit route change 9 | setRouteEmitter(to); 10 | }); 11 | } 12 | 13 | export default function createRouteGuard(router: Router) { 14 | setupPageGuard(router); 15 | setupUserLoginInfoGuard(router); 16 | setupPermissionGuard(router); 17 | } 18 | -------------------------------------------------------------------------------- /src/router/guard/permission.ts: -------------------------------------------------------------------------------- 1 | import type { Router, RouteRecordNormalized } from 'vue-router'; 2 | import NProgress from 'nprogress'; // progress bar 3 | import usePermission from '@/hooks/permission'; 4 | import { useAppStore, useUserStore } from '@/store'; 5 | import appRoutes from '@/router/routes'; 6 | import { NOT_FOUND, WHITE_LIST } from '../constants'; 7 | 8 | export default function setupPermissionGuard(router: Router) { 9 | router.beforeEach(async (to, from, next) => { 10 | const appStore = useAppStore(); 11 | const userStore = useUserStore(); 12 | const Permission = usePermission(); 13 | const permissionsAllow = Permission.accessRouter(to); 14 | 15 | // 动态菜单加载 16 | if (appStore.menuFromServer) { 17 | // 如果菜单未加载并且不在白名单中,加载菜单 18 | if ( 19 | !appStore.appAsyncMenus.length && 20 | !WHITE_LIST.find((el) => el.name === to.name) 21 | ) { 22 | await appStore.fetchServerMenuConfig(); 23 | } 24 | 25 | const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST]; 26 | let exist = false; 27 | 28 | // 遍历菜单,检查路由是否存在 29 | while (serverMenuConfig.length && !exist) { 30 | const element = serverMenuConfig.shift(); 31 | if (element?.name === to.name) { 32 | exist = true; 33 | } 34 | if (element?.children) { 35 | serverMenuConfig.push( 36 | ...(element.children as unknown as RouteRecordNormalized[]) 37 | ); 38 | } 39 | } 40 | 41 | // 如果存在该路由且权限允许,跳转 42 | if (exist && permissionsAllow) { 43 | next(); 44 | } else { 45 | next(NOT_FOUND); 46 | } 47 | } else if (permissionsAllow) { 48 | next(); 49 | } else { 50 | const destination = 51 | Permission.findFirstPermissionRoute(appRoutes, userStore.roles) || 52 | NOT_FOUND; 53 | next(destination); 54 | } 55 | 56 | NProgress.done(); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/router/guard/userLoginInfo.ts: -------------------------------------------------------------------------------- 1 | import type { LocationQueryRaw, Router } from 'vue-router'; 2 | import NProgress from 'nprogress'; // progress bar 3 | import { useUserStore } from '@/store'; 4 | import { isLogin } from '@/utils/auth'; 5 | import { DEFAULT_ROUTE_NAME } from '@/router/constants'; 6 | 7 | export default function setupUserLoginInfoGuard(router: Router) { 8 | router.beforeEach(async (to, from, next) => { 9 | NProgress.start(); 10 | const userStore = useUserStore(); 11 | 12 | // 处理 OAuth 回调 13 | if (to.name === 'oauth2CallBack') { 14 | const oauth2 = await userStore.oauth2Login(); 15 | if (oauth2) { 16 | await userStore.info(); 17 | next({ name: DEFAULT_ROUTE_NAME }); 18 | } else { 19 | next({ 20 | name: 'login', 21 | query: { 22 | redirect: to.name, 23 | ...to.query, 24 | } as LocationQueryRaw, 25 | }); 26 | } 27 | return; 28 | } 29 | 30 | if (isLogin()) { 31 | if (userStore.roles) { 32 | next(); 33 | } else { 34 | try { 35 | await userStore.info(); 36 | next(); 37 | } catch (error) { 38 | await userStore.logout(); 39 | next({ 40 | name: 'login', 41 | query: { 42 | redirect: to.name, 43 | ...to.query, 44 | } as LocationQueryRaw, 45 | }); 46 | } 47 | } 48 | } else if (to.name === 'login') { 49 | next(); 50 | } else { 51 | next({ 52 | name: 'login', 53 | query: { 54 | redirect: to.name, 55 | ...to.query, 56 | } as LocationQueryRaw, 57 | }); 58 | } 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import NProgress from 'nprogress'; // progress bar 3 | import 'nprogress/nprogress.css'; 4 | 5 | import appRoutes from '@/router/routes'; 6 | import { NOT_FOUND_ROUTE, OAUTH2_CALLBACK, REDIRECT_MAIN } from './routes/base'; 7 | import createRouteGuard from './guard'; 8 | 9 | NProgress.configure({ showSpinner: false }); // NProgress Configuration 10 | 11 | const router = createRouter({ 12 | history: createWebHistory(), 13 | routes: [ 14 | { 15 | path: '/', 16 | redirect: 'login', 17 | }, 18 | { 19 | path: '/login', 20 | name: 'login', 21 | component: () => import('@/views/login/index.vue'), 22 | meta: { 23 | requiresAuth: false, 24 | }, 25 | }, 26 | ...appRoutes, 27 | REDIRECT_MAIN, 28 | NOT_FOUND_ROUTE, 29 | OAUTH2_CALLBACK, 30 | ], 31 | scrollBehavior() { 32 | return { top: 0 }; 33 | }, 34 | }); 35 | 36 | createRouteGuard(router); 37 | 38 | export default router; 39 | -------------------------------------------------------------------------------- /src/router/routes/base.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router'; 2 | import { REDIRECT_ROUTE_NAME } from '@/router/constants'; 3 | 4 | export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue'); 5 | export const NOTFOUND_LAYOUT = () => import('@/views/not-found/index.vue'); 6 | 7 | export const REDIRECT_MAIN: RouteRecordRaw = { 8 | path: '/redirect', 9 | name: 'redirectWrapper', 10 | component: DEFAULT_LAYOUT, 11 | meta: { 12 | requiresAuth: true, 13 | hideInMenu: true, 14 | }, 15 | children: [ 16 | { 17 | path: '/redirect/:path', 18 | name: REDIRECT_ROUTE_NAME, 19 | component: () => import('@/views/redirect/index.vue'), 20 | meta: { 21 | requiresAuth: true, 22 | hideInMenu: true, 23 | }, 24 | }, 25 | ], 26 | }; 27 | 28 | export const NOT_FOUND_ROUTE: RouteRecordRaw = { 29 | path: '/:pathMatch(.*)*', 30 | name: 'notFound', 31 | component: NOTFOUND_LAYOUT, 32 | }; 33 | 34 | export const OAUTH2_CALLBACK: RouteRecordRaw = { 35 | path: '/oauth2/callback', 36 | name: 'oauth2CallBack', 37 | component: () => import('@/views/login/components/oauth_callback.vue'), 38 | meta: { 39 | requiresAuth: false, 40 | hideInMenu: true, 41 | roles: ['oauth2'], 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordNormalized } from 'vue-router'; 2 | 3 | const modules = import.meta.glob('./modules/*.ts', { eager: true }); 4 | 5 | function formatModules(_modules: any, result: RouteRecordNormalized[]) { 6 | Object.keys(_modules).forEach((key) => { 7 | const defaultModule = _modules[key].default; 8 | if (!defaultModule) return; 9 | const moduleList = Array.isArray(defaultModule) 10 | ? [...defaultModule] 11 | : [defaultModule]; 12 | result.push(...moduleList); 13 | }); 14 | return result; 15 | } 16 | 17 | const appRoutes: RouteRecordNormalized[] = formatModules(modules, []); 18 | 19 | export default appRoutes; 20 | -------------------------------------------------------------------------------- /src/router/routes/modules/admin.ts: -------------------------------------------------------------------------------- 1 | import { AppRouteRecordRaw } from '@/router/routes/types'; 2 | import { DEFAULT_LAYOUT } from '@/router/routes/base'; 3 | 4 | const SYSTEM: AppRouteRecordRaw = { 5 | path: '/admin', 6 | name: 'admin', 7 | component: DEFAULT_LAYOUT, 8 | meta: { 9 | locale: 'menu.admin', 10 | requiresAuth: true, 11 | icon: 'icon-settings', 12 | order: 1, 13 | }, 14 | children: [ 15 | { 16 | path: 'sys-dept', 17 | name: 'SysDept', 18 | component: () => import('@/views/admin/dept/index.vue'), 19 | meta: { 20 | locale: 'menu.admin.sysDept', 21 | requiresAuth: true, 22 | roles: ['*'], 23 | }, 24 | }, 25 | { 26 | path: 'sys-user', 27 | name: 'SysUser', 28 | component: () => import('@/views/admin/user/index.vue'), 29 | meta: { 30 | locale: 'menu.admin.sysUser', 31 | requiresAuth: true, 32 | roles: ['*'], 33 | }, 34 | }, 35 | { 36 | path: 'sys-role', 37 | name: 'SysRole', 38 | component: () => import('@/views/admin/role/index.vue'), 39 | meta: { 40 | locale: 'menu.admin.sysRole', 41 | requiresAuth: true, 42 | roles: ['*'], 43 | }, 44 | }, 45 | { 46 | path: 'sys-menu', 47 | name: 'SysMenu', 48 | component: () => import('@/views/admin/menu/index.vue'), 49 | meta: { 50 | locale: 'menu.admin.sysMenu', 51 | requiresAuth: true, 52 | roles: ['*'], 53 | }, 54 | }, 55 | { 56 | path: 'sys-api', 57 | name: 'SysApi', 58 | component: () => import('@/views/admin/api/index.vue'), 59 | meta: { 60 | locale: 'menu.admin.sysApi', 61 | requiresAuth: true, 62 | roles: ['*'], 63 | }, 64 | }, 65 | { 66 | path: 'sys-data-rule', 67 | name: 'SysDataRule', 68 | component: () => import('@/views/admin/data-rule/index.vue'), 69 | meta: { 70 | locale: 'menu.admin.sysDataRule', 71 | requiresAuth: true, 72 | roles: ['*'], 73 | }, 74 | }, 75 | ], 76 | }; 77 | 78 | export default SYSTEM; 79 | -------------------------------------------------------------------------------- /src/router/routes/modules/automation.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LAYOUT } from '../base'; 2 | import { AppRouteRecordRaw } from '../types'; 3 | 4 | const DASHBOARD: AppRouteRecordRaw = { 5 | path: '/automation', 6 | name: 'automation', 7 | component: DEFAULT_LAYOUT, 8 | meta: { 9 | locale: 'menu.automation', 10 | requiresAuth: true, 11 | icon: 'icon-code-square', 12 | order: 2, 13 | hideInMenu: false, 14 | }, 15 | children: [ 16 | { 17 | path: 'code-generator', 18 | name: 'CodeGenerator', 19 | component: () => import('@/views/automation/code-generator/index.vue'), 20 | meta: { 21 | locale: 'menu.automation.codeGenerator', 22 | requiresAuth: true, 23 | roles: ['*'], 24 | hideInMenu: false, 25 | }, 26 | }, 27 | ], 28 | }; 29 | 30 | export default DASHBOARD; 31 | -------------------------------------------------------------------------------- /src/router/routes/modules/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LAYOUT } from '../base'; 2 | import { AppRouteRecordRaw } from '../types'; 3 | 4 | const DASHBOARD: AppRouteRecordRaw = { 5 | path: '/dashboard', 6 | name: 'dashboard', 7 | component: DEFAULT_LAYOUT, 8 | meta: { 9 | locale: 'menu.dashboard', 10 | requiresAuth: true, 11 | icon: 'icon-dashboard', 12 | order: 0, 13 | hideInMenu: false, 14 | }, 15 | children: [ 16 | { 17 | path: 'workplace', 18 | name: 'Workplace', 19 | component: () => import('@/views/dashboard/workplace/index.vue'), 20 | meta: { 21 | locale: 'menu.dashboard.workplace', 22 | requiresAuth: true, 23 | roles: ['*'], 24 | hideInMenu: false, 25 | }, 26 | }, 27 | ], 28 | }; 29 | 30 | export default DASHBOARD; 31 | -------------------------------------------------------------------------------- /src/router/routes/modules/log.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LAYOUT } from '../base'; 2 | import { AppRouteRecordRaw } from '../types'; 3 | 4 | const LOG: AppRouteRecordRaw = { 5 | path: '/log', 6 | name: 'log', 7 | component: DEFAULT_LAYOUT, 8 | meta: { 9 | locale: 'menu.log', 10 | requiresAuth: true, 11 | icon: 'icon-bug', 12 | order: 3, 13 | }, 14 | children: [ 15 | { 16 | path: 'login', 17 | name: 'Login', 18 | component: () => import('@/views/log/login/index.vue'), 19 | meta: { 20 | locale: 'menu.log.login', 21 | requiresAuth: true, 22 | roles: ['*'], 23 | }, 24 | }, 25 | { 26 | path: 'opera', 27 | name: 'Opera', 28 | component: () => import('@/views/log/opera/index.vue'), 29 | meta: { 30 | locale: 'menu.log.opera', 31 | requiresAuth: true, 32 | roles: ['*'], 33 | }, 34 | }, 35 | ], 36 | }; 37 | 38 | export default LOG; 39 | -------------------------------------------------------------------------------- /src/router/routes/modules/monitor.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LAYOUT } from '../base'; 2 | import { AppRouteRecordRaw } from '../types'; 3 | 4 | const LOG: AppRouteRecordRaw = { 5 | path: '/monitor', 6 | name: 'monitor', 7 | component: DEFAULT_LAYOUT, 8 | meta: { 9 | locale: 'menu.monitor', 10 | requiresAuth: true, 11 | icon: 'icon-computer', 12 | order: 2, 13 | }, 14 | children: [ 15 | { 16 | path: 'redis', 17 | name: 'Redis', 18 | component: () => import('@/views/monitor/redis/index.vue'), 19 | meta: { 20 | locale: 'menu.monitor.redis', 21 | requiresAuth: true, 22 | roles: ['*'], 23 | }, 24 | }, 25 | { 26 | path: 'server', 27 | name: 'Server', 28 | component: () => import('@/views/monitor/server/index.vue'), 29 | meta: { 30 | locale: 'menu.monitor.server', 31 | requiresAuth: true, 32 | roles: ['*'], 33 | }, 34 | }, 35 | ], 36 | }; 37 | 38 | export default LOG; 39 | -------------------------------------------------------------------------------- /src/router/routes/types.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import type { NavigationGuard, RouteMeta } from 'vue-router'; 3 | 4 | export type Component = 5 | | ReturnType 6 | | (() => Promise) 7 | | (() => Promise); 8 | 9 | export interface AppRouteRecordRaw { 10 | path: string; 11 | name?: string | symbol; 12 | meta?: RouteMeta; 13 | redirect?: string; 14 | component: Component | string; 15 | children?: AppRouteRecordRaw[]; 16 | alias?: string | string[]; 17 | props?: Record; 18 | beforeEnter?: NavigationGuard | NavigationGuard[]; 19 | fullPath?: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/router/typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router'; 2 | 3 | declare module 'vue-router' { 4 | interface RouteMeta { 5 | title?: string; 6 | roles?: string[]; // Controls roles that have access to the page 7 | requiresAuth: boolean; // Whether login is required to access the current page (every route must declare) 8 | icon?: string; // The icon show in the side menu 9 | hideInMenu?: boolean; // If true, it is not displayed in the side menu 10 | ignoreCache?: boolean; // if set true, the page will not be cached 11 | order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is 12 | locale?: string; // The locale name show in side menu and breadcrumb 13 | activeMenu?: string; // if set name, the menu will be highlighted according to the name you set 14 | noAffix?: boolean; // if set true, the tag will not affix in the tab-bar 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import useAppStore from './modules/app'; 3 | import useUserStore from './modules/user'; 4 | import useTabBarStore from './modules/tab-bar'; 5 | 6 | const pinia = createPinia(); 7 | 8 | export { useAppStore, useUserStore, useTabBarStore }; 9 | export default pinia; 10 | -------------------------------------------------------------------------------- /src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { Notification, NotificationReturn } from '@arco-design/web-vue'; 3 | import { RouteRecordNormalized } from 'vue-router'; 4 | import defaultSettings from '@/config/settings.json'; 5 | import { getUserMenuList } from '@/api/user'; 6 | import convertToCamelCase, { convertToKebabCase } from '@/utils/string'; 7 | import { WHITE_LIST } from '@/router/constants'; 8 | import { AppRouteRecordRaw } from '@/router/routes/types'; 9 | import DASHBOARD from '@/router/routes/modules/dashboard'; 10 | import { DEFAULT_LAYOUT } from '@/router/routes/base'; 11 | import { AppState, MenuItem } from './types'; 12 | 13 | function generateMenu( 14 | data: MenuItem[], 15 | parentName?: string 16 | ): AppRouteRecordRaw[] { 17 | const menuData: AppRouteRecordRaw[] = []; 18 | const views = import.meta.glob('@/views/**/*.vue'); 19 | 20 | data.forEach((menu) => { 21 | const localeName = convertToCamelCase(menu.name); 22 | const path = menu.path || `${convertToKebabCase(menu.name)}`; 23 | const component = menu.component 24 | ? views[`/src/views${menu.component}`] 25 | : DEFAULT_LAYOUT; 26 | 27 | const menuItem: AppRouteRecordRaw = { 28 | path, 29 | name: menu.name, 30 | component, 31 | children: [], 32 | meta: { 33 | title: menu.title, 34 | roles: ['*'], // TODO: menu.perms ? menu.perms.split(',') : [], 35 | requiresAuth: !WHITE_LIST.some((item) => item.name === menu.name), 36 | icon: menu.icon, 37 | hideInMenu: menu.display === 0, 38 | ignoreCache: menu.cache === 0, 39 | order: menu.sort, 40 | locale: parentName 41 | ? `menu.${parentName}.${localeName}` 42 | : `menu.${localeName}`, 43 | }, 44 | }; 45 | 46 | if (menu.children && menu.children.length > 0) { 47 | menuItem.children = generateMenu(menu.children, localeName); 48 | } 49 | 50 | menuData.push(menuItem); 51 | }); 52 | 53 | return menuData; 54 | } 55 | 56 | const useAppStore = defineStore('app', { 57 | state: (): AppState => ({ ...defaultSettings }), 58 | 59 | getters: { 60 | appCurrentSetting(state: AppState): AppState { 61 | return { ...state }; 62 | }, 63 | appDevice(state: AppState) { 64 | return state.device; 65 | }, 66 | appAsyncMenus(state: AppState): RouteRecordNormalized[] { 67 | return state.serverMenu as unknown as RouteRecordNormalized[]; 68 | }, 69 | }, 70 | 71 | actions: { 72 | // Update app settings 73 | updateSettings(partial: Partial) { 74 | // @ts-ignore-next-line 75 | this.$patch(partial); 76 | }, 77 | 78 | // Change theme color 79 | toggleTheme(dark: boolean) { 80 | this.theme = dark ? 'dark' : 'light'; 81 | document.body.setAttribute('arco-theme', this.theme); 82 | }, 83 | 84 | // Toggle device type 85 | toggleDevice(device: string) { 86 | this.device = device; 87 | }, 88 | 89 | // Toggle menu visibility 90 | toggleMenu(value: boolean) { 91 | this.hideMenu = value; 92 | }, 93 | 94 | // Fetch server menu configuration 95 | async fetchServerMenuConfig() { 96 | let notifyInstance: NotificationReturn | null = null; 97 | try { 98 | notifyInstance = Notification.info({ 99 | id: 'menuNotice', // Keep the instance id the same 100 | content: 'loading', 101 | closable: true, 102 | }); 103 | 104 | const data = await getUserMenuList(); 105 | 106 | if ( 107 | data.length === 0 || 108 | !data.some((item) => item.name === 'dashboard') 109 | ) { 110 | this.serverMenu = [DASHBOARD].concat( 111 | generateMenu(data) 112 | ) as unknown as RouteRecordNormalized[]; 113 | } else { 114 | this.serverMenu = generateMenu( 115 | data 116 | ) as unknown as RouteRecordNormalized[]; 117 | } 118 | 119 | notifyInstance = Notification.success({ 120 | id: 'menuNotice', 121 | content: 'success', 122 | closable: true, 123 | }); 124 | } catch (error) { 125 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 126 | notifyInstance = Notification.error({ 127 | id: 'menuNotice', 128 | content: 'error', 129 | closable: true, 130 | }); 131 | } 132 | }, 133 | 134 | // Clear server menu 135 | clearServerMenu() { 136 | this.serverMenu = []; 137 | }, 138 | }, 139 | }); 140 | 141 | export default useAppStore; 142 | -------------------------------------------------------------------------------- /src/store/modules/app/types.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordNormalized } from 'vue-router'; 2 | 3 | export interface AppState { 4 | theme: string; 5 | colorWeak: boolean; 6 | navbar: boolean; 7 | menu: boolean; 8 | topMenu: boolean; 9 | hideMenu: boolean; 10 | menuCollapse: boolean; 11 | footer: boolean; 12 | themeColor: string; 13 | menuWidth: number; 14 | globalSettings: boolean; 15 | device: string; 16 | tabBar: boolean; 17 | menuFromServer: boolean; 18 | serverMenu: RouteRecordNormalized[]; 19 | [key: string]: unknown; 20 | } 21 | 22 | export interface MenuItem { 23 | id: number; 24 | title: string; 25 | name: string; 26 | level: number; 27 | sort: number; 28 | icon?: string; 29 | path?: string; 30 | menu_type: number; 31 | component?: string; 32 | perms?: string; 33 | status: 0 | 1; 34 | remark?: string; 35 | display: 0 | 1; 36 | cache: 0 | 1; 37 | parent_id?: number; 38 | children: MenuItem[] | []; 39 | } 40 | -------------------------------------------------------------------------------- /src/store/modules/tab-bar/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized } from 'vue-router'; 2 | import { defineStore } from 'pinia'; 3 | import { 4 | DEFAULT_ROUTE, 5 | DEFAULT_ROUTE_NAME, 6 | REDIRECT_ROUTE_NAME, 7 | } from '@/router/constants'; 8 | import { isString } from '@/utils/is'; 9 | import { TabBarState, TagProps } from './types'; 10 | 11 | const formatTag = (route: RouteLocationNormalized): TagProps => { 12 | const { name, meta, fullPath, query } = route; 13 | return { 14 | title: meta.locale || '', 15 | name: String(name), 16 | fullPath, 17 | query, 18 | ignoreCache: meta.ignoreCache, 19 | }; 20 | }; 21 | 22 | const BAN_LIST = [REDIRECT_ROUTE_NAME]; 23 | 24 | const useAppStore = defineStore('tabBar', { 25 | state: (): TabBarState => ({ 26 | cacheTabList: new Set([DEFAULT_ROUTE_NAME]), 27 | tagList: [DEFAULT_ROUTE], 28 | }), 29 | 30 | getters: { 31 | getTabList(): TagProps[] { 32 | return this.tagList; 33 | }, 34 | getCacheList(): string[] { 35 | return Array.from(this.cacheTabList); 36 | }, 37 | }, 38 | 39 | actions: { 40 | updateTabList(route: RouteLocationNormalized) { 41 | if (BAN_LIST.includes(route.name as string)) return; 42 | this.tagList.push(formatTag(route)); 43 | if (!route.meta.ignoreCache) { 44 | this.cacheTabList.add(route.name as string); 45 | } 46 | }, 47 | deleteTag(idx: number, tag: TagProps) { 48 | this.tagList.splice(idx, 1); 49 | this.cacheTabList.delete(tag.name); 50 | }, 51 | addCache(name: string) { 52 | if (isString(name) && name !== '') this.cacheTabList.add(name); 53 | }, 54 | deleteCache(tag: TagProps) { 55 | this.cacheTabList.delete(tag.name); 56 | }, 57 | freshTabList(tags: TagProps[]) { 58 | this.tagList = tags; 59 | this.cacheTabList.clear(); 60 | // 要先判断ignoreCache 61 | this.tagList 62 | .filter((el) => !el.ignoreCache) 63 | .map((el) => el.name) 64 | .forEach((x) => this.cacheTabList.add(x)); 65 | }, 66 | resetTabList() { 67 | this.tagList = [DEFAULT_ROUTE]; 68 | this.cacheTabList.clear(); 69 | this.cacheTabList.add(DEFAULT_ROUTE_NAME); 70 | }, 71 | }, 72 | }); 73 | 74 | export default useAppStore; 75 | -------------------------------------------------------------------------------- /src/store/modules/tab-bar/types.ts: -------------------------------------------------------------------------------- 1 | export interface TagProps { 2 | title: string; 3 | name: string; 4 | fullPath: string; 5 | query?: any; 6 | ignoreCache?: boolean; 7 | } 8 | 9 | export interface TabBarState { 10 | tagList: TagProps[]; 11 | cacheTabList: Set; 12 | } 13 | -------------------------------------------------------------------------------- /src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { getUserInfo } from '@/api/user'; 3 | import { clearToken, setToken } from '@/utils/auth'; 4 | import { removeRouteListener } from '@/utils/route-listener'; 5 | import { UserState } from '@/store/modules/user/types'; 6 | import { 7 | CaptchaRes, 8 | getCaptcha, 9 | login as userLogin, 10 | LoginData, 11 | logout as userLogout, 12 | } from '@/api/auth'; 13 | import useAppStore from '../app'; 14 | 15 | const useUserStore = defineStore('user', { 16 | state: (): UserState => ({ 17 | username: undefined, 18 | nickname: undefined, 19 | avatar: undefined, 20 | is_superuser: false, 21 | is_staff: false, 22 | roles: '', 23 | }), 24 | 25 | getters: { 26 | userInfo(state: UserState): UserState { 27 | return { ...state }; 28 | }, 29 | }, 30 | 31 | actions: { 32 | // switchRoles() { 33 | // return new Promise((resolve) => { 34 | // this.role = this.role === 'user' ? 'admin' : 'user'; 35 | // resolve(this.role); 36 | // }); 37 | // }, 38 | 39 | // Set user's information 40 | setInfo(partial: Partial) { 41 | this.$patch(partial); 42 | }, 43 | 44 | // Reset user's information 45 | resetInfo() { 46 | this.$reset(); 47 | }, 48 | 49 | // Get user's information 50 | async info() { 51 | const res = await getUserInfo(); 52 | this.setInfo(res); 53 | }, 54 | 55 | // Get captcha 56 | async captcha() { 57 | const res: CaptchaRes = await getCaptcha(); 58 | return res.image; 59 | }, 60 | 61 | // Login 62 | async login(loginForm: LoginData) { 63 | try { 64 | const res = await userLogin(loginForm); 65 | setToken(res.access_token); 66 | } catch (err) { 67 | clearToken(); 68 | throw err; 69 | } 70 | }, 71 | 72 | // OAuth2 login 73 | async oauth2Login() { 74 | const params = new URLSearchParams(window.location.search); 75 | const token = params.get('access_token'); 76 | if (token) { 77 | setToken(token); 78 | return true; 79 | } 80 | return false; 81 | }, 82 | 83 | // Logout 84 | logoutCallBack() { 85 | const appStore = useAppStore(); 86 | this.resetInfo(); 87 | clearToken(); 88 | removeRouteListener(); 89 | appStore.clearServerMenu(); 90 | }, 91 | 92 | async logout() { 93 | try { 94 | await userLogout(); 95 | } finally { 96 | this.logoutCallBack(); 97 | } 98 | }, 99 | }, 100 | }); 101 | 102 | export default useUserStore; 103 | -------------------------------------------------------------------------------- /src/store/modules/user/types.ts: -------------------------------------------------------------------------------- 1 | export interface UserState { 2 | username?: string; 3 | nickname?: string; 4 | avatar?: string; 5 | is_superuser: boolean; 6 | is_staff: boolean; 7 | roles: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/echarts.ts: -------------------------------------------------------------------------------- 1 | import { CallbackDataParams } from 'echarts/types/dist/shared'; 2 | 3 | export interface ToolTipFormatterParams extends CallbackDataParams { 4 | axisDim: string; 5 | axisIndex: number; 6 | axisType: string; 7 | axisId: string; 8 | axisValue: string; 9 | axisValueLabel: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/global.ts: -------------------------------------------------------------------------------- 1 | import { PaginationProps } from '@arco-design/web-vue'; 2 | 3 | export interface AnyObject { 4 | [key: string]: unknown; 5 | } 6 | 7 | export type Pagination = PaginationProps; 8 | 9 | export interface treeSelectDataType { 10 | id?: number | null; 11 | title?: string; 12 | name?: string; 13 | disabled?: boolean; 14 | children: T[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | const TOKEN_KEY = 'access_token'; 2 | 3 | const isLogin = () => { 4 | return !!localStorage.getItem(TOKEN_KEY); 5 | }; 6 | 7 | const getToken = () => { 8 | return localStorage.getItem(TOKEN_KEY); 9 | }; 10 | 11 | const setToken = (token: string) => { 12 | localStorage.setItem(TOKEN_KEY, token); 13 | }; 14 | 15 | const clearToken = () => { 16 | localStorage.removeItem(TOKEN_KEY); 17 | }; 18 | 19 | export { isLogin, getToken, setToken, clearToken }; 20 | -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | // 随机十六进制颜色 2 | const getRandomColor = (sign: string): string => { 3 | let hash = 0; 4 | for (let i = 0; i < sign.length; i += 1) { 5 | hash = sign.charCodeAt(i) + hash * 31; 6 | } 7 | 8 | // 生成高亮色 9 | const r = (Math.abs(hash % 256) + 128) % 256; // 保证亮度较高 10 | const g = (Math.abs((hash * 3) % 256) + 128) % 256; // 保证亮度较高 11 | const b = (Math.abs((hash * 5) % 256) + 128) % 256; // 保证亮度较高 12 | 13 | // eslint-disable-next-line no-bitwise 14 | return `#${((1 << 24) | (r << 16) | (g << 8) | b) 15 | .toString(16) 16 | .slice(1) 17 | .toUpperCase()}`; 18 | }; 19 | 20 | export default getRandomColor; 21 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | const debug = import.meta.env.MODE !== 'production'; 2 | 3 | export default debug; 4 | -------------------------------------------------------------------------------- /src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export function addEventListen( 2 | target: Window | HTMLElement, 3 | event: string, 4 | handler: EventListenerOrEventListenerObject, 5 | capture = false 6 | ) { 7 | if ( 8 | target.addEventListener && 9 | typeof target.addEventListener === 'function' 10 | ) { 11 | target.addEventListener(event, handler, capture); 12 | } 13 | } 14 | 15 | export function removeEventListen( 16 | target: Window | HTMLElement, 17 | event: string, 18 | handler: EventListenerOrEventListenerObject, 19 | capture = false 20 | ) { 21 | if ( 22 | target.removeEventListener && 23 | typeof target.removeEventListener === 'function' 24 | ) { 25 | target.removeEventListener(event, handler, capture); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | type TargetContext = '_self' | '_parent' | '_blank' | '_top'; 2 | 3 | export const openWindow = ( 4 | url: string, 5 | opts?: { target?: TargetContext; [key: string]: any } 6 | ) => { 7 | const { target = '_blank', ...others } = opts || {}; 8 | window.open( 9 | url, 10 | target, 11 | Object.entries(others) 12 | .reduce((preValue: string[], curValue) => { 13 | const [key, value] = curValue; 14 | return [...preValue, `${key}=${value}`]; 15 | }, []) 16 | .join(',') 17 | ); 18 | }; 19 | 20 | export const regexUrl = new RegExp( 21 | '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$', 22 | 'i' 23 | ); 24 | 25 | export default null; 26 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const opt = Object.prototype.toString; 2 | 3 | export function isArray(obj: any): obj is any[] { 4 | return opt.call(obj) === '[object Array]'; 5 | } 6 | 7 | export function isObject(obj: any): obj is { [key: string]: any } { 8 | return opt.call(obj) === '[object Object]'; 9 | } 10 | 11 | export function isString(obj: any): obj is string { 12 | return opt.call(obj) === '[object String]'; 13 | } 14 | 15 | export function isNumber(obj: any): obj is number { 16 | return opt.call(obj) === "[object Number]" && obj === obj; // eslint-disable-line 17 | } 18 | 19 | export function isRegExp(obj: any) { 20 | return opt.call(obj) === '[object RegExp]'; 21 | } 22 | 23 | export function isFile(obj: any): obj is File { 24 | return opt.call(obj) === '[object File]'; 25 | } 26 | 27 | export function isBlob(obj: any): obj is Blob { 28 | return opt.call(obj) === '[object Blob]'; 29 | } 30 | 31 | export function isUndefined(obj: any): obj is undefined { 32 | return obj === undefined; 33 | } 34 | 35 | export function isNull(obj: any): obj is null { 36 | return obj === null; 37 | } 38 | 39 | export function isFunction(obj: any): obj is (...args: any[]) => any { 40 | return typeof obj === 'function'; 41 | } 42 | 43 | export function isEmptyObject(obj: any): boolean { 44 | return isObject(obj) && Object.keys(obj).length === 0; 45 | } 46 | 47 | export function isExist(obj: any): boolean { 48 | return obj || obj === 0; 49 | } 50 | 51 | export function isWindow(el: any): el is Window { 52 | return el === window; 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/list.ts: -------------------------------------------------------------------------------- 1 | // 比较两个列表是否相等 2 | export function listEqual(list1: T[], list2: T[]): boolean { 3 | if (list1.length !== list2.length) { 4 | return false; 5 | } 6 | return list1.every((item) => list2.includes(item)); 7 | } 8 | 9 | // 比较两个列表是否相等(严格模式) 10 | export function listEqualStrict(list1: T[], list2: T[]): boolean { 11 | if (list1.length !== list2.length) { 12 | return false; 13 | } 14 | return list1.every((item, index) => item === list2[index]); 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/route-listener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Listening to routes alone would waste rendering performance. Use the publish-subscribe model for distribution management 3 | * 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。 4 | */ 5 | import mitt, { Handler } from 'mitt'; 6 | import type { RouteLocationNormalized } from 'vue-router'; 7 | 8 | const emitter = mitt(); 9 | 10 | const key = Symbol('ROUTE_CHANGE'); 11 | 12 | let latestRoute: RouteLocationNormalized; 13 | 14 | export function setRouteEmitter(to: RouteLocationNormalized) { 15 | emitter.emit(key, to); 16 | latestRoute = to; 17 | } 18 | 19 | export function listenerRouteChange( 20 | handler: (route: RouteLocationNormalized) => void, 21 | immediate = true 22 | ) { 23 | emitter.on(key, handler as Handler); 24 | if (immediate && latestRoute) { 25 | handler(latestRoute); 26 | } 27 | } 28 | 29 | export function removeRouteListener() { 30 | emitter.off(key); 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | // 转换为小字母开头的驼峰命名 2 | export default function convertToCamelCase(input: string): string { 3 | let camelCase = input.replace(/^[_-]+/, ''); 4 | camelCase = camelCase.replace(/[-_](.)/g, (_, char) => char.toUpperCase()); 5 | camelCase = camelCase.charAt(0).toLowerCase() + camelCase.slice(1); 6 | return camelCase; 7 | } 8 | 9 | // 转换为小字母加中划线命名 10 | export function convertToKebabCase(input: string): string { 11 | let snakeCase = input.replace(/^[_-]+/, ''); 12 | snakeCase = snakeCase.replace(/([A-Z])/g, '-$1').toLowerCase(); 13 | return snakeCase; 14 | } 15 | -------------------------------------------------------------------------------- /src/views/admin/api/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysApi': 'API Manage', 3 | // form 4 | 'admin.api.form.name': 'API name', 5 | 'admin.api.form.name.placeholder': 'Please enter API name', 6 | 'admin.api.form.path': 'API path', 7 | 'admin.api.form.path.placeholder': 'Please enter API path', 8 | 'admin.api.form.method': 'Request method', 9 | 'admin.api.form.method.placeholder': 'All', 10 | 'admin.api.form.search': 'Search', 11 | 'admin.api.form.reset': 'Reset', 12 | 'admin.api.form.name.help': 'API name is required', 13 | 'admin.api.form.path.help': 'API path is required', 14 | 'admin.api.form.method.help': 'Request method is required', 15 | 'admin.api.form.remark.placeholder': 'Please enter remark', 16 | // button 17 | 'admin.api.button.create': 'Create', 18 | 'admin.api.button.delete': 'Delete', 19 | // drawer 20 | 'admin.api.columns.new.drawer': 'Create API', 21 | 'admin.api.columns.edit.drawer': 'Edit API', 22 | 'admin.api.columns.delete.drawer': 'Delete API', 23 | // columns 24 | 'admin.api.columns.edit': 'Edit', 25 | 'admin.api.columns.name': 'API name', 26 | 'admin.api.columns.path': 'API path', 27 | 'admin.api.columns.method': 'Method', 28 | 'admin.api.columns.remark': 'Remark', 29 | 'admin.api.columns.operate': 'Operate', 30 | // alert 31 | 'admin.api.alert': 'This configuration is only used for Casbin permissions', 32 | }; 33 | -------------------------------------------------------------------------------- /src/views/admin/api/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysApi': 'API 管理', 3 | // form 4 | 'admin.api.form.name': 'API 名称', 5 | 'admin.api.form.name.placeholder': '请输入 API 名称', 6 | 'admin.api.form.path': 'API 路径', 7 | 'admin.api.form.path.placeholder': '请输入 API 路径', 8 | 'admin.api.form.method': '请求方式', 9 | 'admin.api.form.method.placeholder': '全部', 10 | 'admin.api.form.search': '搜索', 11 | 'admin.api.form.reset': '重置', 12 | 'admin.api.form.name.help': 'API 名称是必填项', 13 | 'admin.api.form.path.help': 'API 路径是必填项', 14 | 'admin.api.form.method.help': '请求方式是必填项', 15 | 'admin.api.form.remark.placeholder': '请输入备注', 16 | // button 17 | 'admin.api.button.create': '新增', 18 | 'admin.api.button.delete': '删除', 19 | // drawer 20 | 'admin.api.columns.new.drawer': '新增 API', 21 | 'admin.api.columns.edit.drawer': '编辑 API', 22 | 'admin.api.columns.delete.drawer': '删除 API', 23 | // columns 24 | 'admin.api.columns.edit': '编辑', 25 | 'admin.api.columns.name': 'API 名称', 26 | 'admin.api.columns.path': 'API 路径', 27 | 'admin.api.columns.method': '请求方式', 28 | 'admin.api.columns.remark': '备注', 29 | 'admin.api.columns.operate': '操作', 30 | // alert 31 | 'admin.api.alert': '此配置仅用于 Casbin 权限', 32 | }; 33 | -------------------------------------------------------------------------------- /src/views/admin/data-rule/local/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysDataRule': 'Data Rule Manage', 3 | // form 4 | 'admin.data-rule.form.name': 'Name', 5 | 'admin.data-rule.form.search': 'Search', 6 | 'admin.data-rule.form.reset': 'Reset', 7 | 'admin.data-rule.form.name.help': 'The rule name is required', 8 | 'admin.data-rule.form.model.help': 'The rule model is required', 9 | 'admin.data-rule.form.column.help': 'The rule model column is required', 10 | 'admin.data-rule.form.value.help': 'The rule value is required', 11 | // placeholder 12 | 'admin.data-rule.form.name.placeholder': 'Please enter the data rule name', 13 | 'admin.data-rule.form.model.placeholder': 'Please select a data rule model', 14 | 'admin.data-rule.form.column.placeholder': 15 | 'Please select a data rule model column', 16 | 'admin.data-rule.form.value.placeholder': 'Please enter the data rule value', 17 | // button 18 | 'admin.data-rule.button.create': 'New', 19 | 'admin.data-rule.button.delete': 'Delete', 20 | // columns 21 | 'admin.data-rule.columns.name': 'Name', 22 | 'admin.data-rule.columns.model': 'Model', 23 | 'admin.data-rule.columns.column': 'Column', 24 | 'admin.data-rule.columns.operator': 'Operator', 25 | 'admin.data-rule.columns.expression': 'Expression', 26 | 'admin.data-rule.columns.value': 'Value', 27 | 'admin.data-rule.columns.operate': 'Operate', 28 | 'admin.data-rule.columns.edit': 'Edit', 29 | // drawer 30 | 'admin.data-rule.columns.new.drawer': 'New data rule', 31 | 'admin.data-rule.columns.edit.drawer': 'Edit data rule', 32 | 'admin.data-rule.columns.delete.drawer': 'Delete data rule', 33 | }; 34 | -------------------------------------------------------------------------------- /src/views/admin/data-rule/local/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysDataRule': '数据规则管理', 3 | // form 4 | 'admin.data-rule.form.name': '名称', 5 | 'admin.data-rule.form.search': '搜索', 6 | 'admin.data-rule.form.reset': '重置', 7 | 'admin.data-rule.form.name.help': '规则名称是必填项', 8 | 'admin.data-rule.form.model.help': '规则模型是必填项', 9 | 'admin.data-rule.form.column.help': '规则模型列是必填项', 10 | 'admin.data-rule.form.value.help': '规则值是必填项', 11 | // placeholder 12 | 'admin.data-rule.form.name.placeholder': '请输入数据规则名称', 13 | 'admin.data-rule.form.model.placeholder': '请选择数据规则模型', 14 | 'admin.data-rule.form.column.placeholder': '请选择数据规则模型列', 15 | 'admin.data-rule.form.value.placeholder': '请输入数据规则值', 16 | // button 17 | 'admin.data-rule.button.create': '新增', 18 | 'admin.data-rule.button.delete': '删除', 19 | // columns 20 | 'admin.data-rule.columns.name': '名称', 21 | 'admin.data-rule.columns.model': '模型', 22 | 'admin.data-rule.columns.column': '模型列', 23 | 'admin.data-rule.columns.operator': '运算符', 24 | 'admin.data-rule.columns.expression': '表达式', 25 | 'admin.data-rule.columns.value': '规则值', 26 | 'admin.data-rule.columns.operate': '操作', 27 | 'admin.data-rule.columns.edit': '编辑', 28 | // drawer 29 | 'admin.data-rule.columns.new.drawer': '新增数据规则', 30 | 'admin.data-rule.columns.edit.drawer': '更新数据规则', 31 | 'admin.data-rule.columns.delete.drawer': '删除数据规则', 32 | }; 33 | -------------------------------------------------------------------------------- /src/views/admin/dept/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysDept': 'Dept Manage', 3 | // form 4 | 'admin.dept.form.name': 'Dept name', 5 | 'admin.dept.form.name.placeholder': 'Please enter the department name', 6 | 'admin.dept.form.leader': 'Leader', 7 | 'admin.dept.form.leader.placeholder': 'Please enter the leader', 8 | 'admin.dept.form.phone': 'Phone', 9 | 'admin.dept.form.phone.placeholder': 'Please enter the phone', 10 | 'admin.dept.form.status': 'Status', 11 | 'admin.dept.form.status.1': 'Enable', 12 | 'admin.dept.form.status.0': 'Disable', 13 | 'admin.dept.form.selectDefault': 'All', 14 | 'admin.dept.form.search': 'Search', 15 | 'admin.dept.form.reset': 'Reset', 16 | 'admin.dept.form.parent_name': 'Parent dept', 17 | 'admin.dept.form.parent_name.placeholder': 'Top', 18 | 'admin.dept.form.name.help': 'The department name is required', 19 | 'admin.dept.form.email.placeholder': 'Please enter the email', 20 | 'admin.dept.form.sort.placeholder': 'Please enter', 21 | // button 22 | 'admin.dept.button.create': 'Create', 23 | 'admin.dept.button.collapse': 'Expand/collapse', 24 | // drawer 25 | 'admin.dept.columns.new.drawer': 'New dept', 26 | 'admin.dept.columns.edit.drawer': 'Edit dept', 27 | 'admin.dept.columns.delete.drawer': 'Delete dept', 28 | // columns 29 | 'admin.dept.columns.name': 'Dept name', 30 | 'admin.dept.columns.parent_name': 'Parent dept', 31 | 'admin.dept.columns.sort': 'Sort', 32 | 'admin.dept.columns.leader': 'Leader', 33 | 'admin.dept.columns.phone': 'Phone', 34 | 'admin.dept.columns.email': 'Email', 35 | 'admin.dept.columns.status': 'Status', 36 | 'admin.dept.columns.created_time': 'Created time', 37 | }; 38 | -------------------------------------------------------------------------------- /src/views/admin/dept/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysDept': '部门管理', 3 | // form 4 | 'admin.dept.form.name': '部门名称', 5 | 'admin.dept.form.name.placeholder': '请输入部门名称', 6 | 'admin.dept.form.leader': '负责人', 7 | 'admin.dept.form.leader.placeholder': '请输入负责人', 8 | 'admin.dept.form.phone': '联系电话', 9 | 'admin.dept.form.phone.placeholder': '请输入联系电话', 10 | 'admin.dept.form.status': '状态', 11 | 'admin.dept.form.status.1': '正常', 12 | 'admin.dept.form.status.0': '停用', 13 | 'admin.dept.form.selectDefault': '全部', 14 | 'admin.dept.form.search': '搜索', 15 | 'admin.dept.form.reset': '重置', 16 | 'admin.dept.form.parent_name': '父级部门', 17 | 'admin.dept.form.parent_name.placeholder': '顶级', 18 | 'admin.dept.form.name.help': '部门名称是必填项', 19 | 'admin.dept.form.email.placeholder': '请输入邮箱', 20 | 'admin.dept.form.sort.placeholder': '请输入排序', 21 | // button 22 | 'admin.dept.button.create': '新增', 23 | 'admin.dept.button.collapse': '展开/收起', 24 | // drawer 25 | 'admin.dept.columns.new.drawer': '新增部门', 26 | 'admin.dept.columns.edit.drawer': '编辑部门', 27 | 'admin.dept.columns.delete.drawer': '删除部门', 28 | // columns 29 | 'admin.dept.columns.name': '部门名称', 30 | 'admin.dept.columns.parent_name': '父级部门', 31 | 'admin.dept.columns.sort': '排序', 32 | 'admin.dept.columns.leader': '负责人', 33 | 'admin.dept.columns.phone': '联系电话', 34 | 'admin.dept.columns.email': '邮箱', 35 | 'admin.dept.columns.status': '状态', 36 | 'admin.dept.columns.created_time': '创建时间', 37 | }; 38 | -------------------------------------------------------------------------------- /src/views/admin/menu/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysMenu': 'Menu Manage', 3 | // form 4 | 'admin.menu.form.name': 'Menu name', 5 | 'admin.menu.form.name.placeholder': 'Please enter a menu name', 6 | 'admin.menu.form.status': 'Status', 7 | 'admin.menu.form.status.1': 'Enable', 8 | 'admin.menu.form.status.0': 'Disable', 9 | 'admin.menu.form.selectDefault': 'All', 10 | 'admin.menu.form.search': 'Search', 11 | 'admin.menu.form.reset': 'Reset', 12 | 'admin.menu.form.title': 'Menu title', 13 | 'admin.menu.form.title.placeholder': 'Please enter a title', 14 | 'admin.menu.form.title.help': 'The menu title is required', 15 | 'admin.menu.form.path.name': 'Please enter a route name', 16 | 'admin.menu.form.name.help': 17 | 'The route name should be consistent with the `name` field in the route configuration.', 18 | 'admin.menu.form.path.placeholder': 'Please enter the routing path', 19 | 'admin.menu.form.component.placeholder': 'Please enter the component path', 20 | 'admin.menu.form.perms.placeholder': 'Please enter permission characters', 21 | 'admin.menu.form.remark.placeholder': 'Please enter a comment', 22 | 'admin.menu.form.parent_id.placeholder': 'Top', 23 | 'admin.menu.form.path.help': 24 | 'The route address to access, such as: `admin`, if it is empty, the route address is converted to lowercase letters plus hyphens by default, such as `SysMenu` -> `sys-menu`, if it is an external network address that needs to be accessed internally, then `http(s)://` is used at the beginning', 25 | 'admin.menu.form.component.help': 26 | 'The access component path, such as: `/log/login/index.vue`, defaults to the `views` directory', 27 | 'admin.menu.form.perms.help': 28 | 'Used as a server-side API authentication, such as `admin:list`, and the `,` (comma) interval is used when multiple permissions, please modify it carefully', 29 | // button 30 | 'admin.menu.button.create': 'New', 31 | 'admin.menu.button.collapse': 'Expand/collapse', 32 | // columns 33 | 'admin.menu.columns.title': 'Menu title', 34 | 'admin.menu.columns.name': 'Name', 35 | 'admin.menu.columns.parent_name': 'Parent menu', 36 | 'admin.menu.columns.type': 'Menu type', 37 | 'admin.menu.columns.type.0': 'Directory', 38 | 'admin.menu.columns.type.1': 'Menu', 39 | 'admin.menu.columns.type.2': 'Button', 40 | 'admin.menu.columns.icon': 'Icon', 41 | 'admin.menu.columns.path': 'Route path', 42 | 'admin.menu.columns.component': 'Component', 43 | 'admin.menu.columns.perms': 'Permissions', 44 | 'admin.menu.columns.sort': 'Sort', 45 | 'admin.menu.columns.sort.placeholder': 'Please enter', 46 | 'admin.menu.columns.display': 'Is display', 47 | 'admin.menu.columns.cache': 'Is cache', 48 | 'admin.menu.columns.status': 'Status', 49 | 'admin.menu.columns.remark': 'Remark', 50 | 'admin.menu.columns.created_time': 'Created time', 51 | 'admin.menu.columns.operate': 'Operate', 52 | 'admin.menu.columns.new': 'New', 53 | 'admin.menu.columns.new.drawer': 'New menu', 54 | 'admin.menu.columns.edit': 'Edit', 55 | 'admin.menu.columns.edit.drawer': 'Edit menu', 56 | 'admin.menu.columns.delete': 'Delete', 57 | 'admin.menu.columns.delete.drawer': 'Delete menu', 58 | 'admin.menu.columns.view': 'View', 59 | }; 60 | -------------------------------------------------------------------------------- /src/views/admin/menu/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysMenu': '菜单管理', 3 | // form 4 | 'admin.menu.form.name': '菜单名称', 5 | 'admin.menu.form.name.placeholder': '请输入菜单名称', 6 | 'admin.menu.form.status': '状态', 7 | 'admin.menu.form.status.1': '正常', 8 | 'admin.menu.form.status.0': '停用', 9 | 'admin.menu.form.selectDefault': '全部', 10 | 'admin.menu.form.search': '搜索', 11 | 'admin.menu.form.reset': '重置', 12 | 'admin.menu.form.title': '菜单标题', 13 | 'admin.menu.form.title.placeholder': '请输入标题', 14 | 'admin.menu.form.title.help': '菜单标题是必填项', 15 | 'admin.menu.form.path.name': '请输入路由名称', 16 | 'admin.menu.form.name.help': '路由名称要与路由配置里的 `name` 字段保持一致', 17 | 'admin.menu.form.path.placeholder': '请输入路由路径', 18 | 'admin.menu.form.component.placeholder': '请输入组件路径', 19 | 'admin.menu.form.perms.placeholder': '请输入权限字符', 20 | 'admin.menu.form.remark.placeholder': '请输入备注', 21 | 'admin.menu.form.parent_id.placeholder': '顶级', 22 | 'admin.menu.form.path.help': 23 | '访问的路由地址,如:`admin`,如果为空,则默认使用路由名称转化的小写字母加中划线为路由地址,如 `SysMenu` -> `sys-menu`,如果是外网地址需内链访问,则以`http(s)://`开头', 24 | 'admin.menu.form.component.help': 25 | '访问的组件路径,如:`/log/login/index.vue`,默认在`views`目录下', 26 | 'admin.menu.form.perms.help': 27 | '作为 server 端 API 验权使用,如 `admin:list`,多个权限时使用 `,`(英文逗号) 间隔,请谨慎修改', 28 | // button 29 | 'admin.menu.button.create': '新增', 30 | 'admin.menu.button.collapse': '展开/收起', 31 | // columns 32 | 'admin.menu.columns.title': '菜单标题', 33 | 'admin.menu.columns.name': '路由名称', 34 | 'admin.menu.columns.parent_name': '父级菜单', 35 | 'admin.menu.columns.type': '菜单类型', 36 | 'admin.menu.columns.type.0': '目录', 37 | 'admin.menu.columns.type.1': '菜单', 38 | 'admin.menu.columns.type.2': '按钮', 39 | 'admin.menu.columns.icon': '图标', 40 | 'admin.menu.columns.path': '路由路径', 41 | 'admin.menu.columns.component': '组件路径', 42 | 'admin.menu.columns.perms': '权限标识', 43 | 'admin.menu.columns.sort': '排序', 44 | 'admin.menu.columns.sort.placeholder': '请输入', 45 | 'admin.menu.columns.display': '是否显示', 46 | 'admin.menu.columns.cache': '是否缓存', 47 | 'admin.menu.columns.status': '状态', 48 | 'admin.menu.columns.remark': '备注', 49 | 'admin.menu.columns.created_time': '创建时间', 50 | 'admin.menu.columns.operate': '操作', 51 | 'admin.menu.columns.new': '新增', 52 | 'admin.menu.columns.new.drawer': '新增菜单', 53 | 'admin.menu.columns.edit': '编辑', 54 | 'admin.menu.columns.edit.drawer': '编辑菜单', 55 | 'admin.menu.columns.delete': '删除', 56 | 'admin.menu.columns.delete.drawer': '删除菜单', 57 | 'admin.menu.columns.view': '查看', 58 | }; 59 | -------------------------------------------------------------------------------- /src/views/admin/role/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysRole': 'Role Manage', 3 | // form 4 | 'admin.role.form.name': 'Menu name', 5 | 'admin.role.form.name.placeholder': 'Please enter a menu name', 6 | 'admin.role.form.name.help': 'Menu name is required', 7 | 'admin.role.form.status': 'Status', 8 | 'admin.role.form.status.1': 'Enable', 9 | 'admin.role.form.status.0': 'Disable', 10 | 'admin.role.form.selectDefault': 'All', 11 | 'admin.role.form.search': 'Search', 12 | 'admin.role.form.reset': 'Reset', 13 | // button 14 | 'admin.role.button.create': 'Create', 15 | 'admin.role.button.delete': 'Delete', 16 | // columns 17 | 'admin.role.columns.new.drawer': 'New role', 18 | 'admin.role.columns.delete.drawer': 'Delete role', 19 | 'admin.role.columns.edit.drawer': 'Edit role', 20 | 'admin.role.columns.name': 'Role name', 21 | 'admin.role.columns.status': 'Status', 22 | 'admin.role.columns.remark': 'Remark', 23 | 'admin.role.columns.operate': 'Edit', 24 | 'admin.role.columns.perms': 'Permission', 25 | 'admin.role.columns.edit': 'Edit', 26 | 'admin.role.columns.delete': 'Delete', 27 | 'admin.role.columns.menus': 'Menus', 28 | 'admin.role.columns.created_time': 'Created time', 29 | // modal 30 | 'admin.role.modal.delete': 31 | 'Are you sure you want to delete it? Role deletion does not set forced detection. After the role is deleted, the user’s corresponding role permissions will be cleared, which may cause irreparable consequences. Please operate with caution!', 32 | // drawer 33 | 'admin.role.drawer.menu': 'Role menu', 34 | 'admin.role.drawer.api': 'Casbin', 35 | 'admin.role.drawer.dataRule': 'Data Rule', 36 | 'admin.role.drawer.menu.button.select': 'Select all/Cancel all', 37 | 'admin.role.drawer.menu.button.collapse': 'Expand/Collapse', 38 | // alert 39 | 'admin.role.alert.data_scope': 40 | 'When you set the data permissions to all, you will ignore the menu authorization or API authorization and directly have all the permissions, so please be careful!', 41 | }; 42 | -------------------------------------------------------------------------------- /src/views/admin/role/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysRole': '角色管理', 3 | // form 4 | 'admin.role.form.name': '菜单名称', 5 | 'admin.role.form.name.placeholder': '请输入菜单名称', 6 | 'admin.role.form.name.help': '菜单名称是必填项', 7 | 'admin.role.form.status': '状态', 8 | 'admin.role.form.status.1': '正常', 9 | 'admin.role.form.status.0': '停用', 10 | 'admin.role.form.selectDefault': '全部', 11 | 'admin.role.form.search': '搜索', 12 | 'admin.role.form.reset': '重置', 13 | // button 14 | 'admin.role.button.create': '新建', 15 | 'admin.role.button.delete': '删除', 16 | // columns 17 | 'admin.role.columns.new.drawer': '新建角色', 18 | 'admin.role.columns.delete.drawer': '删除角色', 19 | 'admin.role.columns.edit.drawer': '编辑角色', 20 | 'admin.role.columns.name': '角色名称', 21 | 'admin.role.columns.status': '状态', 22 | 'admin.role.columns.remark': '备注', 23 | 'admin.role.columns.operate': '编辑', 24 | 'admin.role.columns.perms': '权限设置', 25 | 'admin.role.columns.edit': '编辑', 26 | 'admin.role.columns.delete': '删除', 27 | 'admin.role.columns.menus': '菜单', 28 | 'admin.role.columns.created_time': '创建时间', 29 | // modal 30 | 'admin.role.modal.delete': 31 | '确定要删除吗?角色删除没有设置强制检测,删除角色后,用户对应的角色权限将会被清空,可能造成无法挽回的后果,请慎重操作!', 32 | // drawer 33 | 'admin.role.drawer.menu': '角色菜单', 34 | 'admin.role.drawer.api': 'Casbin', 35 | 'admin.role.drawer.dataRule': '数据规则', 36 | 'admin.role.drawer.menu.button.select': '全选/取消全选', 37 | 'admin.role.drawer.menu.button.collapse': '展开/收起', 38 | // alert 39 | 'admin.role.alert.data_scope': 40 | '设置数据权限为全部时,将忽略菜单授权或API授权,直接拥有所有权限,请谨慎操作!', 41 | }; 42 | -------------------------------------------------------------------------------- /src/views/admin/user/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysUser': 'User Manage', 3 | // form 4 | 'admin.user.form.dept': 'Department', 5 | 'admin.user.form.dept.required': 'Department is required', 6 | 'admin.user.form.dept.placeholder': 'Please input department', 7 | 'admin.user.form.username': 'Username', 8 | 'admin.user.form.username.required': 'Username is required', 9 | 'admin.user.form.username.placeholder': 'Please enter username', 10 | 'admin.user.form.phone': 'Phone', 11 | 'admin.user.form.phone.placeholder': 'Please enter phone', 12 | 'admin.user.form.status': 'Status', 13 | 'admin.user.form.status.1': 'Enabled', 14 | 'admin.user.form.status.0': 'Disabled', 15 | 'admin.user.form.selectDefault': 'All', 16 | 'admin.user.form.search': 'Search', 17 | 'admin.user.form.reset': 'Reset', 18 | 'admin.user.form.avatar': 'Avatar', 19 | 'admin.user.form.avatar.required': 'Avatar is required', 20 | 'admin.user.form.avatar.help': 21 | 'Maximum supported link length: 2083, details:https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers', 22 | 'admin.user.form.avatar.placeholder': 23 | 'Please enter the avatar HTTP(s) address', 24 | 'admin.user.form.nickname': 'Nickname', 25 | 'admin.user.form.nickname.required': 'Nickname is required', 26 | 'admin.user.form.nickname.placeholder': 'Please enter nickname', 27 | 'admin.user.form.password': 'Password', 28 | 'admin.user.form.password.required': 'Password is required', 29 | 'admin.user.form.password.placeholder': 'Please enter password', 30 | 'admin.user.form.email': 'Email', 31 | 'admin.user.form.email.required': 'Email is required', 32 | 'admin.user.form.email.placeholder': 'Please enter email', 33 | 'admin.user.form.role': 'Role', 34 | 'admin.user.form.role.required': 'Role is required', 35 | 'admin.user.form.role.placeholder': 'Please select role', 36 | // columns 37 | 'admin.user.columns.updateUserRoles.placeholder': 38 | "Sure you want to modify the user's role?", 39 | 'admin.user.columns.switch.true': 'Enable', 40 | 'admin.user.columns.switch.1': 'Enable', 41 | 'admin.user.columns.switch.false': 'Disable', 42 | 'admin.user.columns.switch.0': 'Disable', 43 | 'admin.user,columns.edit': 'Edit', 44 | 'admin.user.columns.edit.userinfo': 'Update userinfo', 45 | 'admin.user.columns.edit.avatar': 'Update avatar', 46 | 'admin.user.columns.edit.role': 'Update role', 47 | 'admin.user.columns.avatar': 'Avatar', 48 | 'admin.user.columns.username': 'Username', 49 | 'admin.user.columns.nickname': 'Nickname', 50 | 'admin.user.columns.dept': 'Department', 51 | 'admin.user.columns.roles': 'Roles', 52 | 'admin.user.columns.email': 'Email', 53 | 'admin.user.columns.phone': 'Phone', 54 | 'admin.user.columns.join_time': 'Join time', 55 | 'admin.user.columns.last_login_time': 'Last login time', 56 | 'admin.user.columns.status': 'Status', 57 | 'admin.user.columns.is_superuser': 'Superuser', 58 | 'admin.user.columns.is_staff': 'Backend login', 59 | 'admin.user.columns.is_multi_login': 'Multi login', 60 | 'admin.user.columns.operate': 'Operate', 61 | 'admin.user.columns.edit': 'Edit', 62 | 'admin.user.columns.delete': 'Delete', 63 | 'admin.user.columns.add': 'Add User', 64 | // button 65 | 'admin.user.button.add': 'Add User', 66 | }; 67 | -------------------------------------------------------------------------------- /src/views/admin/user/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.admin.sysUser': '用户管理', 3 | // form 4 | 'admin.user.form.dept': '部门', 5 | 'admin.user.form.dept.required': '部门是必选项', 6 | 'admin.user.form.dept.placeholder': '请输入部门', 7 | 'admin.user.form.username': '用户名', 8 | 'admin.user.form.username.required': '用户名是必填项', 9 | 'admin.user.form.username.placeholder': '请输入用户名', 10 | 'admin.user.form.phone': '手机号', 11 | 'admin.user.form.phone.placeholder': '请输入手机号', 12 | 'admin.user.form.status': '状态', 13 | 'admin.user.form.status.1': '正常', 14 | 'admin.user.form.status.0': '停用', 15 | 'admin.user.form.selectDefault': '全部', 16 | 'admin.user.form.search': '搜索', 17 | 'admin.user.form.reset': '重置', 18 | 'admin.user.form.avatar': '头像', 19 | 'admin.user.form.avatar.required': '头像是必填项', 20 | 'admin.user.form.avatar.help': 21 | '支持最大链接长度:2083,详情:https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers', 22 | 'admin.user.form.avatar.placeholder': '请输入头像 HTTP(s) 地址', 23 | 'admin.user.form.nickname': '昵称', 24 | 'admin.user.form.nickname.required': '昵称是必填项', 25 | 'admin.user.form.nickname.placeholder': '请输入昵称', 26 | 'admin.user.form.password': '密码', 27 | 'admin.user.form.password.required': '密码是必填项', 28 | 'admin.user.form.password.placeholder': '请输入密码', 29 | 'admin.user.form.email': '邮箱', 30 | 'admin.user.form.email.required': '邮箱是必填项', 31 | 'admin.user.form.email.placeholder': '请输入邮箱', 32 | 'admin.user.form.role': '角色', 33 | 'admin.user.form.role.required': '角色是必选项', 34 | 'admin.user.form.role.placeholder': '请选择角色', 35 | // columns 36 | 'admin.user.columns.updateUserRoles.placeholder': '确定要修改用户的角色吗?', 37 | 'admin.user.columns.switch.true': '开启', 38 | 'admin.user.columns.switch.1': '开启', 39 | 'admin.user.columns.switch.false': '关闭', 40 | 'admin.user.columns.switch.0': '关闭', 41 | 'admin.user,columns.edit': '编辑', 42 | 'admin.user.columns.edit.userinfo': '更新用户信息', 43 | 'admin.user.columns.edit.avatar': '更新头像', 44 | 'admin.user.columns.edit.role': '更新角色', 45 | 'admin.user.columns.avatar': '头像', 46 | 'admin.user.columns.username': '用户名', 47 | 'admin.user.columns.nickname': '昵称', 48 | 'admin.user.columns.dept': '部门', 49 | 'admin.user.columns.roles': '角色', 50 | 'admin.user.columns.email': '邮箱', 51 | 'admin.user.columns.phone': '手机号', 52 | 'admin.user.columns.join_time': '注册时间', 53 | 'admin.user.columns.last_login_time': '最后登录时间', 54 | 'admin.user.columns.status': '状态', 55 | 'admin.user.columns.is_superuser': '超级管理员', 56 | 'admin.user.columns.is_staff': '后台登陆', 57 | 'admin.user.columns.is_multi_login': '多点登录', 58 | 'admin.user.columns.operate': '操作', 59 | 'admin.user.columns.edit': '编辑', 60 | 'admin.user.columns.delete': '删除', 61 | 'admin.user.columns.add': '添加用户', 62 | // button 63 | 'admin.user.button.add': '添加用户', 64 | }; 65 | -------------------------------------------------------------------------------- /src/views/automation/code-generator/local/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.automation.codeGenerator': '代码生成器', 3 | // card 4 | 'menu.automation.card.dataImport': '数据导入', 5 | 'menu.automation.card.codeGenerate': '代码生成', 6 | // tooltip 7 | 'automation.code-gen.tooltip.import': 8 | '导入:从指定数据库导入指定数据库表,将自动创建用于代码生成的业务和模型', 9 | 'automation.code-gen.tooltip.business': 10 | '业务:代码生成的相关配置(部分功能需要选择业务后才能执行相关操作)', 11 | 'automation.code-gen.tooltip.model': 12 | '模型:代码生成的相关模型列(默认存在 id 主键自增列,默认时间列请查看业务配置)', 13 | 'automation.code-gen.tooltip.gen': '此功能将进行磁盘写入,亲谨慎操作', 14 | 'automation.code-gen.button.tooltip.business': 15 | '此功能或将弃用,建议优先使用导入功能', 16 | 'automation.code-gen.tooltip.code.copy': '复制代码', 17 | // button 18 | 'automation.code-gen.button.import': '导入', 19 | 'automation.code-gen.button.business': '创建业务表', 20 | 'automation.code-gen.button.model': '创建模型列', 21 | 'automation.code-gen.button.preview': '预览', 22 | 'automation.code-gen.button.write': '生成', 23 | 'automation.code-gen.button.download': '下载', 24 | // columns 25 | 'automation.code-gen.columns.app_name': '应用名称', 26 | 'automation.code-gen.columns.table_name_en': '表名称(英)', 27 | 'automation.code-gen.columns.table_name_zh': '表名称(中)', 28 | 'automation.code-gen.columns.table_simple_name_zh': '表名称(中简)', 29 | 'automation.code-gen.columns.table_comment': '表描述', 30 | 'automation.code-gen.columns.schema_name': 'Schema 名称', 31 | 'automation.code-gen.columns.default_datetime_column': '默认时间列', 32 | 'automation.code-gen.columns.api_version': 'API 版本', 33 | 'automation.code-gen.columns.gen_path': '生成路径', 34 | 'automation.code-gen.columns.remark': '备注', 35 | 'automation.code-gen.columns.operate': '操作', 36 | 'automation.code-gen.columns.name': '名称', 37 | 'automation.code-gen.columns.comment': '描述', 38 | 'automation.code-gen.columns.type': 'SQLA 类型', 39 | 'automation.code-gen.columns.pd_type': 'Pydantic 类型', 40 | 'automation.code-gen.columns.default': '默认值', 41 | 'automation.code-gen.columns.sort': '排序', 42 | 'automation.code-gen.columns.length': '长度', 43 | 'automation.code-gen.columns.is_pk': '主键', 44 | 'automation.code-gen.columns.is_nullable': '允许空值', 45 | // form 46 | 'automation.code-gen.form.db_name': '数据库名', 47 | 'automation.code-gen.form.db_name.placeholder': '请输入数据库名称', 48 | 'automation.code-gen.form.db_name.help': 49 | '数据库名是必填项,且仅支持大小写字母和下划线', 50 | 'automation.code-gen.form.db_name.tooltip': 51 | '仅支持当前系统已连接数据库内已存在的数据库', 52 | 'automation.code-gen.form.app': '应用名', 53 | 'automation.code-gen.form.app.tooltip': '用于代码生成到指定的后端 app', 54 | 'automation.code-gen.form.app.placeholder': '请输入应用名称', 55 | 'automation.code-gen.form.app.help': 56 | '应用名称是必填项,且仅支持大小写字母和下划线', 57 | 'automation.code-gen.form.table_name': '数据库表名', 58 | 'automation.code-gen.form.table_name.tooltip': '已输入数据库下的数据库表', 59 | 'automation.code-gen.form.table_name.placeholder': '请选择数据库表名', 60 | 'automation.code-gen.form.table_name.help': 61 | '数据库表名是必填项,且仅支持大小写字母和下划线', 62 | 'automation.code-gen.form.name': '名称', 63 | 'automation.code-gen.form.name.placeholder': '请输入名称', 64 | 'automation.code-gen.form.name.help': '名称是必填项', 65 | 'automation.code-gen.form.comment': '描述', 66 | 'automation.code-gen.form.comment.placeholder': '请输入描述', 67 | 'automation.code-gen.form.type': 'SQLA 类型', 68 | 'automation.code-gen.form.type.placeholder': '请选择 SQLA 类型(首字母排序)', 69 | 'automation.code-gen.form.type.help': 'SQLA 类型是必填项', 70 | 'automation.code-gen.form.default': '默认值', 71 | 'automation.code-gen.form.default.placeholder': '请输入默认值', 72 | 'automation.code-gen.form.sort': '排序', 73 | 'automation.code-gen.form.sort.placeholder': '请输入排序', 74 | 'automation.code-gen.form.sort.help': '排序是必填项', 75 | 'automation.code-gen.form.length': '长度', 76 | 'automation.code-gen.form.length.placeholder': '请输入长度', 77 | 'automation.code-gen.form.length.help': '长度是必填项', 78 | 'automation.code-gen.form.is_pk': '是否主键', 79 | 'automation.code-gen.form.is_pk.tooltip': '默认主键为 id,不建议开启', 80 | 'automation.code-gen.form.is_nullable': '是否允许为空', 81 | 'automation.code-gen.form.gen_business_id': '业务绑定', 82 | 'automation.code-gen.form.gen_business_id.placeholder': '请选择业务', 83 | 'automation.code-gen.form.gen_business_id.help': '业务是必填项', 84 | 'automation.code-gen.form.app_name': '应用名称', 85 | 'automation.code-gen.form.app_name.tooltip': '代码将生成到此 app 目录下', 86 | 'automation.code-gen.form.app_name.placeholder': '请输入应用名称', 87 | 'automation.code-gen.form.app_name.help': '应用名称是必填项', 88 | 'automation.code-gen.form.table_name_en': '英文表名称', 89 | 'automation.code-gen.form.table_name_en.placeholder': '请输入英文表名称', 90 | 'automation.code-gen.form.table_name_en.help': '英文表名称是必填项', 91 | 'automation.code-gen.form.table_name_zh': '中文表名称', 92 | 'automation.code-gen.form.table_name_zh.placeholder': '请输入中文表名称', 93 | 'automation.code-gen.form.table_name_zh.help': 94 | '中文表名称是必填项,且至少包含一个中文', 95 | 'automation.code-gen.form.table_simple_name_zh': '中文简称', 96 | 'automation.code-gen.form.table_simple_name_zh.placeholder': 97 | '请输入中文简短表名称', 98 | 'automation.code-gen.form.table_simple_name_zh.help': 99 | '中文简短名称是必填项,且至少包含一个中文', 100 | 'automation.code-gen.form.table_comment': '表描述', 101 | 'automation.code-gen.form.table_comment.placeholder': '请输入表描述', 102 | 'automation.code-gen.form.schema_name': 'Schema 名称', 103 | 'automation.code-gen.form.schema_name.tooltip': '默认为英文表名称', 104 | 'automation.code-gen.form.schema_name.placeholder': '请输入 Schema 名称', 105 | 'automation.code-gen.form.default_datetime_column': '默认时间列', 106 | 'automation.code-gen.form.default_datetime_column.tooltip': 107 | '业务模型是否包含默认时间列 created_time 和 updated_time', 108 | 'automation.code-gen.form.api_version': 'API 版本', 109 | 'automation.code-gen.form.api_version.tooltip': '建议输入默认 API 版本:v1', 110 | 'automation.code-gen.form.api_version.placeholder': '请输入 API 版本', 111 | 'automation.code-gen.form.api_version.help': 'API 版本是必填项', 112 | 'automation.code-gen.form.gen_path': '生成路径', 113 | 'automation.code-gen.form.gen_path.tooltip': 114 | '默认代码生成到当前项目 app 目录下,自定义路径时,生成目录规则为:自定义路径 + app/...;例如,' + 115 | '如果自定义路径为 "root/fba/backend",那么最终代码将生成到 "root/fba/backend/app/..." 目录下', 116 | 'automation.code-gen.form.gen_path.placeholder': '请输入生成路径', 117 | 'automation.code-gen.form.remark': '备注', 118 | 'automation.code-gen.form.remark.placeholder': '请输入备注', 119 | // modal 120 | 'automation.code-gen.modal.import': '导入', 121 | 'automation.code-gen.modal.business': '创建业务', 122 | 'automation.code-gen.modal.business.edit': '编辑业务', 123 | 'automation.code-gen.modal.business.delete': 124 | '删除业务会同步删除所有关联模型列,确定删除吗?', 125 | 'automation.code-gen.modal.model': '创建模型列', 126 | 'automation.code-gen.modal.model.edit': '创建模型列', 127 | 'automation.code-gen.modal.generate': '代码生成', 128 | 'automation.code-gen.modal.generate.warning': 129 | '代码生成将进行磁盘IO写入,如果生成的代码文件与当前系统内代码文件重叠,文件将被覆盖写入,请谨慎操作!', 130 | 'automation.code-gen.modal.generate.okText': '不怂!就是干!', 131 | 'automation.code-gen.modal.generate.list.header': '代码写入路径', 132 | 'automation.code-gen.modal.generate.submit': '代码生成成功', 133 | // table 134 | 'automation.code-gen.table.model.empty': '此业务暂无模型数据', 135 | }; 136 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/components/banner.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 36 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/components/data-panel.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 109 | 110 | 140 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 26 | 27 | 102 | 103 | 118 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.dashboard.workplace': 'Workplace', 3 | 'workplace.welcome': 'Welcome!', 4 | 'workplace.onlineContent': 'Online Content', 5 | 'workplace.putIn': 'Put In', 6 | 'workplace.newDay': 'Daily Additional Comments', 7 | 'workplace.newFromYesterday': 'New From Yesterday', 8 | 'workplace.pecs': 'pecs', 9 | }; 10 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.dashboard.workplace': '工作台', 3 | 'workplace.welcome': '欢迎回来!', 4 | 'workplace.onlineContent': '线上总内容', 5 | 'workplace.putIn': '投放中内容', 6 | 'workplace.newDay': '日新增评论', 7 | 'workplace.newFromYesterday': '较昨日新增', 8 | 'workplace.pecs': '个', 9 | }; 10 | -------------------------------------------------------------------------------- /src/views/log/login/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.log.login': 'Login log', 3 | // form 4 | 'log.login.form.username': 'Username', 5 | 'log.login.form.username.placeholder': 'Please enter username', 6 | 'log.login.form.ip': 'IP', 7 | 'log.login.form.ip.placeholder': 'Please enter IP', 8 | 'log.login.form.status': 'Status', 9 | 'log.login.form.status.1': 'Success', 10 | 'log.login.form.status.0': 'Failure', 11 | 'log.login.form.selectDefault': 'All', 12 | 'log.login.form.search': 'Search', 13 | 'log.login.form.reset': 'Reset', 14 | // columns 15 | 'log.login.columns.index': 'ID', 16 | 'log.login.columns.username': 'Username', 17 | 'log.login.columns.ip': 'IP', 18 | 'log.login.columns.browser': 'Browser', 19 | 'log.login.columns.device': 'Device', 20 | 'log.login.columns.city': 'City', 21 | 'log.login.columns.status': 'Status', 22 | 'log.login.columns.msg': 'Message', 23 | 'log.login.columns.login_time': 'Login time', 24 | }; 25 | -------------------------------------------------------------------------------- /src/views/log/login/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.log.login': '登录日志', 3 | // form 4 | 'log.login.form.username': '用户名', 5 | 'log.login.form.username.placeholder': '请输入用户名', 6 | 'log.login.form.ip': 'IP', 7 | 'log.login.form.ip.placeholder': '请输入IP', 8 | 'log.login.form.status': '状态', 9 | 'log.login.form.status.1': '成功', 10 | 'log.login.form.status.0': '失败', 11 | 'log.login.form.selectDefault': '全部', 12 | 'log.login.form.search': '搜索', 13 | 'log.login.form.reset': '重置', 14 | // columns 15 | 'log.login.columns.index': 'ID', 16 | 'log.login.columns.username': '用户名', 17 | 'log.login.columns.ip': 'IP', 18 | 'log.login.columns.browser': '浏览器', 19 | 'log.login.columns.device': '设备', 20 | 'log.login.columns.city': '城市', 21 | 'log.login.columns.status': '状态', 22 | 'log.login.columns.msg': '消息', 23 | 'log.login.columns.login_time': '登录时间', 24 | }; 25 | -------------------------------------------------------------------------------- /src/views/log/opera/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.log.opera': 'Opera log', 3 | // form 4 | 'log.opera.form.username': 'Username', 5 | 'log.opera.form.username.placeholder': 'Please enter a username', 6 | 'log.opera.form.ip': 'IP', 7 | 'log.opera.form.ip.placeholder': 'Please enter the IP', 8 | 'log.opera.form.status': 'Status', 9 | 'log.opera.form.status.1': 'Success', 10 | 'log.opera.form.status.0': 'Fail', 11 | 'log.opera.form.selectDefault': 'All', 12 | 'log.opera.form.search': 'Search', 13 | 'log.opera.form.reset': 'Reset', 14 | // columns 15 | 'log.opera.columns.trace_id': 'Trace ID', 16 | 'log.opera.columns.username': 'Username', 17 | 'log.opera.columns.method': 'Method', 18 | 'log.opera.columns.title': 'Title', 19 | 'log.opera.columns.path': 'Path', 20 | 'log.opera.columns.code': 'Code', 21 | 'log.opera.columns.ip': 'IP', 22 | 'log.opera.columns.city': 'City', 23 | 'log.opera.columns.browser': 'Browser', 24 | 'log.opera.columns.device': 'Device', 25 | 'log.opera.columns.status': 'Status', 26 | 'log.opera.columns.msg': 'Message', 27 | 'log.opera.columns.args': 'Args', 28 | 'log.opera.columns.cost_time': 'Cost time', 29 | 'log.opera.columns.opera_time': 'Opera time', 30 | }; 31 | -------------------------------------------------------------------------------- /src/views/log/opera/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.log.opera': '操作日志', 3 | // form 4 | 'log.opera.form.username': '用户名', 5 | 'log.opera.form.username.placeholder': '请输入用户名', 6 | 'log.opera.form.ip': 'IP', 7 | 'log.opera.form.ip.placeholder': '请输入IP', 8 | 'log.opera.form.status': '状态', 9 | 'log.opera.form.status.1': '成功', 10 | 'log.opera.form.status.0': '失败', 11 | 'log.opera.form.selectDefault': '全部', 12 | 'log.opera.form.search': '搜索', 13 | 'log.opera.form.reset': '重置', 14 | // columns 15 | 'log.opera.columns.trace_id': '跟踪 ID', 16 | 'log.opera.columns.username': '用户名', 17 | 'log.opera.columns.method': '请求方式', 18 | 'log.opera.columns.title': '操作名称', 19 | 'log.opera.columns.path': '请求路径', 20 | 'log.opera.columns.code': '状态码', 21 | 'log.opera.columns.ip': 'IP', 22 | 'log.opera.columns.city': '城市', 23 | 'log.opera.columns.browser': '浏览器', 24 | 'log.opera.columns.device': '设备', 25 | 'log.opera.columns.status': '状态', 26 | 'log.opera.columns.msg': '消息', 27 | 'log.opera.columns.args': '参数', 28 | 'log.opera.columns.cost_time': '耗时(ms)', 29 | 'log.opera.columns.opera_time': '操作时间', 30 | }; 31 | -------------------------------------------------------------------------------- /src/views/login/components/banner.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 43 | 44 | 87 | -------------------------------------------------------------------------------- /src/views/login/components/oauth_callback.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 63 | 64 | 107 | 108 | 118 | -------------------------------------------------------------------------------- /src/views/login/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'login.form.title': 'Login FBA', 3 | 'login.form.sub_title': 'fastapi_best_architecture', 4 | 'login.form.username.errMsg': 'Username cannot be empty', 5 | 'login.form.password.errMsg': 'Password cannot be empty', 6 | 'login.form.captcha.errMsg': 'Captcha cannot be empty', 7 | 'login.form.login.errMsg': 'Login error, retry with light refresh', 8 | 'login.form.login.staff.errMsg': 'This user is disabled admin login', 9 | 'login.form.login.success': 'Welcome', 10 | 'login.form.userName.placeholder': 'username:test/admin', 11 | 'login.form.password.placeholder': 'password:123456', 12 | 'login.form.captcha.placeholder': 'Please enter a verification code', 13 | 'login.form.rememberPassword': 'remember password', 14 | 'login.form.forgetPassword': 'forgot password', 15 | 'login.form.login': 'login', 16 | 'login.form.oauth_login': 'Other logins', 17 | 'login.form.register': 'Register an account', 18 | 'login.banner.slogan1': 'high-quality template out of the box', 19 | 'login.banner.subSlogan1': 20 | 'Rich page templates, covering most typical business scenarios', 21 | 'login.banner.slogan2': 'Built-in solutions to common problems', 22 | 'login.banner.subSlogan2': 23 | 'Internationalization, route configuration, state management', 24 | 'login.banner.slogan3': 'Access to AUX', 25 | 'login.banner.subSlogan3': 'Enables flexible block-based development', 26 | }; 27 | -------------------------------------------------------------------------------- /src/views/login/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'login.form.title': '登录 FBA', 3 | 'login.form.sub_title': 'fastapi_best_architecture', 4 | 'login.form.username.errMsg': '用户名不能为空', 5 | 'login.form.password.errMsg': '密码不能为空', 6 | 'login.form.captcha.errMsg': '验证码不能为空', 7 | 'login.form.login.errMsg': '登录出错,轻刷新重试', 8 | 'login.form.login.staff.errMsg': '此用户禁止后台管理登录', 9 | 'login.form.login.success': '欢迎使用', 10 | 'login.form.userName.placeholder': '用户名:test/admin', 11 | 'login.form.password.placeholder': '密码:123456', 12 | 'login.form.captcha.placeholder': '请输入验证码', 13 | 'login.form.rememberPassword': '记住密码', 14 | 'login.form.forgetPassword': '忘记密码', 15 | 'login.form.login': '登录', 16 | 'login.form.oauth_login': '其他登录方式', 17 | 'login.form.register': '注册账号', 18 | 'login.banner.slogan1': '开箱即用的高质量模板', 19 | 'login.banner.subSlogan1': '丰富的的页面模板,覆盖大多数典型业务场景', 20 | 'login.banner.slogan2': '内置了常见问题的解决方案', 21 | 'login.banner.subSlogan2': '国际化,路由配置,状态管理应有尽有', 22 | 'login.banner.slogan3': '接入可视化增强工具AUX', 23 | 'login.banner.subSlogan3': '实现灵活的区块式开发', 24 | }; 25 | -------------------------------------------------------------------------------- /src/views/monitor/redis/components/active-series.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/views/monitor/redis/components/commands-series.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/views/monitor/redis/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 153 | 154 | 170 | -------------------------------------------------------------------------------- /src/views/monitor/redis/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.monitor.redis': 'Redis Monitor', 3 | // showData 4 | 'monitor.redis.showData.title': 'Basic Info', 5 | 'monitor.redis.showData.version': 'Version', 6 | 'monitor.redis.showData.mode': 'Mode', 7 | 'monitor.redis.showData.os': 'OS', 8 | 'monitor.redis.showData.arch': 'Arch', 9 | 'monitor.redis.showData.uptime': 'Uptime', 10 | 'monitor.redis.showData.clients': 'Connections', 11 | 'monitor.redis.showData.memory_human': 'Allocated Memory', 12 | 'monitor.redis.showData.connections_received': 'Connections Received', 13 | 'monitor.redis.showData.commands_processed': 'Commands Processed', 14 | 'monitor.redis.showData.rejected_connections': 'Rejected Connections', 15 | 'monitor.redis.showData.keys_command_stats': 'Keys Stats', 16 | 'monitor.redis.showData.role': 'Role', 17 | 'monitor.redis.showData.used_cpu': 'Used CPU', 18 | 'monitor.redis.showData.used_cpu_children': 'Used CPU Children', 19 | 'monitor.redis.showData.keys_num': 'Keys Num', 20 | // stats 21 | 'monitor.redis.stats.title.commands': 'Commands', 22 | 'monitor.redis.stats.title.used_memory': 'Used Memory', 23 | }; 24 | -------------------------------------------------------------------------------- /src/views/monitor/redis/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.monitor.redis': 'Redis 监控', 3 | // showData 4 | 'monitor.redis.showData.title': '基础信息', 5 | 'monitor.redis.showData.version': '版本', 6 | 'monitor.redis.showData.mode': '模式', 7 | 'monitor.redis.showData.os': '操作系统', 8 | 'monitor.redis.showData.arch': '架构', 9 | 'monitor.redis.showData.uptime': '运行时间', 10 | 'monitor.redis.showData.clients': '连接数', 11 | 'monitor.redis.showData.memory_human': '已分配内存', 12 | 'monitor.redis.showData.connections_received': '可接受连接数', 13 | 'monitor.redis.showData.commands_processed': '已执行命令', 14 | 'monitor.redis.showData.rejected_connections': '已拒绝连接', 15 | 'monitor.redis.showData.keys_command_stats': '查询次数', 16 | 'monitor.redis.showData.role': '角色', 17 | 'monitor.redis.showData.used_cpu': 'CPU 消耗', 18 | 'monitor.redis.showData.used_cpu_children': '后台 CPU 占用', 19 | 'monitor.redis.showData.keys_num': 'Keys 数量', 20 | // stats 21 | 'monitor.redis.stats.title.commands': '命令统计', 22 | 'monitor.redis.stats.title.used_memory': '已用内存', 23 | }; 24 | -------------------------------------------------------------------------------- /src/views/monitor/server/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.monitor.server': 'Server monitor', 3 | 'menu.monitor.server.memory': 'Memory', 4 | 'menu.monitor.server.system': 'System', 5 | 'menu.monitor.server.disk': 'Disk', 6 | 'menu.monitor.server.service': 'Service', 7 | 'menu.monitor.server.placeholder': 'Unknown', 8 | 'menu.monitor.server.cpu.usage': 'Usage', 9 | 'menu.monitor.server.cpu.max-frequency': 'Max-fre', 10 | 'menu.monitor.server.cpu.min-frequency': 'Min-fre', 11 | 'menu.monitor.server.cpu.current-frequency': 'Current-fre', 12 | 'menu.monitor.server.cpu.logical-cores': 'L-cores', 13 | 'menu.monitor.server.cpu.physical-cores': 'P-cores', 14 | 'menu.monitor.server.memory.total': 'Total', 15 | 'menu.monitor.server.memory.used': 'Used', 16 | 'menu.monitor.server.memory.free': 'Free', 17 | 'menu.monitor.server.memory.usage': 'Usage', 18 | // columns 19 | 'monitor.server.columns.dir': 'dir', 20 | 'monitor.server.columns.type': 'type', 21 | 'monitor.server.columns.device': 'device', 22 | 'monitor.server.columns.total': 'total', 23 | 'monitor.server.columns.free': 'free', 24 | 'monitor.server.columns.used': 'used', 25 | 'monitor.server.columns.usage': 'usage', 26 | }; 27 | -------------------------------------------------------------------------------- /src/views/monitor/server/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.monitor.server': '服务器监控', 3 | 'menu.monitor.server.memory': '内存', 4 | 'menu.monitor.server.system': '系统', 5 | 'menu.monitor.server.disk': '磁盘', 6 | 'menu.monitor.server.service': '服务', 7 | 'menu.monitor.server.placeholder': '未知', 8 | 'menu.monitor.server.cpu.usage': '使用率', 9 | 'menu.monitor.server.cpu.max-frequency': '最大频率', 10 | 'menu.monitor.server.cpu.min-frequency': '最小频率', 11 | 'menu.monitor.server.cpu.current-frequency': '当前频率', 12 | 'menu.monitor.server.cpu.logical-cores': '逻辑核心数', 13 | 'menu.monitor.server.cpu.physical-cores': '物理核心数', 14 | 'menu.monitor.server.memory.total': '总量', 15 | 'menu.monitor.server.memory.used': '已使用', 16 | 'menu.monitor.server.memory.free': '剩余', 17 | 'menu.monitor.server.memory.usage': '使用率', 18 | // columns 19 | 'monitor.server.columns.dir': '路径', 20 | 'monitor.server.columns.type': '类型', 21 | 'monitor.server.columns.device': '设备', 22 | 'monitor.server.columns.total': '总计', 23 | 'monitor.server.columns.free': '空闲', 24 | 'monitor.server.columns.used': '已使用', 25 | 'monitor.server.columns.usage': '使用率', 26 | }; 27 | -------------------------------------------------------------------------------- /src/views/not-found/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": [ 14 | "src/*" 15 | ] 16 | }, 17 | "lib": [ 18 | "es2020", 19 | "dom" 20 | ], 21 | "skipLibCheck": true 22 | }, 23 | "include": [ 24 | "src/**/*", 25 | "src/**/*.vue" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------