【SearchPanel组件】详细配置文档

SearchPanel是一个基于Vue的搜索组件,包含各类搜索项和搜索结果表格,支持响应式数据更新

  • 2025.02.17 更新:联想输入框的fetchSuggestFn字段支持返回对象数组,支持显示值和绑定值分离。
  • 2025.02.14 创建本文档。

组件说明

SearchPanel是我封装的一个基于Vue的搜索组件,包含各类搜索项和搜索结果表格,UI设计继承企业微信团队一致风格,支持响应式调用接口实现实时数据更新。具体来说,组件分为三个部分:搜索项(search-items)、操作项(search-actions)和搜索结果表格(search-result)。

效果图:

引入组件

在需要使用SearchPanel的页面中引入组件:

 1<template>
 2<div class="record-page">
 3    <!-- SearchPanel -->
 4    <SearchPanel
 5      :fields="fields"
 6      :actions="actions"
 7      :columns="columns"
 8
 9      :fetchTableFn="fetchTableData"  
10      :pageSize="page_size"
11      showPagination
12      showTable
13      autoFetch
14
15      @action="onPanelAction"
16      @select="onSuggestItemSelect"
17      @change="onFieldChange"
18      @row-click="onRowClick"
19    />
20</div>
21</template>
22
23
24<script>
25import SearchPanel from '/path/to/SearchPanel.vue';
26export default {
27    components: {
28        SearchPanel
29    }
30}
31</script>

引号内的字符串可以不做更改,在父组件的data()methods中定义为实际值即可。各字段含义如下。

功能字段

  • fieldsArray。定义搜索面板中的各个搜索项。每个搜索项可以是普通输入框、联想输入框、日期选择框、单选按钮组、单选下拉列表等。通过配置 fields,可以灵活地定制搜索面板的内容和样式。
  • actionsArray。定义搜索面板中的操作按钮。可以配置按钮的显示文本、点击事件等。可以用于提供手动搜索、导出结果等功能。
  • columnsArray。定义表格的列配置。每个列配置项包括列的标识、显示标签、样式等。用于展示搜索结果的表格结构。
  • fetchTableFnFunction。用于获取表格数据的函数。推荐值为一个异步函数,接收搜索条件、当前页码、每页条数等参数,并返回表格数据和总条数。
  • pageSizeNumber。定义每页显示的条数。用于分页控制。
  • showPaginationBoolean。是否显示分页控件。用于控制表格的分页显示。
  • showTableBoolean。是否显示表格。用于控制搜索结果的显示。
  • autoFetchBoolean。是否在组件加载时自动获取数据。通常用于初始化时自动加载数据。

事件字段

  • @selectFunction。监听联想输入框的选项选择事件。用于处理用户选择联想项后的逻辑。
  • @changeFunction。监听搜索项值变化事件。用于处理用户输入或选择后的逻辑。
  • @actionFunction。用于处理搜索面板操作按钮的点击事件。
  • @row-clickFunction。用于处理表格行点击事件。

组件结构

总体配置

配置方式

data()配置

以前述引入时的字段名为示例,在父组件的data()中配置以下字段:

 1data() {
 2    return {
 3        fields: [], // Object 数组,此字段名与引入时保持一致,每个对象代表一个搜索项
 4        page_size: 10, // Number,此字段名与引入时保持一致,用于每页显示的条数
 5        actions: [], // Object 数组,此字段名与引入时保持一致,每个对象代表一个操作按钮
 6        columns: [], // Object 数组,此字段名与引入时保持一致,每个对象代表一个表格列
 7        autoFetch: true, // Boolean,是否在组件加载时自动获取数据
 8        showTable: true, // Boolean,是否显示表格
 9        showPagination: true // Boolean,是否显示分页控件
10    }
11}

搜索项的相关参数在data()中的fields字段中按顺序配置,以普通输入框为例,配置如下:

 1export default {
 2    data() {
 3        return {
 4            fields: [
 5                {
 6                    type: "text",
 7                    key: "name",
 8                    label: "姓名",
 9                    placeholder: "请输入姓名"
10                }
11            ]
12        }
13    }
14}

操作项的相关参数在data()中的actions字段中按顺序配置,示例配置如下:

 1export default {
 2    data() {
 3        return {
 4            actions: [
 5                {
 6                    key: "search",
 7                    label: "搜索"
 8                }
 9            ]
10        }
11    }
12}

