关键词:
最近包工头喊农民工小郑搬砖,小郑搬完砖后沉思片刻,决定写篇小作文分享下,作为一个初学者的全栈项目,去学习它的搭建,到落地,再到部署维护,是非常好的。
------题记
写在前面
通过本文的学习,你可以学到
vue2、element ui、vue-element-admin在前端的使用
组件设计
echarts在前端中的使用
eggjs在后端node项目中的使用
docker一键化部署
需求分析
背景
近些年,网络诈骗案频发,有假扮家里茶叶滞销的茶花女,有假扮和男朋友分手去山区支教的女教师,有告知你中了非常6+1的大奖主持人,有假扮越南那边过来结婚的妹子,各类案件层出不穷。作为公民,我们应该在社会主义新时代下积极学习组织上宣传反诈骗知识,提高防范意识。除此之外,对于种种诈骗案件,是网站的我们就应该封其网站,是电话的我们就应该封其电话,是银行的我们就该封其银行账号,是虚拟账号的我们就应该封其虚拟账号。我相信,在我们的不懈努力之下,我们的社会将会更和谐更美好!
需求
长话短说,需求大致是这样子的:有管理员、市局接警员、县区局接警员、电话追查专员、网站追查专员、银行追查专员、虚拟账号专员这几类角色, 相关的角色可以进入相关的页面进行相关的操作,其中市局和管理员的警情录入是不需要审核,直接派单下去,而县区局的警情录入需要进行审核。当审核通过后,会进行相应的派单。各类追查员将结果反馈给该警单。系统管理员这边还可以进行人员、机构、警情类别,银行卡、数据统计、导出等功能。希望是越快越好,越简单越好,领导要看的。
部分效果如图:
技术预研
这个项目不是很大,复杂度也不是很高,并发量也不会太大,毕竟是部署在public police network下的。所以我这边选用vue2,结合花裤衩大佬的vue-element-admin,前端这边就差不多了,后端这边用的是阿里开源的eggjs,因为它使用起来很方便。数据库用的是mysql。部署这边提供了两套方案,一套是传统的nginx、mysql、node、一个一个单独安装配置。另一种是docker部署的方式。
功能实现
前端
vue代码规范
参见:https://www.yuque.com/ng46ql/tynary
vue工程目录结构
参见:https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/#%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84
vue组件设计与封装
这里我选了几个有代表性的典型的组件来讲解,我们先来看一张图找找组件设计和封装的感觉。
通过观察我们发现,在后台管理界面中,蛮多的页面是长这样子的,我们不可能来一个页面我们就再写一次布局,这样人都要搞没掉。所以我们会有想法地把它封装成一个container.vue
,它主要包含头部的标题和右边的新增按钮、中间的过滤面板以及下方的表格。
container.vue
是一个布局组件,它主要是框定了你一个页面大致的布局, 在适当的位置,我们加入插槽slot
去表示这块未知的区域,container.vue
代码实现如下:
<template>
<div>
<el-row class="top">
<el-col :span="24">
<el-row>
<el-col :span="12">
<div
v-if="title"
class="title"
>
title
</div>
</el-col>
<el-col
:span="12"
class="btn-group"
>
<slot name="topExtra" />
<el-col />
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<slot name="tab" />
</el-col>
</el-row>
<div class="content">
<slot name="content" />
</div>
</div>
</template>
<script>
export default
name: 'CommonContainer',
props:
title: type: String, default: ''
</script>
<style lang="scss" scoped>
.top
padding: 15px;
min-height: 100px;
background-color: #fff;
box-shadow: 0 3px 5px -3px rgba(0, 0, 0, 0.1);
.title-box
height: 100px;
line-height: 100px;
display: flex;
justify-content: space-between;
align-items: center;
.title
font-size: 30px;
font-weight: 700;
.content
margin: 20px 5px 0;
padding: 20px 10px;
background: #fff;
.btn-group
text-align: right;
padding: 0 10px;
</style>
往下走,我们会想到怎么去设计表格这个组件,在设计这个组件的时候,我们需要清楚的知道,这个组件的输入以及输出是什么?比如说table-query.vue
这个组件,从名字我们能够看出,它是有查询请求的,那么对于请求,很容易抽象出的一些东西是,请求地址,请求参数,请求方法等等,所以这边的props大致可以这么敲定。
props:
// 请求表格数据的url地址
url: type: String, required: true ,
// 默认分页数
pageSize: type: Number, default: 10 ,
// 是否展示序号
index: type: Boolean, default: true ,
// 表格的列的结构
columns: type: Array, required: true ,
orgId: type: String, required: false, default: '' ,
// 请求表格数据的方法
method: type: String, default: 'post' ,
// 请求表格数据的参数
params: type: Object, default: () => () ,
// 是否支持高亮选中
isHighlightRow: type: Boolean, default: false ,
// 是否显示分页
isShowPagination: type: Boolean, default: true ,
// 是否显示迷你分页
isPaginationSizeSmall: type: Boolean, default: false
,
这里的输出,我们期望的是,当用户点击详情、查看、删除的时候,我要知道这一行的具体数据,那么大致可以这么敲定。
handleClick(row, type, title)
this.$emit('click-action', row, type, title)
,
这边作为组件的数据通信已经敲定了,剩下的也就是一些封装请求的逻辑,页面交互的逻辑,具体地可以看一下table-query.vue
的实现
<template>
<div>
<el-table
ref="table"
border
:data="data"
:loading="isLoading"
:highlight-row="isHighlightRow"
:row-class-name="tableRowClassName"
>
<template v-for="column in columns">
<template v-if="column.key === 'actions'">
<el-table-column
:key="column.key"
align="center"
:width="column.width"
:label="column.title"
>
<template slot-scope="scope">
<el-button
v-for="action in filterOperate(
column.actions,
scope.row.btnList
)"
:key="action.type"
type="text"
size="small"
@click="handleClick(scope.row, action.type, action.title)"
> action.title </el-button>
</template>
</el-table-column>
</template>
<template v-else-if="column.key === 'NO'">
<el-table-column
:key="column.key"
type="index"
width="80"
align="center"
:label="column.title"
/>
</template>
<template v-else>
<el-table-column
:key="column.key"
align="center"
:prop="column.key"
:width="column.width"
:label="column.title"
:formatter="column.formatter"
:show-overflow-tooltip="true"
/>
</template>
</template>
</el-table>
<el-row type="flex" justify="center" style="margin-top: 10px;">
<el-col :span="24">
<el-pagination
v-if="isShowPagination"
:small="true"
:total="total"
:background="true"
:page-sizes="pageSizeOptions"
:current-page="pagination.page"
:page-size="pagination.pageSize"
@current-change="changePage"
@size-change="changePageSize"
/>
</el-col>
</el-row>
</div>
</template>
<script>
import request from '@/utils/request'
import getLength from '@/utils/tools'
export default
name: 'CommonTableQuery',
props:
// 请求表格数据的url地址
url: type: String, required: true ,
// 默认分页数
pageSize: type: Number, default: 10 ,
// 是否展示序号
index: type: Boolean, default: true ,
// 表格的列的结构
columns: type: Array, required: true ,
orgId: type: String, required: false, default: '' ,
// 请求表格数据的方法
method: type: String, default: 'post' ,
// 请求表格数据的参数
params: type: Object, default: () => () ,
// 是否支持高亮选中
isHighlightRow: type: Boolean, default: false ,
// 是否显示分页
isShowPagination: type: Boolean, default: true ,
// 是否显示迷你分页
isPaginationSizeSmall: type: Boolean, default: false
,
data()
return
// 表格的行
data: [],
// 分页总数
total: 0,
// 表格数据是否加载
isLoading: false,
// 是否全选
isSelectAll: false,
// 渲染后的列数据字段
renderColumns: [],
// 分页
pagination:
page: 1,
pageSize: this.pageSize
,
computed:
// 是否有数据
hasData()
return getLength(this.data) > 0
,
// 分页条数
pageSizeOptions()
return this.isPaginationSizeSmall ? [10, 20, 30] : [10, 20, 30, 50, 100]
,
created()
this.getTableData()
,
methods:
tableRowClassName( row, rowIndex )
// if (rowIndex === 1)
// return 'warning-row'
// else if (rowIndex === 3)
// return 'success-row'
//
if (row.alarmNo && row.alarmNo.startsWith('FZYG'))
return 'warning-row'
return ''
,
// 改变分页
changePage(page)
this.pagination.page = page
this.getTableData()
,
// 改变分页大小
changePageSize(pageSize)
this.pagination.pageSize = pageSize
this.getTableData()
,
// 获取表格的数据
getTableData()
if (!this.url)
return
const
url,
params,
orgId,
pagination: page, pageSize ,
isShowPagination,
method
= this
this.isLoading = true
this.isSelectAll = false
const parameter = isShowPagination
? page, pageSize, orgId, ...params
: orgId, ...params
request(
method,
url,
[method === 'post' ? 'data' : 'params']: parameter
)
.then(res =>
const
data: list = [], total, page, pageSize
= res ||
this.isLoading = false
this.data = list
if (this.isShowPagination)
this.total = total === null ? 0 : total
this.pagination =
page,
pageSize
)
.catch(err =>
this.isLoading = false
console.log(err)
)
,
// 手动挡分页查询
query(page = 1, pageSize = 10)
this.pagination = page, pageSize
this.getTableData()
,
handleClick(row, type, title)
this.$emit('click-action', row, type, title)
,
filterOperate(actions, btnList)
return actions.filter(action => btnList.includes(action.type))
</script>
<style>
.el-table .warning-row
background: oldlace;
.el-table .success-row
background: #f0f9eb;
.el-tooltip__popper
max-width: 80%;
.el-tooltip__popper,
.el-tooltip__popper.is-dark
background: #f5f5f5 !important;
color: #303133 !important;
</style>
element-table: https://element.eleme.cn/#/zh-CN/component/table
element-pagination: https://element.eleme.cn/#/zh-CN/component/pagination
文件上传与下载,这个是点开警情、追查的相关页面进去的功能,大体上和楼上的表格类似,就是在原来的基础上,去掉了分页,加上了文件上传的组件。
“DO NOT REPEAT"原则, 我们期望的是写一次核心代码就好,剩下的我们每次只需要在用到的地方引入table-file.vue
就好了,这样子维护起来也方便,这就有个这个组件的想法。
我们还是想一下,对于文件我们不外乎有这些操作,上传、下载、删除、修改、预览等等,所以这边组件的输入大致可以这么敲定。
props:
canUpload: type: Boolean, default: true ,
canDelete: type: Boolean, default: true ,
canDownload: type: Boolean, default: true ,
columns: type: Array, default: () => [] ,
affix: type: String, default: ''
,
输出的话,跟楼上的table-query.vue
差不多
handleClick(row, type, title)
this.$emit('click-action', row, type, title)
,
具体地可以看下table-file.vue
的实现
<template>
<el-row>
<el-col v-if="canUpload" :span="24">
<el-upload
ref="upload"
:action="url"
drag
:limit="9"
name="affix"
:multiple="true"
:auto-upload="false"
:with-credentials="true"
:on-error="onError"
:file-list="fileList"
:on-remove="onRemove"
:on-change="onChange"
:on-exceed="onExceed"
:on-success="onSuccess"
:on-preview="onPreview"
:before-upload="beforeUpload"
:before-remove="beforeRemove"
:on-progress="onProgress"
:headers="headers"
>
<!-- <el-button size="small" type="primary">选择文件</el-button> -->
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处,或<em>选择文件</em></div>
<div slot="tip" class="el-upload__tip">
文件格式不限,一次最多只能上传9个文件,单个文件允许最大100MB
</div>
</el-upload>
</el-col>
<el-col v-if="canUpload" style="margin: 10px auto;">
<el-button
size="small"
type="primary"
@click="upload"
>确认上传</el-button>
</el-col>
<el-col :span="24">
<el-table
ref="table"
border
:data="data"
style="width: 100%; margin: 20px auto;"
>
<template v-for="column in mapColumns">
<template v-if="column.key === 'actions'">
<el-table-column
:key="column.key"
align="center"
:label="column.title"
>
<template slot-scope="scope">
<el-button
v-for="action in column.actions"
:key="action.type"
type="text"
size="small"
@click="handleClick(scope.row, action.type, action.title)"
> action.title </el-button>
</template>
</el-table-column>
</template>
<template v-else-if="column.key === 'NO'">
<el-table-column
:key="column.key"
type="index"
width="80"
align="center"
:label="column.title"
/>
</template>
<template v-else>
<el-table-column
:key="column.key"
:prop="column.key"
align="center"
:label="column.title"
/>
</template>
</template>
</el-table>
</el-col>
</el-row>
</template>
<script>
import Cookies from 'js-cookie'
import getByIds from '@/api/file'
import formatDate from '@/utils/tools'
export default
name: 'TableFile',
props:
canUpload: type: Boolean, default: true ,
canDelete: type: Boolean, default: true ,
canDownload: type: Boolean, default: true ,
columns: type: Array, default: () => [] ,
affix: type: String, default: ''
,
data()
return
fileList: [],
data: [],
ids: [],
headers:
'x-csrf-token': Cookies.get('csrfToken')
,
mapColumns: [],
url: process.env.VUE_APP_UPLOAD_API
,
watch:
affix:
async handler(newAffix)
this.data = []
this.ids = []
if (newAffix)
this.ids = newAffix.split(',').map(id => Number(id))
if (this.ids.length > 0)
const data = await getByIds( ids: this.ids )
this.data = data.map(item =>
const createTime, ...rest = item
return
createTime: formatDate(
'YYYY-MM-DD HH:mm:ss',
createTime * 1000
),
...rest
)
,
immediate: true
,
canDelete:
handler(newVal)
if (newVal)
this.mapColumns = JSON.parse(JSON.stringify(this.columns))
else
if (this.mapColumns[this.mapColumns.length - 1])
this.mapColumns[this.mapColumns.length - 1].actions = [
title: '下载',
type: 'download'
]
,
immediate: true
,
created()
this.mapColumns = JSON.parse(JSON.stringify(this.columns))
if (!this.canDelete)
if (this.mapColumns[this.mapColumns.length - 1])
this.mapColumns[this.mapColumns.length - 1].actions = [
title: '下载',
type: 'download'
]
,
methods:
beforeUpload(file, fileList)
console.log('beforeUpload: ', file, fileList)
,
onSuccess(response, file, fileList)
const
data: id, createTime, ...rest
= response
this.data.push(
id,
createTime: formatDate('YYYY-MM-DD HH:mm:ss', createTime * 1000),
...rest
)
this.ids.push(id)
this.clear()
,
onError(err, file, fileList)
console.log(err, file, fileList)
,
onPreview(file, fileList)
console.log('onPreview: ', file, fileList)
,
beforeRemove(file, fileList)
console.log('beforeRemove: ', file, fileList)
,
onExceed(files, fileList)
console.log('onExceed: ', files, fileList)
// this.$message.warning(`当前限制选择 3 个文件,本次选择了 $files.length 个文件,共选择了 $files.length + fileList.length 个文件`)
,
onRemove(file, fileList)
console.log('onRemove: ', file, fileList)
,
onChange(file, fileList)
console.log('onChange: ', file, fileList)
,
onProgress(file, fileList)
console.log('onProgress: ', file, fileList)
,
upload()
this.$refs.upload.submit()
,
clear()
this.$refs.upload.clearFiles()
this.fileList = []
,
handleClick(row, type, title)
this.$emit('click-action', row, type, title)
,
deleteData(id)
const index = this.ids.indexOf(id)
this.ids.splice(index, 1)
this.data.splice(index, 1)
</script>
<style scoped>
.center
display: flex;
justify-content: center;
</style>
element-upload: https://element.eleme.cn/#/zh-CN/component/upload
功能实现-文件导出
数据的导出也是这种后台管理系统比较常见的场景,这件事情可以前端做,也可以后端做。那么在这里结合xlsx
、file-saver
这两个包,在src下新建一个excel文件夹, 然后新建一个js文件export2Excel.js
/* eslint-disable */
import saveAs from 'file-saver'
import XLSX from 'xlsx'
function generateArray(table)
var out = [];
var rows = table.querySelectorAll('tr');
var ranges = [];
for (var R = 0; R < rows.length; ++R)
var outRow = [];
var row = rows[R];
var columns = row.querySelectorAll('td');
for (var C = 0; C < columns.length; ++C)
var cell = columns[C];
var colspan = cell.getAttribute('colspan');
var rowspan = cell.getAttribute('rowspan');
var cellValue = cell.innerText;
if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
//Skip ranges
ranges.forEach(function (range)
if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c)
for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
);
//Handle Row Span
if (rowspan || colspan)
rowspan = rowspan || 1;
colspan = colspan || 1;
ranges.push(
s:
r: R,
c: outRow.length
,
e:
r: R + rowspan - 1,
c: outRow.length + colspan - 1
);
;
//Handle Value
outRow.push(cellValue !== "" ? cellValue : null);
//Handle Colspan
if (colspan)
for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
out.push(outRow);
return [out, ranges];
;
function datenum(v, date1904)
if (date1904) v += 1462;
var epoch = Date.parse(v);
return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
function sheet_from_array_of_arrays(data, opts)
var ws = ;
var range =
s:
c: 10000000,
r: 10000000
,
e:
c: 0,
r: 0
;
for (var R = 0; R != data.length; ++R)
for (var C = 0; C != data[R].length; ++C)
if (range.s.r > R) range.s.r = R;
if (range.s.c > C) range.s.c = C;
if (range.e.r < R) range.e.r = R;
if (range.e.c < C) range.e.c = C;
var cell =
v: data[R][C]
;
if (cell.v == null) continue;
var cell_ref = XLSX.utils.encode_cell(
c: C,
r: R
);
if (typeof cell.v === 'number') cell.t = 'n';
else if (typeof cell.v === 'boolean') cell.t = 'b';
else if (cell.v instanceof Date)
cell.t = 'n';
cell.z = XLSX.SSF._table[14];
cell.v = datenum(cell.v);
else cell.t = 's';
ws[cell_ref] = cell;
if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
return ws;
function Workbook()
if (!(this instanceof Workbook)) return new Workbook();
this.SheetNames = [];
this.Sheets = ;
function s2ab(s)
var buf = new ArrayBuffer(s.length);
var view = new Uint8Array(buf);
for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
return buf;
export function export_table_to_excel(id)
var theTable = document.getElementById(id);
var oo = generateArray(theTable);
var ranges = oo[1];
/* original data */
var data = oo[0];
var ws_name = "SheetJS";
var wb = new Workbook(),
ws = sheet_from_array_of_arrays(data);
/* add ranges to worksheet */
// ws['!cols'] = ['apple', 'banan'];
ws['!merges'] = ranges;
/* add worksheet to workbook */
wb.SheetNames.push(ws_name);
wb.Sheets[ws_name] = ws;
var wbout = XLSX.write(wb,
bookType: 'xlsx',
bookSST: false,
type: 'binary'
);
saveAs(new Blob([s2ab(wbout)],
type: "application/octet-stream"
), "test.xlsx")
export function export_json_to_excel(
multiHeader = [],
header,
data,
filename,
merges = [],
autoWidth = true,
bookType = 'xlsx'
= )
/* original data */
filename = filename || 'excel-list'
data = [...data]
data.unshift(header);
for (let i = multiHeader.length - 1; i > -1; i--)
data.unshift(multiHeader[i])
var ws_name = "SheetJS";
var wb = new Workbook(),
ws = sheet_from_array_of_arrays(data);
if (merges.length > 0)
if (!ws['!merges']) ws['!merges'] = [];
merges.forEach(item =>
ws['!merges'].push(XLSX.utils.decode_range(item))
)
if (autoWidth)
/*设置worksheet每列的最大宽度*/
const colWidth = data.map(row => row.map(val =>
/*先判断是否为null/undefined*/
if (val == null)
return
'wch': 10
;
/*再判断是否为中文*/
else if (val.toString().charCodeAt(0) > 255)
return
'wch': val.toString().length * 2
;
else
return
'wch': val.toString().length
;
))
/*以第一行为初始值*/
let result = colWidth[0];
for (let i = 1; i < colWidth.length; i++)
for (let j = 0; j < colWidth[i].length; j++)
if (result[j]['wch'] < colWidth[i][j]['wch'])
result[j]['wch'] = colWidth[i][j]['wch'];
ws['!cols'] = result;
/* add worksheet to workbook */
wb.SheetNames.push(ws_name);
wb.Sheets[ws_name] = ws;
var wbout = XLSX.write(wb,
bookType: bookType,
bookSST: false,
type: 'binary'
);
saveAs(new Blob([s2ab(wbout)],
type: "application/octet-stream"
), `$filename.$bookType`);
逻辑代码如下
downloadExcel()
this.$confirm('将导出为excel文件,确认导出?', '提示',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
)
.then(() =>
this.export2Excel()
)
.catch((e) =>
this.$Message.error(e);
)
,
// 数据写入excel
export2Excel()
import('@/excel/export2Excel').then(excel =>
const tHeader = [
'警情编号',
'警情性质',
'受害人姓名',
'受害人账号',
'嫌疑人账号',
'嫌疑人电话',
'涉案总金额',
'案发时间',
'警情状态'
] // 导出的excel的表头字段
const filterVal = [
'alarmNo',
'alarmProp',
'informantName',
'informantBankAccount',
'suspectsAccount',
'suspectsMobile',
'fraudAmount',
'crimeTime',
'alarmStatus'
] // 对象属性,对应于tHeader
const list = this.$refs.inputTable.data
const data = this.formatJson(filterVal, list)
excel.export_json_to_excel(
header: tHeader,
data,
filename: this.filename,
autoWidth: this.autoWidth,
bookType: this.bookType
)
this.downloadLoading = false
)
,
// 格式转换,直接复制即可
formatJson(filterVal, jsonData)
return jsonData.map(v =>
filterVal.map(j =>
if (j === 'crimeTime')
return formatDate('YYYY-MM-DD HH:mm:ss', v[j] * 1000)
else if (j === 'alarmProp')
return this.alarmPropOptionsArr[v[j]]
else if (j === 'alarmStatus')
return this.alarmStatusOptionsArr[v[j]]
else
return v[j]
)
)
参见:https://panjiachen.gitee.io/vue-element-admin-site/zh/feature/component/excel.html
功能实现-数据统计与展示
单纯的数据只有存储的价值,而对存储下来的数据进行相应的分析,并加以图表的形式输出,可以更直观地看到数据的变化,体现数据的价值,实现新生代农民工的劳动价值。这边结合echarts对某一个时间段的警情中各部分追查的占比进行了一个统计,除此之外,对该时间段的每月的止付金额进行了一个统计,最终结合扇形和柱形对其进行展示。
翻一翻npm包,笔者物色到了两位包包可以做这件事,考虑到针对本项目对于图表的需求量不是特别大,我也懒得看两套API,就还是用了echarts。
vue-echarts: https://www.npmjs.com/package/vue-echarts
echarts: https://www.npmjs.com/package/echarts
我们会有一个数据接口,前端带上相关的请求参数通过请求/prod-api/statistics/calculate
这个接口就能够拿到后端的从数据库处理出来的相关数据,这里因为前后端都是我写的,所以我制定的规则就是,所有的计算都有后端去完成,前端只负责展示,并且约定了相关的参数格式。这样做的一个好处是,省去了前端这边对数据的封装处理。返回的格式如下:
"status": 200,
"message": "success",
"data":
"pieData": [
"name": "银行查控",
"count": 13
,
"name": "电话查控",
"count": 10
,
"name": "虚拟账号查控",
"count": 3
,
"name": "网站查控",
"count": 5
],
"barData": [
"name": "2021年1月",
"amount": 0
,
"name": "2021年2月",
"amount": 0
,
"name": "2021年3月",
"amount": 0
,
"name": "2021年4月",
"amount": 0
,
"name": "2021年5月",
"amount": 0
,
"name": "2021年6月",
"amount": 0
,
"name": "2021年7月",
"amount": 0
,
"name": "2021年8月",
"amount": 1311601
],
"totalAmount": 1311601
这里以画饼图和柱形图为例,其他的也是类似的,可以参考https://echarts.apache.org/examples/zh/index.html
公共部分
npm i echarts -S
安装echarts的npm包,然后在相应的文件引入它。
import echarts from 'echarts'
画饼图
在template中我们搞一个饼图的div
<div ref="pieChart" class="chart" />
在vue的方法里面,我们定义一个画饼的方法,这里定义的输入就是请求后端返回的数据,其他的看echarts的配置项,这边都配好了(如果写成单个组件,需要根据业务考虑相关的配置项,目前这边就care数据项)。逻辑是这样子的,定义了一个基于数据项变动的配置项options
,然后当执行drawPie
方法的时候,如果没有初始化echarts,那么我们这边就初始化一个echarts的饼,如果有,那么我们就只有更新相关的options
就好了。
drawPie(source)
const options =
title:
text: '各追查类型占比统计'
,
tooltip:
trigger: 'item',
formatter: 'b : (d%)'
,
legend:
orient: 'vertical',
x: 'left',
y: 'bottom',
data: ['银行查控', '电话查控', '虚拟账号查控', '网站查控']
,
dataset:
source
,
series:
type: 'pie',
label:
position: 'outer',
alignTo: 'edge',
margin: 10,
formatter: '@name: @count (d%)'
,
encode:
itemName: 'name',
value: 'count'
if (this.pieChart)
this.pieChart.setOption(options, true)
else
this.pieChart = echarts.init(this.$refs.pieChart)
this.pieChart.setOption(options, true)
画柱形图
跟楼上的类似的,画柱子如楼下所示:
drawBar(source)
const options =
title:
text: `各月份止付金额之和统计, 合计: $this.totalAmount元`
,
dataset:
source
,
xAxis:
type: 'category',
name: '时间'
,
yAxis: [
type: 'value',
name: '止付金额'
],
series: [
type: 'bar',
encode:
x: 'name',
y: 'amount'
,
label:
normal:
show: true,
position: 'top'
]
if (this.barChart)
this.barChart.setOption(options, true)
else
this.barChart = echarts.init(this.$refs.barChart)
this.barChart.setOption(options, true)
,
备注:考虑到需求量不大,这里笔者是为了赶进度偷懒写成这样的,学习的话,建议封装成一个个组件,例如pie.vue
,bar.vue
这样子去搞。
功能实现-页面权限控制和页面权限的按钮权限粒度控制
因为这个项目涉及到多个角色,这就涉及到对多个角色的页面控制了,每个角色分配的页面权限是不一样的,第二个就是进入到页面后,针对某一条记录,该登录用户按钮的权限控制。
页面权限控制
页面的权限这边有两种做法,分别是控制权在前端,和控制权在后端两种,在前端的话是通过获取用户信息的角色,根据角色去匹配,匹配中了就加到路由里面。在后端的话,就是登录的时候后端就把相应的路由返回给你,前端这边注册路由。
借着vue-element-admin
的东风,笔者这边是将控制权放在前端,在路由的meta中加入roles角色去做页面的权限控制的。
参见 vue-element-admin - 路由和侧边栏:https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/essentials/router-and-nav.html#%E9%85%8D%E7%BD%AE%E9%A1%B9
参见 vue-element-admin - 权限验证:https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/essentials/permission.html#%E9%80%BB%E8%BE%91%E4%BF%AE%E6%94%B9
按钮权限控制
首先我们来分析下,针对我们这个系统,不外乎删除、修改、详情、审核、追查等等按钮权限,不是特别多,所以我们可以用detail
、modify
、delete
、audit
、check
等去表示这些按钮,后端在service层进行相关业务处理,把它们这些包到一个数组btnList
里面返回给前端,跟前端这边做对比,如果命中那么我们就展示按钮。
核心代码如下:
template
<template v-if="column.key === 'actions'">
<el-table-column
:key="column.key"
align="center"
:width="column.width"
:label="column.title"
>
<template slot-scope="scope">
<el-button
v-for="action in filterOperate(
column.actions,
scope.row.btnList
)"
:key="action.type"
type="text"
size="small"
@click="handleClick(scope.row, action.type, action.title)"
> action.title </el-button>
</template>
</el-table-column>
</template>
filterOperate(actions, btnList)
return actions.filter(action => btnList.includes(action.type))
那么我们就可以这么使用了
columns: [
......
title: '操作',
key: 'actions',
align: 'center',
actions: [
title: '详情',
type: 'detail'
,
title: '修改',
type: 'modify'
]
]
关于权限校验这块,笔者所在的供应链金融团队是这么去实现的,在保理业务中,会有很多个部门,比如市场部、财务部、风控部、董事会等等。每个部门里又有经办、审核、复核等等角色。所以在处理这类业务中的权限控制,需要将用户身上绑定一个按钮权限,比如说他是市场的经办角色,那么他就可以绑定市场经办这个角色的按钮码子上。前端这边除了要在我们楼上的基础上对列表返回的做对比之外,还有对用户的做进一步对比。这里的按钮也不能够像上面一样detail
、modify
这样去写,因为角色多了每个角色这么叫不好,更科学的应该是,整理成一份excel表,然后按照相应的按钮权限去配置相应的code(比如说 20001, 20002),然后根据这个去处理业务。
后端
eggjs中的三层模型(model-service-controller)
model层是对数据库的相关表进行相应的映射和CRUD,service层是处理相关的业务逻辑,controller层是为相关的业务逻辑暴露接口。这三者层序渐进,一环扣一环。
Model
一些约定
原则上,不允许对Model层SQL语句返回的结果进行相关操作,返回什么就是什么。
统一下数据返回的格式
语法错误 null
查不到数据 false
查到数据 JSON | Number
统一下model层文件类的通用方法
add:新增
set:更新
del:删除(本系统由于数据需要,所以不会真的删除这条数据,而是取一个isDelete字段去软删除它)
get: 获取单条数据,
getById
可简写成get
, 若有查询条件, 按getByCondition
getAll: 获取多条记录,若有查询条件 按
getAllByCondition
getAllLimit: 分页获取 若有查询条件 按
getAllLimitByCondition
has: 若有查询条件, 按
hasAttributes
目前本系统业务就用到这么多,其他的参见sequelize文档:http://sequelize.org/
这样做的好处是,一些概念和语义更加清晰了,比如有个user.js
,里面用add
表示新增还是addUser
表示新增好,我认为是前者,在user.js
里面, 除了新增user
用户,难不成还有别的新增,还能新增个鬼啊。除此之外,还方便了新生代农民工的复制粘贴,提高编码效率。
抄表字段真的好累啊
试想一下这样一个场景,这个数据库有一百张表,每张表有100个字段,难道你真的要人肉去一个一个敲出来对应的数据库映射吗?那要敲到什么时候啊,人都快搞没了,我们可是新生代农民工唉,当然要跟上时代。这里介绍一下egg-sequelize-auto
, 它可以快速的将数据库的字段映射到你的代码中,减少很多工作量。
安装
npm i egg-sequelize-auto -g
npm i mysql2 -g
使用
egg-sequelize-auto -h 'your ip' -d 'your database' -u 'db user' -x 'db password' -e mysql -o 'project model path' -t 'table name'
egg-sequelize-auto:https://www.npmjs.com/package/egg-sequelize-auto
sequelize连表查询的应用
在表的关系中,有一对一,一对多,多对多。本系统一对多用的比较多,这里就以银行卡结合银行的的连表做个演示。
主要是三个地方,一个是引入相关表的Model, 第二个是字段初始化,第三个是通过associate
方法建立联系,阉割后的示例代码如下:
'use strict';
const OrganizationModel = require('./organization');
module.exports = app =>
const logger, Sequelize, utils = app;
const DataTypes, Model, Op = Sequelize;
class BankcardModel extends Model
static associate()
const Organization = app.model;
BankcardModel.belongsTo(Organization,
foreignKey: 'bankId',
targetKey: 'id',
as: 'bank',
);
static async getAllLimit(name, prefix, bankId, page = 0, limit = 10 )
let where = ;
if (name)
where = name: [Op.like]: `%$name%` ;
if (prefix)
where.prefix = [Op.like]: `%$prefix%` ;
if (bankId)
where.bankId = bankId;
where.isDelete = 0;
try
const offset = page < 1 ? 1 : (page - 1) * limit;
const total = await this.count( where );
const last = Math.ceil(total / limit);
const list =
total === 0
? []
: await this.findAll(
raw: true,
where,
order: [
['createTime', 'DESC'],
['updateTime', 'DESC'],
],
offset,
limit,
attributes: [
'id',
'name',
'prefix',
'bankId',
[Sequelize.col('bank.name'), 'bankName'],
],
include:
model: app.model.Organization,
as: 'bank',
attributes: [],
,
);
logger.info(this.getAllLimit, page, limit, where, list);
return
page,
pageSize: limit,
list,
total,
last,
;
catch (e)
logger.error(e);
return false;
BankcardModel.init(
id:
type: DataTypes.UUID,
defaultValue()
return utils.generator.generateUUID();
,
allowNull: false,
primaryKey: true,
,
name:
type: DataTypes.STRING(255),
allowNull: true,
,
prefix:
type: DataTypes.STRING(255),
allowNull: true,
,
bankId:
type: DataTypes.STRING(255),
allowNull: false,
references:
model: OrganizationModel,
key: 'id',
,
,
isDelete:
type: DataTypes.INTEGER(1),
allowNull: true,
defaultValue: 0,
,
createTime:
type: DataTypes.INTEGER(10),
allowNull: true,
,
updateTime:
type: DataTypes.INTEGER(10),
allowNull: true,
,
,
sequelize: app.model,
tableName: 't_bankcard',
);
return BankcardModel;
;
sequelize中的表关系:https://sequelize.org/master/manual/assocs.html
Service
这里就是引入相关的model层写好的,然后根据业务逻辑去调用下,还是以银行卡为例
'use strict';
const Service = require('egg');
class BankcardService extends Service
constructor(ctx)
super(ctx);
this.Bankcard = this.ctx.model.Bankcard;
async add(name, prefix, bankId)
const ctx, Bankcard = this;
let result = await Bankcard.hasPrefix(prefix);
if (result)
ctx.throw('卡号前缀已存在');
result = await Bankcard.add(name, prefix, bankId);
if (!result)
ctx.throw('添加卡号失败');
return result;
async getAllLimit(name, prefix, bankId, page, limit)
const ctx, Bankcard = this;
const result = await Bankcard.getAllLimit(name, prefix, bankId,
page,
limit,
);
if (!result)
ctx.throw('暂无数据');
return result;
async set(id, name, prefix, bankId, isDelete)
const ctx, Bankcard = this;
const result = await Bankcard.set(id, name, prefix, bankId, isDelete);
if (result === null)
ctx.throw('更新失败');
return result;
module.exports = BankcardService;
Controller
restful API接口
只要在相应的controller层定义相关的方法,egg程序就能够根据restful api去解析。
Method | Path | Route Name | Controller.Action |
---|---|---|---|
GET | /posts | posts | app.controllers.posts.index |
GET | /posts/new | new_post | app.controllers.posts.new |
GET | /posts/:id | post | app.controllers.posts.show |
GET | /posts/:id/edit | edit_post | app.controllers.posts.edit |
POST | /posts | posts | app.controllers.posts.create |
PUT | /posts/:id | post | app.controllers.posts.update |
DELETE | /posts/:id | post | app.controllers.posts.destroy |
参见:https://eggjs.org/zh-cn/basics/router.html
非restful API接口
这里主要是针对于楼上的情况,进行一个补充,比如说用户,除了这些,他还有登录,登出等等操作,那这个就需要单独在router中制定了, 这里笔者封装了一个resource
方法,来解析restful api的函数接口,具体如下:
'use strict';
/**
* @param Egg.Application app - egg application
*/
module.exports = app =>
const router, controller = app;
router.get('/', controller.home.index);
router.post('/user/login', controller.user.login);
router.post('/user/logout', controller.user.logout);
router.post('/user/info', controller.user.getUserInfo);
router.post('/file/upload', controller.file.upload);
router.post('/file/getall', controller.file.getAllByIds);
router.post('/organization/by-type', controller.organization.getAllByType);
router.post('/statistics/calculate', controller.statistics.calculate);
function resource(path)
const pathArr = path.split('/');
// 删掉第一个空白的
pathArr.shift();
let controllers = controller;
for (const val of pathArr)
controllers = controllers[val];
router.resources(path, path, controllers);
resource('/alarm');
resource('/bank');
resource('/bankcard');
resource('/mobile');
resource('/organization');
resource('/user');
resource('/virtual');
resource('/website');
resource('/file');
resource('/alarmCategory');
;
这里还是以银行卡为例
'use strict';
const Controller = require('egg');
class BankCardController extends Controller
async index()
const ctx, service = this;
const name, prefix, bankId, page, pageSize = ctx.request.query;
const list, ...rest = await service.bankcard.getAllLimit(
name,
prefix,
bankId,
Number(page),
Number(pageSize)
);
const data = list.map(item =>
const role = ctx.session.userinfo;
let btnList = [];
if (role === 'admin')
btnList = ['detail', 'modify', 'delete'];
return
btnList,
...item,
;
);
ctx.success( list: data, ...rest );
async create()
const ctx, service = this;
const name, prefix, bankId = ctx.request.body;
ctx.validate(
name: type: 'string', required: true ,
prefix: type: 'string', required: true ,
bankId: type: 'string', required: true ,
,
name, prefix, bankId
);
const result = await service.bankcard.add(name, prefix, bankId);
ctx.success(result);
// async destory()
// const ctx, service = this;
// const method = ctx;
// this.ctx.body = '删除';
//
async update()
const ctx, service = this;
const id = ctx.params;
const name, prefix, bankId, isDelete = ctx.request.body;
const result = await service.bankcard.set(
id,
name,
prefix,
bankId,
isDelete
);
ctx.success(result);
async show()
const ctx, service = this;
const method = ctx;
this.ctx.body = '查询';
async new()
const ctx, service = this;
const method = ctx;
this.ctx.body = '创建页面';
async edit()
const ctx, service = this;
const method = ctx;
this.ctx.body = '修改页面';
module.exports = BankCardController;
至此,打通这样一个从model到service再到controller的流程,
eggjs中的定时任务schedule
原系统是接入了第三方的数据源去定时读取更新数据,再将数据清洗更新到我们自己的t_alarm
表,一些原因这里我不方便做演示,所以笔者又新建了一张天气表,来向大家介绍eggjs中的定时任务。
在这里,我相中了万年历的接口,准备嫖一嫖给大家做一个演示的例子,它返回的数据格式如下
"data":
"yesterday":
"date": "19日星期四",
"high": "高温 33℃",
"fx": "东风",
"low": "低温 24℃",
"fl": "<![CDATA[1级]]>",
"type": "小雨"
,
"city": "杭州",
"forecast": [
"date": "20日星期五",
"high": "高温 34℃",
"fengli": "<![CDATA[2级]]>",
"low": "低温 25℃",
"fengxiang": "西南风",
"type": "小雨"
,
"date": "21日星期六",
"high": "高温基于java+springboot+vue+node.js的图书购物商城系统详细设计和实现
🍅作者主页:Java李杨勇 🍅简介:Java领域优质创作者🏆、【java李杨勇】公号作者✌ 简历模板、学习资料、面试题库【关注我,都给你】🍅文末获取源码联系🍅前言介绍: 随着信息技... 查看详情
基于java+springboot+vue+node.js的智能农场管理系统详细设计和实现
🍅作者主页:Java李杨勇 🍅简介:Java领域优质创作者🏆、【java李杨勇】公号作者✌ 简历模板、学习资料、面试题库【关注我,都给你】🍅文末获取源码联系🍅 研究背景意义 中国是... 查看详情
基于java+springboot+vue+node.js的图书购物商城系统详细设计和实现
🍅作者主页:Java李杨勇 🍅简介:Java领域优质创作者🏆、【java李杨勇】公号作者✌ 简历模板、学习资料、面试题库【关注我,都给你】🍅文末获取源码联系🍅前言介绍: 随着信息技... 查看详情
计算机毕业设计基于node.js中小企业合同管理系统
项目介绍本项目使用nodejs技术和软件架构选择B/S模式,总体功能模块运用自顶向下的分层思想。再然后就是实现系统并进行代码编写实现功能。论文的最后章节总结一下自己完成本论文和开发本项目的心得和总结。通过中小... 查看详情
使用 socket.io node.js 和传入消息的通知系统的架构实现和设计
】使用socket.ionode.js和传入消息的通知系统的架构实现和设计【英文标题】:ArchitectureImplementationandDesignforaNotificationSystemusingsocket.ionode.jsandincomingmessages【发布时间】:2011-11-0305:11:21【问题描述】:免责声明我之前没有使用过node.js... 查看详情
《基于node.js实现简易聊天室系列之详细设计》
一个完整的项目基本分为三个部分:前端、后台和数据库。依照软件工程的理论知识,应该依次按照以下几个步骤:需求分析、概要设计、详细设计、编码、测试等。由于缺乏相关知识的储备,导致这个Demo系列的文章层次不是... 查看详情
1000个大数据/人工智能毕设选题推荐
...毕业设计题目选择方向。大数据/人工智能毕设选题:基于社交大数据的用户画像系统设计与实现基于TF-IDF和朴素贝叶斯方法的海量文本分类研究基于卷积神经网络的图像修复系统设计与实现智慧校园语音交互系统的设计与实... 查看详情
基于java+springboot+vue+node.js等疫情网课管理系统详细设计和实现
🍅作者简介:CSDN特邀作者✌、java领域优质创作者💪🍅关注公众号【java李杨勇】 简历模板、学习资料、面试题库等都给你🍅文末获取源码联系🍅目录前言介绍:语言技术:功能设计:功能... 查看详情
基于java+springboot+vue+node.js等疫情网课管理系统详细设计和实现
🍅作者简介:CSDN特邀作者✌、java领域优质创作者💪🍅关注公众号【java李杨勇】 简历模板、学习资料、面试题库等都给你🍅文末获取源码联系🍅目录前言介绍:语言技术:功能设计:功能... 查看详情
python基于tensorflow的水果识别软件设计与实现.rar(论文+项目源码+视频演示)(代码片段)
第1章主要技术和工具介绍…21.1TensorFlow…21.2微信小程序…21.3Node.js…21.4VantWeapp…21.5MySql数据库…31.6前后端分离技术…3第2章需求分析…52.1系统功能需求分析…52.1.1需求概述…52.1.2用例分析…62.2系统业务流程分析…7第3章系统设... 查看详情
基于c#和rfid的智能咖啡厅系统设计与实现(代码片段)
智能咖啡厅引言背景及需求分析总体设计账号密码比对及预警会员卡服务智能环境控制无线点单开发环境硬件设计软件设计数据库设计系统登录会员信息录入会员信息查询、修改及销卡充值与消费无线点单环境控制总结与成果显... 查看详情
基于区块链的投票系统的设计与实现
1、本地环境的搭建(Windows10) 1.1安装nodejs,npm,git,web3,solc (1)nodejs:官网下载最新版本https://nodejs.org/en/download/current/ node.js后续还需安装python2.7与visualstudio的c++开发工具包(建议手动)。 (2)npm:... 查看详情
基于cpci系统的高速数字通信接口电路设计与应用
参考技术A基于CPCI系统的高速数字通信接口电路设计与应用 在CPCI系统环境下高速数字通信AFDX协议端系统接口的电路设计与功能实现。采用Verilog编程实现基于FPGA的硬件设计部分,采用C编程实现基于MicroBlaze的嵌入式软件设计... 查看详情
计算机毕业设计node.js+vue学院会议纪要管理系统
项目介绍本次项目研究,主要是基于前端的HTML、CSS、JavaScriptScript、VUE全家桶等知识,以及后端基于谷歌v8引擎的NODE、Express技术和数据库MYSQL,对项目实现前后端分离,以接口的形式进行访问交互。使用这些知识... 查看详情
基于hadoop的信息流推荐系统设计与实现开题报告
下载地址:https://download.csdn.net/download/qq_31293575/18338145一、研究或设计的目的和意义:因为高速发展的信息技术和不断壮大的教育规模,使得高校行政办公室和教学活动产生大量数据资源,各类文件、课本和其他信息的分享和交... 查看详情
camera基于深度学习的车牌检测与识别系统实现(课程设计)
基于深度学习的车牌检测与识别系统实现(课程设计)代码+数据集下载地址:下载地址用python3+opencv3做的中国车牌识别,包括算法和客户端界面,只有2个文件,surface.py是界面代码,predict.py是算法代码,界面不是重点所以用tkint... 查看详情
基于区块链的投票系统的设计与实现之环境的搭建
由于博主的毕设做的是区块链的方向,因此想写博客记录这个过程。 博主是在本地搭建的开发环境,操作系统为window10,使用以太坊开发平台,truffle框架,Solidity开发语言,Atom编辑器。 如果你还不知道区块链的一些基础... 查看详情
基于mobilenet-v3和yolov5的餐饮有害虫鼠识别及防治系统的设计与实现(代码片段)
...的重要产物,并逐渐应用于日常生活中的方方面面。基于此,设计并开发出一款可以有效预防虫鼠害的系统对于提升管理效率及卫生服务质量是非常有必要的……本文阐述的内容主要包括:基于 MobileNet-v3的虫鼠识别... 查看详情