本文档主要介绍收运后台管理系统前端部分的开发架构和开发思想,为后期开发过程提供可借鉴的示例,并统一开发规范。提高项目整体的代码可读性和可持续性。
adminweb/
├── .vscode # vscode配置
│ └── settings.json
├── docker # docker配置
│ ├── Dockerfile
│ ├── default.conf.template
│ └── nginx.conf
├── k8s # k8s配置
│ └── deployment.yaml
├── public # 静态资源目录(不会webpack编译)
│ ├── img # 静态图片目录
│ ├── tinymce # tinymce富文本编辑器资源目录
│ ├── index.html # 网站入口
│ ├── qrcode.js # qrcode二维码方法
│ └── robots.txt
├── src # 项目主目录
│ ├── api # 项目api接口目录
│ ├── assets # 静态资源目录(会webpack编译)
│ ├── components # 项目公共组件
│ ├── constant # map等常用配置
│ ├── directives # 全局vue指令
│ ├── enums # TS 枚举
│ ├── filters # 全局vue过滤器
│ ├── interface # TS interface接口
│ ├── lang # 国际化
│ ├── layout # 框架(头部,菜单栏等)
│ ├── pwa
│ ├── router # 路由配置
│ ├── store # vuex配置
│ ├── styles # 项目公共样式
│ ├── utils # 项目公用方法
│ ├── views-v2 # 项目主体页面目录
│ ├── App.vue # 项目公共入口页面
│ ├── global.d.ts # TS全局对象配置
│ ├── main.ts # 项目入口文件
│ ├── settings.ts # 项目基础配置文件
│ └── shims.d.ts # vue TS 定义
├── tests
│ └── unit
├── .browserslistrc # 配置兼容浏览器
├── .env.development # 环境变量配置--测试环境
├── .env.production # 环境变量配置--正式环境
├── .env.staging # 环境变量配置--演示环境
├── .eslintignore # eslint忽略对象配置
├── .eslintrc.js # eslint配置
├── .prettierrc # prettier配置(格式化)
├── README.md
├── babel.config.js # babel配置
├── jest.config.js # jest配置(单元测试)
├── package.json # 项目依赖第三方插件
├── postcss.config.js # postcss配置
├── tsconfig.json # TS配置
├── vue.config.js # VUE配置
└── yarn.lock # yarn安装依赖记录
该项目基于 vue2.0 搭建
yarn install
yarn run serve
yarn run build:prod
src/utils/request.ts
构建和设置 axios 实例
import axios, { AxiosRequestConfig } from "axios";
import { MessageBox } from "element-ui";
import { UserModule } from "@/store/modules/user";
import { becwError } from "@/components/Notification";
// 添加baseUrl 对全局请求添加 '/api'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
});
// 添加token
service.interceptors.request.use(
(config) => {
if (UserModule.token) {
config.headers["Authorization"] = "Bearer " + UserModule.token;
// 全局get请求添加时间戳,避免缓存
if (config.method === "get") {
config.params = {
requestTime: Date.parse(new Date().toString()),
...config.params,
};
}
}
return config;
},
(error) => {
Promise.reject(error);
}
);
预定义全局请求返回和错误处理
export const requestWrapper = <T = any>(params: AxiosRequestConfig): Promise<T> => {
return new Promise((resolve, reject) => {
service
.request<T>(params)
.then((response) => {
// 正常返回
const { data } = response as any;
if (response.status !== 200 || (typeof data === "object" && data.status === 0)) {
if (response.status === 201 || response.status === 204) {
resolve(response.data);
} else {
becwError({
message: data.message || "Error",
});
}
reject(new Error(data.message || "Error"));
} else {
resolve(response.data);
}
})
.catch((err) => {
// 错误返回
let showMsg = "";
const status = Number(err.response?.status);
switch (status) {
case 401:
MessageBox.confirm("你已被登出,可以取消继续留在该页面,或者重新登录", "确定登出", {
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
UserModule.ResetToken();
location.reload(); // To prevent bugs from vue-router
});
break;
case 425:
showMsg = err.response?.data.message;
break;
case 500:
showMsg = "服务器开小差了...";
break;
default:
showMsg = "网络错误...";
}
if (showMsg !== "") {
becwError({
message: showMsg,
});
}
reject(err);
});
});
};
export default requestWrapper;
获取角色列表示例 src/api/role
// 引入request
import request from "@/utils/request";
// 引入需要的interface
import { IGetRoleRequestDto, IRolePagedResultDto } from "../interface";
// 构建对外方法,设定需要的参数类型
export const getRoles = (params: IGetRoleRequestDto) => {
// request<IRolePagedResultDto> 设定返回的参数类型
return request<IRolePagedResultDto>({
url: "/admin/roles",
method: "get",
params,
});
};
src/views/systemManage/employ/roles.vue
import { getRoles } from '@/api/roles'
async getList() {
this.pageLoading = true
try {
const response = await getRoles(this.pagedRequest)
this.items = response.roles
this.total = response.totalCount
} finally {
this.pageLoading = false
}
}
// 引入excel导出方法
import { exportJson2Excel } from '@/utils/excel'
import { formatJson } from '@/utils'
async handleDownload() {
try {
this.downLoadLoading = true
// 获取数据列表
const res = await getNodes()
if (res.totalCount > 0) {
// 定义excel列名
const tHeader = [
'收运点名称',
'招牌名称',
'收运点类型',
]
// 对应的字段名
const filterVal = [
'nodeName',
'brandName',
'nodeOperationStatusText',
]
const data = formatJson(filterVal, res.items)
exportJson2Excel(tHeader, data, '收运点')
} else {
this.$becwWarning({
message: '警告!暂无数据'
})
}
} finally {
this.downLoadLoading = false
}
}
import { saveAs } from 'file-saver'
async exportSure() {
this.pageLoading = true
try {
// 获取二进制流
const res = await exportAmountDetail()
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
saveAs(blob, this.pagedRequestDate + '称重明细表')
} finally {
this.pageLoading = false
}
}
async downloadQrcode(id: string) {
// 获取收运点二维码
this.pageLoading = true
try {
const res = await getNodeQrcode()
const link = document.createElement('a')
document.getElementById('main')?.appendChild(link)
link.href =
'data:image/png;base64,' +
btoa(new Uint8Array(res).reduce((data, byte) => data + String.fromCharCode(byte), ''))
link.download = 'qrCode.png'
link.click()
} finally {
this.pageLoading = false
}
}
<template>
<el-upload
ref="upload"
action="/api/files/upload"
multiple
:limit="100"
:file-list="fileList"
:data="{ output: 'json2' }"
:on-exceed="handleExceed"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-success="handleSuccess"
:on-error="handleError"
accept=".jpg,.jpeg,.png,.pdf,.doc,.docx"
:disabled="dialogType === 'show'"
:class="dialogType === 'show' ? 'upload-only-show' : ''"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png/pdf/doc/docx文件,且不超过10M</div>
</el-upload>
</template>
<script lang="ts">
export default class extends Vue {
handleExceed() {
// '超出文件数量限制'
}
handleError() {
// '文件上传失败'
}
handleSuccess(res: any, file: any, fileList: any) {
// 文件上传成功
}
handlePreview(file: any) {
// 预览文件
const upFile = this.upFileList.find((item) => {
return item.uid === file.uid;
});
if (upFile) {
window.open(upFile.path + "?download=0");
}
}
handleRemove(file: any, fileList: any) {
// 移除文件
}
}
</script>
这是一个普通的列表页面;一般包含筛选、列表、弹窗三大模块和若干小的模块
<template>
<div class="admin-view-container" v-loading="pageLoading">
<div class="admin-filter-container">
<!-- 筛选模块 -->
<div class="filter-panel">...</div>
</div>
<!-- 列表模块 -->
<div class="app-custom-table">
<el-table :data="list">
...
<Empty slot="empty" />
</el-table>
<!-- 分页模块 -->
<Pagination />
</div>
<!-- 弹窗模块 -->
<el-dialog> ... </el-dialog>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component({
name: "Name",
components: {},
})
export default class extends Vue {
pageLoading = false;
pagedRequest = {
page: 1,
skipCount: 0,
maxResultCount: 10,
};
async created() {
this.getList();
}
resetList() {
this.pagedRequest.page = 1;
this.getList();
}
private async getList() {
// 获取列表数据
}
}
</script>
由多个filter-panel 组成 包含选择框、输入搜索框、日期选择框等,具体查看 element UI
用于用户进行筛选操作
<div class="admin-filter-container">
<div class="filter-panel">
<el-select
class="filter-item"
v-model="pagedRequest.motorcadeId"
placeholder="选择车队"
@change="resetList"
size="mini"
>
<el-option v-for="(item, index) in motorcadeList" :key="index" :label="item.name" :value="item.id" />
</el-select>
</div>
<div class="filter-panel">
<Autocomplete
class="filter-item filter-item-medium"
v-model="pagedRequest.keyword"
name="Car_keyword"
@clear="resetList"
@search="resetList"
placeholder="请输入车牌号码、车辆人员"
/>
</div>
<div class="filter-panel">
<el-button class="filter-btn" type="primary" icon="el-icon-circle-plus-outline" @click="showCreate" size="mini">
新增车辆
</el-button>
</div>
</div>
展示列表数据
<el-table :data="list">
<el-table-column label="序号" type="index" width="80" align="center" />
<el-table-column label="车牌号" prop="carNumber" />
<!-- prop对应字段名称 -->
<el-table-column label="车辆类型">
<template slot-scope="scope"> {{ }} </template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<i @click="showView(scope.row.id)" class="iconfont c-primary list-action-icon icon-detail" title="查看" />
<i @click="showModify(scope.row.id)" class="iconfont c-success list-action-icon icon-edit" title="修改" />
</template>
</el-table-column>
<Empty slot="empty" />
</el-table>
列表分页模块
<Pagination :limit.sync="pagedRequest.maxResultCount" <!-- 每页数量 -->
:page.sync="pagedRequest.page"
<!-- 当前页 -->
:total="total"
<!-- 总数量 -->
@pagination="getList"
<!-- 触发获取数据的方法 -->
v-show="total > 0" /></Pagination
>
弹窗展示详情或者编辑数据
<el-dialog :visible.sync="dialogVisible">
<el-form label-width="150px">
<el-form-item label="填报车辆:" prop="name" class="filter-item">
<el-select placeholder="请选择车辆"> </el-select>
</el-form-item>
<el-form-item label="出车填报里程(km):" prop="name" class="filter-item">
<el-input placeholder="请输入填报里程" />
</el-form-item>
<el-form-item label="到厂填报里程(km):" prop="name" class="filter-item">
<el-input placeholder="请输入填报里程" />
</el-form-item>
</el-form>
<div class="dialog-footer" slot="footer">
<el-button>取消</el-button>
<el-button type="primary">确认</el-button>
</div>
</el-dialog>
在展示数据时,有时候需要对数据进行过滤,如取整,保留 2 位小数,格式化时间等,这时候需要用到数据过滤
<el-form-item class="form-item-half" label="费用:">
<!-- 对费用四舍五入保留2位小数 -->
<span v-if="maintainDetail.totalAmount">{{ maintainDetail.totalAmount | toDecimalNoZero }}</span>
<span v-else>-</span>
</el-form-item>
具体的 filter 方法见 src/filters/ 预设一些常用的 filter。在界面还原时,如有不能满足时,可使用函数方法自定义
<template>
<el-form-item class="form-item-half" label="费用:">
<!-- 对费用四舍五入保留2位小数 -->
<span v-if="maintainDetail.totalAmount"> {{ toDecimalNoZero(maintainDetail.totalAmount) }} </span>
<span v-else>-</span>
</el-form-item>
</template>
<script lang="ts">
export default class extends Vue {
toDecimalNoZero(num: number, x: number = 2) {
const b = Math.pow(10, x);
var f = Math.round(num * b) / b;
var s = f.toString();
return s;
}
}
</script>
项目集成 element el-image
<el-table-column label="照片/视频" prop="initMessagesImg">
<template slot-scope="scope">
<el-image
fit="cover"
<!-- 默认第一张图片 -->
:src="addFileServer(scope.row.initMessagesImg[0]) | thumbnailImg"
<!-- 图片放大后显示图片的列表 -->
:preview-src-list="getImageSrcList(scope.row.initMessagesImg)"
/>
</template>
</el-table-column>
项目全局组件目录
自定义搜索组件,主要用于用户需要记住搜索记录的搜索框。示例:
<Autocomplete
class="filter-item filter-item-medium"
v-model="pagedRequest.keyword"
name="Car_keyword"
@clear="resetList"
@search="resetList"
placeholder="请输入车牌号码、车辆人员"
/>
其中 name 对应当前搜索框的 key,组件会基于 name 保存用户搜索的记录,请务必按照 pageName_inputName 的格式。
图片上传组件,可用于单图或多图上传。
<BelgImage :limit="1" :pictures="detaultArray" @change="change" />
limit: 图片最大上传数量
pictures:默认图片列表,用户图片回显
change: 图片列表发生变化时触发
预定义的全局提示组件,已经全局注入,不需要再单独引入。
this.$becwSuccess({
title: "成功",
message: "修改成功",
});
Vue.prototype.$becwNotify = becwNotifyFn;
Vue.prototype.$becwSuccess = becwSuccess;
Vue.prototype.$becwError = becwError;
Vue.prototype.$becwWarning = becwWarning;
Vue.prototype.$becwInfo = becwInfo;
数字跳动动画组件。
<NumberAnimation :num="nodeCount" :decimal="0" :defaultStr="-" />
num: 数字
decimal:保留几位小数 默认 2
defaultStr:默认显示字符串 默认'-'
分页组件。
<Pagination :limit.sync="limit" :page.sync="page" :total="total" @pagination="pagination" />
limit: 每页数量
page:当前页码
total:总页数
pagination :页码变化时触发方法
头部多窗口组件。用于多窗口组件的切换
<div>
<Tabs :tabs="['车辆管理', '车队管理']" :index.sync="currentTabIndex" />
<CarPage v-if="currentTabIndex === 0" />
<MotorcadePage v-if="currentTabIndex === 1" />
</div>
富文本组件
项目主体页面目录,项目所有的页面都存放这里
目录结构和后台管理菜单保持一致如图:
项目涉及车辆管理的界面,包括车辆管理、收运填报、加油填报等等。
收运管理的界面,包括收运统计、路线管理等等。
客户开发相关,包括收运点管理、申请、审核
用于处理 404 401 的公共界面
右侧通知页,点击头部铃铛弹出
绩效管理界面,包含个人收运统计和车辆运行统计
系统管理,包含员工管理,新闻管理等系统相关界面
全局路由配置,路由过滤等文件
按菜单目录配置路由文件
路由基础配置
路由过滤器,包括部分权限过滤和 token 验证
const carMngtRouter: RouteConfig = {
path: "/carmngt",
component: Layout,
redirect: "noredirect",
name: "carmngt",
meta: {
title: "car_management",
icon: "\ue600",
show: true,
},
children: [
{
path: "car",
component: () => import("@/views-v2/carManage/car/index.vue"),
name: "car_management",
meta: {
title: "car_management",
icon: "\ue616",
permissions: ["AdminWeb.Car.Singleton.CarManage"],
hidden: false,
},
},
],
};
path:url 地址
component:对应的组件目录
name:路由名称
meta:{
title:界面标签名,对应国际化的 key
icon:icon
perimissions:需要的权限数组,没有权限在路由过滤中会跳转登录
hidden: 是否在菜单中显示,false:显示,true:隐藏
}
全局 vuex 配置和 store 文件
store 文件目录
store 主文件,整合所有的 store 文件
本项目 store 采用'vuex-module-decorators'挂件,具体文档见'https://championswimmer.in/vuex-module-decorators/'
store 配置示例 'src/store/modules/car.ts'
import { VuexModule, Module, Action, Mutation, getModule } from 'vuex-module-decorators'
import store from '@/store'
import { ICarDto } from '@/interface/cars'
import { getCars } from '@/api/cars'
export interface ICarState {
totalList: ICarDto[]
carNum:number
}
@Module({ namespaced: true, dynamic: true, store, name: 'cars' })
class Car extends VuexModule implements ICarState {
totalList: ICarDto[] = []
carNum:0
@Mutation
set_carNum(num: number[]) {
this.carNum = num
}
@Mutation
set_totalList(list: ICarDto[]) {
this.totalList = list
}
@Action
public async GetTotalCarList() {
// 把所有的车辆列表存入store 方便后续直接使用
if (this.totalList.length > 0) {
return this.totalList
}
const { items } = await getCars({
motorcadeId: '',
keyword: '',
skipCount: 0,
maxResultCount: 999
})
this.set_totalList(items)
return items
}
}
export const CarModule = getModule(Car, store)
调用示例
import { CarModule } from "@/store/modules/cars";
@Component({
name: 'Car'
})
export default class extends Vue {
// 获取车辆列表
const list = await CarModule.GetTotalCarList();
// 获取车辆数量
// 静态获取
carNum=CarModule.carNum
// 动态获取,绑定
get carNum (){
return CarModule.carNum
}
// 修改store
CarModule.set_carNum(50)
}
全局的 style 配置路径为:'src/styles'
清楚浮动的预设 css,一般不做更改
全局的 scss 变量配置,已在 vue.config 中配置,全局可用
pluginOptions: {
'style-resources-loader': {
preProcessor: 'scss',
patterns: [
path.resolve(__dirname, 'src/styles/_variables.scss'),
path.resolve(__dirname, 'src/styles/_mixins.scss')
]
}
},
配置示例
// Sidebar
$sideBarWidth: 210px;
$subMenuBg: #1f2d3d;
$subMenuHover: #001528;
$subMenuActiveText: #f4f4f5;
$menuBg: #304156;
$menuText: #bfcbd9;
$menuActiveText: #409eff; // Also see settings.sidebarTextTheme
使用示例
.main-container {
min-height: 100%;
transition: margin-left 0.28s;
margin-left: $sideBarWidth;
position: relative;
padding-top: 50px;
}
全局的自定义样子,主要配置一些固定的框架结构样式和插件样式的重写
对 element 样式的部分重新配置
全局的 iconfont 配置,使用方法详见'https://www.iconfont.cn/'
全局 css 默认配置
utils 存放全局的预定义函数方法,包含时间处理,字段验证,权限验证,文件导出等等。
这里简略举例,具体请查看相应文件
export const parseTime = (time?: object | string | number, cFormat?: string): string => {
...
return timeStr
}
/**
* 获取月第一天开始时间
* @param dateStr 'yyyy/MM/dd'
* @return 时间戳
*/
export const getMonthStartTime = (dateStr: string) => {
...
return number
}
/**
* 获取月最后一天结束时间
* @param dateStr 'yyyy/MM/dd'
* @return 时间戳
*/
export const getMonthEndTime = (dateStr: string) => {
...
return number
}
......
export const isNumber = (value: string | number) => {
...
return boolean
}
export const isArray = (arg: any) => {
...
return boolean
}
......
/**
*
* @param {string} item storage的名称
* @param {type} type 可以传,默认session,接受local和session
*/
export const getStorage = (item: string, type?: 'session' | 'local') => {
...
return data
}
/**
*
* @param name Storage key
* @param value Storage value
* @param type session||local
*/
export const setStorage = (name: string, value: any, type?: 'session' | 'local') => {
...
return data
}
......