所有类型的搜索项操作项都支持以下通用配置:

1{
2  key: "string",           // 必填,字段标识
3  label: "string",         // 必填,字段标签文本
4  itemClass: "string",     // 可选,整个搜索项的自定义类名
5  titleClass: "string",    // 可选,标签文本的自定义类名
6  titleStyle: "object",    // 可选,标签文本的内联样式
7  wrapperClass: "string"   // 可选,输入区域的包装容器类名
8}

这里解释一下itemClass是什么。看这段组件源码你就明白了:

 1    <div class="search-area" :class="customAreaClass">
 2      <template v-for="(field, idx) in fields" :key="idx">
 3        <div 
 4        class="search-item" 
 5        :class="[field.itemClass]"
 6        >
 7        <!-- ... -->
 8        </div>
 9      </template>
10    </div>

itemClass是每个搜索项或操作项最外层的容器类名。

methods配置

以前述引入时的字段名为示例,在父组件的methods中配置以下方法:

 1methods: {
 2    async fetchTableData(searchValues, currentPage, pageSize) {
 3        // 实现获取表格数据的逻辑
 4    },
 5    onPanelAction(actionKey) {
 6        // 按钮被点击时的操作
 7    },
 8    onSuggestItemSelect({ key, value }) {
 9        // 用户选中了联想项的操作
10    },
11    onFieldChange({ key, value }) {
12        // 字段值变化时的操作
13    },
14    onRowClick(row) {
15        // 表格行被点击时的操作
16    }
17}
  • fetchTableDataFunction。用于获取表格数据的函数。此函数名与引入时保持一致,推荐值为一个异步函数。
    • 形参:
      • searchValuesObject。搜索条件。
      • currentPageNumber。当前页码。
      • pageSizeNumber。每页显示的条数。
    • 返回值:Promise<{ list: Array, total: number }>。返回一个 Promise,resolve 的对象包含两个属性表格数据和总条数。
  • onPanelActionFunction。用于处理搜索面板操作按钮的点击事件。
    • 形参:
      • actionKeyString。点击的按钮的标识。
  • onSuggestItemSelectFunction。用于处理联想输入框的选项选择事件。
    • 形参:
      • { key, value }Object。选中的联想项的标识和值。
  • onFieldChangeFunction。用于处理搜索项值变化事件。
    • 形参:
      • { key, value }Object。变化的搜索项的标识和值。
  • onRowClickFunction。用于处理表格行点击事件。
    • 形参:
      • rowObject。点击的行数据。

搜索项配置

搜索项包含常用的5类输入组件:普通输入框、联想输入框、日期选择框、单选按钮组、单选下拉列表。

普通输入框

普通输入框是最常用的输入组件,type字段为text。示例:

1{
2    type: "text", // 必填,字段类型:'text'
3    key: "name", // 必填,字段标识,也是发起请求时对应的参数名
4    label: "姓名", // 必填,字段标签文本
5    placeholder: "请输入姓名", // 可选,输入框的占位文本
6}

如果要增加自定义样式,可以增加itemClasstitleClasstitleStylewrapperClass字段。如前所述,这些字段适用于所有类型的搜索项和操作项,后面不再赘述。

 1{
 2    type: "text",
 3    key: "name",
 4    label: "姓名",
 5    placeholder: "请输入姓名",
 6    itemClass: "name-input", // 可选,整个搜索项的自定义类名
 7    titleClass: "name-label", // 可选,标签文本的自定义类名
 8    titleStyle: {
 9        color: "#333",
10        "margin-right": "10px"
11    }, // 可选,标签文本的内联样式
12    wrapperClass: "name-wrapper" // 可选,输入区域的包装容器类名
13}

普通输入框另有2个字段:inputClassinputStyle,用于自定义输入框的样式。

1{
2    inputClass: "name-input-inner", // 可选,输入框的自定义类名
3    inputStyle: {
4        color: "#333",
5        "border-radius": "5px"
6    } // 可选,输入框的内联样式
7}

当然,我们可以增加一个示例,假设接口返回的是新的格式,支持显示值和绑定值分离。

联想输入框

