xkl-admin-pro 使用文档

742 阅读2分钟

一、简介

xkl-admin-pro 是一个基于vue3,element-plus,typescript,所构建的后台管理系统框架,内置代码生成工具(基于tauri),以“配置式”形式实现页面渲染,快速生成所需基础页面,提升开发效率。 项目目录结构如下图,开发过程中重点关注entity,service 和 views目录。

gitee项目地址 gitee.com/xuekl/xkl-a…

image.png

二、入门使用

启动项目

  1. node版本:20.x.x
  2. npm i -g nrm
  3. nrm use taobao
  4. npm i
  5. npm run dev

新建页面

image.png 如图所示,新建一个名为“IndexView.vue”(作为一个菜单的主页面名称必须为IndexView)的文件,会出现下图所示配置面板。

image.png 写入基础文件名称,选择初始模板为“主页”时,会出现创建实体配置,然后输入当前页面所对应的一个(详情/列表)数据接口,也可以点击“新增”按钮手动新增。

image.png 按业务需求配置对应字段的表单属性(这里只做基础配置,生成后的代码可继续修改)。

image.png 填写完毕点击“确定”按钮,生成对应的Entity(下文介绍)

image.png 自行补充上述的“初始模板”(如: 新增/编辑(弹窗))类型最终页面效果如下图。

image.png

三、进阶使用

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层直接使用。

image.png

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各区域之间的距离number20
padding各区域内边距number20
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事件下生效boolentrue
confirmText二次弹窗确认提示内容string当前内容可能存在修改,确认继续?
openType会寻找window对象下同名全局事件并执行string-
...继承el-button相关属性anyel-button

XklButton事件

属性名说明类型默认值
click点击事件() => void-
async携带阻止重复点击回调,执行done函数后可点击第二次(done: () => void) => void-
confirm携带二次确认弹窗的点击事件,prevent 为true下生效() => void-
XklInput、XklInputNumber
return {
    type: Component.Input, // 或 Component.InputNumber
    label: '用户密码'
}

XklInput属性

属性名说明类型默认值
optionentity get方法所返回的配置项(XklInput Option)any-

XklInput Option

属性名说明类型默认值
type表单组件类型Component.Input-
label表单组件labelstring-
span表单组件占位(整行24)number跟随form表单
mode与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。string-
required是否必填boolen | () => boolen-
key唯一标识,标识函数返回值变更组件可重新渲染() => string-
show是否显示boolen | () => boolentrue
element继承el-input相关属性,特殊属性:disabled 支持动态(使用方式,同reqiured属性)anyel-input
events继承el-input事件any-

XklInputNumber属性

属性名说明类型默认值
optionentity get方法所返回的配置项(XklInputNumber Option)any-

XklInputNumber Option

属性名说明类型默认值
type表单组件类型Component.InputNumber-
label表单组件labelstring-
span表单组件占位(整行24)number跟随form表单
mode与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。string-
required是否必填boolen | () => boolen-
key唯一标识,标识函数返回值变更组件可重新渲染() => string-
show是否显示boolen | () => boolentrue
element继承el-input-number相关属性,特殊属性:disabled 支持动态(使用方式,同reqiured属性)anyel-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属性

属性名说明类型默认值
optionentity get方法所返回的配置项(XklCheckbox Option)any-

XklCheckbox Option

属性名说明类型默认值
type表单组件类型Component.Checkbox-
label表单组件labelstring-
span表单组件占位(整行24)number跟随form表单
mode与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。string-
required是否必填boolen | () => boolen-
key唯一标识,标识函数返回值变更组件可重新渲染() => string-
show是否显示boolen | () => boolentrue
element继承el-checkbox相关属性anyel-checkbox
events继承el-checkbox事件any-
config组件配置XklCheckbox/XklRadio Config-

XklRadio属性

属性名说明类型默认值
optionentity get方法所返回的配置项(XklRadio Option)any-

XklRadio Option

属性名说明类型默认值
type表单组件类型Component.Radio-
label表单组件labelstring-
span表单组件占位(整行24)number跟随form表单
mode与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。string-
required是否必填boolen | () => boolen-
key唯一标识,标识函数返回值变更组件可重新渲染() => string-
show是否显示boolen | () => boolentrue
element继承el-radio相关属性anyel-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属性

属性名说明类型默认值
optionentity get方法所返回的配置项(XklSelect Option)any-

XklSelect Option

属性名说明类型默认值
type表单组件类型Component.Checkbox-
label表单组件labelstring-
span表单组件占位(整行24)number跟随form表单
mode与form Entity mode属性匹配成功则显示在对应form Entity 上,如果不指定该属性,则所有实例 Entity都会显示该字段。string-
required是否必填boolen | () => boolen-
key唯一标识,标识函数返回值变更组件可重新渲染() => string-
show是否显示boolen | () => boolentrue
element继承el-select相关属性,特殊属性:disabled 支持动态(使用方式,同reqiured属性)anyel-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-
splitlabel分隔符(如 labelTarget: 'id/name', split: '/', label则可以同时显示id 和 name)string-
cache是否缓存列表数据boolentrue
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获取表单的reffunction-
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>