xkl-admin(vue3 + ts + element-plus + electron)使用文档

353 阅读9分钟

1、简介

  • 基于vue3、element-plus、typescript、pinia、vite搭建的后台管理系统。
  • 集成koa、electron实现增删改查简单生成。
  • 提供简单易用的常用组件,只需进行简单的属性配置即可。
  • 抽离表单配置(entity),以配置的形式组建表单页面。
  • 抽离接口调用层(service),使得代码层次分明,便于维护。
  • 植入装饰器功能简化业务开发冗余代码。

根目录

image.png

源码目录

image.png

提供组件

  • 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

使用

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

项目主要目录

  • entity ---- 表单模板
  • service ---- 接口调用数据处理
  • views ---- 页面呈现

问题

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')

3、cli-panel生成工具