一、简介
xkl-admin-pro 是一个基于vue3,element-plus,typescript,所构建的后台管理系统框架,内置代码生成工具(基于tauri),以“配置式”形式实现页面渲染,快速生成所需基础页面,提升开发效率。 项目目录结构如下图,开发过程中重点关注entity,service 和 views目录。
gitee项目地址 gitee.com/xuekl/xkl-a…
二、入门使用
启动项目
- node版本:20.x.x
- npm i -g nrm
- nrm use taobao
- npm i
- npm run dev
新建页面
如图所示,新建一个名为“IndexView.vue”(作为一个菜单的主页面名称必须为IndexView)的文件,会出现下图所示配置面板。
写入基础文件名称,选择初始模板为“主页”时,会出现创建实体配置,然后输入当前页面所对应的一个(详情/列表)数据接口,也可以点击“新增”按钮手动新增。
按业务需求配置对应字段的表单属性(这里只做基础配置,生成后的代码可继续修改)。
填写完毕点击“确定”按钮,生成对应的Entity(下文介绍)
自行补充上述的“初始模板”(如: 新增/编辑(弹窗))类型最终页面效果如下图。
三、进阶使用
views
vue页面基础代码,其中包含调用业务的基本范例。框架自动注入了 $message,$messageBox,$route,$router 可直接使用,服务端返回结果使用“res.SUCCESSS”做code成功判断。
/**
* file was generated by xuekanglin
* @type { Vue File }
* @name { FILENAME }
*/
<template>
<xkl-container :topSlots="['header']">
<template #header>
<xkl-form :form="form">
<template #_action>
<el-button type="primary" @click="getDataList()">查询</el-button>
<el-button @click="resetDataList()">重置</el-button>
</template>
</xkl-form>
</template>
<xkl-padding-box>
<el-button type="primary" @click="addOrUpdate()">新增</el-button>
<el-button type="danger" :disabled="!table.selectedData.length" @click="deleteUser()">删除</el-button>
</xkl-padding-box>
<xkl-table :table="table">
<template #status="{ row }">
<el-switch activeValue="0" inactiveValue="1" v-model="row.status"
@change="statusChange(row)"></el-switch>
</template>
<template #_action="{ row }">
<el-link type="primary" @click="addOrUpdate(row.userId)">编辑</el-link>
<el-link type="primary" @click="openDetail(row.userId)">详情</el-link>
<el-link type="danger" @click="deleteUser(row.userId)">删除</el-link>
</template>
</xkl-table>
<dialog-operate ref="DialogOperateRef" v-if="dialogVisible" @refresh="getDataList()"></dialog-operate>
<dialog-detail ref="DialogDetailRef" v-if="detailDialogVisible"></dialog-detail>
</xkl-container>
</template>
<script setup lang="ts">
import DialogOperate, { DialogOperateInstance } from "./DialogOperate.vue"
import { nextTick, onMounted, ref } from 'vue';
import User from "@/entity/system/user/User";
import UserTable from "@/entity/system/user/UserTable";
import UserService from "@/service/module/system/user";
import DialogDetail, { DialogDetailInstance } from "./DialogDetail.vue";
const userService = new UserService()
const form = new User({ mode: 'query', collapsible: true }).active
const table = new UserTable().active
const DialogOperateRef = ref<DialogOperateInstance>()
const DialogDetailRef = ref<DialogDetailInstance>()
const dialogVisible = ref(false)
const detailDialogVisible = ref(false)
// 重置列表
const resetDataList = () => {
form.clear()
getDataList()
}
// 查询列表
const getDataList = async () => {
table.loading = true
const queryForm = form.getBindingValue()
const res = await userService.getList({
pageNum: table.page,
pageSize: table.pageSize,
...queryForm
})
if (res.SUCCESS) {
table.list = res.rows
table.totalCount = res.total
}
table.loading = false
}
// 新增编辑
const addOrUpdate = (id?: number) => {
dialogVisible.value = true
nextTick(() => {
DialogOperateRef.value?.init(id)
})
}
const openDetail = (id: number) => {
detailDialogVisible.value = true
nextTick(() => {
DialogDetailRef.value?.init(id)
})
}
const statusChange = async (row: User) => {
if (row && row.userId) {
const res = await userService.statusChange({
status: row.status,
userId: row.userId
})
if (res.SUCCESS) {
$message.success('状态修改成功')
}
}
}
// 删除数据
const deleteUser = (id?: number[]) => {
$messageBox.confirm(
`确认删除数据?`,
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
const ids = id ? [id] : table.selectedData.map(res => res.userId)
const res = await userService.delete(ids.join(','))
if (res.SUCCESS) {
$message.success('删除成功')
getDataList()
}
})
}
// 页码变化
table.pageChange = (val) => {
table.page = val
getDataList()
}
// 每页数量变化
table.sizeChange = (val) => {
table.page = 1
table.pageSize = val
getDataList()
}
// 页面挂载完毕
onMounted(() => {
getDataList()
})
</script>
<style scoped>
.read-the-docs {
color: #888;
}
.footer-action {
position: absolute;
height: 100px;
bottom: 0;
width: 100%;
background-color: #888;
left: 0;
z-index: 1;
}
</style>
service
views层中引入的UserService,即为该vue的service层,其主要功能就是接口的调用和数据处理(提供了常用http请求装饰器: Post、Get ...)。下列是增删改查的样例代码,其中interface文件可自行去工程对应目录下对比查看。
import { Model, Post, Get, Put, Delete } from "@xuekl/cli-core/annotate";
import { GetListOut, GetByIdOut } from "./interface";
import { EmptyOut } from "@/service/common/interface";
import R from "@xuekl/cli-core/r";
@Model('/system/user')
export default class UserService {
//获取列表
@Get('/list')
getList(res) {
return new R<GetListOut>(res.code, res).result()
}
//获取详情
@Get('/:id')
getById(res) {
return new R<GetByIdOut>(res.code, res).result()
}
//新增数据
@Post()
insert(res) {
return new R<EmptyOut>(res.code, res).result()
}
//更新数据
@Put()
update(res) {
return new R<EmptyOut>(res.code, res).result()
}
//删除数据
@Delete('/:ids')
delete(res) {
return new R<EmptyOut>(res.code, res).result()
}
//复用示例
@Post('/share')
share(res) {
return new R<EmptyOut>(res.code, res).result()
}
@Get('/')
getRolePost(res) {
}
@Put('/changeStatus')
statusChange(res) {
return new R<EmptyOut>(res.code, res).result()
}
}
关于数据处理,如下图,菜单列表处理,可在result 回调函数中,处理服务端响应的数据,并进行二次处理,将view所需要的数据接口,在service层处理,以便于view层直接使用。
entity
entity是指一套增删改查业务的一个提交单位,它可以渲染新增、编辑所需要的表单页面,也可以渲染首页的查询条件,以及表格显示。简介中提到的“配置式”主要也是针对entity的配置。
表单
如下代码,如需渲染成可显示的表单,则需要写该字段对应的“get”方法,方法返回值类型,决定表单类型。也可以在方法上标注“Rule”装饰器,快速实现字段校验功能。
import { Component } from "@xuekl/cli-core/enums"
import * as c from "@xuekl/cli-core/components"
import { BaseForm } from "@xuekl/cli-core/builder"
import { Rule } from "@xuekl/cli-core/annotate"
export default class User extends BaseForm {
static postsResolve: (list: any[]) => void
static rolesResolve: (list: any[]) => void
// 排序,通常调整字段位置实现,特殊情况使用
_sort: FieldSort[] = [
{
field: 'email', // 自定义排序,field 会在 leading 之后显示
leading: 'nickName'
}
]
userId = 0
nickName = ''
deptId?: number
deptId2?: number
phonenumber = ''
email = ''
userName = ''
password = ''
sex = ''
status = '0'
postIds: number[] = []
roleIds: number[] = []
remark = ''
time: string[] = []
clear(): void {
this.setForm(new User(this.opts))
this.resetFields()
}
getTime(): c.DatePicker {
return {
type: Component.DatePicker,
label: '筛选时间',
mode: 'query',
element: {
type: 'datetimerange',
func: 'bottomshortcut'
}
}
}
getNickName(): c.Input {
return {
type: Component.Input,
required: true,
label: '用户昵称',
}
}
getDeptId(): c.TreeSelect {
return {
type: Component.TreeSelect,
label: '归属部门',
key: () => this.sex,
config: {
url: '/system/user/deptTree',
valueTarget: 'id',
labelTarget: 'label'
},
element: {
checkStrictly: true
}
}
}
@Rule("phone")
getPhonenumber(): c.Input {
return {
type: Component.Input,
label: '手机号码',
// show: () => this.status === '1'
}
}
@Rule("email")
getEmail(): c.Input {
return {
type: Component.Input,
label: '邮箱'
}
}
getUserName(): c.Input {
return {
type: Component.Input,
label: '用户名称',
mode: 'query',
required: true,
show: () => this.userId === 0
}
}
getPassword(): c.Input {
return {
type: Component.Input,
label: '用户密码',
show: () => this.userId === 0
}
}
getSex(): c.DictSelect {
return {
type: Component.DictSelect,
label: '用户性别',
config: {
dict: 'sys_user_sex'
},
events: {
loaded: (list) => {
console.log('loaded', list);
},
change() {
console.log('性别变换');
},
itemChange: (val) => {
console.log('itemChange', val);
this.emit('sexChange', val)
}
}
}
}
getStatus(): c.Radio | c.DictSelect {
if (this.opts?.mode === 'query') {
return {
type: Component.DictSelect,
mode: 'query',
label: '状态',
config: {
dict: 'sys_normal_disable'
}
}
}
return {
type: Component.Radio,
label: '状态',
config: {
dict: 'sys_normal_disable'
}
}
}
getPostIds(): c.Select {
return {
type: Component.Select,
label: '岗位',
config: {
list: []
},
events: {
loaded: (_list, update) => {
User.postsResolve = update
},
},
element: {
multiple: true,
collapseTags: true
},
}
}
getRoleIds(): c.Select {
return {
type: Component.Select,
label: '角色',
config: {
list: []
},
events: {
loaded: (_list, update) => {
console.log('loaded');
User.rolesResolve = update
},
change: (val) => {
console.log('val', val);
},
itemChange: (val) => {
console.log(val);
},
removeTag: (val) => {
console.log(val);
}
},
element: {
multiple: true,
collapseTags: true
}
}
}
getRemark(): c.Textarea {
return {
type: Component.Textarea,
label: '备注',
span: 24
}
}
}
表格
如下代码,实现了一个基础分页表格,columns是表格对应的字段及配置,携带基础显示转换(如dict 自动翻译字典数据,format:格式化时间显示)
import User from "./User"
import { PaginationTable } from "@xuekl/cli-core/builder"
import { Selection, Index, TableIndex } from '@xuekl/cli-core/helper'
import { TableSelection, TableOperation, TableColumn } from "@xuekl/cli-core/types"
export default class UserTable extends PaginationTable<User> implements Selection<User>, Index {
index: TableIndex = {
width: 60,
label: '序号'
}
selection: TableSelection = {}
selectedData: User[] = []
operation: TableOperation = {
label: '操作',
width: 140,
fixed: 'right'
}
columns: TableColumn<User>[] = [
{
prop: 'nickName',
label: '用户昵称',
element: {
sortable: 'custom',
width: 200
}
},
{
prop: 'userName',
label: '用户名称',
element: {
sortable: true,
width: 200
}
},
{
prop: 'deptId',
label: '部门'
},
{
prop: 'phonenumber',
label: '手机号',
},
{
prop: 'status',
label: '状态',
},
{
prop: 'sex',
label: '性别',
dict: 'sys_user_sex'
},
{
prop: 'createTime',
label: '创建时间',
format: 'YYYY-MM-DD HH:mm:ss'
}
]
}
四、深入了解
系统组件
XklContainer
<xkl-container :topSlots="['header']" :bottomSlots="['bottom']">
<template #header>
// 头部插槽
...
</template>
// 默认插槽
...
<template #bottom>
// 底部插槽
...
</template>
</xkl-container>
XklContainer属性
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| topSlots | 容器默认内容上方插槽 | string[] | - |
| bottomSlots | 容器默认内容下方插槽 | string[] | - |
| backgroundColor | 容器背景色 | string | #fff |
| gap | 各区域之间的距离 | number | 20 |
| padding | 各区域内边距 | number | 20 |
XklPaddingBox
<xkl-padding-box :padding="[10,20,10,20]">
<el-button type="primary" @click="addOrUpdate()">新增</el-button>
<el-button type="danger" :disabled="!table.selectedData.length" @click="deleteUser()">删除</el-button>
</xkl-padding-box>
XklPaddingBox属性
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| padding | 盒子内边距,遵循css[上,右,下,左] | number[] | - |
插槽
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| after | 自定义尾部内容 | - | - |
XklButton、XklLink
XklButton 在系统中注册名仍为 ElButton 所以开发者依然使用"<el-button></el-button>",作为html标签。 常用防多次点击处理如下代码。此外XklButton还提供了confirm,openType(不常用)开发者自行去源码文件研究使用
<el-button type="primary" @click="submit()">提交</el-button>
<el-button type="primary" @async="asyncSubmit">提交</el-button>
<script setup lang="ts">
const submit = () => {
log('提交') // 连续输出
}
const asyncSubmit = (done) => {
log('提交') // 执行done 函数后才可二次输出
setTimeout(() => {
//异步操作,http请求...
done()
}, 2000)
}
</script>
XklButton属性
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| prevent | 点击出现二次弹窗确认,仅在confirm事件下生效 | boolen | true |
| confirmText | 二次弹窗确认提示内容 | string | 当前内容可能存在修改,确认继续? |
| openType | 会寻找window对象下同名全局事件并执行 | string | - |
| ... | 继承el-button相关属性 | any | 同el-button |
XklButton事件
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| click | 点击事件 | () => void | - |
| async | 携带阻止重复点击回调,执行done函数后可点击第二次 | (done: () => void) => void | - |
| confirm | 携带二次确认弹窗的点击事件,prevent 为true下生效 | () => void | - |
XklInput、XklInputNumber
return {
type: Component.Input, // 或 Component.InputNumber
label: '用户密码'
}
XklInput属性
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| option | entity get方法所返回的配置项(XklInput Option) | any | - |
XklInput Option
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| type | 表单组件类型 | Component.Input | - |
| label | 表单组件label | string | - |
| span | 表单组件占位(整行24) | number | 跟随form表单 |
| mode | 与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。 | string | - |
| required | 是否必填 | boolen | () => boolen | - |
| key | 唯一标识,标识函数返回值变更组件可重新渲染 | () => string | - |
| show | 是否显示 | boolen | () => boolen | true |
| element | 继承el-input相关属性,特殊属性:disabled 支持动态(使用方式,同reqiured属性) | any | 同el-input |
| events | 继承el-input事件 | any | - |
XklInputNumber属性
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| option | entity get方法所返回的配置项(XklInputNumber Option) | any | - |
XklInputNumber Option
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| type | 表单组件类型 | Component.InputNumber | - |
| label | 表单组件label | string | - |
| span | 表单组件占位(整行24) | number | 跟随form表单 |
| mode | 与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。 | string | - |
| required | 是否必填 | boolen | () => boolen | - |
| key | 唯一标识,标识函数返回值变更组件可重新渲染 | () => string | - |
| show | 是否显示 | boolen | () => boolen | true |
| element | 继承el-input-number相关属性,特殊属性:disabled 支持动态(使用方式,同reqiured属性) | any | 同el-input-number |
| events | 继承el-input-number事件 | any | - |
XklCheckbox、XklRadio
return {
type: Component.Checkbox, //或 Component.XklRadio
label: '状态',
config: {
//字典类型,根据字典类型渲染选项
dict: 'sys_normal_disable'
// 手动指定数据,与字典类型二选一
list: [{label: '选项1', value: 'opt1'}, {label: '选项1', value: 'opt1'}]
}
}
XklCheckbox属性
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| option | entity get方法所返回的配置项(XklCheckbox Option) | any | - |
XklCheckbox Option
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| type | 表单组件类型 | Component.Checkbox | - |
| label | 表单组件label | string | - |
| span | 表单组件占位(整行24) | number | 跟随form表单 |
| mode | 与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。 | string | - |
| required | 是否必填 | boolen | () => boolen | - |
| key | 唯一标识,标识函数返回值变更组件可重新渲染 | () => string | - |
| show | 是否显示 | boolen | () => boolen | true |
| element | 继承el-checkbox相关属性 | any | 同el-checkbox |
| events | 继承el-checkbox事件 | any | - |
| config | 组件配置 | XklCheckbox/XklRadio Config | - |
XklRadio属性
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| option | entity get方法所返回的配置项(XklRadio Option) | any | - |
XklRadio Option
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| type | 表单组件类型 | Component.Radio | - |
| label | 表单组件label | string | - |
| span | 表单组件占位(整行24) | number | 跟随form表单 |
| mode | 与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。 | string | - |
| required | 是否必填 | boolen | () => boolen | - |
| key | 唯一标识,标识函数返回值变更组件可重新渲染 | () => string | - |
| show | 是否显示 | boolen | () => boolen | true |
| element | 继承el-radio相关属性 | any | 同el-radio |
| events | 继承el-radio事件 | any | - |
| config | 组件配置 | XklCheckbox/XklRadio Config | - |
XklCheckbox/XklRadio Config
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| dict | 数据字典类型,获取对应字典列表作为选项列表 | string | - |
| list | 指定选项列表 | {label: string, value: string | number}[] | - |
XklSelect
return {
type: Component.Select,
label: '状态',
config: {
// 手动指定数据
list: [{label: '选项1', value: 'opt1'}, {label: '选项1', value: 'opt1'}]
}
}
XklSelect属性
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| option | entity get方法所返回的配置项(XklSelect Option) | any | - |
XklSelect Option
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| type | 表单组件类型 | Component.Checkbox | - |
| label | 表单组件label | string | - |
| span | 表单组件占位(整行24) | number | 跟随form表单 |
| mode | 与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。 | string | - |
| required | 是否必填 | boolen | () => boolen | - |
| key | 唯一标识,标识函数返回值变更组件可重新渲染 | () => string | - |
| show | 是否显示 | boolen | () => boolen | true |
| element | 继承el-select相关属性,特殊属性:disabled 支持动态(使用方式,同reqiured属性) | any | 同el-select |
| events | 继承el-select事件 | any | - |
| config | 组件配置 | XklSelect Config | - |
XklSelect Config
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| list | 指定选项列表 | {label: string, value: string | number}[] | - |
XklDictSelect
return {
type: Component.DictSelect,
label: '状态',
config: {
//字典类型,根据字典类型渲染选项
dict: 'sys_normal_disable'
}
}
XklDictSelect Option(同XklSelect Option) option.config = XklDictSelect Config
XklDictSelect Config
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| dict | 数据字典类型,获取对应字典列表作为选项列表 | string | - |
XklUrlSelect、XklTreeSelect
return {
type: Component.UrlSelect, //Component.TreeSelect 数据中需含有 children
label: '组织',
config: {
//字典类型,根据字典类型渲染选项
url: '/sys/org/list',
labelTarget: 'orgName', // 对应label字段
valueTarget: 'orgCode' // 对应value字段
}
}
XklUrlSelect Option(同XklSelect Option) option.config = XklUrlSelect Config
XklUrlSelect Config
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| url | 接口路径,返回数据列表 | string | - |
| labelTarget | 对应label字段 | string | - |
| valueTarget | 对应value字段 | string | - |
| split | label分隔符(如 labelTarget: 'id/name', split: '/', label则可以同时显示id 和 name) | string | - |
| cache | 是否缓存列表数据 | boolen | true |
| params | 接口参数,示例:() => ({p1: 'aa', p2: 'bb'}) | () => object | - |
XklDatePicker
return {
type: Component.DatePicker,
label: '时间',
element: {
type: 'datetimerange', //element官网 属性
func: 'bottomshortcut' // 显示自定义底部功能
}
}
XklForm
<xkl-form :form="form">
<template #_action>
<el-button type="primary" @click="getDataList()">查询</el-button>
<el-button @click="resetDataList()">重置</el-button>
</template>
</xkl-form>
<script setup lang="ts">
const form = new User({ mode: 'query', collapsible: true }).active
// 重置列表
const resetDataList = () => {
form.clear()
getDataList()
}
// 查询列表
const getDataList = async () => {
table.loading = true
const queryForm = form.getBindingValue()
const res = await userService.getList({
pageNum: table.page,
pageSize: table.pageSize,
...queryForm
})
if (res.SUCCESS) {
table.list = res.rows
table.totalCount = res.total
}
table.loading = false
}
</script>
form实例
| 方法 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| getBindingValue | 获取表单双向绑定的值 | function | - |
| getRef | 获取表单的ref | function | - |
| submit | 表单提交方法 | function | - |
XklTable
<xkl-table :table="table">
<template #status="{ row }">
<el-switch activeValue="0" inactiveValue="1" v-model="row.status"
@change="statusChange(row)"></el-switch>
</template>
<template #_action="{ row }">
<el-link type="primary" @click="addOrUpdate(row.userId)">编辑</el-link>
<el-link type="primary" @click="openDetail(row.userId)">详情</el-link>
<el-link type="danger" @click="deleteUser(row.userId)">删除</el-link>
</template>
</xkl-table>
<script setup lang="ts">
// 新增编辑
const addOrUpdate = (id?: number) => {
dialogVisible.value = true
nextTick(() => {
DialogOperateRef.value?.init(id)
})
}
const openDetail = (id: number) => {
detailDialogVisible.value = true
nextTick(() => {
DialogDetailRef.value?.init(id)
})
}
const statusChange = async (row: User) => {
if (row && row.userId) {
const res = await userService.statusChange({
status: row.status,
userId: row.userId
})
if (res.SUCCESS) {
$message.success('状态修改成功')
}
}
}
// 删除数据
const deleteUser = (id?: number[]) => {
$messageBox.confirm(
`确认删除数据?`,
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
const ids = id ? [id] : table.selectedData.map(res => res.userId)
const res = await userService.delete(ids.join(','))
if (res.SUCCESS) {
$message.success('删除成功')
getDataList()
}
})
}
// 页码变化
table.pageChange = (val) => {
table.page = val
getDataList()
}
// 每页数量变化
table.sizeChange = (val) => {
table.page = 1
table.pageSize = val
getDataList()
}
</script>