联想输入框支持输入文字并在输入框下方显示联想项,type字段为suggest。示例:

 1{
 2    type: "suggest", // 必填,字段类型:'suggest'
 3    key: "senderData", // 必填,字段标识,也是发起请求时对应的参数名
 4    label: "发送人", // 必填,字段标签文本
 5    placeholder: "请输入发送人", // 可选,输入框的占位文本 
 6    inputClass: "sender-input", // 可选,输入框的自定义类名
 7    inputStyle: {
 8        color: "#333",
 9        "border-radius": "5px"
10    }, // 可选,输入框的内联样式
11    fetchSuggestFn: async (value) => {
12        // 必填,用于获取联想项的函数。
13    }
14}

其中fetchSuggestFn字段是用于从API获取联想项的函数。

  • 形参:

    • valString。输入框的值。
  • 返回值:Promise<Array>。返回一个 Promise,resolve 的对象为联想项的数组。每个联想项可以是一个字符串或一个对象,支持以下两种格式:

    1. 字符串数组:直接返回字符串数组,显示值和绑定值相同。
    2. 对象数组:返回对象数组,支持显示值和绑定值分离。对象格式为 { label: string, value: any }

假设有一个接口POST /api/sender,请求体为{ "searchTerm": "三" },返回{ "results": ["张三", "李三", "王三"] },则可以这样实现:

 1fetchSuggestFn: async (val) => {
 2    try {
 3        const response = await fetch(`/api/sender`, {   
 4            method: "POST",
 5            body: JSON.stringify({ searchTerm: val })
 6        })
 7        const data = await response.json()
 8        // 返回字符串数组
 9        return data?.results || []
10    } catch (error) {
11        console.error("获取联想项失败", error)
12        return []
13    }
14}

或者用axios实现:

 1fetchSuggestFn: async (val) => {  
 2    try {
 3        const response = await axios.post('/api/sender', { searchTerm: val })
 4        // 返回字符串数组
 5        return response.data?.results || []
 6    } catch (error) {
 7        console.error("获取联想项失败", error)
 8        return []
 9    }
10}

假设接口返回的是对象数组(每个对象是键值对的形式),则支持显示值和绑定值分离,例如:

1{
2    "results": [
3        { "label": "张三 <zhangsan@example.com>", "value": "zhangsan" },
4        { "label": "李四 <lisi@example.com>", "value": "lisi" },
5        { "label": "王五 <wangwu@example.com>", "value": "wangwu" }
6    ]
7}

则可以如下实现:

 1fetchSuggestFn: async (val) => {
 2    try {
 3        const response = await axios.post('/api/sender', { searchTerm: val })
 4        // 返回对象数组,支持显示值和绑定值分离
 5        return response.data?.results || []
 6    } catch (error) {
 7        console.error("获取联想项失败", error)
 8        return []
 9    }
10}

日期选择框

日期选择框支持选择日期,type字段为date。其返回值为一个字符串,格式为YYYY年MM月DD日。示例:

 1{
 2    type: "date", // 必填,字段类型:'date'
 3    key: "beginDate", // 必填,字段标识,也是发起请求时对应的参数名
 4    label: "开始日期", // 必填,字段标签文本
 5    inputClass: "date-input", // 可选,输入框的自定义类名
 6    inputStyle: {
 7        color: "#333",
 8        "border-radius": "5px"
 9    }, // 可选,输入框的内联样式
10    dateConfig: {
11        min: '2025年01月01日', // 可选,最小日期
12        max: '2025年12月31日' // 可选,最大日期
13        // 注:如果不设置min和max,则默认max是当前日期
14        defaultValue: '2025年02月01日' // 可选,默认日期
15    }
16}

也可以动态获取相关值,比如设置defaultValue为当前日期:

