管理系统最佳实践
前端管理系统代码编写最佳示例。
内容目录
- 表单验证提交 标准代码
- 新增页面
- Table操作按钮样式
- 关于element 、vxe-table 组件属性
- el-select 组件增强功能
- 对象深拷贝
- 前端本地字典
- 关于接口调用
- 导入引用
- el-form 样式
- 关于 el-form 的 label 宽度
- 关于样式编写
- 关于路由配置
表单验证提交
- 具体参考采购订单新增提交
注意: 先了解 Promise、async 和 await 对理解以下代码有帮助
- Promise (opens in a new tab)
- Promise.resolve (opens in a new tab)
- Promise.reject (opens in a new tab)
- async (opens in a new tab)
- await (opens in a new tab)
- info-form.vue 中的 部分代码
<template>
<el-form class="sht-info-form-5" :model="infoForm" :rules="rules" ref="ruleForm" label-width="$getConfig('EL_FORM_LABEL_WIDTH')">
代码省略...
</el-form>
</template>
<script>
import { getFormErrorMsg } from '@/util/prompt';
export default {
methods: {
submitForm() {
return new Promise((resolve, reject) => {
this.$refs.ruleForm.validate((valid, o) => {
// 校验是否成功
if (valid) {
// 验证成功
resolve();
} else {
// 验证失败 如果失败调用 getFormErrorMsg 方法传入 o 参数 (o -> callback 的第二个参数)
// getFormErrorMsg 返回错误的验证消息
reject(getFormErrorMsg(o));
}
});
});
},
}
}
</script>
- info-goods.vue 中的 部分代码
<template>
<vxe-table
ref="xTable"
show-footer
show-overflow
:max-height="getHeight()"
:edit-rules="validRules"
:loading="loadingTable"
:footer-method="footerMethod"
:data="infoForm.purchaseOrderInfoList"
:edit-config="{ trigger: 'click', mode: 'row' }"
>
代码省略...
</vxe-table>
</template>
<script>
import { getTableErrorMsg } from '@/util/prompt';
export default {
methods: {
// 提交验证
async submitForm() {
if (!this.infoForm.purchaseOrderInfoList.length) {
return Promise.reject('请增加采购商品明细!');
}
// 校验是否成功
const errMap = await this.$refs.xTable.fullValidate(true).catch((err) => err);
if (errMap) {
// 校验失败 调用 getTableErrorMsg 传入errMap 得到 错误的验证消息
return Promise.reject(getTableErrorMsg(errMap));
}
// 校验成功
return Promise.resolve();
},
}
}
</script>
- add.vue 中的 部分代码
<template>
<el-button @click="handleCheck($getConfig('SUBMIT_TYPE'))" type="primary">新增提交</el-button>
代码省略...
<InfoForm :info-form="infoForm" ref="infoForm" />
代码省略...
<InfoGoods :getHeight="getHeight" :info-form="infoForm" ref="infoGoods" />
代码省略...
</template>
<script>
import { promptMessage } from '@/util/prompt';
export default {
methods: {
// 提交
async handleCheck(type) {
try {
// 校验订单info-form form必填项
await this.$refs.infoForm.submitForm();
// 校验订单info-goods table必填项
await this.$refs.infoGoods.submitForm();
/* 代码省略... */
} catch (msg) {
// msg 都是 reject 返回的错误信息 通过 调用promptMessage 进行提示
promptMessage.call(this, msg);
}
}
}
}
</script>
新增页面
- 表单样式
- 新开页面的表单提交中有多个输入框的(一般在15个以上) 使用以下样式 采用一行5个输入框排列
- 弹出框的表单提交中有多个输入框的(一般在15个以上) 使用以下样式 采用一行4个输入框排列
- el-form 一定要加上 label-width="$getConfig('EL_FORM_LABEL_WIDTH')" class="sht-info-form-5"
- label-width 大部分是120px 如果标题超长则自定义
- 新开页面表单样式 (采购订单新增页面)
- 代码片段
<template>
<el-form label-width="$getConfig('EL_FORM_LABEL_WIDTH')" class="sht-info-form-5">
<!-- 此处省略部分代码 -->
<el-form-item label="所属组织">
<el-input v-model="infoForm.tenant" disabled placeholder="系统自动生成" />
</el-form-item>
<!-- 此处省略部分代码 -->
<el-form-item label="备注" style="flex: 0 0 40%">
<el-input type="textarea" v-model="infoForm.remark" />
</el-form-item>
</el-form>
</template>
- 弹出框表单样式 (采购退货单) *
- 代码片段
<template>
<el-dialog title="新增退货单 "width="80%">
<el-form ref="infoForm" :model="form" class="sht-info-form-4" label-width="$getConfig('EL_FORM_LABEL_WIDTH')">
<!-- 此处省略部分代码 -->
<el-form-item label="xxxxx" title="得捷-湿度敏感等级">
<el-input v-model="form.dmsl" disabled />
</el-form-item>
<el-form-item label="xxxxx" style="flex: 0 0 40%">
<el-input v-model="form.dataBookUrl" disabled type="textarea" />
</el-form-item>
</el-form>
<!-- 此处省略部分代码 -->
</el-dialog>
</template>
操作按钮样式
注意: 操作按钮中的 button 大于2个 以上需要使用 OperateButtonGroup 组件 进行包裹
- button 大于2个 标准写法与实际样式
- 代码片段
<template>
<vxe-table
ref="table"
emptyRender
:data="tableData"
:max-height="getHeight()"
@checkbox-change="select"
@checkbox-all="handleSelectionChange"
>
<vxe-table-column title="操作" width="100px" align="center">
<!-- 省略部分代码 -->
<template #default="{ row }">
<OperateButtonGroup>
<el-button type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" @click="handleRecordsOpen(row)">记录</el-button>
<el-button size="mini" type="primary" @click="handleDetailsOpen(row)">详情</el-button>
<el-button type="danger" v-auth="'modelbase_remove'" @click="handleDelete(row)">删除</el-button>
</OperateButtonGroup>
</template>
</vxe-table-column>
</vxe-table>
</template>
- button 小于等于2个 标准写法与实际样式
- 代码片段
<template>
<vxe-table>
<vxe-table-column title="操作" width="100px" align="center">
<!-- 省略部分代码 -->
<template #default="{ row }">
<el-button type="primary" v-auth="'brandnormal_edit'" @click="handleClickEdit(row)">编辑</el-button>
<el-button type="danger" v-auth="'brandnormal_remove'" @click="handleDelete(row)">删除</el-button>
</template>
</vxe-table-column>
</vxe-table>
</template>
组件属性
注意: 在 element-ui 源代码中修改了size属性为mini, vxe-table 中的在全局配置文件中设置了size属性为mini
- element-ui 和 vxe-table 中的 所有组件不应该有 size="mini"
<template>
<!-- 错误示例 -->
<el-input size="mini" v-model="infoForm.bankCharge" />
<!-- 正确示例 -->
<el-input v-model="infoForm.bankCharge" />
</template>
按钮权限控制
- 注意 如果button中有v-if 一定要写注释而且注释方式要同下面代码注释一致
- v-auth="'权限'"
<template>
<!-- 审批状态 => 审核通过 -->
<el-button type="primary" @click="handlePrint" v-auth="'purchase_order_print'" v-if="obj.approveStatus === 3">
打印
</el-button>
<!-- 订单状态 => 未完成 -->
<!-- 审批状态 => 待提交审核,审核驳回 -->
<el-button
type="primary"
@click="handleDoWaste"
v-auth="'purchase_order_cancel_order'"
v-if="obj.status === 0 && (obj.approveStatus === 0 || obj.approveStatus === 2)"
>
订单作废
</el-button>
<!-- 订单状态 => 未完成 -->
<!-- 审批状态 => 待提交审核,审核驳回 -->
<el-button size="mini" type="primary" @click="handleEdit" v-auth="'purchase_order_edit'">编辑</el-button>
</template>
el-select
只要在 el-option 增加 item属性 就能在 el-select的@change 方法的回调函数中从第二个参数获取到
<template>
<el-select
@change="handleSupplierChange"
>
<el-option
:item="item"
:key="item.id"
:value="item.id"
:label="item.supplierName"
v-for="item in supplierOptions"
/>
</el-select>
</template>
<script>
export default {
methods: {
// id 是 item.id, item 当前数据对象
handleSupplierChange(id, item) {}
}
}
</script>
对象深拷贝
- 禁止使用 JSON.parse(JSON.stringify(xxx));
- 使用 cloneDeep 方法 使用方法 'lodash/cloneDeep'
import { cloneDeep } from 'lodash';
const b = { b1: 10, a1: 20 };
const a = cloneDeep(b);
console.log(a === b); // false
console.log(a !== b); // true
前端本地字典
- 本地字典统一放在 /config/data-keys.js 文件中
- 需要在 index.d.ts 加上类型定义
- 统一使用 $getDataKeys 方法获取 $getDataKeys是定义在Vue原型上的方法
<!-- 使用的地方 src\views\purchase\order\details.vue -->
<el-form-item label="结算期">
<el-input v-model="$getDataKeys('settlementPeriodOptions')[infoForm.settlementPeriod]" disabled />
</el-form-item>
// 字典存放的文件位置 src\config\data-keys.js
export const settlementPeriodOptions = {
1: '0天',
2: '5天',
3: '7天',
4: '10天',
5: '15天',
6: '30天',
7: '45天',
8: '60天',
9: '90天',
10: '150天',
};
// 类型定义的位置 src\index.d.ts
type DataKeysType = {
settlementPeriodOptions: {
'1': string
'2': string
'3': string
'4': string
'5': string
'6': string
'7': string
'8': string
'9': string
'10': string
}
}
关于接口调用
注意 不要在 非 /api/目录下 引用 import request from '@/util/http'; 不要写重复接口, 定义接口前 应当全文搜索 url 确保没有重复接口
- 关于请求方法的书写
// good
export const getCaptcha = () =>
request({
url: '/api/blade-auth/captcha',
method: 'get',
});
// bad
// 不要在请求体中对数据做更改
export const getPurchaseApplyForList = async (params) => {
const { data } = await request({
url: '/api/oksht-drp/purchaserequestorder/list/purchase',
method: 'get',
params,
});
return data.data;
};
// bad
// 不要在请求体中做逻辑处理
export const dispatchOutDoWaste = async (data) =>
new Promise((resolve) => {
request({
url: '/api/oksht-drp/purchaserequestorder/invalid',
method: 'post',
data,
})
.then(() => {
resolve(true);
})
.catch(() => {
resolve(false);
});
});
- 对接口返回做异常处理
// good
// 保证在接口异常的情况下 js能正常往下执行
const res = await getMenu(id).catch(() => null);
// 通过可选链的方式 处理数据异常
if (res?.data?.success) {
this.form = res.data.data;
this.parentName = res.data.data?.parentName;
}
// bad
const res = await getMenu(id);
if (res) {
this.form = res.data.data;
this.parentName = res.data.data?.parentName;
}
导入引用
1、第三方库排第一位 绝对路径排第二位 相对路径 排第三位
// good
import day from 'dayjs'; // 第三方库
import { listMixin } from '@/common/mixin/list'; // 绝对路径
import QueueTable from './component/queueTable'; // 相对路径
// bad
import QueueTable from './component/queueTable'; // 相对路径
import day from 'dayjs'; // 第三方库
import { listMixin } from '@/common/mixin/list'; // 绝对路径
样式
注意 sht-info-form 会被移除, 请使用 sht-info-form-x (x 代表 1-5的其中一个数字) 代码示例会省略 3、4列排版
- sht-info-form-1 ( 1列排版 )
<template>
<el-form
class="sht-info-form-1"
:label-width="$getConfig('EL_FORM_LABEL_WIDTH')"
>
<!-- 伪代码 -->
<el-form-item />
<el-form-item />
<el-form-item />
<el-form-item />
</el-form>
</template>

