blog/content/post/实习小记/SearchPanel组件文档.md
2025-02-17 20:37:55 +08:00

626 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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`)。
效果图:
<img src="https://emberauthor.oss-cn-beijing.aliyuncs.com/images/bed/202502141948850.png" width="500">
<img src="https://emberauthor.oss-cn-beijing.aliyuncs.com/images/bed/202502141948606.png" width="500">
# 引入组件
在需要使用SearchPanel的页面中引入组件
```html
<template>
<div class="record-page">
<!-- SearchPanel -->
<SearchPanel
:fields="fields"
:actions="actions"
:columns="columns"
:fetchTableFn="fetchTableData"
:pageSize="page_size"
showPagination
showTable
autoFetch
@action="onPanelAction"
@select="onSuggestItemSelect"
@change="onFieldChange"
@row-click="onRowClick"
/>
</div>
</template>
<script>
import SearchPanel from '/path/to/SearchPanel.vue';
export default {
components: {
SearchPanel
}
}
</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()`中配置以下字段:
```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
<div class="search-area" :class="customAreaClass">
<template v-for="(field, idx) in fields" :key="idx">
<div
class="search-item"
:class="[field.itemClass]"
>
<!-- ... -->
</div>
</template>
</div>
```
`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 }>`。返回一个 Promiseresolve 的对象包含两个属性表格数据和总条数。
- `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<Array>`。返回一个 Promiseresolve 的对象为联想项的数组。每个联想项可以是一个字符串或一个对象,支持以下两种格式:
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": "张三 <zhangsan@example.com>", "value": "zhangsan" },
{ "label": "李四 <lisi@example.com>", "value": "lisi" },
{ "label": "王五 <wangwu@example.com>", "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 }>`。返回一个 Promiseresolve 的对象包含两个属性表格数据和总条数。
**注此逻辑假定后端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)}`)
// 实现用户点击表格特定行的其他逻辑
}
}
```