From 1ab7d0a308e055459c9c12f5253cf4e9e7524efa Mon Sep 17 00:00:00 2001 From: ember <1279347317@qq.com> Date: Fri, 14 Feb 2025 23:16:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/page/about/index.md | 2 + content/post/实习小记/SearchPanel组件文档.md | 579 +++++++++++++++++++ content/post/实习小记/下拉联想.md | 15 +- 3 files changed, 591 insertions(+), 5 deletions(-) create mode 100644 content/post/实习小记/SearchPanel组件文档.md diff --git a/content/page/about/index.md b/content/page/about/index.md index 98ed1a4..65305e5 100644 --- a/content/page/about/index.md +++ b/content/page/about/index.md @@ -104,6 +104,8 @@ links: - 🔄 基于**Protocol Buffers + node-gyp**设计分布式微服务协议,编写中间件的统一RPC接口,并基于**Protobuf**的**oneof**特性,实现了中间件对CGI-BIN的**多态**调用。 +- 📂 为团队封装了**SearchPanel**等基于Vue的响应式高可扩展性组件,实现了多页面的组件复用与样式统一,并编写了详细的**文档**,方便团队其他成员使用。 + ### 🌍 **NEU 小站 | 校园综合服务网站** ([🌐东北大学.com](https://东北大学.com)) - **独立开发** 🎯,提供课程评分📊、资源共享📂、攻略分享📖等功能。 - 基于 **Hugo(Go 语言)** 的模板引擎开发前端整体框架,实现 **高效静态页面渲染** ⚡ diff --git a/content/post/实习小记/SearchPanel组件文档.md b/content/post/实习小记/SearchPanel组件文档.md new file mode 100644 index 0000000..b8f21fd --- /dev/null +++ b/content/post/实习小记/SearchPanel组件文档.md @@ -0,0 +1,579 @@ +--- +title: 【SearchPanel组件】详细配置文档 +description: SearchPanel是一个基于Vue的搜索组件,包含各类搜索项和搜索结果表格,支持响应式数据更新 +date: 2025-02-14T17:44:00+08:00 +slug: SearchPanel组件文档 +categories: + - 实习小记 +tags: [ + "Vue", + "前端", + "JavaScript" +] +lastmod: 2025-02-14T23:13:00+08:00 +links: + - title: 【SearchPanel组件】下拉联想结果的全局监听click事件技术实现 + description: 写SearchPanel组件时实现下拉联想结果点击外部自动关闭,同时用了全局click事件监听来代替常用的onBlurSuggest逻辑 + website: /post/实习小记/下拉联想/ +--- + +# 组件说明 + +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 的对象为联想项的数组。 +假设有一个接口`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 [] + } +} +``` +### 日期选择框 +日期选择框支持选择日期,`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`,则可以在按钮点击事件中手动更新表格数据。 +## 按钮点击事件 +按钮点击事件在`onPanelAction`方法中配置。示例: +```js +methods: { + onPanelAction(actionKey) { + // 实现按钮点击事件的逻辑 + if (actionKey === 'export') { + alert('导出按钮被点击了!') + // 导出表格数据 + } + } +} +``` +## 用户选中联想项事件 + +**注意:自动更新表格数据只需要打开`autoFetch`选项,不需要再次配置`onSuggestItemSelect`事件。** + +用户选中联想项事件的逻辑在`onSuggestItemSelect`方法中配置。示例: +```js +methods: { + onSuggestItemSelect({ key, value }) { + console.log(`用户选中联想项:${value},组件为:${key}`) + // 实现用户选中联想项的其他逻辑 + } +} +``` +## 用户输入内容变化事件 + +**注意:自动更新表格数据只需要打开`autoFetch`选项,不需要再次配置`onFieldChange`事件。** + +用户输入内容变化事件的逻辑在`onFieldChange`方法中配置。示例: +```js +methods: { + onFieldChange({ key, value }) { + console.log(`用户输入内容变化为:${value},组件为:${key}`) + // 实现用户输入内容变化的其他逻辑 + } +} +``` +## 用户点击表格特定行事件 +用户点击表格特定行事件的逻辑在`onRowClick`方法中配置。示例: +```js +methods: { + onRowClick(row) { + alert(`用户点击表格,记录:${JSON.stringify(row)}`) + // 实现用户点击表格特定行的其他逻辑 + } +} +``` + diff --git a/content/post/实习小记/下拉联想.md b/content/post/实习小记/下拉联想.md index 70f9ef0..e67ec4c 100644 --- a/content/post/实习小记/下拉联想.md +++ b/content/post/实习小记/下拉联想.md @@ -1,6 +1,6 @@ --- -title: 下拉联想结果的全局监听click事件技术实现 -description: 写Vue组件时实现下拉联想结果点击外部自动关闭,同时用了全局click事件监听来代替常用的onBlurSuggest逻辑 +title: 【SearchPanel组件】下拉联想结果的全局监听click事件技术实现 +description: 写SearchPanel组件时实现下拉联想结果点击外部自动关闭,同时用了全局click事件监听来代替常用的onBlurSuggest逻辑 date: 2025-02-14T01:31:00+08:00 slug: 下拉联想 categories: @@ -8,9 +8,14 @@ categories: - 奇技淫巧 tags: [ "Vue", - "前端" + "前端", + "JavaScript" ] lastmod: 2025-02-14T02:58:00+08:00 +links: + - title: 【SearchPanel组件】详细配置文档 + description: SearchPanel是一个基于Vue的搜索组件,包含各类搜索项和搜索结果表格,支持响应式数据更新 + website: /post/实习小记/searchpanel组件文档/ --- 给公司写Vue组件时,需要实现一个下拉联想结果,这个下拉联想结果是根据用户输入内容实时调用API获取的,当用户输入内容时,下拉联想结果会自动弹出。自然地,如果用户点击了其他地方,下拉联想结果应该自动关闭,以保证用户体验。 @@ -121,7 +126,7 @@ beforeDestroy() { 另外,页面中有多个联想输入框时,使用全局`click`事件就能统一管理所有输入框的失焦逻辑,避免为每个输入框单独绑定`blur`事件可能带来的冗余代码,或者一些其他的不一致状态。 -又熬到3点了,看来这辈子不可能早睡了。。。附组件的HTML和JS部分完整代码: +又熬到3点了,看来这辈子不可能早睡了。。。附组件的HTML和JS部分完整代码:(此组件已修改,下面为修改前的代码,非最终版本) ```html