数字档案管理系统
首页
操作手册
部署手册
微服务框架
首页
操作手册
部署手册
微服务框架
  • 简介
  • 快速上手
  • 项目介绍
  • 架构设计
  • 前端开发

    • 开发规范
      • 优先级 B 的规则:强烈推荐 (增强代码可读性)
        • 组件文件强烈推荐
        • 反例
        • 好例子
        • 单文件组件文件的大小写强烈推荐
        • 反例
        • 好例子
        • 基础组件名称强烈推荐
        • 反例
        • 好例子
        • 单组件名称强烈推荐
        • 反例
        • 好例子
        • 紧密耦合的组件名称强烈推荐
        • 反例
        • 好例子
        • 组件名称中的单词顺序强烈推荐
        • 反例
        • 好例子
        • 自闭合组件强烈推荐
        • 反例
        • 好例子
        • 模板中的组件名称大小写强烈推荐
        • 反例
        • 好例子
        • JS/JSX 中使用的组件名称强烈推荐
        • 反例
        • 好例子
        • 完整单词的组件名称强烈推荐
        • 反例
        • 好例子
        • Prop 名称强烈推荐
        • 反例
        • 好例子
        • 多个 attribute 的元素强烈推荐
        • 反例
        • 好例子
        • 模板中的简单表达式强烈推荐
        • 反例
        • 好例子
        • 简单的计算属性强烈推荐
        • 反例
        • 好例子
        • 带引号的 attribute 值强烈推荐
        • 反例
        • 好例子
        • 指令缩写强烈推荐
        • 反例
        • 好例子
      • 优先级 C 的规则:推荐 (将选择和认知成本最小化)
        • 组件/实例的选项顺序推荐
        • 元素 attribute 的顺序推荐
        • 组件/实例选项中的空行推荐
        • 好例子
        • 单文件组件的顶级元素的顺序推荐
        • 反例
        • 好例子
      • 优先级 D 的规则:谨慎使用 (潜在风险)
        • scoped 中的元素选择器谨慎使用
        • 反例
        • 好例子
        • 隐性的父子组件通信谨慎使用
        • 反例
        • 好例子
        • 非 Flux 的全局状态管理谨慎使用
        • 反例
        • 好例子
        • 常见问题
        • alias @ 不起作用
        • Prettier失效
        • 快速构建一个页面
        • 创建页面vue文件
        • 配置国际化
        • 添加菜单
        • 运行效果
        • 快速构建XXX模块
        • 创建XXX管理
        • 配置国际化
        • 编写表格
    • 全局配置
  • 后端开发
  • 生产部署
  • 帮助文档与常见问题
  • 测试手册
  • 前端开发
数字档案管理系统
2025-04-17
目录

开发规范

# 前端编码规范

本项目已集成 eslint 和 prettier 插件,WebStorm 可使用 Ctrl + shift + alt + P 进行代码格式化,无法进行格式化的代码请参照 Terminal 提示信息进行修改

# 规则类别

# 优先级 A:必要的

这些规则会帮你规避错误,所以学习并接受它们带来的全部代价吧。这里面可能存在例外,但应该非常少,且只有你同时精通 JavaScript 和 Vue 才可以这样做。

# 优先级 B:强烈推荐

这些规则能够在绝大多数工程中改善可读性和开发体验。即使你违反了,代码还是能照常运行,但例外应该尽可能少且有合理的理由。

# 优先级 C:推荐

当存在多个同样好的选项,选任意一个都可以确保一致性。在这些规则里,我们描述了每个选项并建议一个默认的选择。也就是说只要保持一致且理由充分,你可以随意在你的代码库中做出不同的选择。请务必给出一个好的理由!通过接受社区的标准,你将会:

  1. 训练你的大脑,以便更容易的处理你在社区遇到的代码;
  2. 不做修改就可以直接复制粘贴社区的代码示例;
  3. 能够经常招聘到和你编码习惯相同的新人,至少跟 Vue 相关的东西是这样的。

# 优先级 D:谨慎使用

有些 Vue 特性的存在是为了照顾极端情况或帮助老代码的平稳迁移。当被过度使用时,这些特性会让你的代码难于维护甚至变成 bug 的来源。这些规则是为了给有潜在风险的特性敲个警钟,并说明它们什么时候不应该使用以及为什么。

# 优先级 A 的规则:必要的 (规避错误)

# 代码结构目录必要

目录结构参照

sofast-web
├── babel.config.js                                // Babel配置文件
├── package-lock.json
├── package.json
├── postcss.config.js                              // PostCss配置文件
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── static                                     // 静态文件目录
│       └── NEditor                                // 富文本编辑器(基于UEditor)
├── src
│   ├── App.vue
│   ├── api                                        // 配置后端接口
│   ├── assets                                     // 图片
│   ├── components                                 // 共通组件目录
│   ├── filters                                    // 自定义过滤器
│   ├── icons
│   │   └── svg                                    // 存放SVG图标
│   ├── i18n                                       // 国际化目录
│   │   ├── common                                 // 共通Label,Message配置
│   │   └── ...                                    // 业务模块Label,Message配置
│   ├── layout                                     // 全局布局
│   │   ├── components
│   │   │   ├── AppMain.vue                        // 中间内容区
│   │   │   ├── Navbar.vue                         // 头部Header
│   │   │   ├── Sidebar                            // 左侧菜单栏
│   │   │   ├── TagsView                           // 标签页
│   │   │   └── index.js
│   │   ├── index.vue
│   │   └── mixin
│   │       └── ResizeHandler.js                   // 响应式混入(区分desktop和mobile)
│   ├── main.js                                    // Vue入口文件
│   ├── mixins                                     // 自定义混入
│   ├── permission.js                              // 权限控制
│   ├── router                                     // 路由定义
│   ├── settings.js                                // 通用设置
│   ├── store                                      // Vuex
│   │   ├── modules                                // vuex模块
│   │   ├── getters.js                             
│   │   └── index.js                               
│   ├── styles                                     // 样式文件
│   ├── themes                                     // 主题
│   │   └── default                                // 主题文件夹(默认)
│   ├── utils                                      // 工具
│   │   ├── auth.js                                // token工具
│   │   ├── base64Util.js                          // base64加密解密
│   │   ├── get-page-title.js                      // 获取页面title
│   │   ├── request.js                             // axios实例
│   │   ├── scroll-to.js                           // 滚动动画
│   │   ├── treeUtils.js                           // 树形数据工具(Array2Tree)
│   │   └── validate.js                            // 校验工具
│   └── views
│   │   ├── 404.vue                                // 404页面
│   │   ├── contentMgt                             // 内容管理模块
│   │   ├── dashboard                              // 首页
│   │   ├── login                                  // 登录页
│   │   ├── system                                 // 系统管理模块
│   │   │   ├── apiMgt                             // API管理
│   │   │   ├── dictionaryMgt                      // 字典管理
│   │   │   ├── log                                // 日志
│   │   │   ├── menuMgt                            // 菜单管理
│   │   │   ├── organizationMgt                    // 组织机构管理
│   │   │   ├── permissionMgt                      // 权限管理
│   │   │   ├── roleMgt                            // 角色管理
│   │   │   └── userMgt                            // 用户管理
│   │   └── userProfile                            // 个人中心
└── vue.config.js                                  // vue-cli3配置文件
└── .env.development                               // 开发环境变量配置
└── .env.staging                                   // 测试环境变量配置
└── .env.production                                // 生产环境变量配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# 组件名为多个单词必要

组件名应该始终是多个单词的,根组件 App 以及 <transition> 、 <component> 之类的 Vue 内置组件除外。

这样做可以避免跟现有的以及未来的 HTML 元素相冲突 (opens new window),因为所有的 HTML 元素名称都是单个单词的。

# 反例

app.component('todo', {
  // ...
})
export default {
  name: 'Todo',
  // ...
}
1
2
3
4
5
6
7
1
2
3
4
5
6
7

# 好例子

app.component('todo-item', {
  // ...
})
export default {
  name: 'TodoItem',
  // ...
}
1
2
3
4
5
6
7
1
2
3
4
5
6
7

# Prop 定义必要

Prop 定义应尽量详细

在你提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。

