今年的一个项目要用到lin-cms-vue框架,长话短说,就是在lin-cms-vue的基础上封装了路由、接口请求、axios的拦截、一些处理函数、一些公共的框架(如lin-table)。当然,已经有成熟轮子的用户完全用不到。我也是抱着试一下的心态看看。之间也接触过一些好的框架,包括一些大公司牛人自己封装一些框架。
还是啰嗦了,其实lin-cms-vue就是对vue进行了二开,方便后续的开发。
官网地址:https://doc.cms.talelin.com/client/
项目clone下来后运行。
因为后端项目lin-cms-koa调用了插件’@koa/cors’,所以不用配置代理,直接和后端联调就可以了。
现在进入正题说一下lin-cms-vue的开发心得:
一、组件封装
1、table
对于一个管理后台类目框架,最重要的莫过于对el-table的封装,然而,lin-table封装的不够好,并不尽如人意。
槽点一:
首先吐槽操作栏operate,这种方式需要将operate中定义的方法在重新定义一次,否则会找不到这个方法,弃用了(但是代码中保留了operate方法),最后直接render的。
render方法如下:
<el-table-column
v-if="operate && operate.length"
label="操作"
fixed="right"
class="action-btn-wrap"
:width="actionWidth"
>
<template slot-scope="scope">
<template v-for="(item, index) in operate">
<el-button
v-if="item.key ? testBtn(scope.row, item.key, item.value) : true"
:type="item.type"
plain
:key="index"
size="mini"
v-permission="{ permission: item.permission ? item.permission : '', type: 'disabled' }"
@click.native.prevent.stop="buttonMethods(item.func, scope.$index, scope.row)"
>
{{ item.name }}
</el-button>
</template>
</template>
</el-table-column>
// 就直接render吧
<el-table-column
v-else-if="useActionRender"
label="操作"
fixed="right"
class="action-btn-wrap"
:width="actionWidth"
>
<template slot-scope="scope">
<table-column
:index="scope.$index"
:row="scope.row"
:col="scope.column"
:render="actionRender" />
</template>
</el-table-column>
然后组件中需要渲染的地址直接jsx语法:
<!-- 表格 -->
<lin-table
title="工单列表"
:tableColumn="tableColumn"
:tableData="tableData"
:useActionRender="true"
:action-render="actionRender"
:showSelectCol="true"
:index="true"
:actionWidth="150"
:pagination="pagination"
:currentChange="currentChange"
:sizeChange="sizeChange"
v-loading="loading"
></lin-table>
// 方法写在了methods中:
actionRender(h, { index, row, col }) {
const authority = this.user.role === 0 || this.user.role === 1 ? true : false
return (
<div class="action-wrap">
{/*客服登录才有编辑按钮*/}
<el-button v-permission={['客服']} size="mini" plain type="primary" onClick={() => this.handleEdit(index, row, col)}>
编辑
</el-button>
<el-button size="mini" plain type="primary" onClick={() => this.goToGroupEditPage(index, row, col)}>
详情
</el-button>
</div>
)
}
槽点二:
对于table来说,最重要的就是搜索栏了。但是很遗憾没有配套的,只煎蛋的lin-search之类的。不知道这样的封装意义何在?
看一下框架自带的代码:
无用的很。
自己做了seach的封装。可以用mixin方法将search、reset都封装为公共的,这样就不用每个table页面都单独search。最后因为实际项目的原因,只将reset方法封装在了mixin中。
<template>
<div class="basic-search">
<div class="basic-search__hd" v-show="showHd">
<span class="basic-search__hd-line"></span>
<span class="basic-search__hd-title">筛选条件</span>
</div>
<el-row class="basic-search__bd">
<el-col class="basic-search__col-l" :span="leftSpan">
<el-form
ref="form"
:model="query"
:label-position="labelPosition"
:label-width="labelWidth + 'px'"
@submit.native.prevent
:size="size"
>
<slot> </slot>
</el-form>
</el-col>
<el-col class="basic-search__col-r" :span="24 - leftSpan">
<template v-if="mode === 2">
<el-row>
<el-col :span="24">
<el-button type="primary" icon="el-icon-search" @click="handleSearch(query)">查询</el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-button icon="el-icon-refresh" @click="handleReset" v-if="showReset">重置</el-button>
</el-col>
</el-row>
</template>
<template v-else>
<el-button type="primary" icon="el-icon-search" @click="handleSearch(query)">查询</el-button>
<span style="margin:0 2px;"> </span>
<el-button icon="el-icon-refresh" @click="handleReset" v-if="showReset">重置</el-button>
</template>
<a
title="展开更多"
class="toggle-more-btn"
:class="[closed ? 'open' : 'close']"
@click="toggleMore"
v-if="showMore"
></a>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
props: {
showHd: {
type: Boolean,
default: true
},
labelPosition: {
type: String,
default: 'right',
},
labelWidth: {
type: Number,
default: 80,
},
// 模式(1: 单行, 2: 多行)
mode: {
type: Number,
default: 2,
},
showMore: {
type: Boolean,
default: false,
},
showReset: {
type: Boolean,
default: true,
},
closed: {
type: Boolean,
default: true,
},
query: {
type: Object,
default() {
return {}
},
},
toggleMore: {
type: Function,
default: () => {},
},
handleSearch: {
type: Function,
default: () => {},
},
handleReset: {
type: Function,
default: () => {},
},
size: {
type: String,
default: 'small',
},
},
computed: {
leftSpan() {
if (this.showReset) {
return this.mode === 2 ? 21 : 18
}
return 21
},
},
}
</script>
<style lang="scss" scoped>
/deep/.el-select {
width: 100%;
}
.basic-search {
border-bottom: 1px solid #e2e2e5;
padding: 20px 30px;
}
.basic-search__hd {
margin-bottom: 24px;
}
.basic-search__hd-line {
display: inline-block;
margin-right: 10px;
width: 4px;
height: 16px;
border-radius: 4px;
vertical-align: middle;
background: #4186f6;
}
.basic-search__hd-title {
color: #3f4656;
font-size: 14px;
}
.basic-search__bd {
padding: 0 20px 0 15px;
}
.basic-search__col-l {
padding-right: 16px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.basic-search__col-r {
position: relative;
/deep/ .el-row:first-child {
margin-bottom: 22px;
}
.toggle-more-btn {
position: absolute;
top: 50%;
right: -18px;
width: 14px;
height: 14px;
margin-top: -8px;
background: no-repeat center center;
background-size: contain;
&.open {
background-image: url('~@/assets/image/icon-open.png');
}
&.close {
background-image: url('~@/assets/image/icon-close.png');
}
}
}
/deep/.el-form-item__label {
text-align: left;
}
</style>
然后mix中处理:
import Utils from 'lin/util/util'
import BasicSearch from '@/component/base/basic-search'
/**
* 列表页查询表单公共方法混入
*/
export default {
components: {
BasicSearch,
},
props: {
updateState: Function,
handleSearch: Function,
},
data() {
return {
query: this.getDefaultQuery(),
closed: true,
}
},
methods: {
toggleMore() {
this.closed = !this.closed
},
handleReset() {
this.query = this.getDefaultQuery()
this.handleSearch(this.query)
},
getDefaultQuery() {
const { fields } = this
const query = {}
for (let i = 0; i < fields.length; i++) {
if (Utils.isObject(fields[i])) {
query[fields[i].key] = fields[i].default
} else {
query[fields[i]] = undefined
}
}
return query
},
},
}
最后在每个模块中单独一个search文件:
<template>
<basic-search
:label-width="90"
:query="query"
:show-more="true"
:closed="closed"
:mode="1"
:toggle-more="toggleMore"
:handle-search="handleSearch"
:handle-reset="handleReset"
>
<el-row :gutter="24">
</el-row>
<div class="more-row-wrap" v-show="!closed">
</div>
</basic-search>
</template>
<script>
import ExactSearch from '@/lin/mixin/exact-search'
import { customerSourceData } from 'lin/format/replace-sheet'
export default {
mixins: [ExactSearch],
components: {},
props: {},
data() {
return {}
},
watch: {
'query.dateRange':function(val) {
if(val && val.length === 2) {
this.query.start_time = val[0]
this.query.end_time = val[1]
} else {
this.query.start_time = undefined
this.query.end_time = undefined
}
}
},
beforeCreate() {
this.fields = ['phone', 'source', 'name', 'client_type', 'dateRange']
},
}
</script>
然后在table页面引入search组件:
import ExactSearch from './components/exactSearch'
import LinTable from '@/component/base/table/lin-table'
export default {
name: 'CustomerMg',
components: {
LinTable,
ExactSearch,
},
最后呈现效果如下图:
二、说一下亮点吧
1、api的封装
每个模块api都是一个构造函数,鄙人之前习惯于将每个模块api写在同一个js文件中,但是每一个接口是一个函数export出去,这样方便组件单独引入每一个接口。当然也可以通过import * as 来将所有的函数作为一个对象暴露出来。也很方便。
2、关于content部分是放在keep-alive中呢,还是不放呢,众说纷纭。本框架显然是没有放的。
三、打包部署和本地ngix来检查你的打包
1、配置publicPath
因为项目不是在域名的根目录下,而是子目录/service下,所以有三处地方做了更改:
- vue.config.js中的publicPath
- router中index.js在router实例下添加了base
- 因为config中的sade-bar配置都是请求的public中的图片,特意在gutter.js下做了处理
说明一下,因为接口请求的时候服务器也没有nginx代理(通过标识),我都是绝对路径,所以没有必要配置/service。
三处截图如下:
我一直觉得这种处理方式不够优雅,我觉得可以服务器在nginx做配置,我这里不需要做处理了。不过既然服务器没有做nginx配置,那只能我这里处理了。
2、本地nginx检查dist包
为了校验build包是否再服务器可用,我在本地下载了一个适合window版本的nginx。
下载地址:http://nginx.org/en/download.html
我选择的还是1.16.1版本
通过cmd启动nginx,之后配置如下:
我的打包后的文件放在了d盘额Service/service文件夹下:
nginx.conf文件如下:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
# 系统
server {
listen 3000;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
root D:/Service/;
### 因为模拟了生产环境就是****.cn/service 的子目录
location /service{
index index.html;
### 采用的hsitory模式,此处配置了,当找不到文件路径重定向到index.html
try_files $uri $uri/ /service/index.html;
}
location / {
### 此处是项目部署的域名
proxy_pass http://*******.cn;
proxy_set_header Host ********.cn;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
至此,大功完成,nginx -s reload 后,输入,本地路径http://localhost:3000/service/index.html, 成功进入页面。
、