├── .prettierignore ├── .env.uat ├── src ├── vite-env.d.ts ├── vue-test-utils.d.ts ├── App.vue ├── utils │ ├── foo.ts │ ├── tests │ │ ├── request.test.ts │ │ └── foo.test.ts │ ├── cacheStore.ts │ └── request.ts ├── env.d.ts ├── views │ ├── Table │ │ ├── index.vue │ │ ├── Child │ │ │ ├── index.vue │ │ │ ├── List1 │ │ │ │ ├── Edit.vue │ │ │ │ └── index.vue │ │ │ └── List2 │ │ │ │ └── index.vue │ │ └── Table1.vue │ ├── NestRoute │ │ ├── index.vue │ │ ├── Child1 │ │ │ ├── index.vue │ │ │ └── GrandSon │ │ │ │ ├── Template1.vue │ │ │ │ ├── Template2.vue │ │ │ │ └── Template3.vue │ │ └── Child2 │ │ │ ├── index.vue │ │ │ └── GrandSon │ │ │ ├── Template1.vue │ │ │ ├── Template2.vue │ │ │ └── Template3.vue │ ├── Component │ │ └── Tinymce.vue │ ├── 404.vue │ ├── Home.vue │ ├── User │ │ └── Info.vue │ └── Login.vue ├── window.d.ts ├── assets │ └── img │ │ ├── 404.jpg │ │ ├── avatar.png │ │ └── logo │ │ └── logo.png ├── store │ ├── tests │ │ ├── index.test.ts │ │ └── getters.test.ts │ ├── modules │ │ ├── setting.ts │ │ ├── permission.ts │ │ ├── user.ts │ │ └── tagsView.ts │ ├── index.ts │ └── getters.ts ├── router │ ├── tests │ │ └── index.test.ts │ ├── modules │ │ ├── user.ts │ │ ├── compontents.ts │ │ ├── table.ts │ │ └── nestRoute.ts │ └── index.ts ├── api │ ├── tests │ │ └── user.test.ts │ ├── user.ts │ └── table.ts ├── shims-vue.d.ts ├── components │ ├── tests │ │ └── HellowWorld.test.ts │ ├── HelloWorld.vue │ └── tinymce │ │ └── index.vue ├── layout │ ├── index.vue │ ├── mixin │ │ └── resize.js │ └── components │ │ ├── SliderBar │ │ ├── Logo.vue │ │ ├── Item.vue │ │ └── index.vue │ │ ├── NavBar │ │ ├── Collapse.vue │ │ ├── index.vue │ │ ├── Breadcrumb.vue │ │ ├── AvatarDropDown.vue │ │ └── VisitedViews.vue │ │ └── AppMain.vue ├── main.ts └── styles │ └── index.scss ├── .env.development ├── .env.production ├── postcss.config.js ├── .prettierrc ├── cypress.json ├── .gitignore ├── public ├── favicon.ico └── tinymce │ └── zh_CN.js ├── .eslintignore ├── tests └── e2e │ ├── specs │ └── first.spec.ts │ ├── fixtures │ └── example.json │ ├── tsconfig.json │ ├── support │ ├── index.ts │ └── commands.ts │ └── plugins │ └── index.ts ├── index.html ├── babel.config.js ├── jest.config.js ├── README.md ├── README.en.md ├── tsconfig.json ├── mock ├── user.ts └── table.ts ├── .eslintrc ├── scripts └── verify-commit-msg.js ├── LICENSE ├── vite.config.ts └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage -------------------------------------------------------------------------------- /.env.uat: -------------------------------------------------------------------------------- 1 | NODE_ENV = "uat" 2 | VITE_BASE_API = "uat" 3 | 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/vue-test-utils.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@vue/test-utils"; 2 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV = "development" 2 | VITE_BASE_API = "" 3 | -------------------------------------------------------------------------------- /src/utils/foo.ts: -------------------------------------------------------------------------------- 1 | export function foo() { 2 | return "foo"; 3 | } 4 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | VITE_BASE_API: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/Table/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/NestRoute/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV = "production" 2 | VITE_BASE_API = "production" 3 | 4 | -------------------------------------------------------------------------------- /src/views/Table/Child/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/window.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | mode: string; 3 | baseUrl: string; 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "printWidth": 80 5 | } 6 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins//index.ts", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | coverage 7 | yarn-error.log -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenyuWang/vue3-element-admin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | src/assets 4 | src/styles 5 | script 6 | package.json 7 | index.html -------------------------------------------------------------------------------- /src/assets/img/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenyuWang/vue3-element-admin/HEAD/src/assets/img/404.jpg -------------------------------------------------------------------------------- /src/assets/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenyuWang/vue3-element-admin/HEAD/src/assets/img/avatar.png -------------------------------------------------------------------------------- /src/assets/img/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenyuWang/vue3-element-admin/HEAD/src/assets/img/logo/logo.png -------------------------------------------------------------------------------- /src/views/NestRoute/Child1/index.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/views/NestRoute/Child2/index.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/store/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import store from "../index"; 2 | test("store", () => { 3 | expect(store).not.toBeNull(); 4 | expect(store).not.toBeUndefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /src/router/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import router from "../index"; 2 | test("router", () => { 3 | expect(router).not.toBeNull(); 4 | expect(router).not.toBeUndefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/e2e/specs/first.spec.ts: -------------------------------------------------------------------------------- 1 | describe("first", () => { 2 | it("one", () => { 3 | cy.visit("http://localhost:3000/"); 4 | cy.get("button").click(); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/api/tests/user.test.ts: -------------------------------------------------------------------------------- 1 | import { apiLogin } from "../user"; 2 | test("apiLogin", () => { 3 | expect(apiLogin).not.toBeNull(); 4 | expect(apiLogin).not.toBeUndefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /src/utils/tests/request.test.ts: -------------------------------------------------------------------------------- 1 | import request from "../request"; 2 | test("request", () => { 3 | expect(request).not.toBeNull(); 4 | expect(request).not.toBeUndefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /src/utils/tests/foo.test.ts: -------------------------------------------------------------------------------- 1 | import { foo } from "../foo"; 2 | test("1+1=2", () => { 3 | expect(1 + 1).toBe(2); 4 | }); 5 | test("foo", () => { 6 | expect(foo()).toBe("foo"); 7 | }); 8 | -------------------------------------------------------------------------------- /src/store/tests/getters.test.ts: -------------------------------------------------------------------------------- 1 | import userInfo from "../getters"; 2 | 3 | test("userInfo", () => { 4 | expect(userInfo).not.toBeNull(); 5 | expect(userInfo).not.toBeUndefined(); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/e2e/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /src/views/Table/Table1.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { defineComponent } from "vue"; 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 4 | const component: defineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /src/views/NestRoute/Child1/GrandSon/Template1.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/views/NestRoute/Child1/GrandSon/Template2.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/views/NestRoute/Child1/GrandSon/Template3.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/views/NestRoute/Child2/GrandSon/Template1.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/views/NestRoute/Child2/GrandSon/Template2.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/views/NestRoute/Child2/GrandSon/Template3.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/components/tests/HellowWorld.test.ts: -------------------------------------------------------------------------------- 1 | import HelloWorld from "@/components/HelloWorld.vue"; 2 | import { mount } from "@vue/test-utils"; 3 | 4 | test("HelloWorld", () => { 5 | const wrapper = mount(HelloWorld, { 6 | props: { 7 | msg: "hello,vue3", 8 | }, 9 | }); 10 | expect(wrapper.html()).toMatch("hello,vue3"); 11 | }); 12 | -------------------------------------------------------------------------------- /src/store/modules/setting.ts: -------------------------------------------------------------------------------- 1 | interface setting { 2 | isCollapse: boolean; 3 | } 4 | export default { 5 | namespaced: true, 6 | state: { 7 | // 左侧菜单是否展开 8 | isCollapse: false, 9 | }, 10 | mutations: { 11 | SET_COLLAPSE(state: setting, val: boolean) { 12 | state.isCollapse = val; 13 | }, 14 | }, 15 | actions: {}, 16 | }; 17 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | // 内容页主体架子 2 | 5 | 14 | -------------------------------------------------------------------------------- /tests/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vue3-element3-admin 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/router/modules/user.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout/index.vue"; 2 | export default { 3 | path: "/user", 4 | name: "User", 5 | meta: { title: "个人中心" }, 6 | component: Layout, 7 | redirect: "/user/info", 8 | hidden: true, 9 | children: [ 10 | { 11 | path: "info", 12 | name: "UserInfo", 13 | meta: { title: "个人信息" }, 14 | component: () => import("@/views/User/Info.vue"), 15 | }, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import getters from "./getters"; 3 | import user from "./modules/user"; 4 | import permission from "./modules/permission"; 5 | import tagsView from "./modules/tagsView"; 6 | import setting from "./modules/setting"; 7 | 8 | const modules: any = { 9 | user, 10 | permission, 11 | tagsView, 12 | setting, 13 | }; 14 | const store = createStore({ 15 | modules, 16 | getters, 17 | }); 18 | export default store; 19 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | function () { 4 | return { 5 | visitor: { 6 | MetaProperty(path) { 7 | path.replaceWithSourceString("process"); 8 | }, 9 | }, 10 | }; 11 | }, 12 | ], 13 | presets: [ 14 | [ 15 | "@babel/preset-env", 16 | { useBuiltIns: "entry", corejs: "2", targets: { node: "current" } }, 17 | ], 18 | "@babel/preset-typescript", 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.jsx?$": "babel-jest", 4 | "^.+\\.vue$": "vue-jest", 5 | "^.+\\.tsx?$": "ts-jest", 6 | }, 7 | testMatch: ["**/?(*.)+(test).[jt]s?(x)"], 8 | moduleNameMapper: { 9 | "^@/(.*)$": "/src/$1", 10 | }, 11 | collectCoverage: true, 12 | collectCoverageFrom: [ 13 | "src/**/*.{js,ts,vue}", 14 | "!**/node_modules/**", 15 | "!**/shims-vue.d.ts", 16 | "!**/vite-env.d.ts", 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3-element-admin 2 | 3 | 简体中文 | [English](./README.en.md) 4 | 5 | #### 介绍 6 | 7 | 一个基于 Vite+Vue3+Vue Router+Vuex+TS+Element3+axios+Jest+Cypress 的后台管理系统 8 | 9 | #### 技术栈 10 | 11 | Vite+Vue3+Vue Router+Vuex+TS+Element3+axios+Jest+Cypress 12 | 13 | #### 安装 14 | 15 | yarn 16 | 17 | #### 启动服务 18 | 19 | yarn dev 20 | 21 | #### 掘金对应文章 22 | 23 | [从零到一:搭建一个Vue3开发框架](https://juejin.cn/post/7019625617943445512) 24 | 25 | [从零到一:搭建Vue3后台管理系统](https://juejin.cn/post/7020698415658958879) 26 | -------------------------------------------------------------------------------- /src/router/modules/compontents.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout/index.vue"; 2 | export default { 3 | path: "/compontents", 4 | name: "Components", 5 | meta: { title: "组件", icon: "el-icon-s-data" }, 6 | redirect: "/compontents/tinymce", 7 | component: Layout, 8 | children: [ 9 | { 10 | path: "tinymce", 11 | name: "TinymceDemo", 12 | meta: { title: "富文本", needCache: true }, 13 | component: () => import("@/views/Component/Tinymce.vue"), 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/cacheStore.ts: -------------------------------------------------------------------------------- 1 | import store from "@/store"; 2 | //在页面加载时读取sessionStorage里的状态信息 3 | const sessionStore = sessionStorage.getItem("store"); 4 | if (sessionStore) { 5 | store.replaceState(Object.assign({}, store.state, JSON.parse(sessionStore))); 6 | store.dispatch("permission/handleRoutes", null, { root: true }); 7 | } 8 | 9 | //在页面刷新时将vuex里的信息保存到sessionStorage里 10 | window.addEventListener("beforeunload", () => { 11 | sessionStorage.setItem("store", JSON.stringify(store.state)); 12 | }); 13 | export default store; 14 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # vue3-element-admin 2 | 3 | [简体中文](./README.md) | English 4 | 5 | #### Description 6 | 7 | A background management system based on Vite+Vue3+Vue Router+Vuex+TS+Element3+axios+Jest+Cypress 8 | 9 | #### Software Architecture 10 | 11 | Vite+Vue3+Vue Router+Vuex+TS+Element3+axios+Jest+Cypress 12 | 13 | #### Installation 14 | 15 | yarn 16 | 17 | #### Start service 18 | 19 | yarn dev 20 | 21 | #### Corresponding article 22 | 23 | [从零到一:搭建一个 Vue3 开发框架](https://juejin.cn/post/7019625617943445512) 24 | 25 | [从零到一:搭建 Vue3 后台管理系统](https://juejin.cn/post/7020698415658958879) 26 | -------------------------------------------------------------------------------- /src/layout/mixin/resize.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data() { 3 | return { 4 | width: 992, 5 | }; 6 | }, 7 | mounted() { 8 | window.addEventListener("resize", this.resizeHander); 9 | }, 10 | beforeDestroy() { 11 | window.removeEventListener("resize", this.resizeHander); 12 | }, 13 | methods: { 14 | resizeHander() { 15 | const rect = document.body.getBoundingClientRect(); 16 | if (rect.width > this.width) { 17 | this.$store.commit("setting/SET_COLLAPSE", false); 18 | } else { 19 | this.$store.commit("setting/SET_COLLAPSE", true); 20 | } 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/views/Component/Tinymce.vue: -------------------------------------------------------------------------------- 1 | 7 | 23 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 30 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 17 | 27 | -------------------------------------------------------------------------------- /src/layout/components/SliderBar/Logo.vue: -------------------------------------------------------------------------------- 1 | 7 | 13 | 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "lib": ["esnext", "dom"], 14 | "types": ["vite/client", "jest", "node"], 15 | "baseUrl": "./", 16 | "paths": { 17 | "@/*": ["src/*"] 18 | } 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "src/**/*.d.ts", 23 | "src/**/*.tsx", 24 | "src/**/*.vue", 25 | "tests/unit/*.ts" 26 | ], 27 | "exclude": ["node_modules", "tests/e2e/*"] 28 | } 29 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 9 | 22 | 33 | -------------------------------------------------------------------------------- /tests/e2e/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /mock/user.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | url: `/module/user/login`, 4 | method: "post", 5 | response: (req) => { 6 | return { 7 | header: { 8 | code: 0, 9 | msg: "OK", 10 | }, 11 | body: { 12 | name: `${req.body.name}小明`, 13 | token: "test_token", 14 | avatar: 15 | "https://img0.baidu.com/it/u=1570224879,3082274194&fm=253&fmt=auto&app=120&f=JPEG?w=432&h=288", 16 | roles: ["admin", "集团管理员"], 17 | }, 18 | }; 19 | }, 20 | }, 21 | { 22 | url: `/module/user/signout`, 23 | method: "post", 24 | response: () => { 25 | return { 26 | header: { 27 | code: 0, 28 | msg: "OK", 29 | }, 30 | }; 31 | }, 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/layout/components/NavBar/Collapse.vue: -------------------------------------------------------------------------------- 1 | 7 | 28 | -------------------------------------------------------------------------------- /src/views/User/Info.vue: -------------------------------------------------------------------------------- 1 | 17 | 28 | -------------------------------------------------------------------------------- /src/layout/components/NavBar/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 22 | 29 | -------------------------------------------------------------------------------- /src/store/getters.ts: -------------------------------------------------------------------------------- 1 | type state = { 2 | user: { 3 | userInfo: { 4 | name: string; 5 | token: string; 6 | avatar: string; 7 | roles: string[]; 8 | }; 9 | }; 10 | permission: { 11 | routes: any[]; 12 | permissionRoutes: any[]; 13 | }; 14 | tagsView: { 15 | cachedViews: string[]; 16 | visitedViews: any[]; 17 | }; 18 | setting: { 19 | isCollapse: boolean; 20 | }; 21 | }; 22 | export default { 23 | userInfo: (state: state) => state.user.userInfo, 24 | isCollapse: (state: state) => state.setting.isCollapse, 25 | routes: (state: state) => state.permission.routes, 26 | permissionRoutes: (state: state) => state.permission.permissionRoutes, 27 | cachedViews: (state: state) => state.tagsView.cachedViews, 28 | visitedViews: (state: state) => state.tagsView.visitedViews, 29 | }; 30 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | type Method = 3 | | "get" 4 | | "GET" 5 | | "delete" 6 | | "DELETE" 7 | | "head" 8 | | "HEAD" 9 | | "options" 10 | | "OPTIONS" 11 | | "post" 12 | | "POST" 13 | | "put" 14 | | "PUT" 15 | | "patch" 16 | | "PATCH" 17 | | "purge" 18 | | "PURGE" 19 | | "link" 20 | | "LINK" 21 | | "unlink" 22 | | "UNLINK"; 23 | const curryRequest = ( 24 | url: string, 25 | method: Method, 26 | data?: Record | any 27 | ) => { 28 | return request(`/module/user/${url}`, method, data); 29 | }; 30 | // 登录 31 | export function apiLogin(data: { 32 | name: string; 33 | password: string; 34 | }): PromiseLike { 35 | return curryRequest("login", "post", data); 36 | } 37 | // 退出登录 38 | export function apiSignout(): PromiseLike { 39 | return curryRequest("signout", "post"); 40 | } 41 | -------------------------------------------------------------------------------- /src/views/Table/Child/List1/Edit.vue: -------------------------------------------------------------------------------- 1 | 8 | 38 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /src/router/modules/table.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout/index.vue"; 2 | export default { 3 | path: "/table", 4 | name: "Table", 5 | meta: { title: "表格", icon: "el-icon-s-data" }, 6 | redirect: "/table/child/list1", 7 | component: Layout, 8 | children: [ 9 | { 10 | path: "child/list1", 11 | name: "TableChildList1", 12 | meta: { title: "表格1-列表1", needCache: true }, 13 | component: () => import("@/views/Table/Child/List1/index.vue"), 14 | }, 15 | { 16 | path: "child/list1/edit/:id", 17 | name: "TableChildList1Edit", 18 | meta: { title: "表格1-列表1-编辑", needCache: true }, 19 | props: true, 20 | hidden: true, 21 | component: () => import("@/views/Table/Child/List1/Edit.vue"), 22 | }, 23 | { 24 | path: "child/list2", 25 | name: "TableChildList2", 26 | meta: { title: "表格1-列表2", needCache: true }, 27 | component: () => import("@/views/Table/Child/List2/index.vue"), 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /src/layout/components/NavBar/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 14 | 29 | 38 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es2021": true 7 | }, 8 | "extends": [ 9 | "plugin:vue/vue3-recommended", 10 | "eslint:recommended", 11 | "@vue/typescript/recommended", 12 | "@vue/prettier", 13 | "@vue/prettier/@typescript-eslint" 14 | ], 15 | "parserOptions": { 16 | "ecmaVersion": 2021 17 | }, 18 | "rules": { 19 | // ts每个函数都要显式声明返回值 20 | "@typescript-eslint/explicit-module-boundary-types": "off", 21 | // 关闭 ts any type 校验 22 | "@typescript-eslint/no-explicit-any": ["off"], 23 | // 忽略 Require statement not part of import statement. 24 | "@typescript-eslint/no-var-requires": "off", 25 | // 在数组开括号后和闭括号前强制换行 26 | "array-bracket-newline": "off", 27 | // 强制数组元素间出现换行 28 | "array-element-newline": "off", 29 | // 要求或禁止在三元操作数中间换行 30 | "multiline-ternary": "off", 31 | // 要求或禁止在变量声明周围换行 32 | "one-var-declaration-per-line": "off", 33 | // 强制操作符使用一致的换行符 34 | "operator-linebreak": "off" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/api/table.ts: -------------------------------------------------------------------------------- 1 | import request from "../utils/request"; 2 | type Method = 3 | | "get" 4 | | "GET" 5 | | "delete" 6 | | "DELETE" 7 | | "head" 8 | | "HEAD" 9 | | "options" 10 | | "OPTIONS" 11 | | "post" 12 | | "POST" 13 | | "put" 14 | | "PUT" 15 | | "patch" 16 | | "PATCH" 17 | | "purge" 18 | | "PURGE" 19 | | "link" 20 | | "LINK" 21 | | "unlink" 22 | | "UNLINK"; 23 | const curryRequest = ( 24 | url: string, 25 | method: Method, 26 | data: Record 27 | ) => { 28 | return request(`/module/table/${url}`, method, data); 29 | }; 30 | // table 列表1 31 | export function apiList1(data: { 32 | pageNo: number; 33 | pageSize: number; 34 | }): PromiseLike { 35 | return curryRequest("list1", "post", data); 36 | } 37 | // table 列表2 38 | export function apiList2(data: { 39 | pageNo: number; 40 | pageSize: number; 41 | }): PromiseLike { 42 | return curryRequest("list2", "post", data); 43 | } 44 | // 获取详情 45 | export function apiItemInfo(data: { id: string }): PromiseLike { 46 | return curryRequest("itemInfo", "post", data); 47 | } 48 | -------------------------------------------------------------------------------- /scripts/verify-commit-msg.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | // 添加本行使 win10系统 vscode 终端 console 变色生效 3 | chalk.level = 1; 4 | const msgPath = process.env.GIT_PARAMS; 5 | const msg = require("fs").readFileSync(msgPath, "utf-8").trim(); 6 | 7 | const commitRE = 8 | /^(revert: )?(feat|fix|polish|docs|style|refactor|perf|test|workflow|ci|chore|types|build)(\(.+\))?: .{1,50}/; 9 | 10 | if (!commitRE.test(msg)) { 11 | console.error( 12 | ` ${chalk.bgRed.white(" ERROR ")} ${chalk.red( 13 | `invalid commit message format.` 14 | )}\n\n` + 15 | chalk.red( 16 | `Proper commit message format is required for automated changelog generation. Examples:\n\n` 17 | ) + 18 | `${chalk.green(`feat(compiler): add 'comments' option`)}\n` + 19 | `${chalk.green(`fix(v-model): handle events on blur (close #28)`)}\n\n` + 20 | chalk.red(`See .github/COMMIT_CONVENTION.md for more details.\n`) + 21 | chalk.red( 22 | `You can also use ${chalk.cyan( 23 | `npm run commit` 24 | )} to interactively generate a commit message.\n` 25 | ) 26 | ); 27 | process.exit(1); 28 | } 29 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | return Object.assign({}, config, { 23 | fixturesFolder: "tests/e2e/fixtures", 24 | integrationFolder: "tests/e2e/specs", 25 | screenshotsFolder: "tests/e2e/screenshots", 26 | videosFolder: "tests/e2e/videos", 27 | supportFile: "tests/e2e/support/index.ts", 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ZhenyuWang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/layout/components/NavBar/AvatarDropDown.vue: -------------------------------------------------------------------------------- 1 | 12 | 38 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "@/App.vue"; 3 | import "@/styles/index.scss"; 4 | import store from "@/utils/cacheStore"; 5 | import router from "./router/index"; 6 | // Element3相关 7 | import "element3/lib/theme-chalk/index.css"; 8 | import { 9 | ElIcon, 10 | ElButton, 11 | ElLink, 12 | ElForm, 13 | ElFormItem, 14 | ElInput, 15 | ElMenu, 16 | ElSubmenu, 17 | ElMenuItem, 18 | ElMenuItemGroup, 19 | ElAside, 20 | ElContainer, 21 | ElHeader, 22 | ElMain, 23 | ElTable, 24 | ElTableColumn, 25 | ElPagination, 26 | ElBreadcrumb, 27 | ElBreadcrumbItem, 28 | ElDropdown, 29 | ElDropdownMenu, 30 | ElDropdownItem, 31 | ElAvatar, 32 | } from "element3"; 33 | 34 | createApp(App) 35 | .use(store) 36 | .use(router) 37 | .use(ElIcon) 38 | .use(ElButton) 39 | .use(ElLink) 40 | .use(ElForm) 41 | .use(ElFormItem) 42 | .use(ElInput) 43 | .use(ElMenu) 44 | .use(ElSubmenu) 45 | .use(ElMenuItem) 46 | .use(ElMenuItemGroup) 47 | .use(ElAside) 48 | .use(ElContainer) 49 | .use(ElHeader) 50 | .use(ElMain) 51 | .use(ElTable) 52 | .use(ElTableColumn) 53 | .use(ElPagination) 54 | .use(ElBreadcrumb) 55 | .use(ElBreadcrumbItem) 56 | .use(ElDropdown) 57 | .use(ElDropdownMenu) 58 | .use(ElDropdownItem) 59 | .use(ElAvatar) 60 | .mount("#app"); 61 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfigExport, ConfigEnv, loadEnv } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import { resolve } from "path"; 4 | import { viteMockServe } from "vite-plugin-mock"; 5 | export default ({ command, mode }: ConfigEnv): UserConfigExport => { 6 | const plugins = [vue()]; 7 | const env = loadEnv(mode, process.cwd()); 8 | // 如果当前是测试环境,使用添加mock插件 9 | if (mode === "development") { 10 | plugins.push( 11 | viteMockServe({ 12 | mockPath: "mock", 13 | localEnabled: command === "serve", 14 | }) 15 | ); 16 | } 17 | // 处理使用import.meta.env jest 测试报错问题 18 | const envWithProcessPrefix = Object.entries(env).reduce( 19 | (prev, [key, val]) => { 20 | return { 21 | ...prev, 22 | // 环境变量添加process.env 23 | ["process.env." + key]: `"${val}"`, 24 | }; 25 | }, 26 | {} 27 | ); 28 | return { 29 | base: "./", 30 | server: { 31 | open: true, 32 | host: "0.0.0.0", 33 | // proxy: { 34 | // "/api": { 35 | // target: "http://jsonplaceholder.typicode.com", 36 | // changeOrigin: true, 37 | // rewrite: (path) => path.replace(/^\/api/, ""), 38 | // }, 39 | // }, 40 | }, 41 | resolve: { 42 | alias: { 43 | "@": resolve("./src"), 44 | }, 45 | }, 46 | plugins, 47 | build: { 48 | target: "es2015", 49 | }, 50 | define: envWithProcessPrefix, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import store from "@/store"; 2 | import axios from "axios"; 3 | import { Message } from "element3"; 4 | // 创建axios实例 5 | // vite环境变量直接使用jest测试报错,所以绑定到window上 6 | const service = axios.create({ 7 | baseURL: process.env.VITE_BASE_API, 8 | timeout: 10000, 9 | }); 10 | // 请求拦截器 11 | service.interceptors.request.use( 12 | (config) => { 13 | const token = store.getters.userInfo.token; 14 | if (token) config.headers["X-Token"] = token; 15 | return config; 16 | }, 17 | (error) => { 18 | return Promise.reject(error); 19 | } 20 | ); 21 | // 响应拦截器 22 | service.interceptors.response.use( 23 | (response) => { 24 | const res = response.data; 25 | if (res.header.code !== 0) { 26 | Message.error(res.header.msg || "Error"); 27 | return Promise.reject(new Error(res.header.msg || "Error")); 28 | } 29 | return res; 30 | }, 31 | (error) => { 32 | Message.error("错了哦,这是一条错误消息"); 33 | return Promise.reject(error); 34 | } 35 | ); 36 | /** 37 | * 封装接口请求方法 38 | * @param url 域名后需补齐的接口地址 39 | * @param method 接口请求方式 40 | * @param data data下请求数据体 41 | */ 42 | type Method = 43 | | "get" 44 | | "GET" 45 | | "delete" 46 | | "DELETE" 47 | | "head" 48 | | "HEAD" 49 | | "options" 50 | | "OPTIONS" 51 | | "post" 52 | | "POST" 53 | | "put" 54 | | "PUT" 55 | | "patch" 56 | | "PATCH" 57 | | "purge" 58 | | "PURGE" 59 | | "link" 60 | | "LINK" 61 | | "unlink" 62 | | "UNLINK"; 63 | const request = ( 64 | url: string, 65 | method: Method, 66 | data: Record 67 | ) => { 68 | return service({ 69 | url, 70 | method, 71 | data, 72 | }); 73 | }; 74 | export default request; 75 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 22 | 43 | 58 | -------------------------------------------------------------------------------- /src/store/modules/permission.ts: -------------------------------------------------------------------------------- 1 | import router, { routes, permissionRoutes } from "@/router"; 2 | import store from "@/store"; 3 | type routes = any[]; 4 | type state = { 5 | routes: routes; 6 | permissionRoutes: routes; 7 | }; 8 | type context = { 9 | state: Record; 10 | mutations: Record; 11 | actions: Record; 12 | commit: any; 13 | }; 14 | // 处理访问过的路由固定到visited views tabbar 15 | function handleFixedVisitedViews(context: context, routes: any[]) { 16 | routes.forEach((route) => { 17 | if (route.meta && route.meta.fixed) { 18 | store.dispatch("tagsView/addFixedVisitedView", route); 19 | } 20 | if (route.children && route.children.length) 21 | handleFixedVisitedViews(context, route.children); 22 | }); 23 | } 24 | export default { 25 | namespaced: true, 26 | state: { 27 | // 完整路由 28 | routes: [], 29 | // 权限路由 30 | permissionRoutes: [], 31 | }, 32 | mutations: { 33 | // 设置路由 34 | setRoutes(state: state, permissionRoutes: routes) { 35 | state.routes = routes.concat(permissionRoutes); 36 | state.permissionRoutes = permissionRoutes; 37 | }, 38 | }, 39 | actions: { 40 | // 处理路由 41 | handleRoutes(context: context) { 42 | permissionRoutes.forEach((item) => { 43 | router.addRoute(item); 44 | }); 45 | handleFixedVisitedViews(context, permissionRoutes); 46 | context.commit("setRoutes", permissionRoutes); 47 | }, 48 | // 重置路由 49 | resetRoute({ commit }: context) { 50 | permissionRoutes.forEach((item) => { 51 | if (item.name) router.removeRoute(item.name); 52 | }); 53 | commit("setRoutes", []); 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/router/modules/nestRoute.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout/index.vue"; 2 | export default { 3 | path: "/nest-route", 4 | name: "NestRoute", 5 | meta: { title: "多级嵌套", icon: "el-icon-s-grid" }, 6 | redirect: "/nest-route/child1/grandson/template1", 7 | component: Layout, 8 | children: [ 9 | { 10 | path: "child1/grandson/template1", 11 | name: "Child1Template1", 12 | meta: { title: "子级1-1", needCache: true }, 13 | component: () => 14 | import("@/views/NestRoute/Child1/GrandSon/Template1.vue"), 15 | }, 16 | { 17 | path: "child1/grandson/template2", 18 | name: "Child1Template2", 19 | meta: { title: "子级1-2", needCache: true }, 20 | component: () => 21 | import("@/views/NestRoute/Child1/GrandSon/Template2.vue"), 22 | }, 23 | { 24 | path: "child1/grandson/template3", 25 | name: "Child1Template3", 26 | meta: { title: "子级1-3", needCache: true }, 27 | component: () => 28 | import("@/views/NestRoute/Child1/GrandSon/Template3.vue"), 29 | }, 30 | { 31 | path: "child2/grandson/template1", 32 | name: "Child2Template1", 33 | meta: { title: "子级2-1", needCache: true }, 34 | component: () => 35 | import("@/views/NestRoute/Child2/GrandSon/Template1.vue"), 36 | }, 37 | { 38 | path: "child2/grandson/template2", 39 | name: "Child2Template2", 40 | meta: { title: "子级2-2", needCache: true }, 41 | component: () => 42 | import("@/views/NestRoute/Child2/GrandSon/Template2.vue"), 43 | }, 44 | { 45 | path: "child2/grandson/template3", 46 | name: "Child2Template3", 47 | meta: { title: "子级2-3", needCache: true }, 48 | component: () => 49 | import("@/views/NestRoute/Child2/GrandSon/Template3.vue"), 50 | }, 51 | ], 52 | }; 53 | -------------------------------------------------------------------------------- /src/layout/components/SliderBar/Item.vue: -------------------------------------------------------------------------------- 1 | 35 | 56 | 65 | -------------------------------------------------------------------------------- /src/views/Table/Child/List1/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-element3-admin", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "cross-env NODE_ENV=development vite", 7 | "build:uat": "vite build --mode uat", 8 | "build": "vite build", 9 | "serve": "vite preview --port 8888", 10 | "lint": "eslint --ext .js,ts,vue src/** --no-error-on-unmatched-pattern", 11 | "lint:fix": "eslint --ext .js,ts,vue src/** --no-error-on-unmatched-pattern --fix", 12 | "test-unit": "jest", 13 | "test-e2e": "cypress open", 14 | "test-e2e-ci": "cypress run", 15 | "test": "npm run test-unit && npm run test-e2e-ci" 16 | }, 17 | "gitHooks": { 18 | "commit-msg": "node scripts/verify-commit-msg.js", 19 | "pre-commit": "lint-staged", 20 | "pre-push": "npm run test" 21 | }, 22 | "lint-staged": { 23 | "*.{js,ts,vue}": "eslint --fix", 24 | "*": "prettier -w -u" 25 | }, 26 | "dependencies": { 27 | "@tinymce/tinymce-vue": "^4.0.4", 28 | "axios": "^0.21.1", 29 | "element3": "^0.0.40", 30 | "mockjs": "^1.1.0", 31 | "vue": "^3.0.5", 32 | "vue-router": "^4.0.10", 33 | "vuex": "^4.0.2" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.14.8", 37 | "@babel/preset-env": "^7.14.9", 38 | "@babel/preset-typescript": "^7.14.5", 39 | "@types/jest": "^26.0.24", 40 | "@types/node": "^16.4.10", 41 | "@typescript-eslint/eslint-plugin": "^4.15.2", 42 | "@typescript-eslint/parser": "^4.15.2", 43 | "@vitejs/plugin-vue": "^1.3.0", 44 | "@vue/compiler-sfc": "^3.0.5", 45 | "@vue/eslint-config-prettier": "^6.0.0", 46 | "@vue/eslint-config-typescript": "^7.0.0", 47 | "@vue/test-utils": "^2.0.0-rc.11", 48 | "autoprefixer": "^10.2.5", 49 | "babel-jest": "26.6.3", 50 | "cross-env": "^7.0.3", 51 | "cypress": "^8.2.0", 52 | "eslint": "^7.20.0", 53 | "eslint-plugin-prettier": "^3.3.1", 54 | "eslint-plugin-vue": "^7.6.0", 55 | "jest": "26.6.3", 56 | "lint-staged": "^11.1.2", 57 | "postcss": "8.1.0", 58 | "prettier": "^2.2.1", 59 | "sass": "^1.32.8", 60 | "ts-jest": "^26.5.6", 61 | "typescript": "^4.3.5", 62 | "vite": "^2.4.4", 63 | "vite-plugin-mock": "^2.9.4", 64 | "vue-jest": "^5.0.0-alpha.10", 65 | "vue-tsc": "^0.2.2", 66 | "yorkie": "^2.0.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/layout/components/SliderBar/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 37 | 99 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { apiLogin, apiSignout } from "@/api/user"; 2 | type userInfo = { 3 | name: string; 4 | token: string; 5 | avatar: string; 6 | roles: string[]; 7 | }; 8 | type state = { 9 | userInfo: userInfo; 10 | }; 11 | type loginData = { 12 | name: string; 13 | password: string; 14 | }; 15 | type context = { 16 | state: Record; 17 | mutations: Record; 18 | actions: Record; 19 | dispatch: any; 20 | commit: any; 21 | }; 22 | export default { 23 | namespaced: true, 24 | state: { 25 | userInfo: { 26 | name: "", 27 | token: "", 28 | avatar: "", 29 | roles: [], 30 | }, 31 | }, 32 | mutations: { 33 | setUserInfo(state: state, val: userInfo) { 34 | state.userInfo = val; 35 | }, 36 | }, 37 | actions: { 38 | // 登录 39 | login({ commit, dispatch }: context, data: loginData) { 40 | return new Promise((resolve) => { 41 | apiLogin(data).then(async (res) => { 42 | // 更新用户信息 43 | commit("setUserInfo", { 44 | name: res.body.name, 45 | token: res.body.token, 46 | avatar: res.body.avatar, 47 | roles: res.body.roles, 48 | }); 49 | // 处理权限路由 50 | await dispatch("permission/handleRoutes", null, { 51 | root: true, 52 | }); 53 | resolve("success"); 54 | }); 55 | }); 56 | }, 57 | // 退出登录 58 | signout({ commit, dispatch }: context) { 59 | return new Promise((resolve) => { 60 | apiSignout().then(async () => { 61 | commit("setUserInfo", { 62 | name: "", 63 | token: "", 64 | avatar: "", 65 | roles: [], 66 | }); 67 | // 重置路由 68 | await dispatch("permission/resetRoute", null, { 69 | root: true, 70 | }); 71 | // 清理缓存路由 72 | commit("tagsView/CLEAR_CACHE_VIEW", null, { 73 | root: true, 74 | }); 75 | // 清理访问过的路由 76 | commit("tagsView/CLEAR_VISITED_VIEW", null, { 77 | root: true, 78 | }); 79 | // 清理固定路由 80 | commit("tagsView/CLEAR_FIXED_VISITED_VIEW", null, { 81 | root: true, 82 | }); 83 | resolve("success"); 84 | }); 85 | }); 86 | }, 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /src/views/Table/Child/List2/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 82 | -------------------------------------------------------------------------------- /src/store/modules/tagsView.ts: -------------------------------------------------------------------------------- 1 | interface TagsViewState { 2 | cachedViews: string[]; 3 | fixedVisitedViews: any[]; 4 | visitedViews: any[]; 5 | } 6 | type context = { 7 | state: Record; 8 | mutations: Record; 9 | actions: Record; 10 | commit: any; 11 | }; 12 | export default { 13 | namespaced: true, 14 | state: { 15 | // 缓存路由 16 | cachedViews: [], 17 | // 固定tabbar的路由 18 | fixedVisitedViews: [], 19 | // 访问过的路由 20 | visitedViews: [], 21 | }, 22 | mutations: { 23 | // 添加缓存路由 24 | ADD_CACHE_VIEW(state: TagsViewState, name: string) { 25 | if (state.cachedViews.indexOf(name) === -1) state.cachedViews.push(name); 26 | }, 27 | // 删除缓存路由 28 | DELETE_CACHE_VIEW(state: TagsViewState, name: string) { 29 | const index = state.cachedViews.indexOf(name); 30 | if (index > -1) state.cachedViews.splice(index, 1); 31 | }, 32 | // 清空缓存路由 33 | CLEAR_CACHE_VIEW(state: TagsViewState) { 34 | state.cachedViews = []; 35 | }, 36 | // 添加固定路由 37 | ADD_FIXED_VISITED_VIEW(state: TagsViewState, view: any) { 38 | if (!state.fixedVisitedViews.find((item) => item.name === view.name)) 39 | state.fixedVisitedViews.push(view); 40 | }, 41 | // 清空固定路由 42 | CLEAR_FIXED_VISITED_VIEW(state: TagsViewState) { 43 | state.fixedVisitedViews = []; 44 | }, 45 | // 添加访问过的路由 46 | ADD_VISITED_VIEW(state: TagsViewState, view: any) { 47 | if (!state.visitedViews.find((item) => item.name === view.name)) 48 | state.visitedViews.push(view); 49 | }, 50 | // 删除访问过的路由 51 | DELETE_VISITED_VIEW(state: TagsViewState, name: string) { 52 | state.visitedViews = state.visitedViews.filter((item) => { 53 | return item.name !== name; 54 | }); 55 | }, 56 | // 删除其他访问过的路由 57 | DELETE_OTHER_VISITED_VIEW(state: TagsViewState, view: any) { 58 | state.visitedViews = [...state.fixedVisitedViews, view]; 59 | }, 60 | // 清空访问过的路由 61 | CLEAR_VISITED_VIEW(state: TagsViewState) { 62 | state.visitedViews = [...state.fixedVisitedViews]; 63 | }, 64 | }, 65 | actions: { 66 | // 添加固定路由 67 | addFixedVisitedView({ commit }: context, view: any) { 68 | commit("ADD_FIXED_VISITED_VIEW", view); 69 | commit("ADD_VISITED_VIEW", view); 70 | }, 71 | // 删除访问过的路由 72 | deleteVisitedView({ commit }: context, name: string) { 73 | commit("DELETE_VISITED_VIEW", name); 74 | commit("DELETE_CACHE_VIEW", name); 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/tinymce/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 83 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import store from "@/store"; 3 | 4 | /* Layout */ 5 | import Layout from "@/layout/index.vue"; 6 | // modules 7 | import table from "./modules/table"; 8 | import nestRoute from "./modules/nestRoute"; 9 | import compontents from "./modules/compontents"; 10 | import user from "./modules/user"; 11 | 12 | /* 13 | config 14 | path 路径 15 | fullPath 完整路径 16 | name 唯一name 大驼峰 17 | redirect 重定向(默认 '') 18 | component 路由对应组件 19 | hidden:true 是否在左侧菜单隐藏(默认 false) 20 | meta 21 | title 名称 (默认 '') 22 | icon 左侧菜单icon (默认 '') 23 | notNeedAuth:true 该路由是否不需要鉴权(默认 false) 24 | needCache:true 该路由是否需要缓存(默认 false) 25 | fixed:true 如果设置为true,该路由会固定在visited-view中(默认 false) 26 | */ 27 | // 无需权限的路由 28 | export const routes = [ 29 | { 30 | path: "/login", 31 | name: "Login", 32 | hidden: true, 33 | meta: { notNeedAuth: true }, 34 | component: () => import("@/views/Login.vue"), 35 | }, 36 | // 匹配所有路径 vue2使用* vue3使用/:pathMatch(.*)或/:catchAll(.*) 37 | { 38 | path: "/404", 39 | name: "404", 40 | hidden: true, 41 | meta: { notNeedAuth: true }, 42 | component: () => import("@/views/404.vue"), 43 | }, 44 | ]; 45 | // 需要校验权限的路由 46 | export const permissionRoutes = [ 47 | { 48 | path: "/", 49 | name: "Root", 50 | component: Layout, 51 | redirect: "/home", 52 | children: [ 53 | { 54 | path: "home", 55 | name: "Home", 56 | meta: { 57 | title: "首页", 58 | icon: "el-icon-s-home", 59 | needCache: true, 60 | fixed: true, 61 | }, 62 | component: () => import("@/views/Home.vue"), 63 | }, 64 | ], 65 | }, 66 | table, 67 | nestRoute, 68 | compontents, 69 | user, 70 | { 71 | path: "/:catchAll(.*)", 72 | hidden: true, 73 | redirect: "/404", 74 | }, 75 | ]; 76 | 77 | // 路由实例 78 | const router = createRouter({ 79 | history: createWebHashHistory(), 80 | routes, 81 | // 始终滚动到顶部 82 | scrollBehavior() { 83 | return { top: 0 }; 84 | }, 85 | }); 86 | 87 | // 路由前置守卫 88 | router.beforeEach((to) => { 89 | /* 90 | false 以取消导航 91 | 一个路由 92 | 不返回或者返回true 则去to 93 | 如果遇到了意料之外的情况,可能会抛出一个 Error 这会取消导航并且调用 router.onError() 注册过的回调 94 | 也就是用不到next了,但是next还是可以使用 95 | */ 96 | // 如果to需要鉴权 97 | if (!to.meta.notNeedAuth) { 98 | // 获取userInfo 99 | const userInfo = store.getters.userInfo; 100 | // 如果未登录 101 | if (!userInfo.name || !userInfo.roles.length) { 102 | return { name: "Login" }; 103 | } 104 | } 105 | }); 106 | // 路由后置守卫 107 | router.afterEach((to: any) => { 108 | // 添加路由缓存 109 | if (to.name && to.meta.needCache) { 110 | store.commit("tagsView/ADD_CACHE_VIEW", to.name); 111 | } 112 | // 添加访问过路由 113 | if (to.meta && !to.meta.notNeedAuth) { 114 | const { fullPath, name, meta, params, query } = to; 115 | store.commit("tagsView/ADD_VISITED_VIEW", { 116 | fullPath, 117 | name, 118 | meta, 119 | params, 120 | query, 121 | }); 122 | } 123 | }); 124 | 125 | export default router; 126 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 31 | 88 | 120 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // 修改滚动条样式 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | &::-webkit-scrollbar { 6 | // 滚动条整体样式 7 | width: 6px; 8 | height: 6px; 9 | } 10 | &::-webkit-scrollbar-thumb { 11 | // 滚动条里面的条 12 | border-radius: 6px; 13 | box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); 14 | background: #3477f2; 15 | } 16 | &::-webkit-scrollbar-track { 17 | // 滚动条里面轨道 18 | border-radius: 10px; 19 | background: #ffffff; 20 | } 21 | } 22 | html, 23 | body, 24 | #app { 25 | height: 100%; 26 | margin: 0; 27 | padding: 0; 28 | box-sizing: border-box; 29 | } 30 | body { 31 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, 32 | Microsoft YaHei, Arial, sans-serif; 33 | } 34 | *, 35 | *:before, 36 | *:after { 37 | box-sizing: inherit; 38 | } 39 | input::-webkit-outer-spin-button, 40 | input::-webkit-inner-spin-button { 41 | -webkit-appearance: none; 42 | } 43 | a:focus, 44 | a:active { 45 | outline: none; 46 | } 47 | a, 48 | a:focus, 49 | a:hover { 50 | cursor: pointer; 51 | text-decoration: none; 52 | } 53 | div:focus { 54 | outline: none; 55 | } 56 | .block { 57 | display: block; 58 | } 59 | .pointer { 60 | cursor: pointer; 61 | } 62 | ul { 63 | margin: 0; 64 | padding: 0; 65 | } 66 | li { 67 | list-style: none; 68 | } 69 | .w_100 { 70 | width: 100%; 71 | } 72 | .h_100 { 73 | height: 100%; 74 | } 75 | .bg_fff { 76 | background-color: #fff; 77 | } 78 | .of { 79 | overflow: hidden; 80 | } 81 | .pl_5 { 82 | padding-left: 5px; 83 | } 84 | .pt_10 { 85 | padding-top: 10px; 86 | } 87 | .pr_10 { 88 | padding-right: 10px; 89 | } 90 | .pb_10 { 91 | padding-bottom: 10px; 92 | } 93 | .pl_10 { 94 | padding-left: 10px; 95 | } 96 | .pt_20 { 97 | padding-top: 20px; 98 | } 99 | .pr_20 { 100 | padding-right: 20px; 101 | } 102 | .pb_20 { 103 | padding-bottom: 20px; 104 | } 105 | .pl_20 { 106 | padding-left: 20px; 107 | } 108 | .mt_10 { 109 | margin-top: 10px; 110 | } 111 | .mr_10 { 112 | margin-right: 10px; 113 | } 114 | .mb_10 { 115 | margin-bottom: 10px; 116 | } 117 | .ml_10 { 118 | margin-left: 10px; 119 | } 120 | .mt_20 { 121 | margin-top: 20px; 122 | } 123 | .mr_20 { 124 | margin-right: 20px; 125 | } 126 | .mb_20 { 127 | margin-bottom: 20px; 128 | } 129 | .ml_20 { 130 | margin-left: 20px; 131 | } 132 | .center { 133 | margin: 0 auto; 134 | } 135 | .t_center { 136 | text-align: center; 137 | } 138 | .t_justify { 139 | text-align: justify; 140 | } 141 | .t_right { 142 | text-align: right; 143 | } 144 | .t_nowrap { 145 | overflow: hidden; 146 | white-space: nowrap; 147 | } 148 | .t_ellipse { 149 | overflow: hidden; 150 | white-space: nowrap; 151 | text-overflow: ellipsis; 152 | } 153 | .bold { 154 | font-weight: bold; 155 | } 156 | .c_333 { 157 | color: #333; 158 | } 159 | .c_666 { 160 | color: #666; 161 | } 162 | .c_fff { 163 | color: #fff; 164 | } 165 | .c_3477F2 { 166 | color: #3477f2; 167 | } 168 | .border_t { 169 | border-top: 1px solid #dedede; 170 | } 171 | .border_r { 172 | border-right: 1px solid #dedede; 173 | } 174 | .border_b { 175 | border-bottom: 1px solid #dedede; 176 | } 177 | .border_l { 178 | border-left: 1px solid #dedede; 179 | } 180 | .fontsize_12 { 181 | font-size: 12px; 182 | } 183 | .fontsize_13 { 184 | font-size: 13px; 185 | } 186 | .fontsize_14 { 187 | font-size: 14px; 188 | } 189 | .fontsize_16 { 190 | font-size: 16px; 191 | } 192 | .fontsize_18 { 193 | font-size: 18px; 194 | } 195 | .fontsize_20 { 196 | font-size: 20px; 197 | } 198 | .flex { 199 | display: flex; 200 | } 201 | // 水平 垂直 居中 202 | .flex_center { 203 | justify-content: center; 204 | align-items: center; 205 | } 206 | // 垂直对齐 居中 207 | .flex_align_center { 208 | align-items: center; 209 | } 210 | // 垂直对齐 底部 211 | .flex_align_bottom { 212 | align-items: flex-end; 213 | } 214 | // 水平对齐 两端对齐 215 | .flex_justify_between { 216 | justify-content: space-between; 217 | } 218 | // 水平对齐 项目居中对齐 219 | .flex_justify_center { 220 | justify-content: center; 221 | } 222 | // 水平对齐 项目环绕对齐 223 | .flex_justify_around { 224 | justify-content: space-around; 225 | } 226 | // 水平对齐 项目靠右对齐 227 | .flex_content_end { 228 | justify-content: flex-end; 229 | } 230 | // 子元素垂直排列 231 | .flex_direction_column { 232 | flex-direction: column; 233 | } 234 | // 子元素换行 235 | .flex_wrap { 236 | flex-wrap: wrap; 237 | } 238 | -------------------------------------------------------------------------------- /mock/table.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | url: `/module/table/list1`, 4 | method: "post", 5 | response: () => { 6 | return { 7 | header: { 8 | code: 0, 9 | msg: "OK", 10 | }, 11 | body: { 12 | data: [ 13 | { 14 | id: "1", 15 | name: "小明1", 16 | role: "管理员", 17 | }, 18 | { 19 | id: "2", 20 | name: "小明2", 21 | role: "管理员", 22 | }, 23 | { 24 | id: "3", 25 | name: "小明3", 26 | role: "管理员", 27 | }, 28 | { 29 | id: "4", 30 | name: "小明4", 31 | role: "管理员", 32 | }, 33 | { 34 | id: "5", 35 | name: "小明5", 36 | role: "管理员", 37 | }, 38 | { 39 | id: "6", 40 | name: "小明6", 41 | role: "管理员", 42 | }, 43 | { 44 | id: "7", 45 | name: "小明7", 46 | role: "管理员", 47 | }, 48 | { 49 | id: "8", 50 | name: "小明8", 51 | role: "管理员", 52 | }, 53 | { 54 | id: "9", 55 | name: "小明9", 56 | role: "管理员", 57 | }, 58 | { 59 | id: "10", 60 | name: "小明10", 61 | role: "管理员", 62 | }, 63 | ], 64 | totalCount: 14, 65 | }, 66 | }; 67 | }, 68 | }, 69 | { 70 | url: `/module/table/list2`, 71 | method: "post", 72 | response: (req) => { 73 | const data = [ 74 | { 75 | id: "1", 76 | name: "小明1", 77 | role: "管理员", 78 | }, 79 | { 80 | id: "2", 81 | name: "小明2", 82 | role: "管理员", 83 | }, 84 | { 85 | id: "3", 86 | name: "小明3", 87 | role: "管理员", 88 | }, 89 | { 90 | id: "4", 91 | name: "小明4", 92 | role: "管理员", 93 | }, 94 | { 95 | id: "5", 96 | name: "小明5", 97 | role: "管理员", 98 | }, 99 | { 100 | id: "6", 101 | name: "小明6", 102 | role: "管理员", 103 | }, 104 | { 105 | id: "7", 106 | name: "小明7", 107 | role: "管理员", 108 | }, 109 | { 110 | id: "8", 111 | name: "小明8", 112 | role: "管理员", 113 | }, 114 | { 115 | id: "9", 116 | name: "小明9", 117 | role: "管理员", 118 | }, 119 | { 120 | id: "10", 121 | name: "小明10", 122 | role: "管理员", 123 | }, 124 | { 125 | id: "11", 126 | name: "小明10", 127 | role: "管理员", 128 | }, 129 | { 130 | id: "12", 131 | name: "小明10", 132 | role: "管理员", 133 | }, 134 | { 135 | id: "13", 136 | name: "小明10", 137 | role: "管理员", 138 | }, 139 | { 140 | id: "14", 141 | name: "小明10", 142 | role: "管理员", 143 | }, 144 | ], 145 | pageNo = req.body.pageNo, 146 | pageSize = req.body.pageSize; 147 | const end = pageNo * pageSize; 148 | return { 149 | header: { 150 | code: 0, 151 | msg: "OK", 152 | }, 153 | body: { 154 | data: data.slice(end - pageSize, end), 155 | totalCount: data.length, 156 | }, 157 | }; 158 | }, 159 | }, 160 | { 161 | url: `/module/table/itemInfo`, 162 | method: "post", 163 | response: (req) => { 164 | return { 165 | header: { 166 | code: 0, 167 | msg: "OK", 168 | }, 169 | body: { 170 | data: { 171 | id: req.body.id, 172 | name: "小明", 173 | role: "管理员", 174 | }, 175 | }, 176 | }; 177 | }, 178 | }, 179 | ]; 180 | -------------------------------------------------------------------------------- /src/layout/components/NavBar/VisitedViews.vue: -------------------------------------------------------------------------------- 1 | 35 | 140 | 176 | -------------------------------------------------------------------------------- /public/tinymce/zh_CN.js: -------------------------------------------------------------------------------- 1 | window.tinymce.addI18n("zh_CN", { 2 | Redo: "恢复", 3 | Undo: "撤销", 4 | Cut: "剪切", 5 | Copy: "复制", 6 | Paste: "粘贴", 7 | "Select all": "全选", 8 | "New document": "新建文档", 9 | Ok: "确定", 10 | Cancel: "取消", 11 | "Visual aids": "网格线", 12 | Bold: "粗体", 13 | Italic: "斜体", 14 | Underline: "下划线", 15 | Strikethrough: "删除线", 16 | Superscript: "上标", 17 | Subscript: "下标", 18 | "Clear formatting": "清除格式", 19 | "Align left": "左对齐", 20 | "Align center": "居中", 21 | "Align right": "右对齐", 22 | Justify: "两端对齐", 23 | "Bullet list": "符号列表", 24 | "Numbered list": "数字列表", 25 | "Decrease indent": "减少缩进", 26 | "Increase indent": "增加缩进", 27 | Close: "关闭", 28 | Formats: "格式", 29 | "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": 30 | "当前浏览器不支持访问剪贴板,请使用快捷键Ctrl+X/C/V复制粘贴", 31 | Headers: "标题", 32 | "Header 1": "标题1", 33 | "Header 2": "标题2", 34 | "Header 3": "标题3", 35 | "Header 4": "标题4", 36 | "Header 5": "标题5", 37 | "Header 6": "标题6", 38 | Headings: "标题", 39 | "Heading 1": "标题1", 40 | "Heading 2": "标题2", 41 | "Heading 3": "标题3", 42 | "Heading 4": "标题4", 43 | "Heading 5": "标题5", 44 | "Heading 6": "标题6", 45 | Preformatted: "预格式化", 46 | Div: "Div区块", 47 | Pre: "预格式文本", 48 | Code: "代码", 49 | Paragraph: "段落", 50 | Blockquote: "引用", 51 | Inline: "文本", 52 | Blocks: "区块", 53 | "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": 54 | "当前为纯文本粘贴模式,再次点击可以回到普通粘贴模式。", 55 | Fonts: "字体", 56 | "Font Sizes": "字号", 57 | Class: "Class", 58 | "Browse for an image": "浏览图像", 59 | OR: "或", 60 | "Drop an image here": "拖放一张图片文件至此", 61 | Upload: "上传", 62 | Block: "块", 63 | Align: "对齐", 64 | Default: "默认", 65 | Circle: "空心圆", 66 | Disc: "实心圆", 67 | Square: "方块", 68 | "Lower Alpha": "小写英文字母", 69 | "Lower Greek": "小写希腊字母", 70 | "Lower Roman": "小写罗马字母", 71 | "Upper Alpha": "大写英文字母", 72 | "Upper Roman": "大写罗马字母", 73 | "Anchor...": "锚点...", 74 | Name: "名称", 75 | Id: "id", 76 | "Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": 77 | "id应该以字母开头,后跟字母、数字、横线、点、冒号或下划线。", 78 | "You have unsaved changes are you sure you want to navigate away?": 79 | "你对文档的修改尚未保存,确定离开吗?", 80 | "Restore last draft": "恢复上次的草稿", 81 | "Special characters...": "特殊字符...", 82 | "Source code": "HTML源码", 83 | "Insert/Edit code sample": "插入/编辑代码示例", 84 | Language: "语言", 85 | "Code sample...": "代码示例...", 86 | "Color Picker": "选取颜色", 87 | R: "R", 88 | G: "G", 89 | B: "B", 90 | "Left to right": "从左到右", 91 | "Right to left": "从右到左", 92 | "Emoticons...": "表情符号...", 93 | "Metadata and Document Properties": "元数据和文档属性", 94 | Title: "标题", 95 | Keywords: "关键词", 96 | Description: "描述", 97 | Robots: "机器人", 98 | Author: "作者", 99 | Encoding: "编码", 100 | Fullscreen: "全屏", 101 | Action: "操作", 102 | Shortcut: "快捷键", 103 | Help: "帮助", 104 | Address: "地址", 105 | "Focus to menubar": "移动焦点到菜单栏", 106 | "Focus to toolbar": "移动焦点到工具栏", 107 | "Focus to element path": "移动焦点到元素路径", 108 | "Focus to contextual toolbar": "移动焦点到上下文菜单", 109 | "Insert link (if link plugin activated)": "插入链接 (如果链接插件已激活)", 110 | "Save (if save plugin activated)": "保存(如果保存插件已激活)", 111 | "Find (if searchreplace plugin activated)": "查找(如果查找替换插件已激活)", 112 | "Plugins installed ({0}):": "已安装插件 ({0}):", 113 | "Premium plugins:": "优秀插件:", 114 | "Learn more...": "了解更多...", 115 | "You are using {0}": "你正在使用 {0}", 116 | Plugins: "插件", 117 | "Handy Shortcuts": "快捷键", 118 | "Horizontal line": "水平分割线", 119 | "Insert/edit image": "插入/编辑图片", 120 | "Image description": "图片描述", 121 | Source: "地址", 122 | Dimensions: "大小", 123 | "Constrain proportions": "保持宽高比", 124 | General: "常规", 125 | Advanced: "高级", 126 | Style: "样式", 127 | "Vertical space": "垂直边距", 128 | "Horizontal space": "水平边距", 129 | Border: "边框", 130 | "Insert image": "插入图片", 131 | "Image...": "图片...", 132 | "Image list": "图片列表", 133 | "Rotate counterclockwise": "逆时针旋转", 134 | "Rotate clockwise": "顺时针旋转", 135 | "Flip vertically": "垂直翻转", 136 | "Flip horizontally": "水平翻转", 137 | "Edit image": "编辑图片", 138 | "Image options": "图片选项", 139 | "Zoom in": "放大", 140 | "Zoom out": "缩小", 141 | Crop: "裁剪", 142 | Resize: "调整大小", 143 | Orientation: "方向", 144 | Brightness: "亮度", 145 | Sharpen: "锐化", 146 | Contrast: "对比度", 147 | "Color levels": "色阶", 148 | Gamma: "伽马值", 149 | Invert: "反转", 150 | Apply: "应用", 151 | Back: "后退", 152 | "Insert date/time": "插入日期/时间", 153 | "Date/time": "日期/时间", 154 | "Insert/Edit Link": "插入/编辑链接", 155 | "Insert/edit link": "插入/编辑链接", 156 | "Text to display": "显示文字", 157 | Url: "地址", 158 | "Open link in...": "链接打开方式...", 159 | "Current window": "当前窗口打开", 160 | None: "在当前窗口/框架打开", 161 | "New window": "在新窗口打开", 162 | "Remove link": "删除链接", 163 | Anchors: "锚点", 164 | "Link...": "链接...", 165 | "Paste or type a link": "粘贴或输入链接", 166 | "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": 167 | "你所填写的URL地址为邮件地址,需要加上mailto:前缀吗?", 168 | "The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": 169 | "你所填写的URL地址属于外部链接,需要加上http://:前缀吗?", 170 | "Link list": "链接列表", 171 | "Insert video": "插入视频", 172 | "Insert/edit video": "插入/编辑视频", 173 | "Insert/edit media": "插入/编辑媒体", 174 | "Alternative source": "替代资源", 175 | "Alternative image URL": "资源备用地址", 176 | "Media poster (Image URL)": "封面(图片地址)", 177 | "Paste your embed code below:": "将内嵌代码粘贴在下面:", 178 | Embed: "内嵌", 179 | "Media...": "多媒体...", 180 | "Nonbreaking space": "不间断空格", 181 | "Page break": "分页符", 182 | "Paste as text": "粘贴为文本", 183 | Preview: "预览", 184 | "Print...": "打印...", 185 | Save: "保存", 186 | Find: "查找", 187 | "Replace with": "替换为", 188 | Replace: "替换", 189 | "Replace all": "替换全部", 190 | Previous: "上一个", 191 | Next: "下一个", 192 | "Find and replace...": "查找并替换...", 193 | "Could not find the specified string.": "未找到搜索内容。", 194 | "Match case": "区分大小写", 195 | "Find whole words only": "全单词匹配", 196 | "Spell check": "拼写检查", 197 | Ignore: "忽略", 198 | "Ignore all": "忽略全部", 199 | Finish: "完成", 200 | "Add to Dictionary": "添加到字典", 201 | "Insert table": "插入表格", 202 | "Table properties": "表格属性", 203 | "Delete table": "删除表格", 204 | Cell: "单元格", 205 | Row: "行", 206 | Column: "列", 207 | "Cell properties": "单元格属性", 208 | "Merge cells": "合并单元格", 209 | "Split cell": "拆分单元格", 210 | "Insert row before": "在上方插入", 211 | "Insert row after": "在下方插入", 212 | "Delete row": "删除行", 213 | "Row properties": "行属性", 214 | "Cut row": "剪切行", 215 | "Copy row": "复制行", 216 | "Paste row before": "粘贴到上方", 217 | "Paste row after": "粘贴到下方", 218 | "Insert column before": "在左侧插入", 219 | "Insert column after": "在右侧插入", 220 | "Delete column": "删除列", 221 | Cols: "列", 222 | Rows: "行", 223 | Width: "宽", 224 | Height: "高", 225 | "Cell spacing": "单元格外间距", 226 | "Cell padding": "单元格内边距", 227 | "Show caption": "显示标题", 228 | Left: "左对齐", 229 | Center: "居中", 230 | Right: "右对齐", 231 | "Cell type": "单元格类型", 232 | Scope: "范围", 233 | Alignment: "对齐方式", 234 | "H Align": "水平对齐", 235 | "V Align": "垂直对齐", 236 | Top: "顶部对齐", 237 | Middle: "垂直居中", 238 | Bottom: "底部对齐", 239 | "Header cell": "表头单元格", 240 | "Row group": "行组", 241 | "Column group": "列组", 242 | "Row type": "行类型", 243 | Header: "表头", 244 | Body: "表体", 245 | Footer: "表尾", 246 | "Border color": "边框颜色", 247 | "Insert template...": "插入模板...", 248 | Templates: "模板", 249 | Template: "模板", 250 | "Text color": "文字颜色", 251 | "Background color": "背景色", 252 | "Custom...": "自定义...", 253 | "Custom color": "自定义颜色", 254 | "No color": "无", 255 | "Remove color": "删除颜色", 256 | "Table of Contents": "目录", 257 | "Show blocks": "显示区块边框", 258 | "Show invisible characters": "显示不可见字符", 259 | "Word count": "字数统计", 260 | "Words: {0}": "字数:{0}", 261 | "{0} words": "{0} 个字", 262 | File: "文件", 263 | Edit: "编辑", 264 | Insert: "插入", 265 | View: "查看", 266 | Format: "格式", 267 | Table: "表格", 268 | Tools: "工具", 269 | "Powered by {0}": "Powered by {0}", 270 | "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": 271 | "在编辑区按ALT+F9打开菜单,按ALT+F10打开工具栏,按ALT+0查看帮助", 272 | "Image title": "图片标题", 273 | "Border width": "边框宽度", 274 | "Border style": "边框样式", 275 | Error: "错误", 276 | Warn: "警告", 277 | Valid: "有效", 278 | "To open the popup, press Shift+Enter": "此快捷为软回车(插入
)", 279 | "Rich Text Area. Press ALT-0 for help.": "编辑区. 按Alt+0键打开帮助", 280 | "System Font": "默认字体", 281 | "Failed to upload image: {0}": "图片上传失败: {0}", 282 | "Failed to load plugin: {0} from url {1}": "插件加载失败: {0} - {1}", 283 | "Failed to load plugin url: {0}": "插件加载失败: {0}", 284 | "Failed to initialize plugin: {0}": "插件初始化失败: {0}", 285 | example: "示例", 286 | Search: "查找", 287 | All: "全部", 288 | Currency: "货币", 289 | Text: "文本", 290 | Quotations: "引用", 291 | Mathematical: "数学运算符", 292 | "Extended Latin": "拉丁语扩充", 293 | Symbols: "符号", 294 | Arrows: "箭头", 295 | "User Defined": "自定义", 296 | "dollar sign": "美元", 297 | "currency sign": "货币", 298 | "euro-currency sign": "欧元", 299 | "colon sign": "冒号", 300 | "cruzeiro sign": "克鲁赛罗币", 301 | "french franc sign": "法郎", 302 | "lira sign": "里拉", 303 | "mill sign": "密尔", 304 | "naira sign": "奈拉", 305 | "peseta sign": "比塞塔", 306 | "rupee sign": "卢比", 307 | "won sign": "韩元", 308 | "new sheqel sign": "新谢克尔", 309 | "dong sign": "越南盾", 310 | "kip sign": "老挝基普", 311 | "tugrik sign": "图格里克", 312 | "drachma sign": "德拉克马", 313 | "german penny symbol": "德国便士", 314 | "peso sign": "比索", 315 | "guarani sign": "瓜拉尼", 316 | "austral sign": "澳元", 317 | "hryvnia sign": "格里夫尼亚", 318 | "cedi sign": "塞地", 319 | "livre tournois sign": "里弗弗尔", 320 | "spesmilo sign": "一千spesoj的货币符号,该货币未使用", 321 | "tenge sign": "坚戈", 322 | "indian rupee sign": "印度卢比", 323 | "turkish lira sign": "土耳其里拉", 324 | "nordic mark sign": "北欧马克", 325 | "manat sign": "马纳特", 326 | "ruble sign": "卢布", 327 | "yen character": "日元", 328 | "yuan character": "人民币元", 329 | "yuan character, in hong kong and taiwan": "元的繁体字", 330 | "yen/yuan character variant one": "元(大写)", 331 | "Loading emoticons...": "正在加载表情文字...", 332 | "Could not load emoticons": "不能加载表情文字", 333 | People: "人类", 334 | "Animals and Nature": "动物和自然", 335 | "Food and Drink": "食物和饮品", 336 | Activity: "活动", 337 | "Travel and Places": "旅游和地点", 338 | Objects: "物件", 339 | Flags: "旗帜", 340 | Characters: "字数", 341 | "Characters (no spaces)": "字数(不含空格)", 342 | "Error: Form submit field collision.": "错误: 表单提交字段冲突.", 343 | "Error: No form element found.": "错误: 未找到可用的form.", 344 | Update: "更新", 345 | "Color swatch": "颜色样本", 346 | Turquoise: "青绿", 347 | Green: "绿色", 348 | Blue: "蓝色", 349 | Purple: "紫色", 350 | "Navy Blue": "海军蓝", 351 | "Dark Turquoise": "深蓝绿色", 352 | "Dark Green": "暗绿", 353 | "Medium Blue": "中蓝", 354 | "Medium Purple": "中紫", 355 | "Midnight Blue": "深蓝", 356 | Yellow: "黄色", 357 | Orange: "橙色", 358 | Red: "红色", 359 | "Light Gray": "浅灰", 360 | Gray: "灰色", 361 | "Dark Yellow": "暗黄", 362 | "Dark Orange": "暗橙", 363 | "Dark Red": "暗红", 364 | "Medium Gray": "中灰", 365 | "Dark Gray": "深灰", 366 | Black: "黑色", 367 | White: "白色", 368 | "Switch to or from fullscreen mode": "切换全屏模式", 369 | "Open help dialog": "打开帮助对话框", 370 | history: "历史", 371 | styles: "样式", 372 | formatting: "格式化", 373 | alignment: "对齐", 374 | indentation: "缩进", 375 | "permanent pen": "记号笔", 376 | comments: "注释", 377 | Anchor: "锚点", 378 | "Special character": "特殊字符", 379 | "Code sample": "代码示例", 380 | Color: "颜色", 381 | Emoticons: "表情", 382 | "Document properties": "文档属性", 383 | Image: "图片", 384 | "Insert link": "插入链接", 385 | Target: "目标", 386 | Link: "链接", 387 | Poster: "封面", 388 | Media: "音视频", 389 | Print: "打印", 390 | Prev: "上一个", 391 | "Find and replace": "查找并替换", 392 | "Whole words": "全字匹配", 393 | Spellcheck: "拼写检查", 394 | Caption: "标题", 395 | "Insert template": "插入模板", 396 | //以下为补充汉化内容 by 莫若卿 397 | "Code view": "代码区域", 398 | "Select...": "选择...", 399 | "Format Painter": "格式刷", 400 | "No templates defined.": "无内置模板", 401 | "Special character...": "特殊字符...", 402 | "Open link": "打开链接", 403 | Count: "统计", 404 | Document: "整个文档", 405 | Selection: "选取部分", 406 | Words: "字词数", 407 | "{0} characters": "{0} 个字符", 408 | "Alternative source URL": "替代资源地址", 409 | "Alternative description": "替代说明文字", 410 | Accessibility: "可访问性", 411 | "Image is decorative": "仅用于装饰", 412 | "Line height": "行高", 413 | "Cut column": "剪切列", 414 | "Copy column": "复制列", 415 | "Paste column before": "粘贴到前方", 416 | "Paste column after": "粘贴到后方", 417 | Version: "版本", 418 | "Keyboard Navigation": "键盘导航", 419 | "Open popup menu for split buttons": "该组合键的作用是软回车(插入br)", 420 | }); 421 | --------------------------------------------------------------------------------