--- title: 【SearchPanel组件】详细配置文档 description: SearchPanel是一个基于Vue的搜索组件,包含各类搜索项和搜索结果表格,支持响应式数据更新 date: 2025-02-14T17:44:00+08:00 slug: SearchPanel组件文档 categories: - 实习小记 tags: [ "Vue", "前端", "JavaScript" ] lastmod: 2025-02-17T20:37:00+08:00 links: - title: 【SearchPanel组件】下拉联想结果的全局监听click事件技术实现 description: 写SearchPanel组件时实现下拉联想结果点击外部自动关闭,同时用了全局click事件监听来代替常用的onBlurSuggest逻辑 website: /post/实习小记/下拉联想/ --- > - **2025.02.17** 更新:联想输入框的`fetchSuggestFn`字段支持返回对象数组,支持显示值和绑定值分离。 > - **2025.02.14** 创建本文档。 # 组件说明 SearchPanel是我封装的一个基于Vue的搜索组件,包含各类搜索项和搜索结果表格,UI设计继承企业微信团队一致风格,支持响应式调用接口实现实时数据更新。具体来说,组件分为三个部分:搜索项(`search-items`)、操作项(`search-actions`)和搜索结果表格(`search-result`)。 效果图: # 引入组件 在需要使用SearchPanel的页面中引入组件: ```html ``` 引号内的字符串可以不做更改,在父组件的`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()`中配置以下字段: ```js data() { return { fields: [], // Object 数组,此字段名与引入时保持一致,每个对象代表一个搜索项 page_size: 10, // Number,此字段名与引入时保持一致,用于每页显示的条数 actions: [], // Object 数组,此字段名与引入时保持一致,每个对象代表一个操作按钮 columns: [], // Object 数组,此字段名与引入时保持一致,每个对象代表一个表格列 autoFetch: true, // Boolean,是否在组件加载时自动获取数据 showTable: true, // Boolean,是否显示表格 showPagination: true // Boolean,是否显示分页控件 } } ``` 各**搜索项**的相关参数在`data()`中的`fields`字段中**按顺序**配置,以普通输入框为例,配置如下: ```js export default { data() { return { fields: [ { type: "text", key: "name", label: "姓名", placeholder: "请输入姓名" } ] } } } ``` 各**操作项**的相关参数在`data()`中的`actions`字段中**按顺序**配置,示例配置如下: ```js export default { data() { return { actions: [ { key: "search", label: "搜索" } ] } } } ``` 所有类型的**搜索项**、**操作项**都支持以下通用配置: ```js { key: "string", // 必填,字段标识 label: "string", // 必填,字段标签文本 itemClass: "string", // 可选,整个搜索项的自定义类名 titleClass: "string", // 可选,标签文本的自定义类名 titleStyle: "object", // 可选,标签文本的内联样式 wrapperClass: "string" // 可选,输入区域的包装容器类名 } ``` 这里解释一下`itemClass`是什么。看这段组件源码你就明白了: ```html
``` `itemClass`是每个搜索项或操作项最外层的容器类名。 #### `methods`配置 以前述引入时的字段名为示例,在父组件的`methods`中配置以下方法: ```js methods: { async fetchTableData(searchValues, currentPage, pageSize) { // 实现获取表格数据的逻辑 }, onPanelAction(actionKey) { // 按钮被点击时的操作 }, onSuggestItemSelect({ key, value }) { // 用户选中了联想项的操作 }, onFieldChange({ key, value }) { // 字段值变化时的操作 }, onRowClick(row) { // 表格行被点击时的操作 } } ``` - `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`。示例: ```js { type: "text", // 必填,字段类型:'text' key: "name", // 必填,字段标识,也是发起请求时对应的参数名 label: "姓名", // 必填,字段标签文本 placeholder: "请输入姓名", // 可选,输入框的占位文本 } ``` 如果要增加自定义样式,可以增加`itemClass`、`titleClass`、`titleStyle`、`wrapperClass`字段。如前所述,这些字段**适用于所有类型的搜索项和操作项**,后面不再赘述。 ```js { type: "text", key: "name", label: "姓名", placeholder: "请输入姓名", itemClass: "name-input", // 可选,整个搜索项的自定义类名 titleClass: "name-label", // 可选,标签文本的自定义类名 titleStyle: { color: "#333", "margin-right": "10px" }, // 可选,标签文本的内联样式 wrapperClass: "name-wrapper" // 可选,输入区域的包装容器类名 } ``` 普通输入框另有2个字段:`inputClass`和`inputStyle`,用于自定义输入框的样式。 ```js { inputClass: "name-input-inner", // 可选,输入框的自定义类名 inputStyle: { color: "#333", "border-radius": "5px" } // 可选,输入框的内联样式 } ``` 当然,我们可以增加一个示例,假设接口返回的是新的格式,支持显示值和绑定值分离。 ### 联想输入框 联想输入框支持输入文字并在输入框下方显示联想项,`type`字段为`suggest`。示例: ```js { type: "suggest", // 必填,字段类型:'suggest' key: "senderData", // 必填,字段标识,也是发起请求时对应的参数名 label: "发送人", // 必填,字段标签文本 placeholder: "请输入发送人", // 可选,输入框的占位文本 inputClass: "sender-input", // 可选,输入框的自定义类名 inputStyle: { color: "#333", "border-radius": "5px" }, // 可选,输入框的内联样式 fetchSuggestFn: async (value) => { // 必填,用于获取联想项的函数。 } } ``` 其中`fetchSuggestFn`字段是用于从API获取联想项的函数。 - 形参: - `val`:`String`。输入框的值。 - 返回值:`Promise`。返回一个 Promise,resolve 的对象为联想项的数组。每个联想项可以是一个字符串或一个对象,支持以下两种格式: 1. **字符串数组**:直接返回字符串数组,显示值和绑定值相同。 2. **对象数组**:返回对象数组,支持显示值和绑定值分离。对象格式为 `{ label: string, value: any }`。 假设有一个接口`POST /api/sender`,请求体为`{ "searchTerm": "三" }`,返回`{ "results": ["张三", "李三", "王三"] }`,则可以这样实现: ```js fetchSuggestFn: async (val) => { try { const response = await fetch(`/api/sender`, { method: "POST", body: JSON.stringify({ searchTerm: val }) }) const data = await response.json() // 返回字符串数组 return data?.results || [] } catch (error) { console.error("获取联想项失败", error) return [] } } ``` 或者用`axios`实现: ```js fetchSuggestFn: async (val) => { try { const response = await axios.post('/api/sender', { searchTerm: val }) // 返回字符串数组 return response.data?.results || [] } catch (error) { console.error("获取联想项失败", error) return [] } } ``` 假设接口返回的是对象数组(每个对象是键值对的形式),则支持显示值和绑定值分离,例如: ```json { "results": [ { "label": "张三 ", "value": "zhangsan" }, { "label": "李四 ", "value": "lisi" }, { "label": "王五 ", "value": "wangwu" } ] } ``` 则可以如下实现: ```js fetchSuggestFn: async (val) => { try { const response = await axios.post('/api/sender', { searchTerm: val }) // 返回对象数组,支持显示值和绑定值分离 return response.data?.results || [] } catch (error) { console.error("获取联想项失败", error) return [] } } ``` ### 日期选择框 日期选择框支持选择日期,`type`字段为`date`。其返回值为一个字符串,格式为`YYYY年MM月DD日`。示例: ```js { type: "date", // 必填,字段类型:'date' key: "beginDate", // 必填,字段标识,也是发起请求时对应的参数名 label: "开始日期", // 必填,字段标签文本 inputClass: "date-input", // 可选,输入框的自定义类名 inputStyle: { color: "#333", "border-radius": "5px" }, // 可选,输入框的内联样式 dateConfig: { min: '2025年01月01日', // 可选,最小日期 max: '2025年12月31日' // 可选,最大日期 // 注:如果不设置min和max,则默认max是当前日期 defaultValue: '2025年02月01日' // 可选,默认日期 } } ``` 也可以动态获取相关值,比如设置`defaultValue`为当前日期: ```js dateConfig: { defaultValue: (() => { const date = new Date(); return `${date.getFullYear()}年${String(date.getMonth() + 1).padStart(2, '0')}月${String(date.getDate()).padStart(2, '0')}日`; })() } ``` ### 单选按钮组 单选按钮组支持单选按钮组,`type`字段为`radioGroup`。示例: ```js { type: "radioGroup", // 必填,字段类型:'radioGroup' key: "statusFilter", // 必填,字段标识,也是发起请求时对应的参数名 label: "操作状态", // 必填,字段标签文本 buttonClass: "status-button", // 可选,按钮的统一自定义类名 buttonStyle: { color: "#333", "border-radius": "5px" }, // 可选,按钮的统一内联样式 options: [ { label: "全部", value:'' }, { label: "成功", value:'true' }, { label: "失败", value:'false' }, ] // 必填,单选按钮组选项数组,label是显示文本,value是请求参数值 } ``` 单选按钮组支持为每个按钮增加自定义样式,在`options`字段的每个对象中添加`customClass`和`customStyle`字段: ```js { options: [ { label: "全部", value:'' }, { label: "成功", value:'true', customClass: "success-button", customStyle: { "margin-right": "10px" } }, { label: "失败", value:'false', customClass: "failed-button", customStyle: { "margin-right": "10px" } }, ] } ``` ### 单选下拉列表 单选下拉列表支持点击显示下拉列表,`type`字段为`select`。示例: ```js { type: "select", // 必填,字段类型:'select' key: "sortType", // 必填,字段标识,也是发起请求时对应的参数名 label: "排序方式", // 必填,字段标签文本 dropdownClass: "sort-dropdown", // 可选,下拉选项的统一自定义类名 dropdownStyle: { color: "#333", "border-radius": "5px" }, // 可选,下拉选项的统一内联样式 options: [ { label: "最近记录在前", value:'time_desc' }, { label: "最早记录在前", value:'time_asc' }, ] // 必填,单选下拉列表选项数组,label是显示文本,value是请求参数值 } ``` 单选下拉列表支持为每个选项增加自定义类,在`options`字段的每个对象中添加`customClass`字段: ```js { options: [ { label: "最近记录在前", value:'time_desc', customClass: "time-desc-option" }, { label: "最早记录在前", value:'time_asc', customClass: "time-asc-option" }, ] } ``` ## 操作项配置 操作项即为按钮组件,在`data()`中的`actions`字段中配置。示例: ```js { key: "export", // 必填,按钮点击事件时监测的参数 label: "导出记录", // 必填,按钮显示文本 btnClass: "export-btn" // 可选,按钮的自定义类名 } ``` ## 表格配置 表格的列在`data()`中的`columns`字段中配置。示例: ```js data() { return { columns: [ { key: "sender", label: "发件人", style: { "width": "180px" } }, { key: "subject", label: "主题", style: { "width": "200px" } }, { key: "timestamp", label: "发送时间", style: { "width": "120px" } }, { key: "status", label: "操作状态", style: { "width": "80px" } }, ] } } ``` `key`参数是从API接收到的数据中的字段名。`label`参数是表格列的显示文本。`style`参数是对应列的内联样式。 # 事件配置 事件逻辑在`methods`中配置。 ## 自动获取表格数据事件 获取表格数据在`fetchTableData`方法中配置。**注意:此函数名应为父组件引入时配置的`fetchTableFn`字段名。** ```js methods: { async fetchTableData(searchValues, currentPage, pageSize) { // 实现获取表格数据的逻辑 } } ``` - 形参: - `searchValues`:`Object`。搜索条件。 - `currentPage`:`Number`。当前页码。 - `pageSize`:`Number`。每页显示的条数。 - 返回值:`Promise<{ list: Array, total: number }>`。返回一个 Promise,resolve 的对象包含两个属性表格数据和总条数。 **注:此逻辑假定后端API接口支持分页,如果后端不支持分页,则需要自行实现分页逻辑。** 假设我们有一个接口`POST /api/email_list`,请求体示例如下: ```json { "senderData": "5", "themeData": "", "beginDate": "2025-02-06T16:00:00.000Z", "endDate": "2025-02-14T15:59:59.999Z", "statusFilter": "true", "sortType": "time_desc", "page": 1, // 当前页码 "limit": 8 // 每页显示的条数 } ``` 返回的结果如: ```json { "results": [ { "sender": "5@example.com", "subject": "Theme 5", "timestamp": "2025-02-13T10:27:36.430Z", "status": true }, { "sender": "15@example.com", "subject": "Theme 15", "timestamp": "2025-02-13T00:27:36.430Z", "status": true }, { "sender": "25@example.com", "subject": "Theme 25", "timestamp": "2025-02-12T14:27:36.430Z", "status": true }, { "sender": "50@example.com", "subject": "Theme 50", "timestamp": "2025-02-11T13:27:36.430Z", "status": true } ], "total": 4, "currentPage": 1, "totalPages": 1 } ``` 那么我们可以这样实现`fetchTableData`方法: ```js async fetchTableData(searchValues, currentPage, pageSize) { try { // 抽出外部字段 let { senderData, themeData, beginDate, endDate, statusFilter, sortType } = searchValues // 保证 beginDate<=endDate if (beginDate && endDate) { // 比较日期大小 const beginTime = new Date(beginDate.replace(/年|月|日/g, '')).getTime() const endTime = new Date(endDate.replace(/年|月|日/g, '')).getTime() if (endTime < beginTime) { // 交换开始和结束日期 const temp = beginDate beginDate = endDate endDate = temp } } // 转换成ISO日期字符串 const parseDate = (dateStr) => { if (!dateStr) return null; const [year, month, day] = dateStr.split(/年|月|日/); return new Date(Number(year), Number(month) - 1, Number(day)); } const beginDateISO = beginDate ? new Date(parseDate(beginDate).setHours(0,0,0,0)).toISOString() : '' const endDateISO = endDate ? new Date(parseDate(endDate).setHours(23,59,59,999)).toISOString() : '' const body = { senderData: senderData || '', themeData: themeData || '', beginDate: beginDateISO, endDate: endDateISO, statusFilter: statusFilter || '', sortType: sortType || 'time_desc', page: currentPage, limit: pageSize } // 发起请求 const resp = await axios.post('https://test.ember.ac.cn/api/email_list', body) // 返回结果 const results = resp.data?.results || [] const total = resp.data?.total || 0 return { list: results.map(item => { // item.status是boolean -> 转成 '成功' / '失败' // item.timestamp转成易读格式 const date = new Date(item.timestamp) 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')}` return { ...item, status: item.status === true ? '成功' : '失败', timestamp: formattedDate } }), total: total } } catch (err) { console.error(err) return { list: [], total: 0 } } } ``` 这样配置后,如果打开了`autoFetch`选项,则组件会自动监听搜索条件的变化,每次变化时都会自动调用`fetchTableData`方法来更新表格数据。如果没打开`autoFetch`,则可以在按钮点击事件中手动更新表格数据。 ## 按钮点击事件 按钮点击事件在`@action`事件中配置。在前述的引入中,我们在`onPanelAction`方法中配置。示例: ```js methods: { onPanelAction(actionKey) { // 实现按钮点击事件的逻辑 if (actionKey === 'export') { alert('导出按钮被点击了!') // 导出表格数据 } } } ``` ## 用户选中联想项事件 **注意:自动更新表格数据只需要打开`autoFetch`选项,不需要再次配置`onSuggestItemSelect`事件。** 用户选中联想项事件的逻辑在`@select`事件中配置。在前述的引入中,我们在`onSuggestItemSelect`方法中配置。示例: ```js methods: { onSuggestItemSelect({ key, value }) { console.log(`用户选中联想项:${value},组件为:${key}`) // 实现用户选中联想项的其他逻辑 } } ``` ## 用户输入内容变化事件 **注意:自动更新表格数据只需要打开`autoFetch`选项,不需要再次配置`onFieldChange`事件。** 用户输入内容变化事件的逻辑在引入组件时的`@change`事件配置。在前述的引入中,我们应该在`onFieldChange`方法中配置。示例: ```js methods: { onFieldChange({ key, value }) { console.log(`用户输入内容变化为:${value},组件为:${key}`) // 实现用户输入内容变化的其他逻辑 } } ``` ## 用户点击表格特定行事件 用户点击表格特定行事件的逻辑在`@row-click`事件中配置。在前述的引入中,我们在`onRowClick`方法中配置。示例: ```js methods: { onRowClick(row) { alert(`用户点击表格,记录:${JSON.stringify(row)}`) // 实现用户点击表格特定行的其他逻辑 } } ```