增删改查布局组件 jb-crud-page
jb-crud-page
是一个高级布局封装组件,用于快速构建CRUD页面,它提供以下功能:
- 内置多个插槽,可快速完成布局
- 内置查询条件区域,自带查询按钮和重置按钮,可配置查询接口地址,自动接管查询操作。
- 内置分页组件
- 可关联编辑页面组件,控制编辑页面的弹窗显示
- 对外暴露
state
属性和多个API方法,方便完成增删改查
Props
名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
conditionsAlign | 'start' | 'end' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | center | 控制查询条件区域的排列方式 |
bordered | boolean | false | 整个页面是否显示边框 |
headerShown | boolean | true | 是否显示header区域 |
titleIcon | string | undefined | title插槽显示的图标,显示iconify中的图标 |
titleText | string | undefined | title插槽显示的文字 |
searchUrl | string | undefined | 查询接口地址,点击组件自带的查询按钮,会自动向该接口发起get请求 |
searchConditions | object | undefined | 查询条件,向searchUrl发起请求时需要附带的参数 |
searchBtnShown | boolean | true | 是否显示conditions-btn 插槽自带的查询按钮 |
resetBtnShown | boolean | true | 是否显示conditions-btn 插槽自带的重置按钮 |
pager | boolean | true | 是否显示分页组件,分页组件正常工作的前提是searchUrl 赋值了,并且接口的返回数据是jbolt标准的分页数据结构 |
editComponent | Component | 编辑页面组件,如果传入了该属性,组件可以以弹窗的方式控制编辑页面的显示,以及处理编辑页面提交后的回调 | |
editModalWidth | string | 800px | 编辑页面弹窗的宽度 |
@afterSubmit | ({editingProps,result})=>void | undefined | 编辑页面提交后的回调,editingProps 代表编辑页面组件接收的外部传参,result 代表编辑页面submit 返回的Promise<any> |
@afterSearch | (data)=>void | undefined | 查询成功后的回调,data 代表查询接口返回的列表数据(过滤掉分页属性后的数据)。有的时候我们需要在接收到列表数据后做二次处理再渲染,那么就可以通过该回调属性实现 |
@afterRender | (data)=>void | undefined | 渲染成功后的回调,data 代表渲染使用的列表数据 |
Expose
名称 | 类型 | 说明 |
---|---|---|
state | object | 内部状态,包含以下属性: searching : 是否正在加载数据,list : 渲染用的列表数据,pager : 分页数据,editModalShown : 编辑模态框是否显示,editingProps : 要向编辑组件传入的参数,editModalArgs : 编辑模态框的参数,参考这里,tableStartIndex : 表格序号一列使用的初始值,如果开启了分页,该值会自动递增 |
loadData | (resetPage?:boolean)=>void | 向searchUrl 发起请求加载数据的函数,接收一个参数,表示是否重置分页到第1页,默认false 。组件内置的查询按钮以及分页组件,被点击后都会自动触发该函数。如果你需要手动刷新数据时可以调用该函数 |
showEditModal | (title?:string, editProps?:any, modalArgs?:any)=>void | 打开编辑页面的弹窗,接收三个参数:title 弹窗的标题,editProps 编辑页面组件需要的参数,modalArgs 弹窗的参数,编辑模态框的参数,参考这里 |
closeEditModal | ()=>void | 关闭编辑页面的弹窗。 |
该组件继承了jb-page
,并提供了以下插槽:
示例
最简单的增删改查页面,带一个关键字查询。参考岗位管理功能
- 列表页
post/index.vue
vue
<template>
<jb-crud-page
ref="postPage"
title-icon="carbon:batch-job"
title-text="岗位管理"
search-url="/api/admin/post/datas"
:search-conditions="pageConditions"
:edit-component="PostEdit"
>
<template #conditions-form>
<n-input
v-model:value="pageConditions.keywords"
type="text"
placeholder="输入关键字搜索"
@keyup.enter="postPage?.loadData(true)"
/>
</template>
<template #opt>
<n-button-group>
<jb-btn
ghost
type="primary"
:icon="Icons.ADD"
@click="postPage?.showEditModal('新增岗位')"
>
新增
</jb-btn>
</n-button-group>
</template>
<template #default="{ list,tableStartIndex }">
<jb-table :startIndex="tableStartIndex" :data="list">
>
<jb-column type="seq" title="序号" width="60" fixed="left"></jb-column>
<jb-column
field="name"
title="岗位名称"
min-width="140"
fixed="left"
></jb-column>
<jb-column
field="typeName"
title="岗位类型"
width="120"
fixed="left"
></jb-column>
<jb-column field="sn" title="编码" min-width="120"></jb-column>
<jb-column field="remark" title="备注" min-width="160"></jb-column>
<jb-column field="enable" title="是否启用" width="80" fixed="right">
<template #default="{ row }">
<jb-switch
v-model:value="row.enable"
:url="`/api/admin/post/toggleEnable/${row.id}`"
></jb-switch>
</template>
</jb-column>
<jb-column title="操作" width="110" fixed="right">
<template #default="{ row }">
<jb-btn
tip-text="编辑"
:icon="Icons.EDIT"
type="warning"
secondary
circle
@click="
postPage?.showEditModal('编辑岗位', {
id: row.id
})
"
></jb-btn>
<jb-btn
tip-text="删除"
:icon="Icons.DELETE"
secondary
type="error"
class="mx-8px"
circle
confirm-text="确定删除这条数据?"
:url="`/api/admin/post/delete/${row.id}`"
@success="postPage?.loadData()"
></jb-btn>
</template>
</jb-column>
</jb-table>
</template>
</jb-crud-page>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Icons } from '@/constants'
import { useResetableData } from '@/hooks/common/use-reset-ref'
import JbCrudPage from '@/components/_builtin/jb-crud-page/index.vue'
import PostEdit from './components/post-edit/index.vue'
const postPage = ref<InstanceType<typeof JbCrudPage> | null>(null)
const pageConditions = useResetableData({
keywords: ''
})
</script>
<style scoped></style>
<template>
<jb-crud-page
ref="postPage"
title-icon="carbon:batch-job"
title-text="岗位管理"
search-url="/api/admin/post/datas"
:search-conditions="pageConditions"
:edit-component="PostEdit"
>
<template #conditions-form>
<n-input
v-model:value="pageConditions.keywords"
type="text"
placeholder="输入关键字搜索"
@keyup.enter="postPage?.loadData(true)"
/>
</template>
<template #opt>
<n-button-group>
<jb-btn
ghost
type="primary"
:icon="Icons.ADD"
@click="postPage?.showEditModal('新增岗位')"
>
新增
</jb-btn>
</n-button-group>
</template>
<template #default="{ list,tableStartIndex }">
<jb-table :startIndex="tableStartIndex" :data="list">
>
<jb-column type="seq" title="序号" width="60" fixed="left"></jb-column>
<jb-column
field="name"
title="岗位名称"
min-width="140"
fixed="left"
></jb-column>
<jb-column
field="typeName"
title="岗位类型"
width="120"
fixed="left"
></jb-column>
<jb-column field="sn" title="编码" min-width="120"></jb-column>
<jb-column field="remark" title="备注" min-width="160"></jb-column>
<jb-column field="enable" title="是否启用" width="80" fixed="right">
<template #default="{ row }">
<jb-switch
v-model:value="row.enable"
:url="`/api/admin/post/toggleEnable/${row.id}`"
></jb-switch>
</template>
</jb-column>
<jb-column title="操作" width="110" fixed="right">
<template #default="{ row }">
<jb-btn
tip-text="编辑"
:icon="Icons.EDIT"
type="warning"
secondary
circle
@click="
postPage?.showEditModal('编辑岗位', {
id: row.id
})
"
></jb-btn>
<jb-btn
tip-text="删除"
:icon="Icons.DELETE"
secondary
type="error"
class="mx-8px"
circle
confirm-text="确定删除这条数据?"
:url="`/api/admin/post/delete/${row.id}`"
@success="postPage?.loadData()"
></jb-btn>
</template>
</jb-column>
</jb-table>
</template>
</jb-crud-page>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Icons } from '@/constants'
import { useResetableData } from '@/hooks/common/use-reset-ref'
import JbCrudPage from '@/components/_builtin/jb-crud-page/index.vue'
import PostEdit from './components/post-edit/index.vue'
const postPage = ref<InstanceType<typeof JbCrudPage> | null>(null)
const pageConditions = useResetableData({
keywords: ''
})
</script>
<style scoped></style>
- 编辑页
/post/components/post-edit/index.vue
vue
<template>
<n-form
ref="formRef"
label-placement="top"
:label-width="80"
:model="form"
:rules="rules"
>
<n-form-item label="岗位名称" path="name">
<n-input v-model:value="form.name" placeholder="请输入岗位名称" />
</n-form-item>
<n-form-item label="岗位类型" path="type">
<jb-select
v-model:value="form.type"
url="/api/admin/dictionary/options?typeKey=post_type"
placeholder="=请选择="
:clearable="false"
filterable
class="w-140px"
></jb-select>
</n-form-item>
<n-form-item label="编码" path="sn">
<n-input v-model:value="form.sn" placeholder="请输入编码" />
</n-form-item>
<n-form-item label="备注信息">
<n-input
v-model:value="form.remark"
type="textarea"
placeholder="请输入备注信息"
/>
</n-form-item>
</n-form>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import { Rules } from '@/utils'
import { JBoltApi } from '@/service/request'
import { useResetableData } from '@/hooks/common/use-reset-ref'
import { ResData } from '@/typings/request'
const props = withDefaults(
defineProps<{
id?: string
}>(),
{
id: ''
}
)
/** 表单相关 start */
const formRef = ref<FormInst | null>()
interface PostType {
id: string
name: string
type: number
sn: string
remark: string
}
const form = useResetableData<PostType>({
id: '',
name: '',
type: 0,
sn: '',
remark: ''
})
const rules: FormRules = {
name: new Rules().required('请输入名称').value,
type: new Rules().required('请选择类型').value,
sn: new Rules().required('请输入编码').value
}
/**
* 提交表单
*/
async function submit() {
await formRef.value?.validate()
let url = props.id ? '/api/admin/post/update' : '/api/admin/post/save'
await JBoltApi.tryPost(url, form)
await window.$success('保存成功')
return true
}
function loadEditData() {
JBoltApi.get<ResData>(`/api/admin/post/${props.id}`).then(
({ error, result }) => {
if (error) return
form._reset(result.data)
}
)
}
onMounted(() => {
if (props.id) {
loadEditData()
}
})
defineExpose({
submit
})
/** 表单相关 end */
</script>
<style scoped></style>
<template>
<n-form
ref="formRef"
label-placement="top"
:label-width="80"
:model="form"
:rules="rules"
>
<n-form-item label="岗位名称" path="name">
<n-input v-model:value="form.name" placeholder="请输入岗位名称" />
</n-form-item>
<n-form-item label="岗位类型" path="type">
<jb-select
v-model:value="form.type"
url="/api/admin/dictionary/options?typeKey=post_type"
placeholder="=请选择="
:clearable="false"
filterable
class="w-140px"
></jb-select>
</n-form-item>
<n-form-item label="编码" path="sn">
<n-input v-model:value="form.sn" placeholder="请输入编码" />
</n-form-item>
<n-form-item label="备注信息">
<n-input
v-model:value="form.remark"
type="textarea"
placeholder="请输入备注信息"
/>
</n-form-item>
</n-form>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import { Rules } from '@/utils'
import { JBoltApi } from '@/service/request'
import { useResetableData } from '@/hooks/common/use-reset-ref'
import { ResData } from '@/typings/request'
const props = withDefaults(
defineProps<{
id?: string
}>(),
{
id: ''
}
)
/** 表单相关 start */
const formRef = ref<FormInst | null>()
interface PostType {
id: string
name: string
type: number
sn: string
remark: string
}
const form = useResetableData<PostType>({
id: '',
name: '',
type: 0,
sn: '',
remark: ''
})
const rules: FormRules = {
name: new Rules().required('请输入名称').value,
type: new Rules().required('请选择类型').value,
sn: new Rules().required('请输入编码').value
}
/**
* 提交表单
*/
async function submit() {
await formRef.value?.validate()
let url = props.id ? '/api/admin/post/update' : '/api/admin/post/save'
await JBoltApi.tryPost(url, form)
await window.$success('保存成功')
return true
}
function loadEditData() {
JBoltApi.get<ResData>(`/api/admin/post/${props.id}`).then(
({ error, result }) => {
if (error) return
form._reset(result.data)
}
)
}
onMounted(() => {
if (props.id) {
loadEditData()
}
})
defineExpose({
submit
})
/** 表单相关 end */
</script>
<style scoped></style>
查询
如果想让组件能自动接管查询,需要通过以下几个属性来实现:
searchUrl
查询接口地址,必填。searchConditions
查询条件对象,通常就是conditions-form
插槽中组件绑定的数据。注意: 组件自带的重置按钮如果想正常工作,那么searchConditions
必须是一个useResetableData
处理的对象。pager
是否分页,默认为true
。分页的话,footer
插槽会自动渲染出分页组件。
如果我们需要手动触发查询,可以调用jb-crud-page
组件的loadData
方法,该方法接收一个参数,表示是否重置分页到第1页,默认false 。所以需要在jb-crud-page
组件身上绑上ref,然后通过ref调用该方法。以下示例代码就实现了关键字输入框按回车触发页面查询的功能。
vue
<n-input
v-model:value="pageConditions.keywords"
type="text"
placeholder="输入关键字搜索"
@keyup.enter="postPage?.loadData(true)"
/>
<n-input
v-model:value="pageConditions.keywords"
type="text"
placeholder="输入关键字搜索"
@keyup.enter="postPage?.loadData(true)"
/>
渲染列表
有了查询数据,我们就可以在default
插槽中渲染了。在default
插槽中,可以拿到以下数据:。
list
: 接口返回的列表数据pager
: 分页数据tableStartIndex
:表格序号一列使用的初始值,如果开启了分页,该值会自动递增state
: 同上方文档:
通常列表页最左侧一列显示序号,第一页序号从1开始,如果每页显示20条数据,那么第二页序号就应该从21开始了。jb-table
组件接受一个startIndex
属性,用来控制序号的初始值,这个值就可以从state.tableStartIndex
中获取到。如果开启了分页,该值会自动递增,不需要开发人员维护。
vue
<template #default="{ list,tableStartIndex }">
<jb-table :startIndex="tableStartIndex" :data="list">
</jb-table>
</template>
<template #default="{ list,tableStartIndex }">
<jb-table :startIndex="tableStartIndex" :data="list">
</jb-table>
</template>
弹出编辑页面
如果需要在列表页中弹出编辑页面,可以通过以下步骤实现:
- 将编辑页面组件 import进来
- 将编辑页面组件赋值给
jb-crud-page
组件的editComponent
属性 - 在列表页中调用
jb-crud-page
组件的showEditModal
方法
以下示例代码就是点击编辑按钮时打开编辑页,并向编辑页面传入了id
参数。
vue
<jb-btn tip-text="编辑" :icon="Icons.EDIT" type="warning"
secondary circle
@click="
postPage?.showEditModal('编辑岗位', {
id: row.id
})
"
></jb-btn>
<jb-btn tip-text="编辑" :icon="Icons.EDIT" type="warning"
secondary circle
@click="
postPage?.showEditModal('编辑岗位', {
id: row.id
})
"
></jb-btn>
弹出框有两个按钮,一个关闭
,一个提交
。
- 点击
关闭
按钮时,会自动调用jb-crud-page
组件的closeEditModal
方法,关闭弹出框。 - 点击
提交
按钮时,会尝试调用编辑页面组件的submit
函数。所以这里要求,编辑页面组件必须暴露一个submit
函数,用于提交后的回调,并且这个函数应该是一个异步的,返回值类型为Promise<any>。如果编辑页面组件的submit
函数发生resolve,那么弹出框会自动关闭。如果发生reject,那么弹出框不会关闭。