1710 lines
38 KiB
Markdown
1710 lines
38 KiB
Markdown
# 全站搜索功能实现计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 为碳信网实现全站搜索功能,包括搜索页面、API接口、搜索建议、热门搜索、搜索历史
|
||
|
||
**Architecture:** 前端聚合在 txw-mhzc-web(搜索结果页),后端聚合在 txw-mhzc(SearchController + SearchService)。搜索聚合服务第一阶段只搜索资讯中心数据。
|
||
|
||
**实现策略:**
|
||
- 搜索历史:Phase 1 使用 localStorage(无用户认证),后续接入后端 API
|
||
- 热门搜索:Phase 1 返回硬编码数据,后续从数据库读取
|
||
|
||
**Tech Stack:** Vue 2 + TDesign + Vue Router (前端), Spring Boot + MyBatis Plus (后端)
|
||
|
||
---
|
||
|
||
## 文件结构
|
||
|
||
### 前端 (txw-mhzc-web)
|
||
|
||
```
|
||
txw-mhzc-web/src/pages/index/
|
||
├── views/
|
||
│ └── search/
|
||
│ └── index.vue # 搜索结果页 [新建]
|
||
├── components/
|
||
│ └── search/
|
||
│ ├── SearchArea.vue # 搜索区域 [新建]
|
||
│ ├── SearchSuggestion.vue # 搜索建议下拉 [新建]
|
||
│ ├── SearchHistoryBar.vue # 搜索历史横条 [新建]
|
||
│ ├── SearchCategoryTabs.vue # 分类Tab [新建]
|
||
│ ├── SearchResultList.vue # 结果列表 [新建]
|
||
│ └── SearchResultItem.vue # 结果项 [新建]
|
||
└── api/
|
||
└── search.js # 搜索接口 [新建]
|
||
```
|
||
|
||
### 后端 (txw-mhzc)
|
||
|
||
```
|
||
txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/
|
||
├── controller/
|
||
│ └── SearchController.java # 搜索接口 [新建]
|
||
├── service/
|
||
│ ├── SearchService.java # 搜索服务接口 [新建]
|
||
│ └── impl/
|
||
│ └── SearchServiceImpl.java # 搜索服务实现 [新建]
|
||
├── mapper/
|
||
│ └── SearchMapper.java # 搜索Mapper [新建]
|
||
└── pojo/
|
||
├── req/
|
||
│ └── SearchReqVO.java # 搜索请求 [新建]
|
||
└── vo/
|
||
├── SearchResultVO.java # 搜索结果 [新建]
|
||
└── HotSearchConfig.java # 热门搜索配置 [新建]
|
||
```
|
||
|
||
---
|
||
|
||
## 后端任务
|
||
|
||
### Task 1: 创建搜索请求 VO
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/req/SearchReqVO.java`
|
||
|
||
- [ ] **Step 1: 创建 SearchReqVO**
|
||
|
||
```java
|
||
package com.css.txw.mhzc.pojo.req;
|
||
|
||
import io.swagger.v3.oas.annotations.media.Schema;
|
||
import lombok.Data;
|
||
import org.hibernate.validator.constraints.Length;
|
||
import org.hibernate.validator.constraints.NotBlank;
|
||
|
||
@Data
|
||
@Schema(description = "搜索请求")
|
||
public class SearchReqVO {
|
||
|
||
@Schema(description = "关键词", required = true)
|
||
@NotBlank(message = "关键词不能为空")
|
||
@Length(max = 50, message = "关键词不能超过50个字符")
|
||
private String keyword;
|
||
|
||
@Schema(description = "分类类型: all, carbon_cert, service, news")
|
||
private String categoryType = "all";
|
||
|
||
@Schema(description = "页码")
|
||
private Integer page = 1;
|
||
|
||
@Schema(description = "每页条数")
|
||
private Integer pageSize = 10;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/req/SearchReqVO.java
|
||
git commit -m "feat(search): 添加搜索请求VO"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: 创建搜索结果 VO
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/SearchResultVO.java`
|
||
|
||
- [ ] **Step 1: 创建 SearchResultVO**
|
||
|
||
```java
|
||
package com.css.txw.mhzc.pojo.vo;
|
||
|
||
import io.swagger.v3.oas.annotations.media.Schema;
|
||
import lombok.Data;
|
||
|
||
@Data
|
||
@Schema(description = "搜索结果")
|
||
public class SearchResultVO {
|
||
|
||
@Schema(description = "内容ID")
|
||
private String id;
|
||
|
||
@Schema(description = "标题(关键词高亮)")
|
||
private String title;
|
||
|
||
@Schema(description = "摘要")
|
||
private String summary;
|
||
|
||
@Schema(description = "缩略图URL")
|
||
private String thumbnail;
|
||
|
||
@Schema(description = "分类标签(展示用)")
|
||
private String category;
|
||
|
||
@Schema(description = "分类类型(筛选用)")
|
||
private String categoryType;
|
||
|
||
@Schema(description = "来源名称")
|
||
private String source;
|
||
|
||
@Schema(description = "来源类型")
|
||
private String sourceType;
|
||
|
||
@Schema(description = "发布时间")
|
||
private String publishTime;
|
||
|
||
@Schema(description = "跳转链接")
|
||
private String url;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/SearchResultVO.java
|
||
git commit -m "feat(search): 添加搜索结果VO"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: 创建搜索服务接口和实现
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/SearchService.java`
|
||
- Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/SearchServiceImpl.java`
|
||
|
||
- [ ] **Step 1: 创建 SearchService 接口**
|
||
|
||
```java
|
||
package com.css.txw.mhzc.service;
|
||
|
||
import com.css.txw.mhzc.pojo.req.SearchReqVO;
|
||
import com.css.txw.mhzc.pojo.vo.SearchResultVO;
|
||
import java.util.List;
|
||
|
||
public interface SearchService {
|
||
|
||
/**
|
||
* 搜索
|
||
*/
|
||
List<SearchResultVO> search(SearchReqVO reqVO);
|
||
|
||
/**
|
||
* 获取热门搜索词
|
||
*/
|
||
List<String> getHotSearchKeywords();
|
||
|
||
/**
|
||
* 获取搜索建议
|
||
*/
|
||
List<String> getSearchSuggestions(String keyword);
|
||
|
||
/**
|
||
* 获取搜索历史
|
||
*/
|
||
List<String> getSearchHistory();
|
||
|
||
/**
|
||
* 清除搜索历史
|
||
*/
|
||
void clearSearchHistory();
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 SearchServiceImpl 实现**
|
||
|
||
```java
|
||
package com.css.txw.mhzc.service.impl;
|
||
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||
import com.css.ggzc.framework.common.pojo.CommonResult;
|
||
import com.css.txw.mhzc.pojo.req.SearchReqVO;
|
||
import com.css.txw.mhzc.pojo.vo.SearchResultVO;
|
||
import com.css.txw.mhzc.pojo.vo.SyzxxxVO;
|
||
import com.css.txw.mhzc.service.SearchService;
|
||
import com.css.txw.mhzc.service.TxwMhzcZxxxbService;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.util.StringUtils;
|
||
|
||
import javax.annotation.Resource;
|
||
import java.util.ArrayList;
|
||
import java.util.Arrays;
|
||
import java.util.List;
|
||
import java.util.stream.Collectors;
|
||
|
||
@Service
|
||
@Slf4j
|
||
public class SearchServiceImpl implements SearchService {
|
||
|
||
@Resource
|
||
private TxwMhzcZxxxbService zxxxbService;
|
||
|
||
private static final String HIGHLIGHT_START = "<em>";
|
||
private static final String HIGHLIGHT_END = "</em>";
|
||
|
||
@Override
|
||
public List<SearchResultVO> search(SearchReqVO reqVO) {
|
||
// 调用资讯接口获取数据
|
||
CommonResult<java.util.Map<String, List<SyzxxxVO>>> result = zxxxbService.zxxx();
|
||
|
||
List<SearchResultVO> resultList = new ArrayList<>();
|
||
|
||
if (result != null && result.getData() != null) {
|
||
java.util.Map<String, List<SyzxxxVO>> data = result.getData();
|
||
|
||
// 从资讯数据中筛选
|
||
data.forEach((category, list) -> {
|
||
list.forEach(zxxx -> {
|
||
// 简单匹配逻辑,实际应更复杂
|
||
if (matchKeyword(zxxx.getTitle(), reqVO.getKeyword()) ||
|
||
matchKeyword(zxxx.getContent(), reqVO.getKeyword())) {
|
||
|
||
SearchResultVO vo = new SearchResultVO();
|
||
vo.setId(zxxx.getId());
|
||
vo.setTitle(highlightKeyword(zxxx.getTitle(), reqVO.getKeyword()));
|
||
vo.setSummary(highlightKeyword(truncate(zxxx.getContent(), 200), reqVO.getKeyword()));
|
||
vo.setCategory(category);
|
||
vo.setCategoryType(getCategoryType(category));
|
||
vo.setSource("碳信网");
|
||
vo.setSourceType("news");
|
||
vo.setPublishTime(zxxx.getPublishTime());
|
||
vo.setUrl("/mhzc/news/" + zxxx.getId());
|
||
|
||
resultList.add(vo);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 分页
|
||
int start = (reqVO.getPage() - 1) * reqVO.getPageSize();
|
||
int end = Math.min(start + reqVO.getPageSize(), resultList.size());
|
||
|
||
if (start >= resultList.size()) {
|
||
return new ArrayList<>();
|
||
}
|
||
|
||
return resultList.subList(start, end);
|
||
}
|
||
|
||
@Override
|
||
public List<String> getHotSearchKeywords() {
|
||
// TODO: 从数据库读取运营配置的热门搜索词
|
||
return Arrays.asList("江苏电厂配额", "林业碳汇开发", "CBAM 报告", "零碳展会");
|
||
}
|
||
|
||
@Override
|
||
public List<String> getSearchSuggestions(String keyword) {
|
||
if (!StringUtils.hasText(keyword)) {
|
||
return new ArrayList<>();
|
||
}
|
||
|
||
List<String> suggestions = new ArrayList<>();
|
||
String lowerKeyword = keyword.toLowerCase();
|
||
|
||
// TODO: 实际应从数据库查询匹配的建议词
|
||
// 模拟返回
|
||
suggestions.add(keyword);
|
||
|
||
return suggestions.stream()
|
||
.filter(s -> s.toLowerCase().contains(lowerKeyword))
|
||
.limit(10)
|
||
.collect(Collectors.toList());
|
||
}
|
||
|
||
private boolean matchKeyword(String text, String keyword) {
|
||
if (!StringUtils.hasText(text) || !StringUtils.hasText(keyword)) {
|
||
return false;
|
||
}
|
||
return text.contains(keyword);
|
||
}
|
||
|
||
private String highlightKeyword(String text, String keyword) {
|
||
if (!StringUtils.hasText(text) || !StringUtils.hasText(keyword)) {
|
||
return text;
|
||
}
|
||
return text.replace(keyword, HIGHLIGHT_START + keyword + HIGHLIGHT_END);
|
||
}
|
||
|
||
private String truncate(String text, int maxLength) {
|
||
if (text == null || text.length() <= maxLength) {
|
||
return text;
|
||
}
|
||
return text.substring(0, maxLength) + "...";
|
||
}
|
||
|
||
private String getCategoryType(String category) {
|
||
if ("行业专题".equals(category)) {
|
||
return "news";
|
||
} else if ("碳证中心".equals(category)) {
|
||
return "carbon_cert";
|
||
} else if ("服务中心".equals(category)) {
|
||
return "service";
|
||
}
|
||
return "all";
|
||
}
|
||
|
||
@Override
|
||
public List<String> getSearchHistory() {
|
||
// Phase 1: 返回空列表(前端使用localStorage)
|
||
// TODO: 用户认证后从数据库或Redis获取
|
||
return new ArrayList<>();
|
||
}
|
||
|
||
@Override
|
||
public void clearSearchHistory() {
|
||
// Phase 1: 前端localStorage已清除
|
||
// TODO: 用户认证后清除数据库或Redis中的数据
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/SearchService.java
|
||
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/SearchServiceImpl.java
|
||
git commit -m "feat(search): 添加搜索服务接口和实现"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: 创建搜索控制器
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/controller/SearchController.java`
|
||
|
||
- [ ] **Step 1: 创建 SearchController**
|
||
|
||
```java
|
||
package com.css.txw.mhzc.controller;
|
||
|
||
import com.css.ggzc.framework.common.pojo.CommonResult;
|
||
import com.css.txw.mhzc.pojo.req.SearchReqVO;
|
||
import com.css.txw.mhzc.pojo.vo.SearchResultVO;
|
||
import com.css.txw.mhzc.service.SearchService;
|
||
import io.swagger.v3.oas.annotations.Operation;
|
||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.validation.annotation.Validated;
|
||
import org.springframework.web.bind.annotation.*;
|
||
|
||
import javax.annotation.Resource;
|
||
import javax.validation.Valid;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
@RestController
|
||
@RequestMapping("/search")
|
||
@Tag(name = "搜索接口")
|
||
@Validated
|
||
@Slf4j
|
||
public class SearchController {
|
||
|
||
@Resource
|
||
private SearchService searchService;
|
||
|
||
@GetMapping
|
||
@Operation(summary = "搜索", description = "全站搜索")
|
||
public CommonResult<Map<String, Object>> search(@Valid SearchReqVO reqVO) {
|
||
List<SearchResultVO> list = searchService.search(reqVO);
|
||
|
||
// 构建返回结果
|
||
// 注意:实际应计算 categoryCount,这里简化处理
|
||
Map<String, Object> result = new java.util.HashMap<>();
|
||
result.put("total", list.size());
|
||
result.put("list", list);
|
||
|
||
Map<String, Integer> categoryCount = new java.util.HashMap<>();
|
||
categoryCount.put("all", list.size());
|
||
categoryCount.put("carbon_cert", 0);
|
||
categoryCount.put("service", 0);
|
||
categoryCount.put("news", list.size());
|
||
result.put("categoryCount", categoryCount);
|
||
|
||
return CommonResult.success(result);
|
||
}
|
||
|
||
@GetMapping("/hot")
|
||
@Operation(summary = "热门搜索", description = "获取热门搜索词")
|
||
public CommonResult<List<String>> getHotSearch() {
|
||
return CommonResult.success(searchService.getHotSearchKeywords());
|
||
}
|
||
|
||
@GetMapping("/suggest")
|
||
@Operation(summary = "搜索建议", description = "获取搜索建议")
|
||
public CommonResult<Map<String, List<String>>> getSuggest(@RequestParam String keyword) {
|
||
List<String> suggestions = searchService.getSearchSuggestions(keyword);
|
||
Map<String, List<String>> result = new java.util.HashMap<>();
|
||
result.put("suggestions", suggestions);
|
||
return CommonResult.success(result);
|
||
}
|
||
|
||
@GetMapping("/history")
|
||
@Operation(summary = "搜索历史", description = "获取用户搜索历史")
|
||
public CommonResult<List<String>> getSearchHistory() {
|
||
return CommonResult.success(searchService.getSearchHistory());
|
||
}
|
||
|
||
@DeleteMapping("/history")
|
||
@Operation(summary = "清除搜索历史", description = "清除用户搜索历史")
|
||
public CommonResult<Boolean> clearSearchHistory() {
|
||
searchService.clearSearchHistory();
|
||
return CommonResult.success(true);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/controller/SearchController.java
|
||
git commit -m "feat(search): 添加搜索控制器"
|
||
```
|
||
|
||
---
|
||
|
||
## 前端任务
|
||
|
||
### Task 5: 创建搜索 API
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/api/search.js`
|
||
|
||
- [ ] **Step 1: 创建 search.js API**
|
||
|
||
```javascript
|
||
import { fetchSso } from '@/core/request';
|
||
|
||
const basurl = '';
|
||
|
||
export default {
|
||
/**
|
||
* 搜索
|
||
* @param {Object} params - { keyword, categoryType, page, pageSize }
|
||
*/
|
||
search(params) {
|
||
return fetchSso({
|
||
url: `${basurl}/search`,
|
||
method: 'get',
|
||
loading: true,
|
||
params,
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 获取热门搜索词
|
||
*/
|
||
getHotSearch() {
|
||
return fetchSso({
|
||
url: `${basurl}/search/hot`,
|
||
method: 'get',
|
||
loading: false,
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 获取搜索建议
|
||
* @param {String} keyword - 关键词
|
||
*/
|
||
getSuggest(keyword) {
|
||
return fetchSso({
|
||
url: `${basurl}/search/suggest`,
|
||
method: 'get',
|
||
loading: false,
|
||
params: { keyword },
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 获取搜索历史
|
||
*/
|
||
getSearchHistory() {
|
||
return fetchSso({
|
||
url: `${basurl}/search/history`,
|
||
method: 'get',
|
||
loading: false,
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 清除搜索历史
|
||
*/
|
||
clearSearchHistory() {
|
||
return fetchSso({
|
||
url: `${basurl}/search/history`,
|
||
method: 'delete',
|
||
loading: false,
|
||
});
|
||
},
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/api/search.js
|
||
git commit -m "feat(search): 添加搜索API"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: 创建搜索区域组件
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/components/search/SearchArea.vue`
|
||
|
||
- [ ] **Step 1: 创建 SearchArea.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-area">
|
||
<!-- 搜索区域背景 -->
|
||
<div class="search-bg">
|
||
<div class="search-box">
|
||
<!-- 分类下拉 -->
|
||
<t-select
|
||
v-model="selectedCategory"
|
||
:options="categoryOptions"
|
||
placeholder="请选择分类"
|
||
@change="handleCategoryChange"
|
||
style="width: 120px"
|
||
/>
|
||
|
||
<!-- 搜索输入框 -->
|
||
<t-input
|
||
v-model="keyword"
|
||
placeholder="搜索关键词"
|
||
@enter="handleSearch"
|
||
@input="handleInput"
|
||
style="flex: 1"
|
||
>
|
||
<template #suffix-icon>
|
||
<t-icon name="search" />
|
||
</template>
|
||
</t-input>
|
||
|
||
<!-- 搜索按钮 -->
|
||
<t-button theme="primary" @click="handleSearch">搜索</t-button>
|
||
</div>
|
||
|
||
<!-- 热门搜索 -->
|
||
<div class="hot-search">
|
||
<span class="hot-label">热门搜索:</span>
|
||
<span
|
||
v-for="(item, index) in hotSearchList"
|
||
:key="index"
|
||
class="hot-item"
|
||
@click="handleHotClick(item)"
|
||
>
|
||
{{ item }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 搜索建议下拉 -->
|
||
<SearchSuggestion
|
||
v-if="showSuggestion"
|
||
:suggestions="suggestions"
|
||
@select="handleSuggestionSelect"
|
||
@close="showSuggestion = false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import SearchSuggestion from './SearchSuggestion.vue';
|
||
import searchApi from '@/pages/index/api/search.js';
|
||
|
||
export default {
|
||
name: 'SearchArea',
|
||
components: {
|
||
SearchSuggestion,
|
||
},
|
||
data() {
|
||
return {
|
||
keyword: '',
|
||
selectedCategory: 'all',
|
||
categoryOptions: [
|
||
{ label: '全部', value: 'all' },
|
||
{ label: '碳证中心', value: 'carbon_cert' },
|
||
{ label: '服务中心', value: 'service' },
|
||
{ label: '行业专题', value: 'news' },
|
||
],
|
||
hotSearchList: [],
|
||
suggestions: [],
|
||
showSuggestion: false,
|
||
inputTimer: null,
|
||
};
|
||
},
|
||
mounted() {
|
||
this.loadHotSearch();
|
||
},
|
||
methods: {
|
||
handleSearch() {
|
||
if (!this.keyword.trim()) {
|
||
this.$message.warning('请输入搜索关键词');
|
||
return;
|
||
}
|
||
this.showSuggestion = false;
|
||
this.$emit('search', {
|
||
keyword: this.keyword.trim(),
|
||
categoryType: this.selectedCategory,
|
||
page: 1,
|
||
});
|
||
},
|
||
handleInput(value) {
|
||
// 防抖处理
|
||
if (this.inputTimer) {
|
||
clearTimeout(this.inputTimer);
|
||
}
|
||
if (!value.trim()) {
|
||
this.showSuggestion = false;
|
||
return;
|
||
}
|
||
this.inputTimer = setTimeout(() => {
|
||
this.loadSuggestions(value);
|
||
}, 300);
|
||
},
|
||
handleCategoryChange(value) {
|
||
this.selectedCategory = value;
|
||
},
|
||
handleHotClick(keyword) {
|
||
this.keyword = keyword;
|
||
this.handleSearch();
|
||
},
|
||
handleSuggestionSelect(keyword) {
|
||
this.keyword = keyword;
|
||
this.showSuggestion = false;
|
||
this.handleSearch();
|
||
},
|
||
async loadHotSearch() {
|
||
try {
|
||
const res = await searchApi.getHotSearch();
|
||
if (res && res.data) {
|
||
this.hotSearchList = res.data;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载热门搜索失败', error);
|
||
}
|
||
},
|
||
async loadSuggestions(keyword) {
|
||
try {
|
||
const res = await searchApi.getSuggest(keyword);
|
||
if (res && res.data && res.data.suggestions) {
|
||
this.suggestions = res.data.suggestions;
|
||
this.showSuggestion = this.suggestions.length > 0;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载搜索建议失败', error);
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-area {
|
||
position: relative;
|
||
}
|
||
|
||
.search-bg {
|
||
background-color: #f5f5f5;
|
||
padding: 24px 0;
|
||
}
|
||
|
||
.search-box {
|
||
display: flex;
|
||
gap: 12px;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.hot-search {
|
||
max-width: 800px;
|
||
margin: 12px auto 0;
|
||
font-size: 14px;
|
||
color: #999;
|
||
|
||
.hot-label {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.hot-item {
|
||
margin-right: 16px;
|
||
cursor: pointer;
|
||
color: #666;
|
||
|
||
&:hover {
|
||
color: #00b42a;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/components/search/SearchArea.vue
|
||
git commit -m "feat(search): 添加搜索区域组件"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: 创建搜索建议组件
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/components/search/SearchSuggestion.vue`
|
||
|
||
- [ ] **Step 1: 创建 SearchSuggestion.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-suggestion">
|
||
<div
|
||
v-for="(item, index) in suggestions"
|
||
:key="index"
|
||
class="suggestion-item"
|
||
@click="handleSelect(item)"
|
||
>
|
||
<span v-html="item"></span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'SearchSuggestion',
|
||
props: {
|
||
suggestions: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
},
|
||
methods: {
|
||
handleSelect(keyword) {
|
||
// 去掉高亮标签
|
||
const plainKeyword = keyword.replace(/<em>|<\/em>/g, '');
|
||
this.$emit('select', plainKeyword);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-suggestion {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
background: #fff;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 4px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
z-index: 100;
|
||
|
||
.suggestion-item {
|
||
padding: 10px 16px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
|
||
&:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
:deep(em) {
|
||
color: #00b42a;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/components/search/SearchSuggestion.vue
|
||
git commit -m "feat(search): 添加搜索建议组件"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: 创建搜索历史横条组件
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/components/search/SearchHistoryBar.vue`
|
||
|
||
- [ ] **Step 1: 创建 SearchHistoryBar.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-history-bar" v-if="historyList.length > 0">
|
||
<span class="history-label">搜索历史</span>
|
||
<span
|
||
v-for="(item, index) in historyList"
|
||
:key="index"
|
||
class="history-item"
|
||
@click="handleClick(item)"
|
||
>
|
||
{{ item }}
|
||
</span>
|
||
<span class="clear-btn" @click="handleClear">清除全部</span>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'SearchHistoryBar',
|
||
props: {
|
||
historyList: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
},
|
||
methods: {
|
||
handleClick(keyword) {
|
||
this.$emit('select', keyword);
|
||
},
|
||
handleClear() {
|
||
this.$emit('clear');
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-history-bar {
|
||
max-width: 1200px;
|
||
margin: 16px auto;
|
||
font-size: 14px;
|
||
color: #666;
|
||
|
||
.history-label {
|
||
margin-right: 16px;
|
||
color: #999;
|
||
}
|
||
|
||
.history-item {
|
||
margin-right: 16px;
|
||
cursor: pointer;
|
||
color: #072ca6;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.clear-btn {
|
||
float: right;
|
||
cursor: pointer;
|
||
color: #999;
|
||
|
||
&:hover {
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/components/search/SearchHistoryBar.vue
|
||
git commit -m "feat(search): 添加搜索历史横条组件"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: 创建分类 Tab 组件
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/components/search/SearchCategoryTabs.vue`
|
||
|
||
- [ ] **Step 1: 创建 SearchCategoryTabs.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-category-tabs">
|
||
<div class="tabs-container">
|
||
<span
|
||
v-for="tab in tabs"
|
||
:key="tab.value"
|
||
:class="['tab-item', { active: currentTab === tab.value }]"
|
||
@click="handleTabClick(tab.value)"
|
||
>
|
||
{{ tab.label }}
|
||
<span class="count" v-if="categoryCount">{{ categoryCount[tab.value] || 0 }}</span>
|
||
</span>
|
||
<div class="tab-indicator" :style="indicatorStyle"></div>
|
||
</div>
|
||
<div class="divider"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'SearchCategoryTabs',
|
||
props: {
|
||
currentTab: {
|
||
type: String,
|
||
default: 'all',
|
||
},
|
||
categoryCount: {
|
||
type: Object,
|
||
default: () => ({}),
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
tabs: [
|
||
{ label: '全部', value: 'all' },
|
||
{ label: '碳证中心', value: 'carbon_cert' },
|
||
{ label: '服务中心', value: 'service' },
|
||
{ label: '行业专题', value: 'news' },
|
||
],
|
||
indicatorStyle: {},
|
||
};
|
||
},
|
||
watch: {
|
||
currentTab: {
|
||
immediate: true,
|
||
handler() {
|
||
this.updateIndicator();
|
||
},
|
||
},
|
||
},
|
||
mounted() {
|
||
this.updateIndicator();
|
||
},
|
||
methods: {
|
||
handleTabClick(value) {
|
||
this.$emit('change', value);
|
||
},
|
||
updateIndicator() {
|
||
const index = this.tabs.findIndex((t) => t.value === this.currentTab);
|
||
if (index !== -1) {
|
||
this.indicatorStyle = {
|
||
transform: `translateX(${index * 120}px)`,
|
||
width: '72px',
|
||
};
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-category-tabs {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.tabs-container {
|
||
display: flex;
|
||
position: relative;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.tab-item {
|
||
width: 120px;
|
||
padding: 8px 0;
|
||
text-align: center;
|
||
font-size: 16px;
|
||
color: #666;
|
||
cursor: pointer;
|
||
position: relative;
|
||
|
||
&.active {
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.count {
|
||
margin-left: 4px;
|
||
font-size: 14px;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.tab-indicator {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
height: 3px;
|
||
background-color: #00b42a;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.divider {
|
||
height: 1px;
|
||
background-color: #e5e5e5;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/components/search/SearchCategoryTabs.vue
|
||
git commit -m "feat(search): 添加分类Tab组件"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: 创建搜索结果项组件
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/components/search/SearchResultItem.vue`
|
||
|
||
- [ ] **Step 1: 创建 SearchResultItem.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-result-item" @click="handleClick">
|
||
<!-- 分类标签 -->
|
||
<span class="category-tag" :style="categoryStyle">
|
||
{{ result.category }}
|
||
</span>
|
||
|
||
<!-- 标题 -->
|
||
<h3 class="title" v-html="result.title"></h3>
|
||
|
||
<!-- 日期 -->
|
||
<div class="meta">
|
||
<span class="date">{{ result.publishTime }}</span>
|
||
<span class="source" v-if="result.source">{{ result.source }}</span>
|
||
</div>
|
||
|
||
<!-- 摘要 -->
|
||
<p class="summary" v-html="result.summary"></p>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'SearchResultItem',
|
||
props: {
|
||
result: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
},
|
||
computed: {
|
||
categoryStyle() {
|
||
if (this.result.categoryType === 'news') {
|
||
return { backgroundColor: '#e8f3ff', color: '#072ca6' };
|
||
} else if (this.result.categoryType === 'carbon_cert') {
|
||
return { backgroundColor: '#fff7e6', color: '#d25f00' };
|
||
} else if (this.result.categoryType === 'service') {
|
||
return { backgroundColor: '#e6fff0', color: '#00b42a' };
|
||
}
|
||
return { backgroundColor: '#f5f5f5', color: '#666' };
|
||
},
|
||
},
|
||
methods: {
|
||
handleClick() {
|
||
if (this.result.url) {
|
||
window.location.href = this.result.url;
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-result-item {
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
cursor: pointer;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&:hover {
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.category-tag {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 2px;
|
||
font-size: 14px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.title {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin: 0 0 8px 0;
|
||
line-height: 1.5;
|
||
|
||
:deep(em) {
|
||
color: #00b42a;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.meta {
|
||
font-size: 14px;
|
||
color: #999;
|
||
margin-bottom: 8px;
|
||
|
||
.date {
|
||
margin-right: 16px;
|
||
}
|
||
|
||
.source {
|
||
&::before {
|
||
content: '来源:';
|
||
}
|
||
}
|
||
}
|
||
|
||
.summary {
|
||
font-size: 14px;
|
||
color: #666;
|
||
line-height: 1.8;
|
||
margin: 0;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 3;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
|
||
:deep(em) {
|
||
color: #00b42a;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/components/search/SearchResultItem.vue
|
||
git commit -m "feat(search): 添加搜索结果项组件"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 11: 创建搜索结果列表组件
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/components/search/SearchResultList.vue`
|
||
|
||
- [ ] **Step 1: 创建 SearchResultList.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-result-list">
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading">
|
||
<t-loading size="large" text="加载中..." />
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<SearchEmpty v-else-if="list.length === 0" :keyword="keyword" />
|
||
|
||
<!-- 结果列表 -->
|
||
<div v-else class="result-container">
|
||
<SearchResultItem
|
||
v-for="item in list"
|
||
:key="item.id"
|
||
:result="item"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="list.length > 0 && total > pageSize" class="pagination">
|
||
<t-pagination
|
||
v-model="currentPage"
|
||
:total="total"
|
||
:page-size="pageSize"
|
||
@change="handlePageChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import SearchResultItem from './SearchResultItem.vue';
|
||
import SearchEmpty from './SearchEmpty.vue';
|
||
|
||
export default {
|
||
name: 'SearchResultList',
|
||
components: {
|
||
SearchResultItem,
|
||
SearchEmpty,
|
||
},
|
||
props: {
|
||
list: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
total: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
pageSize: {
|
||
type: Number,
|
||
default: 10,
|
||
},
|
||
loading: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
keyword: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
currentPage: 1,
|
||
};
|
||
},
|
||
watch: {
|
||
currentPage: {
|
||
handler(val) {
|
||
this.currentPage = val;
|
||
},
|
||
},
|
||
},
|
||
methods: {
|
||
handlePageChange(pageInfo) {
|
||
this.$emit('page-change', pageInfo.current);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-result-list {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 0 16px;
|
||
|
||
.loading {
|
||
padding: 60px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.result-container {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.pagination {
|
||
padding: 24px 0;
|
||
text-align: center;
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/components/search/SearchResultList.vue
|
||
git commit -m "feat(search): 添加搜索结果列表组件"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 12: 创建空状态组件
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/components/search/SearchEmpty.vue`
|
||
|
||
- [ ] **Step 1: 创建 SearchEmpty.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-empty">
|
||
<div class="empty-icon">
|
||
<t-icon name="search" size="64px" />
|
||
</div>
|
||
<p class="empty-text">
|
||
未找到与 "<span class="keyword">{{ keyword }}</span>" 相关的搜索结果
|
||
</p>
|
||
<p class="empty-hint">请尝试其他关键词或调整筛选条件</p>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'SearchEmpty',
|
||
props: {
|
||
keyword: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-empty {
|
||
padding: 80px 0;
|
||
text-align: center;
|
||
|
||
.empty-icon {
|
||
color: #ddd;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 16px;
|
||
color: #666;
|
||
margin-bottom: 8px;
|
||
|
||
.keyword {
|
||
color: #00b42a;
|
||
}
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 14px;
|
||
color: #999;
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/components/search/SearchEmpty.vue
|
||
git commit -m "feat(search): 添加空状态组件"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 13: 创建搜索结果页
|
||
|
||
**Files:**
|
||
- Create: `txw-mhzc-web/src/pages/index/views/search/index.vue`
|
||
|
||
- [ ] **Step 1: 创建搜索结果页 index.vue**
|
||
|
||
```vue
|
||
<template>
|
||
<div class="search-page">
|
||
<!-- 顶部导航 (复用现有组件) -->
|
||
<Nav :isyhxx="false" />
|
||
|
||
<!-- 搜索区域 -->
|
||
<SearchArea @search="handleSearch" />
|
||
|
||
<!-- 搜索历史 -->
|
||
<SearchHistoryBar
|
||
:historyList="searchHistory"
|
||
@select="handleHistorySelect"
|
||
@clear="handleClearHistory"
|
||
/>
|
||
|
||
<!-- 分类 Tab -->
|
||
<SearchCategoryTabs
|
||
:currentTab="currentCategory"
|
||
:categoryCount="categoryCount"
|
||
@change="handleCategoryChange"
|
||
/>
|
||
|
||
<!-- 搜索结果列表 -->
|
||
<SearchResultList
|
||
:list="resultList"
|
||
:total="total"
|
||
:pageSize="pageSize"
|
||
:loading="loading"
|
||
:keyword="keyword"
|
||
@page-change="handlePageChange"
|
||
/>
|
||
|
||
<!-- 底部版权 -->
|
||
<div class="footer">
|
||
<p>版权信息等</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import Nav from '@/pages/index/components/nav/index.vue';
|
||
import SearchArea from '@/pages/index/components/search/SearchArea.vue';
|
||
import SearchHistoryBar from '@/pages/index/components/search/SearchHistoryBar.vue';
|
||
import SearchCategoryTabs from '@/pages/index/components/search/SearchCategoryTabs.vue';
|
||
import SearchResultList from '@/pages/index/components/search/SearchResultList.vue';
|
||
import searchApi from '@/pages/index/api/search.js';
|
||
|
||
export default {
|
||
name: 'SearchPage',
|
||
components: {
|
||
Nav,
|
||
SearchArea,
|
||
SearchHistoryBar,
|
||
SearchCategoryTabs,
|
||
SearchResultList,
|
||
},
|
||
data() {
|
||
return {
|
||
keyword: '',
|
||
currentCategory: 'all',
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
resultList: [],
|
||
total: 0,
|
||
categoryCount: {},
|
||
loading: false,
|
||
searchHistory: [],
|
||
};
|
||
},
|
||
mounted() {
|
||
// 从 URL 获取搜索参数
|
||
this.keyword = this.$route.query.keyword || '';
|
||
this.currentCategory = this.$route.query.category || 'all';
|
||
|
||
// 加载搜索历史
|
||
this.loadSearchHistory();
|
||
|
||
// 如果有搜索关键词,执行搜索
|
||
if (this.keyword) {
|
||
this.doSearch();
|
||
}
|
||
},
|
||
methods: {
|
||
handleSearch(params) {
|
||
this.keyword = params.keyword;
|
||
this.currentCategory = params.categoryType;
|
||
this.currentPage = 1;
|
||
|
||
// 更新 URL
|
||
this.$router.push({
|
||
query: {
|
||
keyword: this.keyword,
|
||
category: this.currentCategory,
|
||
},
|
||
});
|
||
|
||
// 保存搜索历史
|
||
this.saveSearchHistory(this.keyword);
|
||
|
||
// 执行搜索
|
||
this.doSearch();
|
||
},
|
||
handleHistorySelect(keyword) {
|
||
this.keyword = keyword;
|
||
this.doSearch();
|
||
},
|
||
handleClearHistory() {
|
||
this.searchHistory = [];
|
||
searchApi.clearSearchHistory();
|
||
},
|
||
handleCategoryChange(category) {
|
||
this.currentCategory = category;
|
||
this.currentPage = 1;
|
||
|
||
// 更新 URL
|
||
this.$router.push({
|
||
query: {
|
||
keyword: this.keyword,
|
||
category: this.currentCategory,
|
||
},
|
||
});
|
||
|
||
// 重新搜索
|
||
this.doSearch();
|
||
},
|
||
handlePageChange(page) {
|
||
this.currentPage = page;
|
||
this.doSearch();
|
||
// 滚动到顶部
|
||
window.scrollTo(0, 0);
|
||
},
|
||
async doSearch() {
|
||
this.loading = true;
|
||
try {
|
||
const res = await searchApi.search({
|
||
keyword: this.keyword,
|
||
categoryType: this.currentCategory,
|
||
page: this.currentPage,
|
||
pageSize: this.pageSize,
|
||
});
|
||
|
||
if (res && res.data) {
|
||
this.resultList = res.data.list || [];
|
||
this.total = res.data.total || 0;
|
||
this.categoryCount = res.data.categoryCount || {};
|
||
}
|
||
} catch (error) {
|
||
console.error('搜索失败', error);
|
||
this.resultList = [];
|
||
this.total = 0;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
loadSearchHistory() {
|
||
// 从 localStorage 读取
|
||
try {
|
||
const history = localStorage.getItem('searchHistory');
|
||
if (history) {
|
||
this.searchHistory = JSON.parse(history);
|
||
}
|
||
} catch (e) {
|
||
console.error('读取搜索历史失败', e);
|
||
}
|
||
},
|
||
saveSearchHistory(keyword) {
|
||
if (!keyword) return;
|
||
|
||
// 去除重复
|
||
this.searchHistory = this.searchHistory.filter((k) => k !== keyword);
|
||
|
||
// 添加到开头
|
||
this.searchHistory.unshift(keyword);
|
||
|
||
// 只保留10条
|
||
if (this.searchHistory.length > 10) {
|
||
this.searchHistory = this.searchHistory.slice(0, 10);
|
||
}
|
||
|
||
// 保存到 localStorage
|
||
try {
|
||
localStorage.setItem('searchHistory', JSON.stringify(this.searchHistory));
|
||
} catch (e) {
|
||
console.error('保存搜索历史失败', e);
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-page {
|
||
min-height: 100vh;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.footer {
|
||
background-color: #f5f5f5;
|
||
padding: 24px 0;
|
||
text-align: center;
|
||
margin-top: 40px;
|
||
|
||
p {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: #999;
|
||
}
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/views/search/index.vue
|
||
git commit -m "feat(search): 添加搜索结果页"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 14: 添加搜索路由
|
||
|
||
**Files:**
|
||
- Modify: `txw-mhzc-web/src/pages/index/router/routes.js`
|
||
|
||
- [ ] **Step 1: 添加搜索路由**
|
||
|
||
在 routes.js 中添加:
|
||
|
||
```javascript
|
||
// 搜索
|
||
function search() {
|
||
return import(/* webpackChunkName: "search" */ '@/pages/index/views/search/index.vue');
|
||
}
|
||
```
|
||
|
||
在 export default 的路由数组中添加:
|
||
|
||
```javascript
|
||
{
|
||
name: 'search',
|
||
path: '/search',
|
||
component: search,
|
||
meta: {
|
||
title: '搜索结果',
|
||
isShowSideBar: false,
|
||
hasHome: true,
|
||
breadCrumbs: [{ title: '首页', to: '/home' }, { title: '搜索结果', to: '/search' }],
|
||
disableBack: true,
|
||
},
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 2: 提交代码**
|
||
|
||
```bash
|
||
git add txw-mhzc-web/src/pages/index/router/routes.js
|
||
git commit -m "feat(search): 添加搜索路由"
|
||
```
|
||
|
||
---
|
||
|
||
## 联调与测试
|
||
|
||
### Task 15: 前后端联调
|
||
|
||
- [ ] **Step 1: 验证后端接口**
|
||
|
||
启动后端服务,访问 Swagger 文档:
|
||
```
|
||
http://localhost:xxxx/swagger-ui.html
|
||
```
|
||
|
||
测试接口:
|
||
- `GET /search?keyword=碳排放&categoryType=all&page=1&pageSize=10`
|
||
- `GET /search/hot`
|
||
- `GET /search/suggest?keyword=碳`
|
||
|
||
- [ ] **Step 2: 验证前端页面**
|
||
|
||
启动前端项目:
|
||
```bash
|
||
cd txw-mhzc-web
|
||
yarn dev
|
||
```
|
||
|
||
访问搜索页面:
|
||
```
|
||
http://localhost:8080/#/search?keyword=碳排放
|
||
```
|
||
|
||
测试功能:
|
||
1. 搜索框输入关键词,点击搜索
|
||
2. 点击热门搜索词
|
||
3. 分类 Tab 切换
|
||
4. 分页功能
|
||
5. 搜索历史展示和清除
|
||
|
||
- [ ] **Step 3: 提交联调代码**
|
||
|
||
```bash
|
||
git add .
|
||
git commit -m "fix(search): 联调修复"
|
||
```
|
||
|
||
---
|
||
|
||
## 完成标准
|
||
|
||
- [ ] 所有组件正常渲染
|
||
- [ ] 搜索功能返回正确结果
|
||
- [ ] 分类筛选正常工作
|
||
- [ ] 分页功能正常
|
||
- [ ] 搜索历史本地存储正常
|
||
- [ ] 热门搜索词展示正常
|
||
- [ ] 移动端适配正常(可选)
|