# 搜索页 — 前端技术方案（C 端 · shop-app）

> **依据：** 《搜索页功能需求.md》v1.0、《搜索页技术方案.md》v1.2  
> **关联：** 《商城首页前端技术方案》、《商品分类前端技术方案》、《商品详情内页前端技术方案》  
> **范围：** 消费者 APP **`shop-app`** 搜索输入页（A）、搜索结果页（B）；**不** 改后端、**不** 实现店铺主页、店铺内搜索、联想/热搜。  
> **实现状态：** 页面与 API 封装已落地，待与 `GET /api/search` 联调。

---

## 1. 技术栈与约定

| 项 | 说明 |
|----|------|
| 框架 | uni-app **Vue 3** + **uview-plus** |
| 请求 | `@/utils/request`；搜索聚合结果为 `data.goods` / `data.shops` |
| 鉴权 | **`header: { isToken: false }`**（`@Anonymous`） |
| 历史 | **仅本地** `uni.setStorageSync`，键 `shop_search_history`，最多 **20** 条 |
| 占位文案 | **「搜索兽药、饲料、店铺」**（SRH15，与首页/分类一致） |
| 参考 | `components/mall/GoodsGrid.vue`、`components/category/GoodsListBlock.vue` |

---

## 2. 页面与路由

| 页面 | 需求代号 | 路径 | 入口 |
|------|----------|------|------|
| 搜索输入 | **A** | `subpackage/search/index` | 首页顶栏搜索、分类页 `SearchEntry` |
| 搜索结果 | **B** | `subpackage/search/result` | A 页提交搜索；B 页「重新搜索」回 A 并带关键词 |

**`pages.json`：**

```text
subpackage/search/index   → navigationBarTitleText: 搜索
subpackage/search/result  → navigationBarTitleText: 搜索结果
```

**Query：**

| 页面 | 参数 | 说明 |
|------|------|------|
| A | `keyword`（可选） | B 页「重新搜索」预填 |
| B | `keyword`（必填） | trim 后检索；空则 Toast 并返回 |

**跳转工具：** `utils/searchNav.js`

| 方法 | 行为 |
|------|------|
| `goSearchInput(keyword?)` | `navigateTo` A 页 |
| `goSearchResult(keyword)` | `navigateTo` B 页（内部 trim，空不跳） |

---

## 3. 文件清单

| 类型 | 路径 | 说明 |
|------|------|------|
| A 页 | `shop-app/subpackage/search/index.vue` | 输入 + 历史 |
| B 页 | `shop-app/subpackage/search/result.vue` | 重新搜索条 + Tab + 列表 |
| Tab | `shop-app/components/search/SearchResultTabs.vue` | 按销量 / 按价格 / 店铺 |
| 店铺列表 | `shop-app/components/search/ShopList.vue` | 店铺卡片 |
| 商品网格 | `shop-app/components/mall/GoodsGrid.vue` | 与分类/首页共用 |
| 搜索入口 | `shop-app/components/mall/SearchEntry.vue` | 跳转 A 页 |
| API | `shop-app/api/search.js` | `searchAll` |
| 常量 | `shop-app/constants/search.js` | Tab、价序、分页大小 |
| 历史 | `shop-app/utils/searchHistory.js` | 增删清空、去重置顶 |
| 导航 | `shop-app/utils/searchNav.js` | A/B 路由 |
| 路径常量 | `shop-app/utils/pageRoute.js` | 主包 + 分包路径统一维护 |
| 商品映射 | `shop-app/utils/goodsDisplay.js` | `mapGoodsCardList` |
| 店铺映射 | `shop-app/utils/shopDisplay.js` | `mapShopCardList` |

---

## 4. 接口封装

**模块：** `shop-app/api/search.js`

| 方法 | HTTP | 路径 | 响应 |
|------|------|------|------|
| `searchAll(params)` | GET | `/api/search` | `data: { goods: { total, rows }, shops: { total, rows } }` |

**Query：**

| 参数 | 默认 | 说明 |
|------|------|------|
| keyword | 必填 | trim 后非空（空搜在 A 页拦截，不调接口） |
| sortBy | `sales_desc` | 仅影响 **商品** 排序 |
| goodsPageNum | 1 | 商品分页 |
| goodsPageSize | 10 | 与 `SEARCH_PAGE_SIZE` 一致 |
| shopPageNum | 1 | 店铺分页 |
| shopPageSize | 10 | 同上 |

**sortBy 与 UI Tab：**

| Tab | sortBy |
|-----|--------|
| 按销量 | `sales_desc` |
| 按价格 · 从低到高 | `price_asc` |
| 按价格 · 从高到低 | `price_desc` |
| 店铺 | 请求仍带 sortBy，后端 **只查店铺** 列表 |

