Skip to content

增删改查布局组件 jb-crud-page

jb-crud-page是一个高级布局封装组件,用于快速构建CRUD页面,它提供以下功能:

  • 内置多个插槽,可快速完成布局
  • 内置查询条件区域,自带查询按钮和重置按钮,可配置查询接口地址,自动接管查询操作。
  • 内置分页组件
  • 可关联编辑页面组件,控制编辑页面的弹窗显示
  • 对外暴露state属性和多个API方法,方便完成增删改查

Props

名称类型默认值说明
conditionsAlign'start' | 'end' | 'center' | 'space-around' | 'space-between' | 'space-evenly'center控制查询条件区域的排列方式
borderedbooleanfalse整个页面是否显示边框
headerShownbooleantrue是否显示header区域
titleIconstringundefinedtitle插槽显示的图标,显示iconify中的图标
titleTextstringundefinedtitle插槽显示的文字
searchUrlstringundefined查询接口地址,点击组件自带的查询按钮,会自动向该接口发起get请求
searchConditionsobjectundefined查询条件,向searchUrl发起请求时需要附带的参数
searchBtnShownbooleantrue是否显示conditions-btn插槽自带的查询按钮
resetBtnShownbooleantrue是否显示conditions-btn插槽自带的重置按钮
pagerbooleantrue是否显示分页组件,分页组件正常工作的前提是searchUrl赋值了,并且接口的返回数据是jbolt标准的分页数据结构
editComponentComponent编辑页面组件,如果传入了该属性,组件可以以弹窗的方式控制编辑页面的显示,以及处理编辑页面提交后的回调
editModalWidthstring800px编辑页面弹窗的宽度
@afterSubmit({editingProps,result})=>voidundefined编辑页面提交后的回调,editingProps代表编辑页面组件接收的外部传参,result代表编辑页面submit返回的Promise<any>
@afterSearch(data)=>voidundefined查询成功后的回调,data代表查询接口返回的列表数据(过滤掉分页属性后的数据)。有的时候我们需要在接收到列表数据后做二次处理再渲染,那么就可以通过该回调属性实现
@afterRender(data)=>voidundefined渲染成功后的回调,data代表渲染使用的列表数据

Expose

名称类型说明
stateobject内部状态,包含以下属性:
searching : 是否正在加载数据,
list : 渲染用的列表数据,
pager : 分页数据,
editModalShown: 编辑模态框是否显示,
editingProps : 要向编辑组件传入的参数,
editModalArgs : 编辑模态框的参数,参考这里
tableStartIndex: 表格序号一列使用的初始值,如果开启了分页,该值会自动递增
loadData(resetPage?:boolean)=>voidsearchUrl发起请求加载数据的函数,接收一个参数,表示是否重置分页到第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,那么弹出框不会关闭。