├── .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 |
2 |
3 |
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 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/views/NestRoute/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | NODE_ENV = "production"
2 | VITE_BASE_API = "production"
3 |
4 |
--------------------------------------------------------------------------------
/src/views/Table/Child/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/src/views/NestRoute/Child2/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
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 |
2 | Table1
3 |
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 |
2 | Child1-Template1
3 |
4 |
5 |
11 |
--------------------------------------------------------------------------------
/src/views/NestRoute/Child1/GrandSon/Template2.vue:
--------------------------------------------------------------------------------
1 |
2 | Child1-Template2
3 |
4 |
5 |
11 |
--------------------------------------------------------------------------------
/src/views/NestRoute/Child1/GrandSon/Template3.vue:
--------------------------------------------------------------------------------
1 |
2 | Child1-Template3
3 |
4 |
5 |
11 |
--------------------------------------------------------------------------------
/src/views/NestRoute/Child2/GrandSon/Template1.vue:
--------------------------------------------------------------------------------
1 |
2 | Child2-Template1
3 |
4 |
5 |
11 |
--------------------------------------------------------------------------------
/src/views/NestRoute/Child2/GrandSon/Template2.vue:
--------------------------------------------------------------------------------
1 |
2 | Child2-Template2
3 |
4 |
5 |
11 |
--------------------------------------------------------------------------------
/src/views/NestRoute/Child2/GrandSon/Template3.vue:
--------------------------------------------------------------------------------
1 |
2 | Child2-Template3
3 |
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 |
3 |
4 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
23 |
--------------------------------------------------------------------------------
/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ msg }}
3 | {{ count }}
4 | plus
5 |
6 |
7 |
23 |
30 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
您访问的页面不存在
5 |
返回首页
8 |
9 |
10 |
17 |
27 |
--------------------------------------------------------------------------------
/src/layout/components/SliderBar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Vue3-element-admin
5 |
6 |
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 |
2 |
3 |

4 |
7 |
8 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
28 |
--------------------------------------------------------------------------------
/src/views/User/Info.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 头像:
5 |
6 |
7 |
8 | 姓名:
9 | {{ userInfo.name }}
10 |
11 |
12 | 角色:
13 | {{ userInfo.roles.join(" | ") }}
14 |
15 |
16 |
17 |
28 |
--------------------------------------------------------------------------------
/src/layout/components/NavBar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
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 |
2 |
3 |
表格1-列表1-编辑
4 |
姓名:{{ userInfo.name }}
5 |
角色:{{ userInfo.role }}
6 |
7 |
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 |
2 |
3 |
4 |
5 |
6 | {{
7 | item.meta.title
8 | }}
9 |
10 |
11 |
12 |
13 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | 个人中心
8 | 退出登录
9 |
10 |
11 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
2 |
3 |
4 |
5 |
16 |
17 |
18 |
19 |
20 | {{ (item.meta && item.meta.title) || item.name }}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ (item.meta && item.meta.title) || item.name }}
31 |
32 |
33 |
34 |
35 |
56 |
65 |
--------------------------------------------------------------------------------
/src/views/Table/Child/List1/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 编辑
13 | 删除
16 |
17 |
18 |
19 |
20 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 | 删除
19 |
20 |
21 |
22 |
28 |
29 |
30 |
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 |
2 |
3 |
9 |
10 |
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 |
2 |
6 |
7 |
8 |
登录
9 |
10 |
11 |
16 |
17 |
18 |
25 |
26 | 登录
27 |
28 |
29 |
30 |
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 |
2 |
3 |
4 |
10 | {{ view.meta.title }}
11 |
16 |
17 |
18 |
33 |
34 |
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 |
--------------------------------------------------------------------------------