**错误处理：** `silent: true`；失败当前 Tab **空态** +「点击重试」，不 Toast 刷屏（对齐 §8.2）。

---

## 5. 搜索历史（本地 SRH4）

**存储键：** `shop_search_history`  
**结构：** `[{ keyword, time }]`，展示按 `time` **倒序**。

| 行为 | 实现 |
|------|------|
| 写入时机 | **成功进入 B 页**（A 提交、B 页 onLoad） |
| 空关键词 | 不写历史、不调接口 |
| 去重 | 同词再次搜索 → 更新 `time` 并 **置顶** |
| 单条删除 | 历史行右侧关闭图标 |
| 全部清空 | 标题栏「清空」+ 二次确认 |
| 无历史 | 文案「暂无搜索历史」 |

---

## 6. 页面结构

### 6.1 A — 搜索输入 `subpackage/search/index`

```text
搜索输入页
├── 顶栏：返回 + 输入框 + 清空 +「搜索」
├── 键盘回车 / 搜索按钮 → 校验非空 → 写历史 → B 页
└── 搜索历史（有则展示，无则空态文案）
```

| 规则 | 实现 |
|------|------|
| SRH3 空搜 | Toast「请输入搜索内容」，不跳转 |
| 预填 | `?keyword=` 来自 B 页重新搜索 |

### 6.2 B — 搜索结果 `subpackage/search/result`

```text
搜索结果页
├── 顶栏：返回 + 可点搜索条（当前关键词）→ A 页预填
├── SearchResultTabs（三 Tab；按价格下再选升/降序）
└── scroll-view 列表
    ├── 按销量 / 按价格 → GoodsGrid + 上拉加载 goodsPageNum
    ├── 店铺 → ShopList + 上拉加载 shopPageNum
    └── 空态「暂无相关」；失败可重试
```

| 行为 | 实现 |
|------|------|
| 默认 Tab | **按销量**（SRH8） |
| 切 Tab | 商品 Tab 重置 `goodsPageNum` 并请求；店铺 Tab **优先用** 同次搜索已返回的 `shops` 缓存 |
| 价序切换 | 仅「按价格」Tab 下；改 `sortBy` 后重置商品列表 |
| 商品点击 | `goGoodsDetail(goodsId)`（SRH11） |
| 店铺点击 | Toast「店铺主页开发中」（SRH12，待店铺主页模块） |
| rating/fansCount | 后端为 null 时 **不展示** 对应行 |

---

## 7. 入口改造

| 位置 | 改动 |
|------|------|
| `pages/index/index.vue` | `onSearchTap` → `/subpackage/search/index` |
| `components/mall/SearchEntry.vue` | 点击 → `goSearchInput()` |
| 分类 A/B/C 页 | 已引用 `SearchEntry`，无需再改 |

---

## 8. 业务规则对照（前端）

| 编号 | 前端落实 |
|------|----------|
| SRH2 | 首页/分类搜索栏 → A 页 |
| SRH3 | A 页空关键词拦截 |
| SRH4 | `searchHistory.js` 本地存储 |
| SRH5 | B 顶栏点击 → A 带 `keyword` |
| SRH7 | 列表 **不** 做四条件过滤（后端浏览层） |
| SRH8～SRH9 | 三 Tab + 价格升/降 |
| SRH11 | 商品 → 详情 |
| SRH12 | 店铺 → 占位 Toast |
| SRH13 | 空态「暂无相关」 |
| SRH14 | 不在搜索页加购/下单 |
| SRH15 | 占位文案常量 `SEARCH_PLACEHOLDER` |

---

## 9. 联调检查清单

- [ ] `GET /api/search?keyword=兽药` 返回 `data.goods.rows`、`data.shops.rows`
- [ ] 按销量 / 按价格 / 店铺 三 Tab 列表与空态独立
- [ ] 商品上拉加载 `goodsPageNum` 递增
- [ ] 店铺上拉 `shopPageNum` 递增（首屏已从聚合接口缓存时从第 2 页请求）
- [ ] 空关键词、仅空格：A 页 Toast，不进 B
- [ ] 历史去重、清空、单删
- [ ] 从首页、分类页点击搜索栏进入 A 页

---

## 10. 非本期（前端不实现）

| 项 | 说明 |
|----|------|
| 店铺主页 | 店铺卡片暂 Toast |
| 店铺内搜索 | 店铺主页专册 |
| 联想 / 热搜 / 语音 | — |
| 历史云端同步 | 仅 localStorage |
| 关键词高亮 | UI 可选增强 |

---

## 11. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：A/B 两页、统一搜索接口封装、本地历史、入口打通 |

---

*文档版本：v1.0 · 关联《搜索页功能需求.md》v1.0、《搜索页技术方案.md》v1.2*