详解 细致的 [prop 定义](http://doc.yangyanqing.cn:4000/book_so-fast-cloud/guide/component-props.html#prop-验证)有两个好处:
  • 它们写明了组件的 API,所以很容易看懂组件的用法;
  • 在开发环境下,如果向一个组件提供格式不正确的 prop,Vue 将会告警,以帮助你捕获潜在的错误来源。 :::

# 反例

// 这样做只有开发原型系统时可以接受
props: ['status']
1
2
1
2

# 好例子

props: {
  status: String
}
// 更好的例子
props: {
  status: {
    type: String,
    required: true,

    validator: value => {
      return [
        'syncing',
        'synced',
        'version-conflict',
        'error'
      ].includes(value)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 为 v-for 设置 key 值必要

总是用 key 配合 v-for

在组件上总是必须用 key 配合 v-for ,以便维护内部组件及其子树的状态。甚至在元素上维护可预测的行为,比如动画中的对象固化 (object constancy) (opens new window) ,也是一种好的做法。

详解 假设你有一个待办事项列表:
data() {
  return {
    todos: [
      {
        id: 1,
        text: 'Learn to use v-for'
      },
      {
        id: 2,
        text: 'Learn to use key'
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14

然后你把它们按照字母顺序排序。在更新 DOM 的时候,Vue 将会优化渲染把可能的 DOM 变更降到最低。即可能删掉第一个待办事项元素,然后把它重新加回到列表的最末尾。

这里的问题在于,不要删除仍然会留在 DOM 中的元素。比如你想使用 <transition-group> 给列表加过渡动画,或想在被渲染元素是 <input> 时保持聚焦。在这些情况下,为每一个项目添加一个唯一的键值 (比如 :key="todo.id" ) 将会让 Vue 知道如何使行为更容易预测。

根据我们的经验,最好始终添加一个唯一的键值,以便你和你的团队永远不必担心这些极端情况。也在少数对性能有严格要求的情况下,为了避免对象固化,你可以刻意做一些非常规的处理。 :::

# 反例

<ul>
  <li v-for="todo in todos">
    {{ todo.text }}
  </li>
</ul>
1
2
3
4
5
1
2
3
4
5

# 好例子

<ul>
  <li
    v-for="todo in todos"
    :key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8

# 避免 v-if 和 v-for 一起使用必要

永远不要把 v-if 和 v-for 同时用在同一个元素上。

一般我们在两种常见的情况下会倾向于这样做:

  • 为了过滤一个列表中的项目 (比如 v-for="user in users" v-if="user.isActive" )。在这种情形下,请将 users 替换为一个计算属性 (比如 activeUsers ),让其返回过滤后的列表。
  • 为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers" )。这种情形下,请将 v-if 移动至容器元素上 (比如 ul 、 ol )。
详解

当 Vue 处理指令时, v-if 比 v-for 具有更高的优先级,所以这个模板:

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

这将抛出一个错误,因为 v-if 指令将首先被使用,而迭代的变量 user 此时不存在。

这可以通过迭代一个计算过的 property 来解决,就像这样:

computed: {
  activeUsers() {
    return this.users.filter(user => user.isActive)
  }
}
<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13

另外,我们也可以使用 <template> 标签和 v-for 来包装 <li> 元素。

<ul>
  <template v-for="user in users" :key="user.id">
    <li v-if="user.isActive">
      {{ user.name }}
    </li>
  </template>
</ul>
1
2
3
4
5
6
7
1
2
3
4
5
6
7

# 反例

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

# 好例子

<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>
<ul>
  <template v-for="user in users" :key="user.id">
    <li v-if="user.isActive">
      {{ user.name }}
    </li>
  </template>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 为组件样式设置作用域必要

对于应用来说,顶层 App 组件和布局组件中的样式可以是全局的,但是其它所有组件都应该是有作用域的。

这条规则只和单文件组件 (opens new window)有关。你不一定要使用 scoped attribute (opens new window)。设置作用域也可以通过 CSS Modules (opens new window),那是一个基于 class 的类似 BEM (opens new window) 的策略,当然你也可以使用其它的库或约定。

不管怎样,对于组件库,我们应该更倾向于选用基于 class 的策略而不是 scoped attribute。

这让覆写内部样式更容易:使用了人类可理解的 class 名称且没有太高的选择器优先级,而且不太会导致冲突。

详解 如果你和其他开发者一起开发一个大型工程,或有时引入三方 HTML/CSS (比如来自 Auth0),设置一致的作用域会确保你的样式只会运用在它们想要作用的组件上。

不止要使用 scoped attribute,使用唯一的 class 名可以帮你确保那些三方库的 CSS 不会运用在你自己的 HTML 上。比如许多工程都使用了 button 、 btn 或 icon class 名,所以即便你不使用类似 BEM 的策略,添加一个 app 专属或组件专属的前缀 (比如 ButtonClose-icon ) 也可以提供很多保护。

# 反例

<template>
  <button class="btn btn-close">×</button>
</template>

<style>
.btn-close {
  background-color: red;
}
</style>
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

# 好例子

<template>
  <button class="button button-close">×</button>
</template>

<!-- 使用 `scoped` attribute -->
<style scoped>
.button {
  border: none;
  border-radius: 2px;
}

.button-close {
  background-color: red;
}
</style>
<template>
  <button :class="[$style.button, $style.buttonClose]">×</button>
</template>

<!-- 使用 CSS modules -->
<style module>
.button {
  border: none;
  border-radius: 2px;
}

.buttonClose {
  background-color: red;
}
</style>
<template>
  <button class="c-Button c-Button--close">×</button>
</template>

<!-- 使用 BEM 约定 -->
<style>
.c-Button {
  border: none;
  border-radius: 2px;
}

.c-Button--close {
  background-color: red;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 私有 property 名称必要

使用模块作用域保持不允许外部访问的函数的私有性。如果无法做到这一点,就始终为插件、mixin 等不考虑作为对外公共 API 的自定义私有 property 使用 $_ 前缀。并附带一个命名空间以回避和其它作者的冲突 (比如 $_yourPluginName_ )。

详解

Vue 使用 _ 前缀来定义其自身的私有 property,所以使用相同的前缀 (比如 _update ) 有覆写实例 property 的风险。即便你检查确认 Vue 当前版本没有用到这个 property 名,也不能保证和将来的版本没有冲突。

对于 $ 前缀来说,其在 Vue 生态系统中的目的是暴露给用户的一个特殊的实例 property,所以把它用于私有 property 并不合适。

不过,我们推荐把这两个前缀结合为 $_ ,作为一个用户定义的私有 property 的约定,以确保不会和 Vue 自身相冲突。

# 反例

const myGreatMixin = {
  // ...
  methods: {
    update() {
      // ...
    }
  }
}
const myGreatMixin = {
  // ...
  methods: {
    _update() {
      // ...
    }
  }
}
const myGreatMixin = {
  // ...
  methods: {
    $update() {
      // ...
    }
  }
}
const myGreatMixin = {
  // ...
  methods: {
    $_update() {
      // ...
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 好例子

const myGreatMixin = {
  // ...
  methods: {
    $_myGreatMixin_update() {
      // ...
    }
  }
}
// Even better!
const myGreatMixin = {
  // ...
  methods: {
    publicMethod() {
      // ...
      myPrivateFunction()
    }
  }
}

function myPrivateFunction() {
  // ...
}

export default myGreatMixin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 优先级 B 的规则:强烈推荐 (增强代码可读性)

# 组件文件强烈推荐

只要有能够拼接文件的构建系统,就把每个组件单独分成文件。

当你需要编辑一个组件或查阅一个组件的用法时,可以更快速的找到它。

# 反例

app.component('TodoList', {
  // ...
})

app.component('TodoItem', {
  // ...
})
1
2
3
4
5
6
7
1
2
3
4
5
6
7

# 好例子

components/
|- TodoList.js
|- TodoItem.js
components/
|- TodoList.vue
|- TodoItem.vue
1
2
3
4
5
6
1
2
3
4
5
6

# 单文件组件文件的大小写强烈推荐

单文件组件 (opens new window)的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。

单词大写开头对于代码编辑器的自动补全最为友好,因为这使得我们在 JS (X) 和模板中引用组件的方式尽可能的一致。然而,混用文件命名方式有的时候会导致大小写不敏感的文件系统的问题,这也是横线连接命名同样完全可取的原因。

# 反例

components/
|- mycomponent.vue
components/
|- myComponent.vue
1
2
3
4
1
2
3
4

# 好例子

components/
|- MyComponent.vue
components/
|- my-component.vue
1
2
3
4
1
2
3
4

# 基础组件名称强烈推荐

应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如 Base 、 App 或 V 。

详解

这些组件为你的应用奠定了一致的基础样式和行为。它们可能只包括:

  • HTML 元素
  • 其它基础组件
  • 第三方 UI 组件库

但是它们绝不会包括全局状态 (比如来自 Vuex store)。

它们的名字通常包含所包裹元素的名字 (比如 BaseButton 、 BaseTable ),除非没有现成的对应功能的元素 (比如 BaseIcon )。如果你为特定的上下文构建类似的组件,那它们几乎总会消费这些组件 (比如 BaseButton 可能会用在 ButtonSubmit 上)。

这样做的几个好处:

  • 当你在编辑器中以字母顺序排序时,你的应用的基础组件会全部列在一起,这样更容易识别。

  • 因为组件名应该始终是多个单词,所以这样做可以避免你在包裹简单组件时随意选择前缀 (比如 MyButton 、 VueButton )。

  • 因为这些组件会被频繁使用,所以你可能想把它们放到全局而不是在各处分别导入它们。使用相同的前缀可以让 webpack 这样工作:

    const requireComponent = require.context("./src", true, /Base[A-Z]\w+\.(vue|js)$/)
    requireComponent.keys().forEach(function (fileName) {
      let baseComponentConfig = requireComponent(fileName)
      baseComponentConfig = baseComponentConfig.default || baseComponentConfig
      const baseComponentName = baseComponentConfig.name || (
        fileName
          .replace(/^.+\//, '')
          .replace(/\.\w+$/, '')
      )
      app.component(baseComponentName, baseComponentConfig)
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

# 反例

components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue
1
2
3
4
1
2
3
4

# 好例子

components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue
components/
|- AppButton.vue
|- AppTable.vue
|- AppIcon.vue
components/
|- VButton.vue
|- VTable.vue
|- VIcon.vue
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12

# 单组件名称强烈推荐

只应该拥有单个活跃实例的组件应该以 The 前缀命名,以示其唯一性。

这不意味着组件只可用于一个单页面,而是每个页面只使用一次。这些组件永远不接受任何 prop,因为它们是为你的应用定制的,而不是它们在你的应用中的上下文。如果你发现有必要添加 prop,那就表明这实际上是一个可复用的组件,只是目前在每个页面里只使用一次。

# 反例

components/
|- Heading.vue
|- MySidebar.vue
1
2
3
1
2
3

# 好例子

components/
|- TheHeading.vue
|- TheSidebar.vue
1
2
3
1
2
3

# 紧密耦合的组件名称强烈推荐

和父组件紧密耦合的子组件应该以父组件名作为前缀命名。

如果一个组件只在某个父组件的场景下有意义,这层关系应该体现在其名字上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。

详解 你可以试着通过在其父组件命名的目录中嵌套子组件以解决这个问题。比如:
components/
|- TodoList/
   |- Item/
      |- index.vue
      |- Button.vue
   |- index.vue
1
2
3
4
5
6
1
2
3
4
5
6

或:

components/
|- TodoList/
   |- Item/
      |- Button.vue
   |- Item.vue
|- TodoList.vue
1
2
3
4
5
6
1
2
3
4
5
6

但是这种方式并不推荐,因为这会导致:

  • 许多文件的名字相同,使得在编辑器中快速切换文件变得困难。
  • 过多嵌套的子目录增加了在编辑器侧边栏中浏览组件所花的时间。

# 反例

components/
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue
components/
|- SearchSidebar.vue
|- NavigationForSearchSidebar.vue
1
2
3
4
5
6
7
1
2
3
4
5
6
7

# 好例子

components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
components/
|- SearchSidebar.vue
|- SearchSidebarNavigation.vue
1
2
3
4
5
6
7
1
2
3
4
5
6
7

# 组件名称中的单词顺序强烈推荐

组件名称应该以高阶的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。

详解 你可能会疑惑:

“为什么我们给组件命名时不多遵从自然语言呢?”

在自然的英文里,形容词和其它描述语通常都出现在名词之前,否则需要使用连接词。比如:

  • Coffee with milk
  • Soup of the day
  • Visitor to the museum

如果你愿意,你完全可以在组件名里包含这些连接词,但是单词的顺序很重要。

同样要注意在你的应用中所谓的 “高阶” 是跟语境有关的。比如对于一个带搜索表单的应用来说,它可能包含这样的组件:

components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
1
2
3
4
5
6
7
1
2
3
4
5
6
7

你可能注意到了,我们很难看出来哪些组件是针对搜索的。现在我们来根据规则给组件重新命名:

components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue
1
2
3
4
5
6
7
1
2
3
4
5
6
7

因为编辑器通常会按字母顺序组织文件,所以现在组件之间的重要关系一目了然。

你可能想换成多级目录的方式,把所有的搜索组件放到 “search” 目录,把所有的设置组件放到 “settings” 目录。我们只推荐在非常大型 (如有 100+ 个组件) 的应用下才考虑这么做,因为:

  • 在多级目录间找来找去,要比在单个 components 目录下滚动查找要花费更多的精力。
  • 存在组件重名 (比如存在多个 ButtonDelete.vue 组件) 的时候在编辑器里更难快速定位。
  • 让重构变得更难,因为为一个移动了的组件更新相关引用时,查找 / 替换通常并不高效。

# 反例

components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
1
2
3
4
5
6
7
1
2
3
4
5
6
7

# 好例子

components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputQuery.vue
|- SearchInputExcludeGlob.vue
|- SettingsCheckboxTerms.vue
|- SettingsCheckboxLaunchOnStartup.vue
1
2
3
4
5
6
7
1
2
3
4
5
6
7

# 自闭合组件强烈推荐

在单文件组件 (opens new window)、字符串模板和 JSX (opens new window) 中没有内容的组件应该是自闭合的 —— 但在 DOM 模板里永远不要这样做。

自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有 “本页有意留白” 标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。

不幸的是,HTML 并不支持自闭合的自定义元素 —— 只有官方的 “空” 元素 (opens new window)。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。

# 反例

<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>
<!-- 在 DOM 模板中                   -->
<my-component/>
1
2
3
4
1
2
3
4

# 好例子

<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>
<!-- 在 DOM 模板中                   -->
<my-component></my-component>
1
2
3
4
1
2
3
4

# 模板中的组件名称大小写强烈推荐

对于绝大多数项目来说,在单文件组件 (opens new window)和字符串模板中组件名称应该总是 PascalCase 的 —— 但是在 DOM 模板中总是 kebab-case 的。

PascalCase 相比 kebab-case 有一些优势:

  • 编辑器可以在模板里自动补全组件名称,因为 PascalCase 同样适用于 JavaScript。
  • MyComponent> 视觉上比 <my-component> 更能够和单个单词的 HTML 元素区别开来,因为前者的不同之处有两个大写字母,后者只有一个横线。
  • 如果你在模板中使用任何非 Vue 的自定义元素,比如一个 Web Component,PascalCase 确保了你的 Vue 组件在视觉上仍然是易识别的。

不幸的是,由于 HTML 是大小写不敏感的,在 DOM 模板中必须仍使用 kebab-case。

还请注意,如果你已经是 kebab-case 的重度用户,那么与 HTML 保持一致的命名约定且在多个项目中保持相同的大小写规则就可能比上述优势更为重要了。在这些情况下,在所有的地方都使用 kebab-case 同样是可以接受的。

# 反例

<!-- 在单文件组件和字符串模板中 -->
<mycomponent/>
<!-- 在单文件组件和字符串模板中 -->
<myComponent/>
<!-- 在 DOM 模板中            -->
<MyComponent></MyComponent>
1
2
3
4
5
6
1
2
3
4
5
6

# 好例子

<!-- 在单文件组件和字符串模板中 -->
<MyComponent/>
<!-- 在 DOM 模板中            -->
<my-component></my-component>
1
2
3
4
1
2
3
4

或者

<!-- 在所有地方 -->
<my-component></my-component>
1
2
1
2

# JS/JSX 中使用的组件名称强烈推荐

JS/JSX (opens new window) 中的组件名应该始终是 PascalCase 的,尽管在较为简单的应用中只使用 app.component 进行全局组件注册时,可以使用 kebab-case 字符串。

详解

在 JavaScript 中,PascalCase 是类和构造函数 (本质上任何可以产生多份不同实例的东西) 的命名约定。Vue 组件也有多份实例,所以同样使用 PascalCase 是有意义的。额外的好处是,在 JSX (和模板) 里使用 PascalCase 使得代码的读者更容易分辨 Vue 组件和 HTML 元素。

然而,对于只通过 app.component 定义全局组件的应用来说,我们推荐 kebab-case 作为替代。原因是:

  • 全局组件很少被 JavaScript 引用,所以遵守 JavaScript 的命名约定意义不大。
  • 这些应用往往包含许多 DOM 内的模板,这种情况下是必须使用 kebab-case (opens new window) 的。

# 反例

app.component('myComponent', {
  // ...
})
import myComponent from './MyComponent.vue'
export default {
  name: 'myComponent',
  // ...
}
export default {
  name: 'my-component',
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12

# 好例子

app.component('MyComponent', {
  // ...
})
app.component('my-component', {
  // ...
})
import MyComponent from './MyComponent.vue'
export default {
  name: 'MyComponent',
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11

# 完整单词的组件名称强烈推荐

组件名称应该倾向于完整单词而不是缩写。

编辑器中的自动补全已经让书写长命名的代价非常之低了,而其带来的明确性却是非常宝贵的。不常用的缩写尤其应该避免。

# 反例

components/
|- SdSettings.vue
|- UProfOpts.vue
1
2
3
1
2
3

# 好例子

components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue
1
2
3
1
2
3

# Prop 名称强烈推荐

在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板和 JSX (opens new window) 中应该始终使用 kebab-case。

我们单纯的遵循每个语言的约定。在 JavaScript 中更自然的是 camelCase。而在 HTML 中则是 kebab-case。

# 反例

props: {
  'greeting-text': String
}
<WelcomeMessage greetingText="hi"/>
1
2
3
4
1
2
3
4

# 好例子

props: {
  greetingText: String
}
<WelcomeMessage greeting-text="hi"/>
1
2
3
4
1
2
3
4

# 多个 attribute 的元素强烈推荐

多个 attribute 的元素应该分多行撰写,每个 attribute 一行。

在 JavaScript 中,用多行分隔对象的多个 property 是很常见的最佳实践,因为这样更易读。模板和 JSX (opens new window) 值得我们做相同的考虑。

# 反例

<img src="https://vuejs.org/images/logo.png" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>
1
2
1
2

# 好例子

<img
  src="https://vuejs.org/images/logo.png"
  alt="Vue Logo"
>
<MyComponent
  foo="a"
  bar="b"
  baz="c"
/>
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

# 模板中的简单表达式强烈推荐

组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。

复杂表达式会让你的模板变得不那么声明式。我们应该尽量描述应该出现的是什么,而非如何计算那个值。而且计算属性和方法使得代码可以重用。

# 反例

{{
  fullName.split(' ').map((word) => {
    return word[0].toUpperCase() + word.slice(1)
  }).join(' ')
}}
1
2
3
4
5
1
2
3
4
5

# 好例子

<!-- 在模板中 -->
{{ normalizedFullName }}
// 复杂表达式已经移入一个计算属性
computed: {
  normalizedFullName() {
    return this.fullName.split(' ')
      .map(word => word[0].toUpperCase() + word.slice(1))
      .join(' ')
  }
}
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10

# 简单的计算属性强烈推荐

应该把复杂计算属性分割为尽可能多的更简单的 property。

详解 更简单、命名得当的计算属性是这样的:
  • 易于测试

    当每个计算属性都包含一个非常简单且很少依赖的表达式时,撰写测试以确保其正确工作就会更加容易。

  • 易于阅读

    简化计算属性要求你为每一个值都起一个描述性的名称,即便它不可复用。这使得其他开发者 (以及未来的你) 更容易专注在他们关心的代码上并搞清楚发生了什么。

  • 更好的 “拥抱变化”

    任何能够命名的值都可能用在视图上。举个例子,我们可能打算展示一个信息,告诉用户他们存了多少钱;也可能打算计算税费,但是可能会分开展现,而不是作为总价的一部分。

    小的、专注的计算属性减少了信息使用时的假设性限制,所以需求变更时也用不着那么多重构了。 :::

# 反例

computed: {
  price() {
    const basePrice = this.manufactureCost / (1 - this.profitMargin)
    return (
      basePrice -
      basePrice * (this.discountPercent || 0)
    )
  }
}
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

# 好例子

computed: {
  basePrice() {
    return this.manufactureCost / (1 - this.profitMargin)
  },

  discount() {
    return this.basePrice * (this.discountPercent || 0)
  },

  finalPrice() {
    return this.basePrice - this.discount
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13

# 带引号的 attribute 值强烈推荐

非空 HTML attribute 值应该始终带引号 (单引号或双引号,选你 JS 里不用的那个)。

在 HTML 中不带空格的 attribute 值是可以没有引号的,但这鼓励了大家在特征值里不写空格,导致可读性变差。

# 反例

<input type=text>
<AppSidebar :style={width:sidebarWidth+'px'}>
1
2
1
2

# 好例子

<input type="text">
<AppSidebar :style="{ width: sidebarWidth + 'px' }">
1
2
1
2

# 指令缩写强烈推荐

指令缩写 (用 : 表示 v-bind: , @ 表示 v-on: 和用 # 表示 v-slot ) 应该要么都用要么都不用。

# 反例

<input
  v-bind:value="newTodoText"
  :placeholder="newTodoInstructions"
>
<input
  v-on:input="onInput"
  @focus="onFocus"
>
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 好例子

<input
  :value="newTodoText"
  :placeholder="newTodoInstructions"
>
<input
  v-bind:value="newTodoText"
  v-bind:placeholder="newTodoInstructions"
>
<input
  @input="onInput"
  @focus="onFocus"
>
<input
  v-on:input="onInput"
  v-on:focus="onFocus"
>
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template v-slot:footer>
  <p>Here's some contact info</p>
</template>
<template #header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 优先级 C 的规则:推荐 (将选择和认知成本最小化)

# 组件 / 实例的选项顺序推荐

组件 / 实例的选项应该有统一的顺序。

这是我们推荐的组件选项默认顺序。它们被划分为几大类,所以你也能知道从插件里添加的新 property 应该放到哪里。

  1. 全局感知 (要求组件以外的知识)
    • name
  2. 模板依赖 (模板内使用的资源)
    • components
    • directives
  3. 组合 (向选项里合并 property)
    • extends
    • mixins
    • provide / inject
  4. 接口 (组件的接口)
    • inheritAttrs
    • props
    • emits
  5. 组合式 API (使用组合式 API 的入口点)
    • setup
  6. 本地状态 (本地的响应式 property)
    • data
    • computed
  7. 事件 (通过响应式事件触发的回调)
    • watch
    • 生命周期钩子 (按照它们被调用的顺序)
      • beforeCreate
      • created
      • beforeMount
      • mounted
      • beforeUpdate
      • updated
      • activated
      • deactivated
      • beforeUnmount
      • unmounted
      • errorCaptured
      • renderTracked
      • renderTriggered
  8. 非响应式的 property (不依赖响应性系统的实例 property)
    • methods
  9. 渲染 (组件输出的声明式描述)
    • template / render

# 元素 attribute 的顺序推荐

元素 (包括组件) 的 attribute 应该有统一的顺序。

这是我们为组件选项推荐的默认顺序。它们被划分为几大类,所以你也能知道新添加的自定义 attribute 和指令应该放到哪里。

  1. 定义 (提供组件的选项)
    • is
  2. 列表渲染 (创建多个变化的相同元素)
    • v-for
  3. 条件渲染 (元素是否渲染 / 显示)
    • v-if
    • v-else-if
    • v-else
    • v-show
    • v-cloak
  4. 渲染修饰符 (改变元素的渲染方式)
    • v-pre
    • v-once
  5. 全局感知 (需要超越组件的知识)
    • id
  6. 唯一的 Attributes (需要唯一值的 attribute)
    • ref
    • key
  7. 双向绑定 (把绑定和事件结合起来)
    • v-model
  8. 其他 Attributes (所有普通的绑定或未绑定的 attribute)
  9. 事件 (组件事件监听器)
    • v-on
  10. 内容 (覆写元素的内容)
    • v-html
    • v-text

# 组件 / 实例选项中的空行推荐

你可能想在多个 property 之间增加一个空行,特别是在这些选项一屏放不下,需要滚动才能都看到的时候。

当你的组件开始觉得密集或难以阅读时,在多个 property 之间添加空行可以让其变得容易。在一些诸如 Vim 的编辑器里,这样格式化后的选项还能通过键盘被快速导航。

# 好例子

props: {
  value: {
    type: String,
    required: true
  },

  focused: {
    type: Boolean,
    default: false
  },

  label: String,
  icon: String
},

computed: {
  formattedValue() {
    // ...
  },

  inputClasses() {
    // ...
  }
}
// 没有空行在组件易于阅读和导航时也没问题。
props: {
  value: {
    type: String,
    required: true
  },

  focused: {
    type: Boolean,
    default: false
  },

  label: String,
  icon: String
},

computed: {
  formattedValue() {
    // ...
  },

  inputClasses() {
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 单文件组件的顶级元素的顺序推荐

单文件组件 (opens new window)应该总是让 <script> 、 <template> 和 <style> 标签的顺序保持一致。且 <style> 要放在最后,因为另外两个标签至少要有一个。

# 反例

<style>/* ... */</style>
<script>/* ... */</script>
<template>...</template>
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12

# 好例子

<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>
<!-- ComponentA.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 优先级 D 的规则:谨慎使用 (潜在风险)

# scoped 中的元素选择器谨慎使用

元素选择器应该避免在 scoped 中出现。

在 scoped 样式中,类选择器比元素选择器更好,因为大量使用元素选择器是很慢的。

详解

为了给样式设置作用域,Vue 会为元素添加一个独一无二的 attribute,例如 data-v-f3f3eg9 。然后修改选择器,使得在匹配选择器的元素中,只有带这个 attribute 才会真正生效 (比如 button[data-v-f3f3eg9] )。

问题在于大量的元素和 attribute 组合的选择器 (opens new window) (比如 button[data-v-f3f3eg9] ) 会比类和 attribute 组合的选择器 (opens new window)慢,所以应该尽可能选用类选择器。

# 反例

<template>
  <button>×</button>
</template>

<style scoped>
button {
  background-color: red;
}
</style>
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

# 好例子

<template>
  <button class="btn btn-close">×</button>
</template>

<style scoped>
.btn-close {
  background-color: red;
}
</style>
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

# 隐性的父子组件通信谨慎使用

应该优先通过 prop 和事件进行父子组件之间的通信,而不是 this.$parent 或变更 prop。

一个理想的 Vue 应用是 prop 向下传递,事件向上传递的。遵循这一约定会让你的组件更易于理解。然而,在一些边界情况下 prop 的变更或 this.$parent 能够简化两个深度耦合的组件。

问题在于,这种做法在很多简单的场景下可能会更方便。但请当心,不要为了一时方便 (少写代码) 而牺牲数据流向的简洁性 (易于理解)。

# 反例

app.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  template: '<input v-model="todo.text">'
})
app.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  methods: {
    removeTodo() {
      this.$parent.todos = this.$parent.todos.filter(todo => todo.id !== vm.todo.id)
    }
  },

  template: `
    <span>
      {{ todo.text }}
      <button @click="removeTodo">
        ×
      </button>
    </span>
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 好例子

app.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  emits: ['input'],

  template: `
    <input
      :value="todo.text"
      @input="$emit('input', $event.target.value)"
    >
  `
})
app.component('TodoItem', {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  emits: ['delete'],

  template: `
    <span>
      {{ todo.text }}
      <button @click="$emit('delete')">
        ×
      </button>
    </span>
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 非 Flux 的全局状态管理谨慎使用

应该优先通过 Vuex (opens new window) 管理全局状态,而不是通过 this.$root 或一个全局事件总线。

通过 this.$root 和 / 或全局事件总线管理状态在很多简单的情况下都是很方便的,但是并不适用于绝大多数的应用。

Vuex 是 Vue 的官方类 flux 实现 (opens new window),其提供的不仅是一个管理状态的中心区域,还是组织、追踪和调试状态变更的好工具。它很好地集成在了 Vue 生态系统之中 (包括完整的 Vue DevTools (opens new window) 支持)。

# 反例

// main.js
import { createApp } from 'vue'
import mitt from 'mitt'
const app = createApp({
  data() {
    return {
      todos: [],
      emitter: mitt()
    }
  },

  created() {
    this.emitter.on('remove-todo', this.removeTodo)
  },

  methods: {
    removeTodo(todo) {
      const todoIdToRemove = todo.id
      this.todos = this.todos.filter(todo => todo.id !== todoIdToRemove)
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 好例子

// store/modules/todos.js
export default {
  state: {
    list: []
  },

  mutations: {
    REMOVE_TODO (state, todoId) {
      state.list = state.list.filter(todo => todo.id !== todoId)
    }
  },

  actions: {
    removeTodo ({ commit, state }, todo) {
      commit('REMOVE_TODO', todo.id)
    }
  }
}
<!-- TodoItem.vue -->
<template>
  <span>
    {{ todo.text }}
    <button @click="removeTodo(todo)">
      X
    </button>
  </span>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  props: {
    todo: {
      type: Object,
      required: true
    }
  },

  methods: mapActions(['removeTodo'])
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 常见问题

# alias @ 不起作用

configureWebpack: {
    // 设置网站标题
    name: name,
    resolve: {
      // 设置别名
      alias: {
        '@': resolve('src')
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10

WebStorm 解决方法 打开 File -> Settings -> Languages & Frameworks -> Javascript -> Webpack 设置 webpack configuration File 为当前项目目录下 node_modules@vue\cli-service\webpack.config.js

# Prettier 失效

使用 Ctrl + Shift + Alt + P 提示 Prettier: Package is not installed

WebStorm 解决方法 打开 File -> Settings -> Languages & Frameworks -> Javascript -> Prettier 设置 Prettier package 为当前项目目录下 node_modules\prettier

# 快速构建一个页面

# 创建页面 vue 文件

根据开发模块的层级在对应的目录中创建目录及 index.vue 文件

注意:因框架中使用了 eslint 来做开发规范,所以即使语法正确也会出现 error;可通过 prettier 快捷键 ctrl+shift+alt+P 进行代码格式化。

例:创建三级菜单 HelloWorld 以下为目录结构

sofast-web
├── src
│   ├── views
│   │   ├── module1                                        // 父模块(对应一级菜单)
│   │   │   ├── subModule1                        // 子模块(对应二级菜单)
│   │   │   │   ├── helloWorld                // 页面
│   │   │   │   │   └── index.vue
│   │   │   ├── subModule2
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8

index.vue

<template>

</template>

<script>
export default {
  name: 'HelloWorld'
}
</script>

<style scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13

# 配置国际化

国际化资源目录结构采用模块级隔离(仅限于父级模块)。

  1. 创建国际化模块目录和资源文件,在 i18n 目录中创建目录,目录结构如下

    sofast-web
    ├── src
    │   └── i18n
    │   │   ├── moduleA            // 父级模块
    │   │   │   ├── zh.js
    │   │   │   ├── en.js
    │   │   │   ├── ...
    
    1
    2
    3
    4
    5
    6
    7
    1
    2
    3
    4
    5
    6
    7
  2. 国际化资源内容以页面为单位进行创建。支持页面组结构。

    // zh.js文件内容:
    export default {
      // 页面名称
      helloWorld: {
        label: {
          clickMe: '点击我'
        },
        message: {
          clickMsg: '你好,世界'
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // en.js文件内容
    export default {
      // 功能名称
      helloWorld: {
        label: {
          clickMe: 'Click Me'
        },
        message: {
          clickMsg: 'Hello World'
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  3. 在 vue 文件中引入国际化

<template>
<div class="hello-world sf-container">
<!-- 国际化资源调用 -->
<el-button @click="$message.info($L_Msg('clickMsg'))">{{ $L_Label('clickMe') }}</el-button>
</div>
</template>

<script>
// 引入国际化混入
import lang from '@/mixins/lang'

// 配置
export default {
name: 'HelloWorld',
mixins: [lang],
// moduleA是i18n下的目录名称;helloWorld是对应的zh.js或en.js中的属性名称
langPrefix: 'moduleA.helloWorld'
}
</script>

<style scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

国际化混入中提供了 4 个方法:

$L_Label (key):获取对应的业务中的国际化 Label (会在传入的 key 的前方拼接 ${langPrefix}.label) $L_Msg (key):获取对应的业务中的国际化 Message (会在传入的 key 的前方拼接 ${langPrefix}.message) $C_Label (key):获取共通中的国际化 Label(common.label.)(会在传入的 key 的前方拼接 common.label) $C_Msg (key):获取共通中的国际化 Message(common.message.)(会在传入的 key 的前方拼接 common.message)

# 添加菜单

进入菜单管理页面 点击新增按钮创建一级菜单 菜单管理 输入菜单名称,英文名称,选择菜单的图标,设置访问路径(相对路径:无需考虑所有的上级菜单的路径),设置组件路径(该路径为 src/views/ 后面的路径)等 新增一级菜单 点击保存 保存一级菜单成功 点击列表中的添加下级按钮 添加二级菜单 输入菜单名称,英文名称,选择菜单的图标,设置访问路径(相对路径:无需考虑所有的上级菜单的路径),设置组件路径(该路径为 src/views/ 后面的路径)等 设置二级菜单 点击列表中二级菜单的添加下级按钮 添加三级菜单 输入菜单名称,英文名称,选择菜单的图标,设置访问路径(相对路径:无需考虑所有的上级菜单的路径),设置组件路径(该路径为 src/views/ 后面的路径)等 设置三级菜单 点击保存 保存三级菜单成功 此时三级菜单已经创建成功,但是左侧菜单栏并没有新建的菜单,此时需要给角色的权限中添加菜单权限

TODO 参照权限设置

注意事项 当创建三级或三级以上的菜单时需要在父级菜单对应的目录下创建 index.vue 模板

index.vue 内容如下

<template>
  <router-view></router-view>
</template>

<script>
export default {}
</script>

<style scoped></style>
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

如不创建则会发现 console 控制台中会报错 会提示找不到模块 找不到模块

# 运行效果

点击左侧菜单进入 HelloWorld 页面 HelloWorld 点击按钮会弹出 Message HelloWorld 点击语言切换,切换成 English 查看一下效果 HelloWorld 英文展示效果 HelloWorld 点击按钮会弹出 Message HelloWorld

完成了!

# 快速构建 XXX 模块

# 创建 XXX 管理

创建 XXX 模块(一级)目录及管理 1(二级)

以下为目录结构

sofast-web
├── src
│   ├── views
│   │   ├── XXX
│   │   │   ├── mgt1
│   │   │   │   └── index.vue
1
2
3
4
5
6
1
2
3
4
5
6

index.vue

<template>

</template>

<script>
export default {
  // 首字母必须大写
  name: 'Mgt1'
}
</script>

<style scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置国际化

1. 创建国际化文本配置文件,在 i18n 目录中创建目录,目录结构如下 (注:目录层级只区分到模块,其余二级三级甚至四级都可在文件中声明)

sofast-web
├── src
│   └── i18n
│   │   ├── XXX
│   │   │   ├── zh.js
│   │   │   ├── en.js
│   │   │   ├── ...
1
2
3
4
5
6
7
1
2
3
4
5
6
7

国际化内容结构如下,可在 “功能名称” 层下添加下级,不限级数,但是记得在使用的时候配置好

// zh.js文件内容
export default {
  // 管理1
  mgt1: {
    label: {
      name: '名称',
      code: '编码',
      status: '状态',
      createTime: '创建时间',
      updateTime: '更新时间',
      mgt1Add: '管理1新增',
      mgt1Edit: '管理1编辑'
    },
    message: {
      nameRequire: '请输入名称',
      codeRequire: '请输入编码'
    }
  }
}
// en.js文件内容
export default {
  // 管理1
  mgt1: {
    label: {
      name: 'Name',
      code: 'Code',
      status: 'Status',
      createTime: 'Create Time',
      updateTime: 'Update Time',
      mgt1Add: 'Add',
      mgt1Edit: 'Edit'
    },
    message: {
      nameRequire: 'Name has required',
      codeRequire: 'Code has required'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

2. 在 vue 文件中引入国际化混入 设置国际化前缀 langPrefix

<template>
  <div class="xxx-mgt1">
  </div>
</template>

<script>
import lang from '@/mixins/lang'

export default {
  name: 'Mgt1',
  mixins: [lang],
  langPrefix: 'XXX.mgt1'
}
</script>

<style scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 编写表格

1. 引入 sf-table 组件,添加 table 列

<template>
  <div class="xxx-mgt1 sf-container">
    <sf-table
      ref="dataTable"
      :list-params="listParams"
      :data="tableData"
    >
      <!-- 复选框 -->
      <el-table-column type="selection" width="44"></el-table-column>
      <!-- 名称 -->
      <el-table-column :label="$L_Label('name')" prop="name" width="150"></el-table-column>
      <!-- 编码 -->
      <el-table-column :label="$L_Label('code')" prop="code" width="150"></el-table-column>
      <!-- 状态 -->
      <el-table-column :label="$L_Label('status')" prop="status" width="150"></el-table-column>
      <!-- 创建时间 -->
      <el-table-column :label="$C_Label('createTime')" width="150">
        <template slot-scope="{ row }">
          {{ row.createTime | dateTimeFmt }}
        </template>
      </el-table-column>
      <!-- 更新时间 -->
      <el-table-column :label="$C_Label('updateTime')" width="150">
        <template slot-scope="{ row }">
          {{ row.updateTime | dateTimeFmt }}
        </template>
      </el-table-column>
      <!-- 操作列 -->
      <el-table-column :label="$C_Label('operation')" min-width="200"></el-table-column>
    </sf-table>
  </div>
</template>

<script>
import lang from '@/mixins/lang'
import SfTable from '@/components/Table/index'

export default {
  name: 'Mgt1',
  mixins: [lang],
  langPrefix: 'XXX.mgt1',
  components: { SfTable },
  data() {
    return {
      tableData: [],
      listParams: {}
    }
  }
}
</script>

<style scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

此时的展示效果 展示效果

2. 添加 API 请求 在 src/api 目录下创建 XXX/mgt1.js 目录结构如下

sofast-web
├── src
│   ├── api
│   │   ├── XXX
│   │   │   └── mgt1.js
1
2
3
4
5
1
2
3
4
5

mgt1.js 内容如下

import request from '@/utils/request'

/**
 * 查询mgt1列表
 * @param data
 * @returns {AxiosPromise}
 */
export function getPage(data) {
  // return request({
  //   url: '/mgt1/page',
  //   method: 'get',
  //   params: data
  // })
  return new Promise(resolve => {
    let records = []
    for (let i = 0; i < 10; i++) {
      records.push({
        id: i + 1,
        name: `name${i}`,
        code: `code${i}`,
        status: `${i % 2}`,
        createTime: new Date(),
        updateTime: new Date()
      })
    }
    resolve({
      status: 0,
      code: null,
      message: null,
      data: {
        records: records,
        total: 25,
        size: 10,
        current: 1,
        orders: [],
        optimizeCountSql: true,
        hitCount: false,
        searchCount: true,
        pages: 3
      },
      e: null
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

3. 使用列表组件 action 属性集成 api 接口,引入 API 列表方法

import { getPage } from '@/api/XXX/mgt1'
1
1

在 data 区域添加 getPage 方法

import { getPage } from '@/api/XXX/mgt1'

export default {
  // 省略...
  data() {
    return {
      // 省略...
      getPage
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11

在 sf-table 中添加 action 属性

<sf-table ref="dataTable" :action="getPage" :list-params="listParams" :data="tableData">
  <!-- 省略 -->
</sf-table>
1
2
3
1
2
3

此时的显示效果 展示效果

4. 对状态值进行字典 label 替换 编辑状态列

<!-- 状态 -->
<el-table-column :label="$L_Label('status')" prop="status" width="150">
  <template slot-scope="{ row }">
    <el-tag :type="row.status === '0' ? 'primary' : 'danger'" size="mini">
      {{ getDictLabel('sys_use_status', row.status) }}
    </el-tag>
  </template>
</el-table-column>
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8

替换后的展示效果 展示效果 5. 添加检索区域

<template>
  <div class="xxx-mgt1 sf-container">
    <sf-table ref="dataTable" :action="getPage" :list-params="listParams" :data="tableData">
      <!-- 此处为检索区域 -->
      <template slot="search-area">
        <el-form
          ref="searchForm"
          :model="searchParams"
          label-width="100px"
          inline
          size="small"
          label-position="right"
        >
          <el-form-item :label="$L_Label('name')" prop="name">
            <!-- 名称 -->
            <el-input v-model="searchParams.name" clearable></el-input>
          </el-form-item>
          <el-form-item :label="$L_Label('code')" prop="code">
            <!-- 编码 -->
            <el-input v-model="searchParams.code" clearable></el-input>
          </el-form-item>
          <el-form-item :label="$C_Label('status')" prop="status">
            <!-- 状态 -->
            <sf-dict-select
              v-model="searchParams.status"
              type="sys_use_status"
              clearable
            ></sf-dict-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="search">{{ $C_Label('search') }}</el-button>
            <el-button @click="clearCondition">{{ $C_Label('reset') }}</el-button>
          </el-form-item>
        </el-form>
      </template>
      <!-- 复选框 -->
      <el-table-column type="selection" width="44"></el-table-column>
      <!-- 名称 -->
      <el-table-column :label="$L_Label('name')" prop="name" width="150"></el-table-column>
      <!-- 编码 -->
      <el-table-column :label="$L_Label('code')" prop="code" width="150"></el-table-column>
      <!-- 状态 -->
      <el-table-column :label="$L_Label('status')" prop="status" width="150">
        <template slot-scope="{ row }">
          <el-tag :type="row.status === '0' ? 'primary' : 'danger'" size="mini">
            {{ getDictLabel('sys_use_status', row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <!-- 创建时间 -->
      <el-table-column :label="$C_Label('createTime')" width="150">
        <template slot-scope="{ row }">
          {{ row.createTime | dateTimeFmt }}
        </template>
      </el-table-column>
      <!-- 更新时间 -->
      <el-table-column :label="$C_Label('updateTime')" width="150">
        <template slot-scope="{ row }">
          {{ row.updateTime | dateTimeFmt }}
        </template>
      </el-table-column>
      <el-table-column :label="$C_Label('operation')" min-width="200"></el-table-column>
    </sf-table>
  </div>
</template>

<script>
import lang from '@/mixins/lang'
import SfTable from '@/components/Table/index'
import SfDictSelect from '@/components/DictSelect/index'
import { getPage } from '@/api/XXX/mgt1'

export default {
  name: 'Mgt1',
  mixins: [lang],
  langPrefix: 'XXX.mgt1',
  components: { SfDictSelect, SfTable },
  data() {
    return {
      searchParams: {
        name: null,
        code: null,
        status: null
      },
      tableData: [],
      listParams: {},
      getPage
    }
  },
  methods: {
    /**
     * 检索
     */
    search() {
      this.listParams = Object.assign({ current: 1, total: 0 }, this.searchParams)
      // 刷新表格数据
      this.$refs.dataTable.getTableData()
    },
    /**
     * 检索重置
     */
    clearCondition() {
      // 清空检索区域
      this.$refs.searchForm.resetFields()
      // 重新检索
      this.search()
    }
  }
}
</script>

<style scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

展示效果 展示效果

6. 添加工具栏新增和列表操作中的修改按钮

模板中添加工具栏区域 slot,在操作列中添加修改

<sf-table ref="dataTable" :action="getPage" :list-params="listParams" :data="tableData">
  <!-- 此处为检索区域 -->
  <template slot="search-area">
  ···
  </template>
  <!-- 此处为工具栏 -->
  <template slot="tools-bar">
    <!-- 新增 -->
    <el-button type="primary" size="small">
      {{ $C_Label('add') }}
    </el-button>
  </template>
  ···
  <el-table-column :label="$C_Label('operation')" min-width="195">
    <template slot-scope="{ row }">
      <el-button type="text" size="mini">
        {{ $C_Label('edit') }}
      </el-button>
    </template>
  </el-table-column>
</sf-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

展示效果 7. 添加保存和更新接口,编辑 mgt1.js,添加以下代码

/**
 * 创建mgt1
 * @param data
 * @returns {AxiosPromise}
 */
export function mgt1Create(data) {
  // return request({
  //   url: '/mgt1/create',
  //   method: 'post',
  //   data
  // })
  return new Promise(resolve => {
    resolve({
      status: 0,
      code: null,
      message: null
    })
  })
}
/**
 * 更新mgt1
 * @param data
 * @returns {AxiosPromise}
 */
export function mgt1Update(data) {
  // return request({
  //   url: '/mgt1/update',
  //   method: 'post',
  //   data
  // })
  return new Promise(resolve => {
    resolve({
      status: 0,
      code: null,
      message: null
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

8. 创建新增 / 编辑组件 Mgt1Form.vue

<template>
  <sf-dialog
    :visible="visible"
    :title="title"
    width="600px"
    @update:visible="val => $emit('update:visible', val)"
  >
    <el-form
      ref="mgt1Form"
      :model="mgt1Form"
      label-width="120px"
      label-position="right"
      size="small"
      :rules="mgt1DataRules"
    >
      <el-form-item :label="$L_Label('name')" prop="name">
        <el-input v-model.trim="mgt1Form.name" minlength="4" />
      </el-form-item>
      <el-form-item :label="$L_Label('code')" prop="code">
        <el-input v-model.trim="mgt1Form.code" minlength="4" />
      </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button type="primary" size="small" :loading="saveLoading" @click="save">
        {{ $C_Label('save') }}
      </el-button>
      <el-button size="small" @click="closeDialog">{{ $C_Label('cancel') }}</el-button>
    </div>
  </sf-dialog>
</template>

<script>
import SfDialog from '@/components/Dialog/index'
import { mgt1Create, mgt1Update } from '@/api/XXX/mgt1'
import lang from '@/mixins/lang'

export default {
  name: 'Mgt1Form',
  langPrefix: 'XXX.mgt1',
  components: { SfDialog },
  props: {
    visible: Boolean,
    data: Object
  },
  mixins: [lang],
  data() {
    return {
      mgt1Form: {
        status: 0,
        roleId: []
      },
      title: null,
      mgt1DataRules: {
        name: [{ required: true, message: this.$L_Msg('nameRequire'), trigger: 'blur' }],
        code: [{ required: true, message: this.$L_Msg('codeRequire'), trigger: 'blur' }]
      },
      saveLoading: false
    }
  },
  methods: {
    /**
     * 保存信息
     */
    save() {
      this.$refs.mgt1Form.validate(valid => {
        if (valid) {
          this.saveLoading = true
          let request = null
          let mgt1Form = Object.assign({}, this.mgt1Form)
          if (mgt1Form.id) {
            request = mgt1Update(mgt1Form)
          } else {
            request = mgt1Create(mgt1Form)
          }
          request
            .then(res => {
              // 保存成功
              this.$message.success(this.$C_Msg('saveSuccess'))
              this.saveLoading = false
              this.$emit('success', {})
              this.closeDialog()
            })
            .catch(() => {
              this.saveLoading = false
            })
        }
      })
    },
    /**
     * 关闭弹窗
     */
    closeDialog() {
      this.$emit('update:visible', false)
    }
  },
  async created() {
    if (this.data.id) {
      // 编辑
      this.title = this.$L_Label('mgt1Edit')
      this.mgt1Form = { ...this.data }
    } else {
      // 新增
      this.title = this.$L_Label('mgt1Add')
    }
  }
}
</script>

<style scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

9. 在 index 组件中引入 Mgt1Form

初始化 formData 变量和 dialogVisible 变量并添加 showDialog 方法

export default {
  data(){
    return {
      // ...省略
      formData: {},
      dialogVisible: false
    }
  },
  // ...省略
  methods: {
    // ...省略
    /**
     * 弹出新增/编辑
     * @param row
     */
    showDialog(row) {
      this.formData = row || {}
      this.dialogVisible = true
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在模板区域引入组件 Mgt1Form

<template>
  <div class="xxx-mgt1 sf-container">
    <sf-table>
    <!-- 表格区域内容 省略 -->
    </sf-table>
    <!-- Mgt1Form表单 -->
    <mgt1-form
      v-if="dialogVisible"
      :visible.sync="dialogVisible"
      :data="formData"
      @success="search"
    ></mgt1-form>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在操作列中添加编辑按钮并在新增和编辑按钮上绑定 click 事件

<sf-table>
<!-- 此处为检索区域 省略 -->
  <template slot="tools-bar">
    <!-- 新增按钮 -->
    <el-button type="primary" size="small" @click="showDialog()">
      {{ $C_Label('add') }}
    </el-button>
  </template>
  <!-- table中的数据列 省略 -->
  <!-- 操作列 -->
  <el-table-column :label="$C_Label('operation')" min-width="195">
    <template slot-scope="{ row }">
      <el-button type="text" size="mini" @click="showDialog(row)">
        {{ $C_Label('edit') }}
      </el-button>
    </template>
  </el-table-column>
</sf-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

点击新增 展示效果 直接点击保存,会触发验证 展示效果 输入名称编码后 展示效果 点击保存 展示效果 点击编辑 展示效果 点击保存 展示效果

10. 在工具栏和操作列中添加删除按钮

<sf-table>
  <!-- 此处为检索区域 省略 -->
  <template slot="tools-bar">
    <!-- 新增按钮 -->
    <el-button type="primary" size="small" @click="showDialog()">
      {{ $C_Label('add') }}
    </el-button>
    <!-- 删除按钮 -->
    <el-button type="danger" size="small">
      {{ $C_Label('delete') }}
    </el-button>
  </template>
  <!-- table中的数据列 省略 -->
  <!-- 操作列 -->
  <el-table-column :label="$C_Label('operation')" min-width="195">
    <template slot-scope="{ row }">
      <el-button type="text" size="mini" @click="showDialog(row)">
        {{ $C_Label('edit') }}
      </el-button>
      <el-button
        type="text"
        size="mini"
        class="danger-text"
        >
        {{ $C_Label('delete') }}
      </el-button>
    </template>
  </el-table-column>
</sf-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

11. 在 api 中添加删除请求

/**
 * 批量删除
 * @param data
 * @returns {AxiosPromise}
 */
export function deleteByIds(data) {
  // return request({
  //   url: '/mgt1/delete',
  //   method: 'post',
  //   data
  // })
  return new Promise(resolve => {
    resolve({
      status: 0,
      code: null,
      message: null
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

12. 在 methods 中添加删除方法,添加变量 currentSelection

export default {
  data() {
    // 省略...
    // 当前选择的数据
    currentSelection: []
  },
  methods: {
    /**
     * 删除
     * @param data
     */
    deleteMgt1(data) {
      if (!data || (data instanceof Array && !data.length)) {
        // 请选择至少一条数据
        this.$message.warning(this.$C_Msg('onlyOneRecord'))
        return
      }

      let deleteIds = data instanceof Array ? data.map(item => item.id) : [data.id]

      // 批量删除
      this.$confirm(this.$C_Label('deleteConfirm'), this.$C_Label('tip'), {
        confirmButtonText: this.$C_Label('confirm'),
        cancelButtonText: this.$C_Label('cancel'),
        type: 'warning'
      })
        .then(() => {
          deleteByIds({ ids: deleteIds }).then(() => {
            this.$message.success(this.$C_Msg('deleteSuccess'))
            // 刷新表格数据
            this.$refs.dataTable.getTableData()
          })
        })
        .catch(() => {})
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

13. 绑定删除按钮的 click 事件,添加 sf-table 的 selection-change 事件,用来记录所选的数据

<template>
  <div class="xxx-mgt1 sf-container">
    <sf-table @selection-change="selectionChange" >
      <!-- 此处为检索区域 省略 -->
      <template slot="tools-bar">
        <!-- 删除按钮 -->
        <el-button type="danger" size="small" @click="deleteMgt1(currentSelection)">
          {{ $C_Label('delete') }}
        </el-button>
      </template>
      <!-- table中的数据列 省略 -->
      <!-- 操作列 -->
      <el-table-column :label="$C_Label('operation')" min-width="195">
        <template slot-scope="{ row }">
          <el-button type="text" size="mini" @click="showDialog(row)">
            {{ $C_Label('edit') }}
          </el-button>
          <el-button
            type="text"
            size="mini"
            class="danger-text"
            @click="deleteMgt1(row)"
            >
            {{ $C_Label('delete') }}
          </el-button>
        </template>
      </el-table-column>
    </sf-table>
  </div>
</template>
<script>
import { deleteByIds } from '@/api/XXX/mgt1'

export default {
  name: 'Mgt1',
  data() {
    return {
      currentSelection: []
    }
  },
  methods: {
    /**
     * 选择的table数据
     * @param selection
     */
    selectionChange(selection) {
      this.currentSelection = selection
    },
    /**
     * 删除
     * @param data
     */
    deleteMgt1(data) {
      if (!data || (data instanceof Array && !data.length)) {
        // 请选择至少一条数据
        this.$message.warning(this.$C_Msg('onlyOneRecord'))
        return
      }

      let deleteIds = data instanceof Array ? data.map(item => item.id) : [data.id]

      // 批量删除
      this.$confirm(this.$C_Label('deleteConfirm'), this.$C_Label('tip'), {
        confirmButtonText: this.$C_Label('confirm'),
        cancelButtonText: this.$C_Label('cancel'),
        type: 'warning'
      })
        .then(() => {
          deleteByIds({ ids: deleteIds }).then(() => {
            this.$message.success(this.$C_Msg('deleteSuccess'))
            // 刷新表格数据
            this.$refs.dataTable.getTableData()
          })
        })
        .catch(() => {})
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

测试一下,勾选前三条数据,点击删除 展示效果 展示效果 完成了!

以下为 XXX 管理的目录结构

sofast-web
├── src
│   ├── api
│   │   ├── XXX
│   │   │   └── mgt1.js
│   ├── i18n
│   │   ├── XXX
│   │   │   ├── zh.js
│   │   │   └── en.js
│   ├── views
│   │   ├── XXX
│   │   │   ├── mgt1
│   │   │   │   ├── index.vue
│   │   │   │   └── Mgt1Form.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14

src/api/XXX/mgt1.js 源码

// import request from '@/utils/request'

/**
 * 查询mgt1列表
 * @param data
 * @returns {AxiosPromise}
 */
export function getPage(data) {
  // return request({
  //   url: '/mgt1/page',
  //   method: 'get',
  //   params: data
  // })
  return new Promise(resolve => {
    let records = []
    for (let i = 0; i < 10; i++) {
      records.push({
        id: i + 1,
        name: `name${i}`,
        code: `code${i}`,
        status: `${i % 2}`,
        createTime: new Date(),
        updateTime: new Date()
      })
    }
    resolve({
      status: 0,
      code: null,
      message: null,
      data: {
        records: records,
        total: 25,
        size: 10,
        current: 1,
        orders: [],
        optimizeCountSql: true,
        hitCount: false,
        searchCount: true,
        pages: 3
      },
      e: null
    })
  })
}

/**
 * 创建mgt1
 * @param data
 * @returns {AxiosPromise}
 */
export function mgt1Create(data) {
  // return request({
  //   url: '/mgt1/page',
  //   method: 'get',
  //   params: data
  // })
  return new Promise(resolve => {
    resolve({
      status: 0,
      code: null,
      message: null
    })
  })
}
/**
 * 更新mgt1
 * @param data
 * @returns {AxiosPromise}
 */
export function mgt1Update(data) {
  // return request({
  //   url: '/mgt1/page',
  //   method: 'get',
  //   params: data
  // })
  return new Promise(resolve => {
    resolve({
      status: 0,
      code: null,
      message: null
    })
  })
}

/**
 * 批量删除
 * @param data
 * @returns {AxiosPromise}
 */
export function deleteByIds(data) {
  // return request({
  //   url: '/mgt1/delete',
  //   method: 'post',
  //   data
  // })
  return new Promise(resolve => {
    resolve({
      status: 0,
      code: null,
      message: null
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

src/i18n/XXX/zh.js 源码

export default {
  // 管理1
  mgt1: {
    label: {
      name: '名称',
      code: '编码',
      status: '状态',
      createTime: '创建时间',
      updateTime: '更新时间',
      mgt1Add: '管理1新增',
      mgt1Edit: '管理1编辑'
    },
    message: {
      nameRequire: '请输入名称',
      codeRequire: '请输入编码'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

src/i18n/XXX/en.js 源码

export default {
  // 管理1
  mgt1: {
    label: {
      name: 'Name',
      code: 'Code',
      status: 'Status',
      createTime: 'Create Time',
      updateTime: 'Update Time',
      mgt1Add: 'Add',
      mgt1Edit: 'Edit'
    },
    message: {
      nameRequire: 'Name has required',
      codeRequire: 'Code has required'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

src/view/XXX/mgt1/index.vue 源码

<template>
  <div class="xxx-mgt1 sf-container">
    <sf-table
      ref="dataTable"
      :action="getPage"
      :list-params="listParams"
      :data="tableData"
      @selection-change="selectionChange"
    >
      <!-- 此处为检索区域 -->
      <template slot="search-area">
        <el-form
          ref="searchForm"
          :model="searchParams"
          label-width="100px"
          inline
          size="small"
          label-position="right"
        >
          <el-form-item :label="$L_Label('name')" prop="name">
            <!-- 名称 -->
            <el-input v-model="searchParams.name" clearable></el-input>
          </el-form-item>
          <el-form-item :label="$L_Label('code')" prop="code">
            <!-- 编码 -->
            <el-input v-model="searchParams.code" clearable></el-input>
          </el-form-item>
          <el-form-item :label="$C_Label('status')" prop="status">
            <!-- 状态 -->
            <sf-dict-select
              v-model="searchParams.status"
              type="sys_use_status"
              clearable
            ></sf-dict-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="search">{{ $C_Label('search') }}</el-button>
            <el-button @click="clearCondition">{{ $C_Label('reset') }}</el-button>
          </el-form-item>
        </el-form>
      </template>
      <template slot="tools-bar">
        <el-button type="primary" size="small" @click="showDialog()">
          {{ $C_Label('add') }}
        </el-button>
        <el-button type="danger" size="small" @click="deleteMgt1(currentSelection)">
          {{ $C_Label('delete') }}
        </el-button>
      </template>
      <!-- 复选框 -->
      <el-table-column type="selection" width="44"></el-table-column>
      <!-- 名称 -->
      <el-table-column :label="$L_Label('name')" prop="name" width="150"></el-table-column>
      <!-- 编码 -->
      <el-table-column :label="$L_Label('code')" prop="code" width="150"></el-table-column>
      <!-- 状态 -->
      <el-table-column :label="$L_Label('status')" prop="status" width="150">
        <template slot-scope="{ row }">
          <el-tag :type="row.status === '0' ? 'primary' : 'danger'" size="mini">
            {{ getDictLabel('sys_use_status', row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <!-- 创建时间 -->
      <el-table-column :label="$C_Label('createTime')" width="150">
        <template slot-scope="{ row }">
          {{ row.createTime | dateTimeFmt }}
        </template>
      </el-table-column>
      <!-- 更新时间 -->
      <el-table-column :label="$C_Label('updateTime')" width="150">
        <template slot-scope="{ row }">
          {{ row.updateTime | dateTimeFmt }}
        </template>
      </el-table-column>
      <el-table-column :label="$C_Label('operation')" min-width="195">
        <template slot-scope="{ row }">
          <el-button type="text" size="mini" @click="showDialog(row)">
            {{ $C_Label('edit') }}
          </el-button>
          <el-button type="text" size="mini" class="danger-text" @click="deleteMgt1(row)">
            {{ $C_Label('delete') }}
          </el-button>
        </template>
      </el-table-column>
    </sf-table>
    <!-- 表单 -->
    <mgt1-form
      v-if="dialogVisible"
      :visible.sync="dialogVisible"
      :data="formData"
      @success="search"
    ></mgt1-form>
  </div>
</template>

<script>
import lang from '@/mixins/lang'
import SfTable from '@/components/Table/index'
import SfDictSelect from '@/components/DictSelect/index'
import { getPage, deleteByIds } from '@/api/XXX/mgt1'
import Mgt1Form from '@/views/level1menu/level2menu/helloWorld/Mgt1Form'

export default {
  name: 'Mgt1',
  mixins: [lang],
  langPrefix: 'XXX.mgt1',
  components: { Mgt1Form, SfDictSelect, SfTable },
  data() {
    return {
      searchParams: {
        name: null,
        code: null,
        status: null
      },
      tableData: [],
      listParams: {},
      dialogVisible: false,
      currentSelection: [],
      getPage
    }
  },
  methods: {
    /**
     * 检索
     */
    search() {
      this.listParams = Object.assign({ current: 1, total: 0 }, this.searchParams)
      // 刷新表格数据
      this.$refs.dataTable.getTableData()
    },
    /**
     * 检索重置
     */
    clearCondition() {
      // 清空检索区域
      this.$refs.searchForm.resetFields()
      // 重新检索
      this.search()
    },
    /**
     * 弹出新增/编辑
     * @param row
     */
    showDialog(row) {
      this.formData = row || {}
      this.dialogVisible = true
    },
    /**
     * 删除
     * @param data
     */
    deleteMgt1(data) {
      if (!data || (data instanceof Array && !data.length)) {
        // 请选择至少一条数据
        this.$message.warning(this.$C_Msg('onlyOneRecord'))
        return
      }

      let deleteIds = data instanceof Array ? data.map(item => item.id) : [data.id]

      // 批量删除
      this.$confirm(this.$C_Label('deleteConfirm'), this.$C_Label('tip'), {
        confirmButtonText: this.$C_Label('confirm'),
        cancelButtonText: this.$C_Label('cancel'),
        type: 'warning'
      })
        .then(() => {
          deleteByIds({ ids: deleteIds }).then(() => {
            this.$message.success(this.$C_Msg('deleteSuccess'))
            // 刷新表格数据
            this.$refs.dataTable.getTableData()
          })
        })
        .catch(() => {})
    },
    /**
     * 选择的table数据
     * @param selection
     */
    selectionChange(selection) {
      this.currentSelection = selection
    }
  }
}
</script>

<style scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188

src/view/XXX/mgt1/Mgt1Form.vue 源码

<template>
  <sf-dialog
    :visible="visible"
    :title="title"
    width="600px"
    @update:visible="val => $emit('update:visible', val)"
  >
    <el-form
      ref="mgt1Form"
      :model="mgt1Form"
      label-width="120px"
      label-position="right"
      size="small"
      :rules="mgt1DataRules"
    >
      <el-form-item :label="$L_Label('name')" prop="name">
        <el-input v-model.trim="mgt1Form.name" minlength="4" />
      </el-form-item>
      <el-form-item :label="$L_Label('code')" prop="code">
        <el-input v-model.trim="mgt1Form.code" minlength="4" />
      </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button type="primary" size="small" :loading="saveLoading" @click="save">
        {{ $C_Label('save') }}
      </el-button>
      <el-button size="small" @click="closeDialog">{{ $C_Label('cancel') }}</el-button>
    </div>
  </sf-dialog>
</template>

<script>
import SfDialog from '@/components/Dialog/index'
import { mgt1Create, mgt1Update } from '@/api/XXX/mgt1'
import lang from '@/mixins/lang'

export default {
  name: 'Mgt1Form',
  langPrefix: 'XXX.mgt1',
  components: { SfDialog },
  props: {
    visible: Boolean,
    data: Object
  },
  mixins: [lang],
  data() {
    return {
      mgt1Form: {
        status: 0,
        roleId: []
      },
      title: null,
      mgt1DataRules: {
        name: [{ required: true, message: this.$L_Msg('nameRequire'), trigger: 'blur' }],
        code: [{ required: true, message: this.$L_Msg('codeRequire'), trigger: 'blur' }]
      },
      saveLoading: false
    }
  },
  methods: {
    /**
     * 保存信息
     */
    save() {
      this.$refs.mgt1Form.validate(valid => {
        if (valid) {
          this.saveLoading = true
          let request = null
          let mgt1Form = Object.assign({}, this.mgt1Form)
          if (mgt1Form.id) {
            request = mgt1Update(mgt1Form)
          } else {
            request = mgt1Create(mgt1Form)
          }
          request
            .then(res => {
              // 保存成功
              this.$message.success(this.$C_Msg('saveSuccess'))
              this.saveLoading = false
              this.$emit('success', {})
              this.closeDialog()
            })
            .catch(() => {
              this.saveLoading = false
            })
        }
      })
    },
    /**
     * 关闭弹窗
     */
    closeDialog() {
      this.$emit('update:visible', false)
    }
  },
  async created() {
    if (this.data.id) {
      // 编辑
      this.title = this.$L_Label('mgt1Edit')
      this.mgt1Form = { ...this.data }
    } else {
      // 新增
      this.title = this.$L_Label('mgt1Add')
    }
  }
}
</script>

<style scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
上次更新: 2025/04/23, 09:03:53

← 架构设计 全局配置→

Theme by | Copyright © -2025
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式