1、简介
- 基于vue3、element-plus、typescript、pinia、vite搭建的后台管理系统。
- 集成koa、electron实现增删改查简单生成。
- 提供简单易用的常用组件,只需进行简单的属性配置即可。
- 抽离表单配置(entity),以配置的形式组建表单页面。
- 抽离接口调用层(service),使得代码层次分明,便于维护。
- 植入装饰器功能简化业务开发冗余代码。
根目录
源码目录
提供组件
- PaddingBox 带有底部间距的一个盒子
- PageContainer 列表页面主要结构
- XklButton 带有防抖处理的按钮
- XklDatePicker 包装el-date-picker
- XklDict 数据字典下拉框
- XklForm 包装form表单
- XklFormInfo 以详情形式显示表单
- XklLink 包装el-link
- XklSelect 包装el-select
- XklTable 包装el-table
- XklTree 包装el-tree
- XklTreeSelect 包装el-tree-select
- XklUpload 包装 el-upload
使用
- node版本:16.17.0
- npm i -g nrm
- nrm use taobao
- npm i
- npm run dev
项目主要目录
- entity ---- 表单模板
- service ---- 接口调用数据处理
- views ---- 页面呈现
问题
- electron可能会安装失败
- 执行npm config set ELECTRON_MIRROR npmmirror.com/mirrors/ele…
2、深入了解
2.1 组件说明
PaddingBox
<div class="padding-box" :class="['pb-' + (padding || 22)]">
<div class="content">
<slot></slot>
</div>
<div class="end">
<slot name="end"></slot>
</div>
</div>
PaddingBox是一个带有下内边距的包装盒,它携带end插槽,用于包装多个子组件,并与其以下元素保持一定的距离。
const props = defineProps({
padding: {
type: Number //底部边距
}
})
PageContainer
<page-container :gutters="20">
<template #query>
....
....
</template>
....
....
<template #footer>
....
....
</template>
</page-container>
PageContainer是一个页面容器,它默认分为上中下三个区域,可以自定义区域间距,使用插槽时可以多添加几个自定义区域。
const props = defineProps({
gutters: {
type: Number, // 区域之间的间距
default: 2
},
queryVisible: {
type: Boolean, // query区域是否显示
default: true
},
footerVisible: {
type: Boolean, // footer区域是否显示
default: true
},
footerFixed: {
type: Boolean // footer区域是否固定底部
},
upperSlots: {
type: Array, // 添加query至body之间的区域块
default: []
},
downSlots: {
type: Array, // 添加body至footer之间的区域块
default: []
},
padding: {
type: Number, // 设置区域块的内边距
default: 20
}
})
XklButton
<el-button type="primary" @click="getDataList()" @async="submit">查询</el-button>
// 使用实例
const submit = async (done) => {
....
.... await axiosFn()
done()
}
// 按钮内部可以触发的事件
emit('async', () => {
setTimeout(() => preventClick = false, 200)
})
XklButton是基于el-button的按钮组件,携带一些额外控制功能。为了避免使用时,出现混淆,XklButton在项目中依然以 <el-button></el-button> 的形式使用。
const props = defineProps({
auth: {
type: String // 按钮权限标识,系统会根据标识显示隐藏按钮
},
prevent: {
type: Boolean //点击按钮会弹出确认提示
},
openType: {
type: String // 定义自定义事件名称,可以触发window上的注册的全局方法
}
})
const emit = defineEmits(['click', 'async'])
click -> 原始按钮点击事件
async -> 异步按钮事件,通常用于防止多次点击,点击按钮触发async自定义事件,该事件中有回调函数done,执行done之后才可以点击第二次。
confirm -> prevent属性需要搭配confirm使用
XklDatePicker
XklDatePicker是基于el-date-picker的时间选择控件。
const props = defineProps({
type: String // 'date', 'dates', 'daterange', 'datetime', 'datetimerange', 'month', 'monthrange'
})
XklDict
<XklDict v-model="form[item.prop]"
v-model:label="form[item.prop + 'Label']"
:config="item.config" v-bind="item.element"
v-on="item.events" :disabled="item.disabled()">
</XklDict>
XklDict是一个基于el-select封装的专门用于数据字典枚举的选择器。
const props = defineProps(['config'])
config: {
dict: string // 字典类型名称,例如‘sys_gender’ 翻译性别字典
}
const emit = defineEmits(['update:label', 'loaded'])
update:label -> 双向绑定一个label 用于存储中文名称
loaded -> (data: 字典数据列表) => void 字典数据初始化完毕回调函数,
XklSelect
<XklSelect v-model="form[item.prop]" :key="item.key()"
v-model:label="form[item.prop + 'Label']"
:config="item.config"
v-bind="item.element"
v-on="item.events" :disabled="item.disabled()">
</XklSelect>
XklSelect是一个基于el-select封装的可以直接传入list静态数据,也可以使用url接口获取数据的选择器。
const props = defineProps(['config', 'list', 'modelValue'])
当作为接口下拉框使用时
config: {
url: string, // 下拉框接口地址
labelTarget: string // 需要绑定的label字段
valueTarget: string // 需要绑定的value字段
params?: any // 接口需要传递的参数
split?: string // 当要显示多个label时候, 可以将labelTarget指定为
// 例子1:label1/label2 -> split: '/'
// 例子2:label1*label2 -> split: '*'
// 根据split拆分并拼接要显示的多个label
}
当作为静态数据下拉框时
list: SelectItem[] | (() => SelectItem[]) // list属性可定义数据列表
XklLink
同XklButton
XklTreeSelect
<XklTreeSelect v-model="form[item.prop]"
:config="item.config"
v-bind="item.element"
v-on="item.events"
:disabled="item.disabled()">
</XklTreeSelect>
XklTreeSelect是一个基于el-tree-select封装的树形选择器。
const props = defineProps(['config'])
config: {
url: string // 接口地址
labelTarget?: string // 需要绑定的label字段
valueTarget?: string // 需要绑定的value字段
toTree?: { // 是否需要转为树形数据? 如果接口本身就是 树形结构,无需配置此属性
id: string // 单数据id
pId: string // 但数据 父级 id
incorporate?: string //根节点名称例如:‘主目录’, 当树形数据根有多条数据,合并为一个根
}
}
XklUpload
<XklUpload :key="form[item.prop].length > 0"
v-model="form[item.prop]"
:config="item.config"
v-bind="item.element"
v-on="item.events">
</XklUpload>
XklUpload是基于el-upload封装的文件上传组件。
const props = defineProps(['config', 'modelValue'])
config: {
url?: string // 接口地址
maxSize?: number // 文件大小限制
authorization?: string // 文件上传接口的token参数名称
accept?: string // 可以上传的文件类型
acceptTip?: string // 上传文件类型错误的提示
headers?: any // 上传接口的请求头
params?: () => any // 上传接口需要的参数
}
XklForm
<XklForm :form="form">
<template #_action>
<el-button type="primary" @click="getDataList()">查询</el-button>
<el-button @click="resetDataList()">重置</el-button>
</template>
</XklForm>
XklForm基于el-form,集成xkl-admin中所有的表单组件。
const props = defineProps(['form'])
form: Entity //Entity 参照Entity说明
const emit = defineEmits(['enter'])
enter -> enter键 触发表单提交
XklTable
<xkl-table :table="table" @selection-change="selectionChange" v-loading="tableLoading">
<template #dictType="{ row }">
<el-link type="primary" @click="toDictData(row)">{{ row.dictType }}</el-link>
</template>
<template #_operation="{ row }">
<el-link type="primary" @click="addOrUpdate(row.dictId)">编辑</el-link>
<el-link class="ml-6" type="danger" @click="removeDict(row.dictId)">删除</el-link>
</template>
</xkl-table>
XklTable是基于el-table动态渲染列的表格组件。
const props = defineProps(['table'])
table: EntityTable //EntityTable 参照Entity说明
2.2 Entity说明
/**
* file was generated by xuekanglin
* @type { Entity File }
* @name { 字典管理 }
*/
import { EntityOptions, TableColumn } from '@xuekl/cli-base/type'
import Base, { BaseTable } from '@xuekl/cli-base/base.entity'
import c from '@xuekl/cli-base/components'
import { Rule } from '@xuekl/cli-base/annotate'
export class Dict extends Base {
id?: string
dictId = ''
dictName = ''
dictType = ''
status = '0'
remark = ''
constructor(opts?: EntityOptions) {
super()
if (opts) this._opts = opts
}
setForm(form: Dict) {
Object.assign(this, form)
}
clear() {
const ref = this.getRef()
ref.resetFields()
this.setForm(new Dict(this._opts))
}
getDictName(): c.Input {
return {
required: () => true,
type: 'input',
label: '字典名称',
mode: 'query'
}
}
@Rule("numberCharacter_")
getDictType(): c.Input {
return {
required: () => true,
type: 'input',
label: '字典类型',
mode: 'query'
}
}
getStatus(): c.Radio {
return {
type: 'radio',
label: '状态',
list: [{ label: '启用', value: '0' }, { label: '禁用', value: '1' }]
}
}
getRemark(): c.Textarea {
return {
type: 'textarea',
label: '备注'
}
}
}
export class DictTable extends BaseTable<Dict>{
selection = true
operation = {
label: '操作'
}
columns: TableColumn[] = [
{
prop: 'dictName',
label: '字典名称',
},
{
prop: 'dictType',
label: '字典类型',
slot: true
},
{
prop: 'status',
label: '状态',
dict: 'sys_normal_disable',
tags: [{ value: '0', color: '' }, { value: '1', color: '#F56C6C' }]
},
{
prop: 'remark',
label: '备注',
},
]
list: Dict[] = []
}
在以上示例中,Dict类的实例对象会执行表单页面的绘制,在Dict中定义一些属性,这些属性用于表单绑定的字段。属性对应的值,也就是表单中各字段的值,如果Dict中的属性需要在表单中显示为一个组件,需要定义一个get方法, 格式:get + 属性大驼峰 例如:属性username 对应 getUsername()。
详细示例
Entity示例
form: Dict = new Dict({
mode: 'query',
labelWidth: 100,
span: 8,
events: {
usernameChange: () => {}
}
})
可选属性-----------------------------------------------------------------------------
mode?: string | string[] // 是一个匹配标识,当组件mode与之对应时,该表单中才会显示该组件。
span?: number // 每个组件所占的横向空间比例,总数24, 此处8 即是 三分之一 空间。
collapsible?: boolean // 是否使用展开收起功能。
placeholder?: boolean // 是否显示placeholder。
labelWidth?: number // 表单label的统一长度。
forceAlignLeft?: boolean //only for XklFormInfo
events?: any // 组件通信事件。
element?: FormElement // 表单绑定的element属性
Input示例
username = ''
...
// c.Input 代表该组件是个Input输入框
getUsername(): c.Input{
return {
type: 'input', //必须,决定组件渲染为input
label: '字典类型',
mode: 'query', // 在Entity,mode为query下显示。也可以定义为数组['query', 'info'],在多种模式下显示
required: () => true, // 该组件是否必填
show: () => true, // 该组件是否隐藏
disabled: () => true, // 该组件是否禁用
element: { // input 绑定的element属性
...
},
events: { // input 触发的事件
change: () => {
this.emit('usernameChange') // 触发Entity示例中的events
}
}
}
}
InputNumber示例
sort = 0
...
getSort(): c.InputNumber {
return {
type:'input-number',
label: '显示排序'
}
}
Select示例(不建议)
gender = ''
...
getGender(): c.Select {
return {
type: 'select',
label: '性别',
list: [{value: '0', label: '男'}, {value: '1', label: '女'}] //通常使用数据字典dict-select
}
}
UrlSelect示例
WarehouseCodeList: string[] = []
...
getWarehouseCodeList(): c.UrlSelect {
return {
type: 'url-select',
label: '所属仓库',
key: () => this.companyId,
config: {
url: '/warehouse/getWarehouseByCompany',
valueTarget: 'warehouseCode',
labelTarget: 'fullName',
params: () => ({
companyId: this.companyId
})
},
element: {
multiple: true
},
show: () => (!!this.companyId)
}
}
DictSelect示例
gender = ''
...
getGender(): c.DictSelect {
type: 'dict-select',
label: '性别',
config: {
dict: 'sys_gender'
},
mode: 'query'
}
TreeSelect示例
menuId = ''
...
getMenuId(): c.TreeSelect {
return {
type: 'tree-select',
label: '上级菜单',
config: {
url: '/system/menu/list',
valueTarget: 'menuId',
labelTarget: 'menuName',
toTree: {
id: 'menuId',
pId: 'parentId',
incorporate: '主目录'
}
},
element: {
checkStrictly: true
},
span: 24
}
}
Radio示例
configType = ''
...
getConfigType(): c.Radio {
return {
type: 'radio',
label: '系统内置',
list: [{ label: '是', value: 'Y' }, { label: '否', value: 'N' }]
}
}
CheckBox示例
hobbies: string[] = []
...
getHobbies(): c.CheckBox {
return {
type: 'radio',
label: '兴趣爱好',
list: [{ label: '篮球', value: '1' }, { label: '游戏', value: '2' }, { label: '午睡', value: '3' }]
}
}
DatePicker示例
createTime = ''
...
getCreateTime(): c.DatePicker {
return {
type: 'date-picker',
label: '创建时间'
}
}
Textarea示例
remark = ''
...
getRemark(): c.Textarea {
return {
type: 'textarea',
label: '备注'
}
}
Upload示例
interface Attachment = {
name: string
url: string
fileName: string
filePath: string
}
uploadList: Attachment[] = []
...
getUploadList(): c.Upload {
return {
type: 'upload',
config: {
url: '/common/upload',
accept: 'image/*',
acceptTip: '只能上传图片文件',
maxSize: 10 //单位M
}
}
}
自定义组件
以上组件只是常用组件,遇到复杂的,自定义的组件需要使用slot(插槽)
cars: Car[] = []
...
getCars() {
return {
label: '自定义段',
labelWidth: 1 // 可设置为1,把label空间保留出来
}
}
...
<xkl-form :form="form">
<template #cars>
自定义组件
</template>
</xkl-form>
Entity表格用法
表格示例
table: DictTable = new DictTable()
-------------------------------------DictTable------------------------------------------------
export class DictTable extends BaseTable<Dict>{
index = true // 是否显示序号列
indexConfig = {} // 序号列的配置同element TableColumn 属性
selection = true // 是否显示选择列
selectionConfig = {} // 选择列的属性
operation = { // 操作列属性
operation = false // 隐藏操作列
label: '操作',
width: 200
}
columns: TableColumn[] = [
{
prop: 'dictName',
label: '字典名称',
},
{
prop: 'dictType',
label: '字典类型',
slot: true // 插槽定义
},
{
prop: 'status',
label: '状态',
dict: 'sys_normal_disable', // 支持字典翻译
tags: [{ value: '0', color: '' }, { value: '1', color: '#F56C6C' }] // 支持tag显示
},
{
prop: 'remark',
label: '备注',
reflect: 'dictName' // 优先显示reflect对应值
},
{
prop: 'createTime',
label: '创建时间',
format: 'YYYY-MM-DD HH:mm' // 如果合法的时间类型,可以使用format
}
]
list: Dict[] = []
}
2.3 Service说明
/**
* file was generated by xuekanglin
* @type { Service File }
* @name { FILENAME }
*/
import { Delete, Get, Model, Post, Put } from "@xuekl/cli-base/annotate"
import { ListResult, OperateResult, InfoResult } from './interface'
import R from "@xuekl/cli-base/r"
@Model('/system/dict')
export default class DictService {
@Get('/type/list')
getList(res) {
const data = res as ListResult
return new R<ListResult>(data.code, data).result()
}
@Get('/type/:id')
getDictById(res) {
const data = res as InfoResult
return new R<InfoResult>(data.code, data).result()
}
@Post()
insertDict(res) {
const data = res as OperateResult
return new R<OperateResult>(data.code, data, data.msg).result()
}
@Put()
updateDict(res) {
const data = res as OperateResult
return new R<OperateResult>(data.code, data, data.msg).result()
}
@Delete('/:ids')
delete(res) {
const data = res as OperateResult
return new R<OperateResult>(data.code, data, data.msg).result()
}
}
Service中提供系统的接口调用方法,且由装饰器辅助实现,经过装饰器修饰的方法的形参将会接收接口调用的返回值 例如:getList(res) 其中 ‘res’ 就是该接口的返回值。
- @Model('/system/dict') // 接口地址的公共部分
- @Get('/getList') // get请求方法
- @Post('/insert')
- @Put('/update')
- @Delete('/:ids')