- 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
中定义为实际值即可。各字段含义如下。
功能字段
fields
:Array
。定义搜索面板中的各个搜索项。每个搜索项可以是普通输入框、联想输入框、日期选择框、单选按钮组、单选下拉列表等。通过配置fields
,可以灵活地定制搜索面板的内容和样式。actions
:Array
。定义搜索面板中的操作按钮。可以配置按钮的显示文本、点击事件等。可以用于提供手动搜索、导出结果等功能。columns
:Array
。定义表格的列配置。每个列配置项包括列的标识、显示标签、样式等。用于展示搜索结果的表格结构。fetchTableFn
:Function
。用于获取表格数据的函数。推荐值为一个异步函数,接收搜索条件、当前页码、每页条数等参数,并返回表格数据和总条数。pageSize
:Number
。定义每页显示的条数。用于分页控制。showPagination
:Boolean
。是否显示分页控件。用于控制表格的分页显示。showTable
:Boolean
。是否显示表格。用于控制搜索结果的显示。autoFetch
:Boolean
。是否在组件加载时自动获取数据。通常用于初始化时自动加载数据。
事件字段
@select
:Function
。监听联想输入框的选项选择事件。用于处理用户选择联想项后的逻辑。@change
:Function
。监听搜索项值变化事件。用于处理用户输入或选择后的逻辑。@action
:Function
。用于处理搜索面板操作按钮的点击事件。@row-click
:Function
。用于处理表格行点击事件。
组件结构
总体配置
配置方式
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}
fetchTableData
:Function
。用于获取表格数据的函数。此函数名与引入时保持一致,推荐值为一个异步函数。- 形参:
searchValues
:Object
。搜索条件。currentPage
:Number
。当前页码。pageSize
:Number
。每页显示的条数。
- 返回值:
Promise<{ list: Array, total: number }>
。返回一个 Promise,resolve 的对象包含两个属性表格数据和总条数。
- 形参:
onPanelAction
:Function
。用于处理搜索面板操作按钮的点击事件。- 形参:
actionKey
:String
。点击的按钮的标识。
- 形参:
onSuggestItemSelect
:Function
。用于处理联想输入框的选项选择事件。- 形参:
{ key, value }
:Object
。选中的联想项的标识和值。
- 形参:
onFieldChange
:Function
。用于处理搜索项值变化事件。- 形参:
{ key, value }
:Object
。变化的搜索项的标识和值。
- 形参:
onRowClick
:Function
。用于处理表格行点击事件。- 形参:
row
:Object
。点击的行数据。
- 形参:
搜索项配置
搜索项包含常用的5类输入组件:普通输入框、联想输入框、日期选择框、单选按钮组、单选下拉列表。
普通输入框
普通输入框是最常用的输入组件,type
字段为text
。示例:
1{
2 type: "text", // 必填,字段类型:'text'
3 key: "name", // 必填,字段标识,也是发起请求时对应的参数名
4 label: "姓名", // 必填,字段标签文本
5 placeholder: "请输入姓名", // 可选,输入框的占位文本
6}
如果要增加自定义样式,可以增加itemClass
、titleClass
、titleStyle
、wrapperClass
字段。如前所述,这些字段适用于所有类型的搜索项和操作项,后面不再赘述。
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个字段:inputClass
和inputStyle
,用于自定义输入框的样式。
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获取联想项的函数。
-
形参:
val
:String
。输入框的值。
-
返回值:
Promise<Array>
。返回一个 Promise,resolve 的对象为联想项的数组。每个联想项可以是一个字符串或一个对象,支持以下两种格式:- 字符串数组:直接返回字符串数组,显示值和绑定值相同。
- 对象数组:返回对象数组,支持显示值和绑定值分离。对象格式为
{ 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
字段的每个对象中添加customClass
和customStyle
字段:
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}
- 形参:
searchValues
:Object
。搜索条件。currentPage
:Number
。当前页码。pageSize
:Number
。每页显示的条数。
- 返回值:
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}