1dateConfig: {
2    defaultValue: (() => {
3        const date = new Date();
4        return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}日`;
5    })()
6}

单选按钮组

单选按钮组支持单选按钮组,type字段为radioGroup。示例:

 1{
 2    type: "radioGroup", // 必填,字段类型:'radioGroup'
 3    key: "statusFilter", // 必填,字段标识,也是发起请求时对应的参数名
 4    label: "操作状态", // 必填,字段标签文本
 5    buttonClass: "status-button", // 可选,按钮的统一自定义类名
 6    buttonStyle: {
 7        color: "#333",
 8        "border-radius": "5px"
 9    }, // 可选,按钮的统一内联样式
10    options: [
11        { label: "全部", value:'' },
12        { label: "成功", value:'true' },
13        { label: "失败", value:'false' },
14    ] // 必填,单选按钮组选项数组,label是显示文本,value是请求参数值
15}

单选按钮组支持为每个按钮增加自定义样式,在options字段的每个对象中添加customClasscustomStyle字段:

1{
2    options: [
3        { label: "全部", value:'' },
4        { label: "成功", value:'true', customClass: "success-button", customStyle: { "margin-right": "10px" } },
5        { label: "失败", value:'false', customClass: "failed-button", customStyle: { "margin-right": "10px" } },
6    ]
7}

单选下拉列表

单选下拉列表支持点击显示下拉列表,type字段为select。示例:

 1{
 2    type: "select", // 必填,字段类型:'select'
 3    key: "sortType", // 必填,字段标识,也是发起请求时对应的参数名
 4    label: "排序方式", // 必填,字段标签文本
 5    dropdownClass: "sort-dropdown", // 可选,下拉选项的统一自定义类名
 6    dropdownStyle: {
 7        color: "#333",
 8        "border-radius": "5px"
 9    }, // 可选,下拉选项的统一内联样式
10    options: [
11        { label: "最近记录在前", value:'time_desc' },
12        { label: "最早记录在前", value:'time_asc' },
13    ] // 必填,单选下拉列表选项数组,label是显示文本,value是请求参数值
14}

单选下拉列表支持为每个选项增加自定义类,在options字段的每个对象中添加customClass字段:

1{
2    options: [
3        { label: "最近记录在前", value:'time_desc', customClass: "time-desc-option" },
4        { label: "最早记录在前", value:'time_asc', customClass: "time-asc-option" },
5    ]
6}

操作项配置

操作项即为按钮组件,在data()中的actions字段中配置。示例:

1{
2    key: "export", // 必填,按钮点击事件时监测的参数
3    label: "导出记录", // 必填,按钮显示文本
4    btnClass: "export-btn" // 可选,按钮的自定义类名
5}

表格配置

表格的列在data()中的columns字段中配置。示例:

 1data() {
 2    return {
 3        columns: [
 4            { key: "sender", label: "发件人", style: { "width": "180px" } },
 5            { key: "subject", label: "主题", style: { "width": "200px" } },
 6            { key: "timestamp", label: "发送时间", style: { "width": "120px" } },
 7            { key: "status", label: "操作状态", style: { "width": "80px" } },
 8        ]
 9    }
10}

key参数是从API接收到的数据中的字段名。label参数是表格列的显示文本。style参数是对应列的内联样式。

事件配置

事件逻辑在methods中配置。

自动获取表格数据事件

获取表格数据在fetchTableData方法中配置。注意:此函数名应为父组件引入时配置的fetchTableFn字段名。

1methods: {
2    async fetchTableData(searchValues, currentPage, pageSize) {
3        // 实现获取表格数据的逻辑
4    }
5}
  • 形参:
    • searchValuesObject。搜索条件。
    • currentPageNumber。当前页码。
    • pageSizeNumber。每页显示的条数。
  • 返回值:Promise<{ list: Array, total: number }>。返回一个 Promise,resolve 的对象包含两个属性表格数据和总条数。

注:此逻辑假定后端API接口支持分页,如果后端不支持分页,则需要自行实现分页逻辑。

假设我们有一个接口POST /api/email_list,请求体示例如下:

 1{
 2    "senderData": "5",
 3    "themeData": "",
 4    "beginDate": "2025-02-06T16:00:00.000Z",
 5    "endDate": "2025-02-14T15:59:59.999Z",
 6    "statusFilter": "true",
 7    "sortType": "time_desc",
 8    "page": 1, // 当前页码
 9    "limit": 8 // 每页显示的条数
10}

返回的结果如:

 1{
 2    "results": [
 3        {
 4            "sender": "5@example.com",
 5            "subject": "Theme 5",
 6            "timestamp": "2025-02-13T10:27:36.430Z",
 7            "status": true
 8        },
 9        {
10            "sender": "15@example.com",
11            "subject": "Theme 15",
12            "timestamp": "2025-02-13T00:27:36.430Z",
13            "status": true
14        },
15        {
16            "sender": "25@example.com",
17            "subject": "Theme 25",
18            "timestamp": "2025-02-12T14:27:36.430Z",
19            "status": true
20        },
21        {
22            "sender": "50@example.com",
23            "subject": "Theme 50",
24            "timestamp": "2025-02-11T13:27:36.430Z",
25            "status": true
26        }
27    ],
28    "total": 4,
29    "currentPage": 1,
30    "totalPages": 1
31}

那么我们可以这样实现fetchTableData方法:

 1async fetchTableData(searchValues, currentPage, pageSize) {
 2    try {
 3    // 抽出外部字段
 4    let { senderData, themeData, beginDate, endDate, statusFilter, sortType } = searchValues
 5    // 保证 beginDate<=endDate
 6    if (beginDate && endDate) {
 7        // 比较日期大小
 8        const beginTime = new Date(beginDate.replace(/年|月|日/g, '')).getTime()
 9        const endTime = new Date(endDate.replace(/年|月|日/g, '')).getTime()
10        if (endTime < beginTime) {
11        // 交换开始和结束日期
12        const temp = beginDate
13        beginDate = endDate 
14        endDate = temp
15        }
16    }
17    // 转换成ISO日期字符串
18    const parseDate = (dateStr) => {
19        if (!dateStr) return null;
20        const [year, month, day] = dateStr.split(/年|月|日/);
21        return new Date(Number(year), Number(month) - 1, Number(day));
22    }
23    
24    const beginDateISO = beginDate ? new Date(parseDate(beginDate).setHours(0,0,0,0)).toISOString() : ''
25    const endDateISO = endDate ? new Date(parseDate(endDate).setHours(23,59,59,999)).toISOString() : ''
26
27    const body = {
28        senderData:    senderData    || '',
29        themeData:     themeData     || '',
30        beginDate:     beginDateISO,
31        endDate:       endDateISO,
32        statusFilter:  statusFilter || '',
33        sortType:      sortType || 'time_desc',
34        page:          currentPage, 
35        limit:         pageSize
36    }
37
38    // 发起请求
39    const resp = await axios.post('https://test.ember.ac.cn/api/email_list', body)
40    // 返回结果
41    const results = resp.data?.results || []
42    const total = resp.data?.total || 0
43    return {
44        list: results.map(item => {
45        // item.status是boolean -> 转成 '成功' / '失败'
46        // item.timestamp转成易读格式
47        const date = new Date(item.timestamp)
48        const formattedDate = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
49        return {
50            ...item,
51            status: item.status === true ? '成功' : '失败',
52            timestamp: formattedDate
53        }
54        }),
55        total: total
56    }
57    } catch (err) {
58    console.error(err)
59    return { list: [], total: 0 }
60    }
61}

这样配置后,如果打开了autoFetch选项,则组件会自动监听搜索条件的变化,每次变化时都会自动调用fetchTableData方法来更新表格数据。如果没打开autoFetch,则可以在按钮点击事件中手动更新表格数据。

按钮点击事件

按钮点击事件在@action事件中配置。在前述的引入中,我们在onPanelAction方法中配置。示例:

1methods: {
2    onPanelAction(actionKey) {
3        // 实现按钮点击事件的逻辑
4        if (actionKey === 'export') {
5            alert('导出按钮被点击了!')
6            // 导出表格数据
7        }
8    }
9}

用户选中联想项事件

注意:自动更新表格数据只需要打开autoFetch选项,不需要再次配置onSuggestItemSelect事件。

用户选中联想项事件的逻辑在@select事件中配置。在前述的引入中,我们在onSuggestItemSelect方法中配置。示例:

1methods: {
2    onSuggestItemSelect({ key, value }) {
3        console.log(`用户选中联想项:${value},组件为:${key}`)
4        // 实现用户选中联想项的其他逻辑
5    }
6}

用户输入内容变化事件

注意:自动更新表格数据只需要打开autoFetch选项,不需要再次配置onFieldChange事件。

用户输入内容变化事件的逻辑在引入组件时的@change事件配置。在前述的引入中,我们应该在onFieldChange方法中配置。示例:

1methods: {
2    onFieldChange({ key, value }) {
3        console.log(`用户输入内容变化为:${value},组件为:${key}`)
4        // 实现用户输入内容变化的其他逻辑
5    }
6}  

用户点击表格特定行事件

用户点击表格特定行事件的逻辑在@row-click事件中配置。在前述的引入中,我们在onRowClick方法中配置。示例:

1methods: {
2    onRowClick(row) {
3        alert(`用户点击表格,记录:${JSON.stringify(row)}`)
4        // 实现用户点击表格特定行的其他逻辑
5    }
6}