- sht-info-form-2 ( 2列排版 )
<template>
<el-form
class="sht-info-form-2"
:label-width="$getConfig('EL_FORM_LABEL_WIDTH')"
>
<!-- 伪代码 -->
<el-form-item />
<el-form-item />
<el-form-item />
<el-form-item />
<!-- 独占一行 -->
<el-form-item style="flex: 0 0 100%" />
<!-- 独占一行 -->
<el-form-item style="flex: 0 0 100%" />
</el-form>
</template>

- sht-info-form-5 ( 5列排版 )
<template>
<el-form
class="sht-info-form-5"
:label-width="$getConfig('EL_FORM_LABEL_WIDTH')"
>
<!-- 伪代码 -->
<el-form-item />
<el-form-item />
<el-form-item />
<el-form-item />
<!-- 独占两列 -->
<el-form-item style="flex: 0 0 40%" />
</el-form>
</template>

el-form的label宽度
注意 如果label的宽度是120 使用 $getConfig('EL_FORM_LABEL_WIDTH') 特殊情况可以不使用
<template>
<el-form :label-width="$getConfig('EL_FORM_LABEL_WIDTH')">
<el-form-item />
</el-form>
</template>
样式编写
- 颜色、字体大小 必须使用使用变量
- 类名穿透使用 ::v-deep
/* Color 颜色 ----------------------- */
$--color-primary: #BF2722; // 主色
$--color-white: #FFFFFF;
$--color-black: #000000;
$--color-E7E7E7: #E7E7E7;
$--color-DFDFDF: #DFDFDF;
$--color-CCCCCC: #CCCCCC;
$--color-F0F0F0: #F0F0F0;
$--color-FFDEDD: #FFDEDD;
$--color-success: #21CA3D; // 成功色
$--color-warning: #FF8C19; // 警告色
$--color-danger: #FF5151; // 失败色
$--color-info: #0090FF; // 提醒色
/* Typography 字体 ----------------------- */
$--size-large: 30px;
$--font-size-extra-large: 20px;
$--font-size-large: 18px;
$--font-size-medium: 16px;
$--font-size-base: 14px;
$--font-size-small: 13px;
$--font-size-extra-small: 12px;
$--font-weight-primary: 500;
$--font-weight-secondary: 100;
书写参考
@import '~@/assets/styles/var.scss';
.download-wrapper {
display: flex;
align-items: center;
justify-content: center;
.text {
margin-right: 20px;
padding: 6px;
color: $--color-info;
font-size: $--font-size-base;
}
.link {
font-size: $--font-size-base;
}
}
错误示例
<!-- 严禁 字体、颜色相关属性 在标签中直接使用 -->
<!-- 在标签中不要书写过多的样式 > 2组样式属性 应当使用 class -->
<template>
<p style="font-size: 12px; color: red">{{ v.createTime }}</p>
</template>
关于路由配置
- 正常情况 文件路径 src/views/purchase/order/index.vue 那路由path就是 /purchase/order/index
- 为了减少path长度 在配置菜单时按照下面图片方式进行配置 /purchase/order 系统会自动索引到 src/views/purchase/order/index.vue 文件
- 系统在配置后缀选择是的情况 会在查找文件时会把 路由path 与 index.vue 拼接
