feat: 添加全站搜索功能和页面
This commit is contained in:
parent
3019ff983d
commit
cfb58db347
@ -244,26 +244,33 @@ public class SearchServiceImpl implements SearchService {
|
||||
CommonResult<java.util.Map<String, List<SyzxxxVO>>> result = zxxxbService.zxxx();
|
||||
|
||||
List<SearchResultVO> resultList = new ArrayList<>();
|
||||
String categoryType = reqVO.getCategoryType();
|
||||
|
||||
if (result != null && result.getData() != null) {
|
||||
java.util.Map<String, List<SyzxxxVO>> data = result.getData();
|
||||
|
||||
// 从资讯数据中筛选
|
||||
data.forEach((category, list) -> {
|
||||
String currentCategoryType = getCategoryType(category);
|
||||
// 如果选择了分类且不匹配,跳过
|
||||
if (!"all".equals(categoryType) && !categoryType.equals(currentCategoryType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.forEach(zxxx -> {
|
||||
// 简单匹配逻辑,实际应更复杂
|
||||
if (matchKeyword(zxxx.getTitle(), reqVO.getKeyword()) ||
|
||||
matchKeyword(zxxx.getContent(), reqVO.getKeyword())) {
|
||||
if (matchKeyword(zxxx.getBt1(), reqVO.getKeyword()) ||
|
||||
matchKeyword(zxxx.getZxNr(), 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.setTitle(highlightKeyword(zxxx.getBt1(), reqVO.getKeyword()));
|
||||
vo.setSummary(highlightKeyword(truncate(zxxx.getZxNr(), 200), reqVO.getKeyword()));
|
||||
vo.setCategory(category);
|
||||
vo.setCategoryType(getCategoryType(category));
|
||||
vo.setCategoryType(currentCategoryType);
|
||||
vo.setSource("碳信网");
|
||||
vo.setSourceType("news");
|
||||
vo.setPublishTime(zxxx.getPublishTime());
|
||||
vo.setPublishTime(zxxx.getFbsj());
|
||||
vo.setUrl("/mhzc/news/" + zxxx.getId());
|
||||
|
||||
resultList.add(vo);
|
||||
@ -1191,7 +1198,76 @@ git commit -m "feat(search): 添加搜索结果项组件"
|
||||
|
||||
---
|
||||
|
||||
### Task 11: 创建搜索结果列表组件
|
||||
### Task 11: 创建空状态组件
|
||||
|
||||
**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 12: 创建搜索结果列表组件
|
||||
|
||||
**Files:**
|
||||
- Create: `txw-mhzc-web/src/pages/index/components/search/SearchResultList.vue`
|
||||
@ -1314,76 +1390,7 @@ 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: 创建搜索结果页
|
||||
### Task 14: 创建搜索结果页
|
||||
|
||||
**Files:**
|
||||
- Create: `txw-mhzc-web/src/pages/index/views/search/index.vue`
|
||||
@ -1611,7 +1618,7 @@ git commit -m "feat(search): 添加搜索结果页"
|
||||
|
||||
---
|
||||
|
||||
### Task 14: 添加搜索路由
|
||||
### Task 15: 添加搜索路由
|
||||
|
||||
**Files:**
|
||||
- Modify: `txw-mhzc-web/src/pages/index/router/routes.js`
|
||||
@ -1655,7 +1662,7 @@ git commit -m "feat(search): 添加搜索路由"
|
||||
|
||||
## 联调与测试
|
||||
|
||||
### Task 15: 前后端联调
|
||||
### Task 16: 前后端联调
|
||||
|
||||
- [ ] **Step 1: 验证后端接口**
|
||||
|
||||
|
||||
319
docs/superpowers/plans/2026-04-22-gxzx-migration-plan.md
Normal file
319
docs/superpowers/plans/2026-04-22-gxzx-migration-plan.md
Normal file
@ -0,0 +1,319 @@
|
||||
# gxzx 服务迁移至 mhzc 方案
|
||||
|
||||
## 一、迁移背景
|
||||
|
||||
`gxzx`(供需大厅/绿色金融/绿色交易/企业入驻)需要合并到 `mhzc` 服务中,以实现:
|
||||
1. 统一服务治理,减少跨服务调用
|
||||
2. 支撑服务中心的搜索聚合功能
|
||||
3. 简化系统架构
|
||||
|
||||
---
|
||||
|
||||
## 二、迁移范围
|
||||
|
||||
### 2.1 数据库表(9张)
|
||||
|
||||
| 序号 | 表名 | 说明 |
|
||||
|------|------|------|
|
||||
| 1 | `txw_gxzx_gxxxb` | 供需信息表 |
|
||||
| 2 | `txw_gxzx_shqkb` | 审核情况表 |
|
||||
| 3 | `txw_gxzx_gxscb` | 供需收藏表 |
|
||||
| 4 | `txw_gxzx_gxbqb` | 供需标签表 |
|
||||
| 5 | `txw_gxzx_qybqb` | 企业标签表 |
|
||||
| 6 | `txw_gxzx_rzsqjlb` | 入驻申请记录表 |
|
||||
| 7 | `txw_gxzx_lsjrcpxx` | 绿色金融产品信息表 |
|
||||
| 8 | `txw_gxzx_dkbxsqxx` | 贷款保险申请信息表 |
|
||||
| 9 | `txw_gxzx_lsjy_zcxx` | 绿色交易资产信息表 |
|
||||
|
||||
### 2.2 Java代码
|
||||
|
||||
| 层次 | 文件数 | 说明 |
|
||||
|------|--------|------|
|
||||
| Controller | 4 | GxdtController, LsjrController, LsjyController, QyRzController |
|
||||
| Service接口 | 8 | 含业务接口定义 |
|
||||
| Service实现 | 7 | 业务逻辑实现 |
|
||||
| Mapper | 9 | MyBatis Mapper接口 |
|
||||
| Mapper XML | 9 | MyBatis XML映射文件 |
|
||||
| Domain | 9 | 数据实体DO |
|
||||
| VO/Req/Res | ~20 | 传输对象 |
|
||||
|
||||
### 2.3 核心业务功能
|
||||
|
||||
1. **供需大厅**(GxdtController)- 供需发布/审批/上下架/收藏
|
||||
2. **绿色金融**(LsjrController)- 信贷/保险产品管理
|
||||
3. **绿色交易**(LsjyController)- 资产信息管理
|
||||
4. **企业入驻**(QyRzController)- 入驻申请审批
|
||||
|
||||
---
|
||||
|
||||
## 三、迁移步骤
|
||||
|
||||
### 阶段一:数据库迁移
|
||||
|
||||
1. **备份源库**:在迁移前备份 `txw-gxzx` 相关表数据
|
||||
2. **创建目标表**:在 `mhzc` 数据库中创建9张表(可保持原表名或重命名)
|
||||
3. **数据同步**:将数据从 `gxzx` 库同步到 `mhzc` 库
|
||||
|
||||
```sql
|
||||
-- 示例:在mhzc库执行(假设两库在同一实例)
|
||||
INSERT INTO mhzc_db.txw_gxzx_gxxxb SELECT * FROM gxzx_db.txw_gxzx_gxxxb;
|
||||
```
|
||||
|
||||
### 阶段二:代码迁移
|
||||
|
||||
#### 2.1 创建目录结构
|
||||
|
||||
在 `txw-mhzc` 项目中创建以下目录:
|
||||
|
||||
```
|
||||
txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/
|
||||
├── controller/gxzx/ # 原 gxzx/controller
|
||||
├── service/gxzx/ # 原 gxzx/service
|
||||
│ └── impl/ # 原 gxzx/service/impl
|
||||
├── mapper/gxzx/ # 原 gxzx/mapper
|
||||
├── pojo/domain/gxzx/ # 原 gxzx/pojo/domain
|
||||
├── pojo/vo/gxzx/ # 原 gxzx/pojo/vo
|
||||
├── pojo/req/gxzx/ # 新建请求对象
|
||||
└── pojo/res/gxzx/ # 新建响应对象
|
||||
```
|
||||
|
||||
#### 2.2 迁移文件清单
|
||||
|
||||
**Controller 层(4个):**
|
||||
```
|
||||
gxzx/controller/GxdtController.java → mhzc/controller/gxzx/GxdtController.java
|
||||
gxzx/controller/LsjrController.java → mhzc/controller/gxzx/LsjrController.java
|
||||
gxzx/controller/LsjyController.java → mhzc/controller/gxzx/LsjyController.java
|
||||
gxzx/controller/QyRzController.java → mhzc/controller/gxzx/QyRzController.java
|
||||
```
|
||||
|
||||
**Service 层(8个接口 + 7个实现):**
|
||||
```
|
||||
gxzx/service/TxwGxzxGxxxbService.java → mhzc/service/gxzx/TxwGxzxGxxxbService.java
|
||||
gxzx/service/TxwGxzxShqkbService.java → mhzc/service/gxzx/TxwGxzxShqkbService.java
|
||||
gxzx/service/TxwGxzxGxscbService.java → mhzc/service/gxzx/TxwGxzxGxscbService.java
|
||||
gxzx/service/TxwGxzxGxbqbService.java → mhzc/service/gxzx/TxwGxzxGxbqbService.java
|
||||
gxzx/service/TxwGxzxQybqbService.java → mhzc/service/gxzx/TxwGxzxQybqbService.java
|
||||
gxzx/service/TxwGxzxRzsqjlbService.java → mhzc/service/gxzx/TxwGxzxRzsqjlbService.java
|
||||
gxzx/service/GxzxLsjrService.java → mhzc/service/gxzx/GxzxLsjrService.java
|
||||
gxzx/service/GxzxLsjyZcxxService.java → mhzc/service/gxzx/GxzxLsjyZcxxService.java
|
||||
|
||||
gxzx/service/impl/... → mhzc/service/gxzx/impl/...
|
||||
```
|
||||
|
||||
**Mapper 层(9个):**
|
||||
```
|
||||
gxzx/mapper/TxwGxzxGxxxbMapper.java → mhzc/mapper/gxzx/TxwGxzxGxxxbMapper.java
|
||||
gxzx/mapper/TxwGxzxShqkbMapper.java → mhzc/mapper/gxzx/TxwGxzxShqkbMapper.java
|
||||
...(其他7个同理)
|
||||
```
|
||||
|
||||
**Domain 层(9个):**
|
||||
```
|
||||
gxzx/pojo/domain/TxwGxzxGxxxbDO.java → mhzc/pojo/domain/gxzx/TxwGxzxGxxxbDO.java
|
||||
...(其他8个同理)
|
||||
```
|
||||
|
||||
**VO/Req/Res 层(~20个):**
|
||||
```
|
||||
gxzx/pojo/vo/*.java → mhzc/pojo/vo/gxzx/
|
||||
gxzx/pojo/req/*.java → mhzc/pojo/req/gxzx/
|
||||
gxzx/pojo/lsjr/*.java → mhzc/pojo/lsjr/gxzx/
|
||||
gxzx/pojo/lsjy/*.java → mhzc/pojo/lsjy/gxzx/
|
||||
```
|
||||
|
||||
**Mapper XML(9个):**
|
||||
```
|
||||
gxzx/src/main/resources/mapper/*.xml → mhzc/src/main/resources/mapper/gxzx/
|
||||
```
|
||||
|
||||
#### 2.3 代码修改要点
|
||||
|
||||
**1. 包名修改**
|
||||
```java
|
||||
// 原
|
||||
package com.css.txw.gxzx.controller;
|
||||
package com.css.txw.gxzx.service;
|
||||
package com.css.txw.gxzx.mapper;
|
||||
package com.css.txw.gxzx.pojo.domain;
|
||||
|
||||
// 改为
|
||||
package com.css.txw.mhzc.controller.gxzx;
|
||||
package com.css.txw.mhzc.service.gxzx;
|
||||
package com.css.txw.mhzc.mapper.gxzx;
|
||||
package com.css.txw.mhzc.pojo.domain.gxzx;
|
||||
```
|
||||
|
||||
**2. import 语句修改**
|
||||
```java
|
||||
// 所有 import com.css.txw.gxzx.* 改为 com.css.txw.mhzc.gxzx.*
|
||||
```
|
||||
|
||||
**3. Controller @RequestMapping 路径调整**
|
||||
```java
|
||||
// 原
|
||||
@RequestMapping("/gxdt")
|
||||
@RequestMapping("/lsjr")
|
||||
@RequestMapping("/lsjy")
|
||||
@RequestMapping("/qyrz")
|
||||
|
||||
// 保持不变(前端已对接)或调整
|
||||
```
|
||||
|
||||
**4. ServiceImpl 类注解修改**
|
||||
```java
|
||||
// 原
|
||||
@Service
|
||||
public class TxwGxzxGxxxbServiceImpl extends ServiceImpl<TxwGxzxGxxxbMapper, TxwGxzxGxxxbDO>
|
||||
|
||||
// 改为
|
||||
@Service
|
||||
public class TxwGxzxGxxxbServiceImpl extends ServiceImpl<TxwGxzxGxxxbMapper, TxwGxzxGxxxbDO>
|
||||
```
|
||||
|
||||
**5. MapperScan 配置**
|
||||
在 `MhzcServiceConfiguration.java` 或新建 `GxzxMapperScanConfiguration.java`:
|
||||
```java
|
||||
@MapperScan({"com.css.txw.mhzc.mapper", "com.css.txw.mhzc.mapper.gxzx"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段三:依赖调整
|
||||
|
||||
#### 3.1 移除 gxzx 依赖
|
||||
|
||||
从 `txw-mhzc/pom.xml` 中移除对 `txw-gxzx-service-api` 的依赖(如果存在)。
|
||||
|
||||
#### 3.2 保留必要依赖
|
||||
|
||||
确保以下依赖存在:
|
||||
```xml
|
||||
<!-- 内部框架 -->
|
||||
<dependency>ggzc-framework-starter-xxzx-api</dependency> <!-- 消息中心 -->
|
||||
|
||||
<!-- 数据库 -->
|
||||
<dependency>DmJdbcDriver18</dependency>
|
||||
|
||||
<!-- 内部模块 -->
|
||||
<dependency>txw-common</dependency>
|
||||
```
|
||||
|
||||
#### 3.3 处理外部服务调用
|
||||
|
||||
**IMhzcApi 调用处理**(原 gxzx 调用 mhzc):
|
||||
- `TxwGxzxRzsqjlbServiceImpl` 中注入了 `IMhzcApi`
|
||||
- 迁移后改为直接调用本地方法(企业入驻逻辑已在 mhzc 中)
|
||||
|
||||
**XxzxApi 调用处理**:
|
||||
- 确保 `txw-mhzc` 有消息中心依赖
|
||||
- 如需迁移,保持调用方式不变
|
||||
|
||||
---
|
||||
|
||||
### 阶段四:配置调整
|
||||
|
||||
#### 4.1 网关路由(如有)
|
||||
|
||||
如果使用了网关,需要将 gxzx 相关路由从 gxzx 服务指向 mhzc:
|
||||
|
||||
```yaml
|
||||
# 网关配置
|
||||
- id: gxzx-route
|
||||
uri: http://mhzc-service
|
||||
predicates:
|
||||
- Path=/gxdt/**
|
||||
- Path=/lsjr/**
|
||||
- Path=/lsjy/**
|
||||
- Path=/qyrz/**
|
||||
```
|
||||
|
||||
#### 4.2 Nacos 注册
|
||||
|
||||
确保 `txw-mhzc` 注册到 Nacos,且 gxzx 相关接口可访问。
|
||||
|
||||
---
|
||||
|
||||
### 阶段五:前端适配
|
||||
|
||||
前端 `txw-mhzc-web` 中已有 gxzx 相关接口调用(见 `fwsc/index.js`):
|
||||
|
||||
```javascript
|
||||
// 当前调用路径(需确认)
|
||||
/gxzx/gxdt/gxxxList → /mhzc/gxdt/gxxxList 或保持不变
|
||||
/gxzx/lsjr/queryJgList → /mhzc/lsjr/queryJgList 或保持不变
|
||||
```
|
||||
|
||||
**方案**:迁移后保持接口路径不变,前端无需修改。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据库表迁移脚本
|
||||
|
||||
```sql
|
||||
-- 1. 备份原表(可选)
|
||||
CREATE TABLE txw_gxzx_gxxxb_bak AS SELECT * FROM txw_gxzx_gxxxb;
|
||||
|
||||
-- 2. 创建新表(在 mhzc 库执行)
|
||||
CREATE TABLE txw_gxzx_gxxxb (
|
||||
gx_uuid VARCHAR(64) PRIMARY KEY,
|
||||
bt_1 VARCHAR(200),
|
||||
fwlx_dm VARCHAR(20),
|
||||
sshy VARCHAR(50),
|
||||
fwfw VARCHAR(500),
|
||||
fwnr TEXT,
|
||||
zt VARCHAR(10),
|
||||
qyuuid VARCHAR(64),
|
||||
sjzt VARCHAR(10),
|
||||
gjjg DECIMAL(18,2),
|
||||
-- 其他字段...
|
||||
);
|
||||
|
||||
-- 3. 数据迁移
|
||||
INSERT INTO mhzc_db.txw_gxzx_gxxxb SELECT * FROM gxzx_db.txw_gxzx_gxxxb;
|
||||
|
||||
-- 4. 验证
|
||||
SELECT COUNT(*) FROM txw_gxzx_gxxxb;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、风险点与注意事项
|
||||
|
||||
### 5.1 数据一致性
|
||||
- 迁移期间禁止在 gxzx 写入数据
|
||||
- 迁移后需验证数据完整性
|
||||
|
||||
### 5.2 接口兼容性
|
||||
- 迁移前后接口路径尽量保持一致
|
||||
- 如有变化需同步通知前端
|
||||
|
||||
### 5.3 事务处理
|
||||
- 跨库迁移时可考虑使用分布式事务或分批迁移
|
||||
- 确保关键业务(供需发布、审批)的事务完整性
|
||||
|
||||
### 5.4 依赖服务
|
||||
- 消息中心(XxzxApi)需确保可用
|
||||
- 确认 IMhzcApi 在 mhzc 中仍有调用需求
|
||||
|
||||
---
|
||||
|
||||
## 六、迁移验证清单
|
||||
|
||||
| 序号 | 验证项 | 方法 |
|
||||
|------|--------|------|
|
||||
| 1 | 数据库表迁移完整 | 对比记录数 |
|
||||
| 2 | Controller 注入正常 | 启动应用无报错 |
|
||||
| 3 | Service 层无异常 | 单元测试 |
|
||||
| 4 | Mapper XML 路径正确 | 查询功能正常 |
|
||||
| 5 | 接口路径可访问 | Postman 测试 |
|
||||
| 6 | 搜索服务聚合供需数据 | 调用搜索接口验证 |
|
||||
| 7 | 前端功能正常 | UI 测试 |
|
||||
|
||||
---
|
||||
|
||||
## 七、后续工作
|
||||
|
||||
1. **旧服务下线**:gxzx 服务相关接口迁移完成后,可逐步停用 gxzx 服务
|
||||
2. **搜索增强**:基于迁移后的供需数据,完善搜索服务聚合逻辑
|
||||
3. **代码清理**:移除 gxzx 项目中已迁移的代码
|
||||
@ -43,17 +43,14 @@ request.interceptors.request.use(
|
||||
if (newConf.loading) {
|
||||
SingleLoading.startLoading();
|
||||
}
|
||||
// 设置随机数, 定位后端日志和解决浏览器缓存
|
||||
const { url } = newConf;
|
||||
if (url.indexOf('?') !== -1) {
|
||||
newConf.url = `${url}&t=${new Date().getTime()}`; // 请求添加时间戳
|
||||
} else {
|
||||
newConf.url = `${url}?t=${new Date().getTime()}`;
|
||||
}
|
||||
|
||||
// get请求映射params参数
|
||||
if (newConf.method === 'get' && newConf.params) {
|
||||
let url = `${newConf.url}?`;
|
||||
// 先加时间戳(如果 URL 还没参数)
|
||||
if (url.indexOf('?') === -1) {
|
||||
newConf.url = `${newConf.url}?t=${new Date().getTime()}&`;
|
||||
}
|
||||
// 再追加 params
|
||||
let paramsUrl = `${newConf.url}`;
|
||||
for (const propName of Object.keys(newConf.params)) {
|
||||
const value = newConf.params[propName];
|
||||
const part = `${encodeURIComponent(propName)}=`;
|
||||
@ -62,16 +59,23 @@ request.interceptors.request.use(
|
||||
for (const key of Object.keys(value)) {
|
||||
const params = `${propName}[${key}]`;
|
||||
const subPart = `${encodeURIComponent(params)}=`;
|
||||
url += `${subPart + encodeURIComponent(value[key])}&`;
|
||||
paramsUrl += `${subPart + encodeURIComponent(value[key])}&`;
|
||||
}
|
||||
} else {
|
||||
url += `${part + encodeURIComponent(value)}&`;
|
||||
paramsUrl += `${part + encodeURIComponent(value)}&`;
|
||||
}
|
||||
}
|
||||
}
|
||||
url = url.slice(0, -1);
|
||||
paramsUrl = paramsUrl.slice(0, -1);
|
||||
newConf.params = {};
|
||||
newConf.url = url;
|
||||
newConf.url = paramsUrl;
|
||||
} else {
|
||||
// 无 params 时,直接加时间戳
|
||||
if (url.indexOf('?') !== -1) {
|
||||
newConf.url = `${url}&t=${new Date().getTime()}`;
|
||||
} else {
|
||||
newConf.url = `${url}?t=${new Date().getTime()}`;
|
||||
}
|
||||
}
|
||||
return newConf;
|
||||
},
|
||||
|
||||
64
txw-mhzc-web/src/pages/index/api/search.js
Normal file
64
txw-mhzc-web/src/pages/index/api/search.js
Normal file
@ -0,0 +1,64 @@
|
||||
import { fetchSso } from '@/core/request';
|
||||
|
||||
const basurl = '/mhzc';
|
||||
|
||||
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,
|
||||
});
|
||||
},
|
||||
};
|
||||
183
txw-mhzc-web/src/pages/index/components/search/SearchArea.vue
Normal file
183
txw-mhzc-web/src/pages/index/components/search/SearchArea.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<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>
|
||||
@ -0,0 +1,117 @@
|
||||
<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>
|
||||
@ -0,0 +1,50 @@
|
||||
<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>
|
||||
@ -0,0 +1,68 @@
|
||||
<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>
|
||||
@ -0,0 +1,120 @@
|
||||
<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>
|
||||
@ -0,0 +1,104 @@
|
||||
<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>
|
||||
@ -0,0 +1,61 @@
|
||||
<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>
|
||||
@ -87,7 +87,7 @@ function tfwgj() {
|
||||
function tfwxq() {
|
||||
return import('@/pages/index/views/gxfb/tfwxq.vue');
|
||||
}
|
||||
|
||||
|
||||
//绿色交易
|
||||
function lsjy() {
|
||||
return import('@/pages/index/views/lsjy/lsjy.vue');
|
||||
@ -98,6 +98,11 @@ function gzt() {
|
||||
return import(/* webpackChunkName: "gzt" */ '@/pages/index/views/gzt/index.vue');
|
||||
}
|
||||
|
||||
// 搜索结果页
|
||||
function search() {
|
||||
return import(/* webpackChunkName: "search" */ '@/pages/index/views/search/index.vue');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -298,4 +303,16 @@ export default [
|
||||
disableBack: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: '/search',
|
||||
component: search,
|
||||
meta: {
|
||||
title: '搜索结果',
|
||||
isShowSideBar: false,
|
||||
hasHome: true,
|
||||
breadCrumbs: [{ title: '首页', to: '/home' }, { title: '搜索结果', to: '/search' }],
|
||||
disableBack: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
195
txw-mhzc-web/src/pages/index/views/search/index.vue
Normal file
195
txw-mhzc-web/src/pages/index/views/search/index.vue
Normal file
@ -0,0 +1,195 @@
|
||||
<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() {
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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() {
|
||||
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);
|
||||
|
||||
if (this.searchHistory.length > 10) {
|
||||
this.searchHistory = this.searchHistory.slice(0, 10);
|
||||
}
|
||||
|
||||
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>
|
||||
@ -286,26 +286,26 @@ module.exports = {
|
||||
// 会误伤 SPA 路由 /view/mhzc/...,刷新时整页请求被转发到后端导致 Proxy error。必须用 ^ 限定为路径前缀。
|
||||
proxy: {
|
||||
'^/sso': {
|
||||
// target: 'http://localhost:9301',
|
||||
target: 'http://carbon.liantu.tech',
|
||||
target: 'http://localhost:9301',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/mhzc': {
|
||||
// target: 'http://localhost:9302',
|
||||
target: 'http://carbon.liantu.tech',
|
||||
target: 'http://localhost:9302',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/gxzx': {
|
||||
// target: 'http://localhost:9303',
|
||||
target: 'http://carbon.liantu.tech',
|
||||
target: 'http://localhost:9303',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/yygl': {
|
||||
// target: 'http://localhost:20010',
|
||||
target: 'http://carbon.liantu.tech',
|
||||
target: 'http://localhost:20010',
|
||||
// target: 'http://carbon.liantu.tech',
|
||||
// target: 'http://10.23.20.13:94/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
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.HashMap;
|
||||
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);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("total", list.size());
|
||||
result.put("list", list);
|
||||
|
||||
Map<String, Integer> categoryCount = new 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 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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
@ -15,4 +15,6 @@ public class SyzxxxVO implements Serializable {
|
||||
|
||||
private String zxLx;
|
||||
|
||||
private String uuid;
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
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();
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
package com.css.txw.mhzc.service.impl;
|
||||
|
||||
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.Map;
|
||||
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) {
|
||||
Map<String, List<SyzxxxVO>> data = zxxxbService.zxxx();
|
||||
List<SearchResultVO> resultList = new ArrayList<>();
|
||||
String categoryType = reqVO.getCategoryType();
|
||||
|
||||
if (data != null) {
|
||||
data.forEach((category, list) -> {
|
||||
String currentCategoryType = getCategoryType(category);
|
||||
if (!"all".equals(categoryType) && !categoryType.equals(currentCategoryType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.forEach(zxxx -> {
|
||||
if (matchKeyword(zxxx.getBt1(), reqVO.getKeyword()) ||
|
||||
matchKeyword(zxxx.getZxNr(), reqVO.getKeyword())) {
|
||||
|
||||
SearchResultVO vo = new SearchResultVO();
|
||||
vo.setId(zxxx.getUuid());
|
||||
vo.setTitle(highlightKeyword(zxxx.getBt1(), reqVO.getKeyword()));
|
||||
vo.setSummary(highlightKeyword(truncate(zxxx.getZxNr(), 200), reqVO.getKeyword()));
|
||||
vo.setCategory(category);
|
||||
vo.setCategoryType(currentCategoryType);
|
||||
vo.setSource("碳信网");
|
||||
vo.setSourceType("news");
|
||||
vo.setPublishTime(zxxx.getFbsj() != null ? zxxx.getFbsj().toString() : null);
|
||||
vo.setUrl("/mhzc/news/" + zxxx.getUuid());
|
||||
|
||||
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() {
|
||||
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();
|
||||
|
||||
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() {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSearchHistory() {
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user