feat(search): Refactor SearchResultView into modular components
Decomposed the main SearchResultView into smaller, reusable components under 'src/components/SearchResult/' to improve maintainability and scalability. - Added specific components for different result types like SummaryCard, AdditiveList, MaterialCard, and PrepackagedList. - Updated 'router/index.ts' to reflect the new structure. - Included project planning and proposal documents in 'project_document/'.
This commit is contained in:
parent
b11ce5e6fe
commit
aa2a7ae542
64
2-参考资料/2-食品添加剂分类.md
Normal file
64
2-参考资料/2-食品添加剂分类.md
Normal file
@ -0,0 +1,64 @@
|
||||
| **分类** | **核心定义** | **安全等级** | **典型示例** |
|
||||
|----------------|------------------------------------------------------------------------------|----------------|---------------------------------------------------------------|
|
||||
| A1 类(最安全) | ADI 值明确或无需规定<br>(毒理学资料充分,长期摄入无健康风险) | 最高安全等级 | - 天然抗氧化剂(如维生素 C、维生素 E)<br>- 天然香料(如香兰素,合规剂量下)<br>- 部分营养强化剂(如钙、铁) |
|
||||
| A2 类(较安全) | 暂定 ADI 值<br>(毒理学资料较充分,但需进一步验证) | 中等安全等级 | - 防腐剂(如苯甲酸钠,ADI 0-5mg/kg 体重)<br>- 甜味剂(如阿斯巴甜,ADI 0-40mg/kg 体重) |
|
||||
| B1 类(需警惕) | 未建立 ADI 值<br>(曾评估但毒理学数据不足,需限制使用) | 限制使用范围 | - 色素(如诱惑红,仅限糖果、饮料中限量使用)<br>- 乳化剂(如硬脂酰乳酸钠,过量可能影响消化) |
|
||||
| B2 类(数据不足) | 未进行安全评估<br>(新型添加剂或研究较少的物质) | 数据不充分 | - 部分新型甜味剂(如某些糖醇类,长期数据不足) |
|
||||
| C1 类(禁止使用) | 明确健康风险<br>(致癌、致畸或代谢毒性证据充分) | 风险最高 | - 工业染料(如苏丹红)<br>- 甲醛(曾用于防腐,因致癌性被禁) |
|
||||
| C2 类(严格限制) | 仅限特定场景使用<br>(需在特定食品中严格限量) | 限制使用场景 | - 增稠剂(如黄原胶,仅限婴幼儿食品中使用)<br>- 某些合成色素(如喹啉黄,仅限特殊医学用途食品) |
|
||||
|
||||
|
||||
|
||||
# 颜色标识
|
||||
| **分类** | **安全等级定位** | 推荐颜色 | 颜色代码(十六进制) | 配色逻辑(含微信绿适配) |
|
||||
|----------------|------------------------|----------------|----------------------|------------------------------------------------------------------------------------------|
|
||||
| A1 类(最安全) | 最高安全等级 | **微信绿** | #07C160 | 采用微信标志性绿色(大众熟悉的“安全、可信”符号),贴合“A1类长期摄入无风险”的“最高安全”定位,视觉友好且易联想。 |
|
||||
| A2 类(较安全) | 中等安全等级(可控) | 浅绿色 | #90ee90ff | 比微信绿更浅的绿色(同色系递进),既关联“A1类的安全属性”,又通过明度差异区分“较安全”与“最安全”。 |
|
||||
| B1 类(需警惕) | 限制使用范围(低风险) | 蓝色 | #1E90FF | 中性蓝色,无风险暗示,代表“需规范使用但无明确风险”,与前后色系(绿→蓝→黄)过渡自然。 |
|
||||
| B2 类(数据不足) | 数据不充分(未知风险) | 黄色 | #ffff00ff | 过渡预警色,暗示“信息不全需注意”,与“未评估的新型添加剂”的“未知性”匹配。 |
|
||||
| C1 类(禁止使用) | 风险最高(明确危害) | 红色 | #CD5C5C | 强警示红,直接关联“危险、禁止”,对应“明确致癌、致畸风险”。 |
|
||||
| C2 类(严格限制) | 限制场景(高风险倾向) | 橙色 | #FF7F50 | 橙色介于黄与红之间,代表“风险高于B类但未到禁止程度”,贴合“仅限特定场景使用”的限制属性。 |
|
||||
|
||||
|
||||
|
||||
|
||||
食品添加剂的安全等级划分依据其毒性评估和监管标准,主要分为以下三类:
|
||||
|
||||
JECFA 分类标准
|
||||
A类 :已制定每日允许摄入量(ADI值)或暂定ADI值,分为A1、A2两类,通常用于安全性较高的食品添加剂。
|
||||
1
|
||||
B类 :未制定ADI值或未完成安全性评价,分为B1、B2两类,需谨慎使用。
|
||||
1
|
||||
C类 :存在安全隐患或需严格限制使用条件,分为C1、C2两类。
|
||||
1
|
||||
2
|
||||
具体应用
|
||||
A类:如 阿斯巴甜 (β-环状糊精、双乙酰酒石酸单双甘油酯等)在特定食品中的使用有明确限量。
|
||||
3
|
||||
B类:部分未完成毒性评估的添加剂需避免直接接触皮肤或高温加工场景。
|
||||
3
|
||||
C类:如 硫酸铝钾 、 焦糖色 等因工艺必要性调整了使用范围。
|
||||
3
|
||||
注:不同国家或地区的分类标准可能存在差异,建议结合具体法规进一步确认。
|
||||
|
||||
|
||||
一、分类体系及定义
|
||||
A类(安全性较高)
|
||||
|
||||
A1类:毒理学资料完善,已制定正式每日允许摄入量(ADI值),允许按标准使用12。
|
||||
A2类:毒理学资料不完善,但已制定暂定ADI值,允许暂时使用12。
|
||||
B类(安全性待评估)
|
||||
|
||||
B1类:JECFA曾评估但资料不足,未制定ADI值12。
|
||||
B2类:未经过JECFA安全性评价12。
|
||||
C类(限制或禁用)
|
||||
|
||||
C1类:经评估认为在食品中使用不安全,原则上禁止使用12。
|
||||
C2类:仅限特定食品中严格限制使用12。
|
||||
二、安全性排序
|
||||
从高到低依次为:A1 > A2 > B1 > B2 > C2 > C134。
|
||||
|
||||
三、典型示例
|
||||
A1类:常见防腐剂苯甲酸钠(正式ADI值0-5 mg/kg体重)15。
|
||||
C1类:部分工业用着色剂因致癌性被禁用23。
|
||||
注:该分类为国际通用标准,具体应用需结合各国法规(如中国GB 2760-2024)调整67。
|
@ -1,6 +1,6 @@
|
||||
# “食话食说”APP - 视图层(Views)工作日志快照
|
||||
|
||||
**最后更新**: 2025-07-24 10:00:00 (UTC+8)
|
||||
**最后更新**: 2025-07-25 01:50:00 (UTC+8)
|
||||
**目的**: 为 `shihuashishuo-ui/src/views` 目录下的所有页面组件提供一份简明扼要的功能说明和状态记录,以便于快速理解项目结构和进行后续开发。
|
||||
|
||||
---
|
||||
@ -34,30 +34,48 @@
|
||||
* **`MessageView-消息列表页.vue`**
|
||||
* **用途**: 展示系统通知、用户互动等消息。
|
||||
* **状态**: 结构占位。
|
||||
* **`CustomerServiceView-客服页.vue`**
|
||||
* **用途**: 提供客服联系方式、常见问题解答。
|
||||
* **状态**: **基础实现 (2025-07-25)**。
|
||||
* **`AnalysisHistoryView-分析历史页.vue`**
|
||||
* **用途**: 展示用户的食品分析历史记录。
|
||||
* **状态**: **基础实现 (2025-07-25)**。
|
||||
* **`HistoryView-搜索历史页.vue`**
|
||||
* **用途**: 展示用户的搜索历史记录。
|
||||
* **状态**: 结构占位。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心体验流程页面 (`核心体验页/`)
|
||||
|
||||
* **`HomeView-首页-2.2.vue`**
|
||||
* **用途**: 用户登录后的主入口,是产品迭代的核心。
|
||||
* **状态**: **v2.5.1 (已完成)**。经过多轮迭代,功能完善,设计现代。
|
||||
* **`ScanView-扫码页.vue`**
|
||||
* **用途**: APP的核心功能,用于扫码识别食品。
|
||||
* **状态**: 结构占位。
|
||||
* **`HomeView-首页-2.5.vue`**
|
||||
* **用途**: 应用的核心仪表盘和功能入口。集成了搜索、扫描快捷方式、每日饮食与营养追踪、内容推荐等功能。
|
||||
* **状态**: **功能完善 (2025-07-25)**。UI/UX 经过多次迭代,是当前最新版本。
|
||||
* **`CameraView-相机页.vue`**
|
||||
* **用途**: 多功能相机视图,整合了“配料查询”(拍照/扫码)和“饮食记录”两大核心功能。是图像识别的统一入口。
|
||||
* **状态**: **功能完善 (2025-07-25)**。
|
||||
* **`SearchView-搜索页.vue`**
|
||||
* **用途**: 手动输入关键词搜索食品、成分。
|
||||
* **状态**: 结构占位。
|
||||
* **用途**: 搜索功能的入口页面,提供关键词输入、历史记录和热门搜索。
|
||||
* **状态**: **功能完善 (2025-07-25)**。
|
||||
* **`SearchResultView-搜索结果页.vue`**
|
||||
* **用途**: 展示搜索结果列表。
|
||||
* **状态**: 结构占位。
|
||||
* **用途**: 展示搜索结果的高级列表页。支持多Tab分类(预包装、添加剂、食谱等)和复杂的排序筛选功能。
|
||||
* **状态**: **功能完善 (2025-07-25)**。
|
||||
* **`ResultView-结果页.vue`**
|
||||
* **用途**: 展示单个食品的最终分析结果。
|
||||
* **用途**: 展示食品或成分的最终分析报告,包括安全/营养评级、成分解读、风险提醒和替代品推荐。
|
||||
* **状态**: **功能完善 (2025-07-25)**。
|
||||
* **`AddFoodView-添加饮食页.vue`**
|
||||
* **用途**: 用于从饮食记录流程中,手动添加食物到特定餐次。
|
||||
* **状态**: 结构占位。
|
||||
* **`ScanView-扫码页.vue`**
|
||||
* **用途**: (已废弃/旧版) 早期的单一功能扫码页面。
|
||||
* **状态**: 其功能已被 `CameraView-相机页.vue` 完全整合和取代。
|
||||
* **备份文件**:
|
||||
* `HomeView-首页-2.0.backup-最简.vue`
|
||||
* `HomeView-首页-2.1.backup-合并前.vue`
|
||||
* `HomeView-首页-2.2.backup.vue`
|
||||
* `HomeView-首页-2.3.backup.vue`
|
||||
* `HomeView-首页-2.4.backup.vue`
|
||||
* `SearchResultView-搜索结果页-2.0.backup.vue`
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# “食话食说”APP - 首页迭代开发工作日志
|
||||
|
||||
**最后更新**: 2025-07-24 10:01:00 (UTC+8)
|
||||
**最后更新**: 2025-07-25 01:50:00 (UTC+8)
|
||||
**核心负责人**: L.star
|
||||
**目标**: 对产品首页进行多轮迭代设计与开发,以满足用户需求并提升产品体验。
|
||||
|
||||
@ -84,16 +84,49 @@
|
||||
* 将 `LoginView`, `OnboardingView`, `SplashView` 等基础功能页面移入 `通用基础页/`。
|
||||
* **路由更新**: 修改 `router/index.ts`,使其路由定义指向新的文件路径,确保应用能够正常导航。
|
||||
* **Git提交**: 将所有变更作为一个独立的 `refactor` 提交。本地提交已完成,远程推送待用户验证后执行。
|
||||
* **关联页面创建**:
|
||||
* 在迭代过程中,为满足首页的功能跳转需求,创建了 `CustomerServiceView-客服页.vue` 和 `AnalysisHistoryView-分析历史页.vue` 两个新页面,并完成了基础实现。
|
||||
|
||||
---
|
||||
|
||||
## 3. 最终状态与关键资产
|
||||
|
||||
* **当前版本**: **v2.5.1 (结构重构后)**
|
||||
* **当前版本**: **v2.5.1 (搜索体验优化后)**
|
||||
* **核心文件**:
|
||||
* `shihuashishuo-ui/src/views/核心体验页/HomeView-首页-2.2.vue` (最终版代码)
|
||||
* `shihuashishuo-ui/src/views/核心体验页/HomeView-首页-2.3.vue` (最终版代码)
|
||||
* `shihuashishuo-ui/src/views/核心体验页/HomeView-首页-2.2.backup.vue` (v2.2 版备份)
|
||||
* `shihuashishuo-ui/src/router/index.ts` (已更新路由)
|
||||
* **本日志**: `3-工作日志/3.2-工作日志-首页迭代开发日志-v1.0.md`
|
||||
|
||||
### **第七轮迭代: 搜索体验优化 (2025-07-25)**
|
||||
|
||||
* **目标**: 提升搜索流程的整体用户体验和UI一致性。
|
||||
* **核心实现**:
|
||||
* **全局组件系统**:
|
||||
* 创建了 `EventBus.ts` 作为全局事件总线。
|
||||
* 开发了 `ConfirmDialog.vue` 全局确认对话框。
|
||||
* 在 `App.vue` 中集成了 `ConfirmDialog`,使其全局可用。
|
||||
* **`SearchView-搜索页.vue` 优化**:
|
||||
* 移除了阻塞性的 `alert` 弹窗。
|
||||
* 使用新的全局确认对话框,重构了历史记录的**单项删除**功能。
|
||||
* 将其顶部搜索栏的样式与首页完全统一。
|
||||
* **`SearchResultView-搜索结果页.vue` 优化**:
|
||||
* 将其顶部搜索栏的样式与首页完全统一。
|
||||
* 新增了分类标签栏(全部、产品、成分等)。
|
||||
* 新增了一个功能全面的、**自适应的弹出式筛选/排序面板**。
|
||||
* 优化了返回按钮的导航逻辑,使其直接返回首页。
|
||||
|
||||
---
|
||||
|
||||
## 3. 最终状态与关键资产
|
||||
|
||||
* **当前版本**: **v2.5.1 (搜索体验优化后)**
|
||||
* **核心文件**:
|
||||
* `shihuashishuo-ui/src/views/核心体验页/HomeView-首页-2.3.vue`
|
||||
* `shihuashishuo-ui/src/views/核心体验页/SearchView-搜索页.vue`
|
||||
* `shihuashishuo-ui/src/views/核心体验页/SearchResultView-搜索结果页.vue`
|
||||
* `shihuashishuo-ui/src/components/ConfirmDialog.vue`
|
||||
* `shihuashishuo-ui/src/utils/EventBus.ts`
|
||||
* **本日志**: `3-工作日志/3.2-工作日志-首页迭代开发日志-v1.0.md`
|
||||
|
||||
**任务已全部完成。**
|
49
3-工作日志/3.3-工作日志-核心体验页分析-v1.0.md
Normal file
49
3-工作日志/3.3-工作日志-核心体验页分析-v1.0.md
Normal file
@ -0,0 +1,49 @@
|
||||
# 工作日志:核心体验页功能分析 (v1.0)
|
||||
|
||||
**日期:** 2025-07-25
|
||||
**分析师:** Roo (AI Assistant)
|
||||
**范围:** `shihuashishuo-ui/src/views/核心体验页/`
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本次分析旨在全面梳理“食话食说”应用的核心用户体验流程,涉及从首页发现、搜索、扫描/拍照到查看结果、记录饮食的完整闭环。通过对相关Vue组件的阅读,明确了各页面的功能定位及它们之间的导航关系。
|
||||
|
||||
## 2. 关键页面组件分析
|
||||
|
||||
- **`HomeView-首页-2.5.vue`**: 核心流量入口和功能仪表盘。集成了搜索栏、扫描/拍照快捷方式、健康数据(饮食、营养、热量)概览以及内容推荐,是引导用户进入各项核心功能的中枢。
|
||||
- **`SearchView-搜索页.vue`**: 搜索功能的起点。提供搜索框、历史记录和热门搜索,引导用户发起查询。
|
||||
- **`SearchResultView-搜索结果页.vue`**: 高级搜索结果展示页。支持多Tab分类(全部、预包装、添加剂、食材、食谱、资讯)和多维度排序筛选,功能设计完善。
|
||||
- **`CameraView-相机页.vue`**: 多功能相机视图。整合了“配料查询”(通过拍照或扫码)和“饮食记录”两大功能,是图像识别功能的核心承载页面。取代了旧版的 `ScanView-扫码页.vue`。
|
||||
- **`ResultView-结果页.vue`**: 最终分析报告页。清晰地展示食品的安全评级、营养评级、成分解读、风险提醒和更优选择推荐。
|
||||
- **`AddFoodView-添加饮食页.vue`**: 饮食记录的补充页面,用于手动添加食物到特定餐次。
|
||||
- **`ScanView-扫码页.vue`**: (已废弃/旧版) 一个简化的扫描页面,其功能已被 `CameraView-相机页.vue` 覆盖。
|
||||
|
||||
## 3. 核心用户流程梳理
|
||||
|
||||
### 3.1 搜索流程
|
||||
|
||||
1. **入口**: 用户在 `HomeView` 点击搜索框或热门标签。
|
||||
2. **搜索**: 跳转至 `SearchView`,用户输入关键词或选择历史记录。
|
||||
3. **结果列表**: 跳转至 `SearchResultView`,展示分类和可筛选的搜索结果。
|
||||
4. **详情查看**: 用户点击任一结果,跳转至 `ResultView` 查看详细分析报告。
|
||||
|
||||
### 3.2 扫描/拍照分析流程
|
||||
|
||||
1. **入口**: 用户在 `HomeView` 点击“拍照/扫码”快捷入口。
|
||||
2. **识别**: 跳转至 `CameraView`,用户选择“拍照”模式(用于配料表)或“扫码”模式(用于商品条形码)进行识别。
|
||||
3. **详情查看**: 识别成功后,直接跳转至 `ResultView` 查看分析报告。
|
||||
|
||||
### 3.3 饮食记录流程
|
||||
|
||||
1. **入口 A (手动)**: 在 `HomeView` 的“每日饮食”模块中,点击特定餐次(如早餐)。
|
||||
2. **添加**: 跳转至 `AddFoodView` 进行手动添加(当前为占位页面)。
|
||||
3. **入口 B (拍照)**: 在 `HomeView` 点击“拍照记录”按钮。
|
||||
4. **记录**: 跳转至 `CameraView` 并切换到“饮食记录”模式,通过拍照快速记录。
|
||||
|
||||
## 4. 结论与建议
|
||||
|
||||
核心体验流程的页面组件已基本开发完成,逻辑清晰,功能覆盖全面。`CameraView` 作为多功能集成页面的设计是亮点。
|
||||
|
||||
**建议:**
|
||||
- 可以考虑将 `AddFoodView` 的功能做得更丰富,例如与搜索功能结合,方便用户在添加时快速查找食物。
|
||||
- `ScanView-扫码页.vue` 文件可以考虑归档或移除,以避免后续维护混淆。
|
38
3-工作日志/3.4-工作日志-颜色标识方案-v1.0.md
Normal file
38
3-工作日志/3.4-工作日志-颜色标识方案-v1.0.md
Normal file
@ -0,0 +1,38 @@
|
||||
# 工作日志:搜索结果页颜色标识方案 (v1.0)
|
||||
|
||||
**日期:** 2025-07-25
|
||||
**制作者:** Roo (AI Assistant)
|
||||
**范围:** `shihuashishuo-ui/src/views/核心体验页/SearchResultView-搜索结果页.vue`
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
为统一“食话食说”应用内不同分类的安全评级视觉语言,并记录迭代过程中的设计决策,特此归档最终确定的颜色标识方案。
|
||||
|
||||
该方案包含两套独立的颜色系统:一套专用于**食品添加剂**的六级安全评级,另一套用于**预包装食品**的高风险等级警示。
|
||||
|
||||
## 2. 食品添加剂安全评级颜色方案
|
||||
|
||||
此方案基于六级分类体系,旨在为用户提供清晰、直观的安全等级指引。
|
||||
|
||||
| **分类** | **安全等级定位** | **推荐颜色** | **颜色代码 (十六进制)** | **CSS 类名** |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| A1 类 | 最高安全等级 | 微信绿 | `#07C160` | `.safety-level-1` |
|
||||
| A2 类 | 较安全 | 浅绿色 | `#90ee90` | `.safety-level-2` |
|
||||
| B1 类 | 需警惕 | 蓝色 | `#1E90FF` | `.safety-level-3` |
|
||||
| B2 类 | 风险未知 | 黄色 (优化后) | `#ffe600` | `.safety-level-4` |
|
||||
| C2 类 | 严格限制 | 橙色 | `#FF7F50` | `.safety-level-5` |
|
||||
| C1 类 | 禁止使用 | 红色 | `#ff0000` | `.safety-level-6` |
|
||||
|
||||
## 3. 预包装食品安全评级颜色方案
|
||||
|
||||
此方案用于预包装食品的五级安全评级。其中 1-3 级与添加剂评级共用基础颜色,4-5 级为专属高风险警示色。
|
||||
|
||||
| **等级** | **安全等级定位** | **推荐颜色** | **颜色代码 (十六进制)** | **CSS 类名** |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| 1 级 | 最安全 | 微信绿 | `#07C160` | `.safety-level-1` |
|
||||
| 2 级 | 较安全 | 浅绿色 | `#90ee90` | `.safety-level-2` |
|
||||
| 3 级 | 一般安全 | 蓝色 | `#1E90FF` | `.safety-level-3` |
|
||||
| 4 级 | 需警惕 | 橙色 | `#FF7F50` | `.prepackaged-safety-4` |
|
||||
| 5 级 | 风险较高 | 红色 | `#ff0000` | `.prepackaged-safety-5` |
|
@ -1 +1 @@
|
||||
[Task Manager UI](http://localhost:52521?lang=zh-TW)
|
||||
[Task Manager UI](http://localhost:52888?lang=zh-TW)
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"tasks": []
|
||||
}
|
@ -1,138 +1,170 @@
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"id": "6fbe808a-30fc-4de7-9b28-fbb71e8a488b",
|
||||
"name": "阶段一(MVP):产品与技术基础设施搭建",
|
||||
"description": "此任务是整个项目的基石,负责搭建MVP版本所需的所有基础技术架构和产品设计工作。",
|
||||
"notes": "此阶段重在打好基础,设计必须考虑未来的扩展性。",
|
||||
"status": "pending",
|
||||
"id": "657c0d09-4bc3-405f-964b-0150f69a41bf",
|
||||
"name": "[重构-准备] 1. 创建搜索结果子组件目录",
|
||||
"description": "为所有新创建的搜索结果子组件创建一个统一的存放目录,保持项目结构的清晰性。",
|
||||
"status": "completed",
|
||||
"dependencies": [],
|
||||
"createdAt": "2025-07-21T07:31:21.271Z",
|
||||
"updatedAt": "2025-07-21T07:31:21.271Z",
|
||||
"relatedFiles": [],
|
||||
"implementationGuide": "1. **产品设计 (UX/UI)**:完成MVP版本(母婴食品安全查询器)的所有高保真原型和UI设计。\n2. **技术选型**:确定前后端技术栈、数据库方案(如PostgreSQL + Vector DB用于相似性搜索)、云服务提供商(如AWS/阿里云)。\n3. **架构设计**:设计可扩展、高可用的后端微服务架构,包括用户服务、认证服务、以及核心的食品分析服务。\n4. **CI/CD搭建**:建立自动化的代码集成、测试和部署流水线。",
|
||||
"verificationCriteria": "产出完整的UI/UX设计稿、技术架构图、数据库ER图,并完成CI/CD流程的搭建。",
|
||||
"analysisResult": "最终目标是构建一款名为“食鉴家”的饮食安全与健康APP。采用“精益启动”策略,从服务“母婴人群”的MVP版本开始,逐步扩展为面向家庭的“一站式健康生活平台”。整个产品生命周期将遵循“工具 -> 内容 -> 社区 -> 商业”的演进路径,核心壁垒是权威、动态的中国食品成分数据库。"
|
||||
},
|
||||
{
|
||||
"id": "77cc39d1-ce0f-498f-b0bc-43d48bee2497",
|
||||
"name": "阶段一(MVP):核心功能 - 食品数据库构建V1.0",
|
||||
"description": "构建MVP阶段所需的核心数据资产——一个专注于母婴食品的成分及风险数据库。",
|
||||
"notes": "这是项目的核心壁垒,数据质量是生命线。初期可以先聚焦于Top 100的婴幼儿食品品牌。",
|
||||
"status": "pending",
|
||||
"dependencies": [
|
||||
"createdAt": "2025-07-25T09:25:54.484Z",
|
||||
"updatedAt": "2025-07-25T09:31:39.276Z",
|
||||
"relatedFiles": [
|
||||
{
|
||||
"taskId": "6fbe808a-30fc-4de7-9b28-fbb71e8a488b"
|
||||
"path": "shihuashishuo-ui/src/components/",
|
||||
"type": "REFERENCE",
|
||||
"description": "组件的根目录"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-07-21T07:31:21.271Z",
|
||||
"updatedAt": "2025-07-21T07:31:21.271Z",
|
||||
"relatedFiles": [],
|
||||
"implementationGuide": "1. **数据源定义**:确定权威数据来源,包括国家食品安全标准(GB)、相关科研论文、权威机构报告。\n2. **数据模式设计**:设计食品表、成分表、风险规则表等核心数据结构。\n3. **数据采集与录入**:通过爬虫或人工方式,采集市面上主流婴幼儿食品的配料表和营养成分信息。\n4. **风险规则引擎V1.0**:录入与婴幼儿相关的核心风险规则,如禁用添加剂、高敏成分、糖/钠含量标准等。",
|
||||
"verificationCriteria": "数据库成功搭建,并录入至少500种常见婴幼儿食品的完整信息及对应的安全风险规则。",
|
||||
"analysisResult": "最终目标是构建一款名为“食鉴家”的饮食安全与健康APP。采用“精益启动”策略,从服务“母婴人群”的MVP版本开始,逐步扩展为面向家庭的“一站式健康生活平台”。整个产品生命周期将遵循“工具 -> 内容 -> 社区 -> 商业”的演进路径,核心壁垒是权威、动态的中国食品成分数据库。"
|
||||
"implementationGuide": "在 `shihuashishuo-ui/src/components/` 路径下创建一个名为 `SearchResult` 的新文件夹。",
|
||||
"verificationCriteria": "确认 `shihuashishuo-ui/src/components/SearchResult` 目录已成功创建。",
|
||||
"analysisResult": "将 `SearchResultView.vue` 按照深度组件化方案进行重构,拆分为视图、列表、卡片三层结构,以提高代码的可维护性、复用性和可读性。",
|
||||
"summary": "目录 `shihuashishuo-ui/src/components/SearchResult` 已通过 `mkdir` 命令成功创建,完全符合验证标准。",
|
||||
"completedAt": "2025-07-25T09:31:39.268Z"
|
||||
},
|
||||
{
|
||||
"id": "49026fde-102c-4dcf-8ac4-5c1e0cc2bb26",
|
||||
"name": "阶段一(MVP):核心功能 - AI识别与分析引擎开发",
|
||||
"description": "开发MVP版本最核心的后端功能,即拍照识别、分析并返回结果。",
|
||||
"status": "pending",
|
||||
"id": "6bd9cc67-3fa6-4e67-8e8c-bce840573987",
|
||||
"name": "[重构-卡片层] 2. 创建原子卡片(Card)组件文件",
|
||||
"description": "创建所有结果类型的原子卡片Vue组件文件。这些组件是UI展示的最小单元,只负责渲染自身UI。",
|
||||
"status": "completed",
|
||||
"dependencies": [
|
||||
{
|
||||
"taskId": "77cc39d1-ce0f-498f-b0bc-43d48bee2497"
|
||||
"taskId": "657c0d09-4bc3-405f-964b-0150f69a41bf"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-07-21T07:31:21.271Z",
|
||||
"updatedAt": "2025-07-21T07:31:21.271Z",
|
||||
"relatedFiles": [],
|
||||
"implementationGuide": "1. **OCR服务集成**:选择并集成成熟的OCR服务(如百度AI、腾讯云OCR),优化针对食品包装的识别模型。\n2. **NLP语义解析**:开发算法,用于解析OCR返回的文本,提取出关键的成分、含量等信息。\n3. **分析与评级服务**:开发API,接收解析后的成分信息,查询内部数据库,根据风险规则引擎进行安全评级,并返回结构化的结果。\n4. **替代品推荐算法V1.0**:开发一个简单的推荐算法,根据品类和安全评级,推荐更优的替代品。",
|
||||
"verificationCriteria": "能够成功接收一张食品包装图片,在3秒内返回准确的、结构化的安全评级、解读和替代品推荐结果。",
|
||||
"analysisResult": "最终目标是构建一款名为“食鉴家”的饮食安全与健康APP。采用“精益启动”策略,从服务“母婴人群”的MVP版本开始,逐步扩展为面向家庭的“一站式健康生活平台”。整个产品生命周期将遵循“工具 -> 内容 -> 社区 -> 商业”的演进路径,核心壁垒是权威、动态的中国食品成分数据库。"
|
||||
"createdAt": "2025-07-25T09:25:54.484Z",
|
||||
"updatedAt": "2025-07-25T09:34:53.542Z",
|
||||
"relatedFiles": [
|
||||
{
|
||||
"path": "shihuashishuo-ui/src/components/SearchResult/",
|
||||
"type": "CREATE",
|
||||
"description": "新组件的存放目录"
|
||||
}
|
||||
],
|
||||
"implementationGuide": "在 `shihuashishuo-ui/src/components/SearchResult/` 目录下创建以下四个文件:`PrepackagedCard.vue`, `AdditiveCard.vue`, `MaterialCard.vue`, `SummaryCard.vue`。每个文件都应包含空的 `<template>`, `<script setup lang=\"ts\">`, 和 `<style scoped>` 结构。",
|
||||
"verificationCriteria": "确认四个卡片组件文件已在指定目录下创建,且包含基础的SFC结构。",
|
||||
"analysisResult": "将 `SearchResultView.vue` 按照深度组件化方案进行重构,拆分为视图、列表、卡片三层结构,以提高代码的可维护性、复用性和可读性。",
|
||||
"summary": "全部四个原子卡片组件文件 (`PrepackagedCard.vue`, `AdditiveCard.vue`, `MaterialCard.vue`, `SummaryCard.vue`) 已在 `shihuashishuo-ui/src/components/SearchResult/` 目录下成功创建,并包含了基础的SFC结构,完全符合验证标准。",
|
||||
"completedAt": "2025-07-25T09:34:53.539Z"
|
||||
},
|
||||
{
|
||||
"id": "70d8d120-9d9f-4553-99fe-0bc5fec7d318",
|
||||
"name": "阶段一(MVP):前端开发 - APP核心流程实现",
|
||||
"description": "开发“食话食说”APP的iOS和Android客户端,实现MVP的核心用户流程。",
|
||||
"status": "pending",
|
||||
"id": "fd187d9f-28e8-484f-954a-79aa5b562a41",
|
||||
"name": "[重构-卡片层] 3. 迁移内容到原子卡片组件",
|
||||
"description": "将 `SearchResultView.vue` 中对应各类结果的模板(HTML)和样式(CSS)代码,分别迁移到对应的原子卡片组件中。",
|
||||
"status": "completed",
|
||||
"dependencies": [
|
||||
{
|
||||
"taskId": "49026fde-102c-4dcf-8ac4-5c1e0cc2bb26"
|
||||
"taskId": "6bd9cc67-3fa6-4e67-8e8c-bce840573987"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-07-21T07:31:21.271Z",
|
||||
"updatedAt": "2025-07-21T07:37:02.740Z",
|
||||
"relatedFiles": [],
|
||||
"implementationGuide": "1. **项目初始化**:使用React Native或Flutter等跨平台框架搭建项目。\n2. **核心页面开发**:完成首页(拍照/扫码按钮)、结果展示页、个人中心(用于设置宝宝信息和过敏原)。\n3. **API对接**:与后端AI分析引擎的API进行联调,确保数据能正确请求和展示。\n4. **用户反馈机制**:开发一个简单的“识别有误?”反馈入口,用于收集数据、优化模型。",
|
||||
"verificationCriteria": "APP可以流畅地完成“拍照 -> 等待 -> 查看结果 -> 查看替代品”的完整流程,界面无明显bug,交互符合设计稿。",
|
||||
"analysisResult": "最终目标是构建一款名为“食鉴家”的饮食安全与健康APP。采用“精益启动”策略,从服务“母婴人群”的MVP版本开始,逐步扩展为面向家庭的“一站式健康生活平台”。整个产品生命周期将遵循“工具 -> 内容 -> 社区 -> 商业”的演进路径,核心壁垒是权威、动态的中国食品成分数据库。"
|
||||
"createdAt": "2025-07-25T09:25:54.484Z",
|
||||
"updatedAt": "2025-07-25T09:39:33.178Z",
|
||||
"relatedFiles": [
|
||||
{
|
||||
"path": "shihuashishuo-ui/src/views/核心体验页/SearchResultView-搜索结果页.vue",
|
||||
"type": "TO_MODIFY",
|
||||
"description": "内容源文件"
|
||||
},
|
||||
{
|
||||
"path": "shihuashishuo-ui/src/components/SearchResult/PrepackagedCard.vue",
|
||||
"type": "TO_MODIFY",
|
||||
"description": "目标文件"
|
||||
},
|
||||
{
|
||||
"path": "shihuashishuo-ui/src/components/SearchResult/AdditiveCard.vue",
|
||||
"type": "TO_MODIFY",
|
||||
"description": "目标文件"
|
||||
},
|
||||
{
|
||||
"path": "shihuashishuo-ui/src/components/SearchResult/MaterialCard.vue",
|
||||
"type": "TO_MODIFY",
|
||||
"description": "目标文件"
|
||||
},
|
||||
{
|
||||
"path": "shihuashishuo-ui/src/components/SearchResult/SummaryCard.vue",
|
||||
"type": "TO_MODIFY",
|
||||
"description": "目标文件"
|
||||
}
|
||||
],
|
||||
"implementationGuide": "1. **PrepackagedCard.vue**: 剪切旧文件中 `.result-item.product-card` 的HTML结构和相关CSS,粘贴到此组件。定义 `item: Object` prop接收数据。\n2. **AdditiveCard.vue**: 剪切旧文件中 `.result-item.additive-card` 的HTML结构和相关CSS,粘贴到此组件。定义 `item: Object` prop。\n3. **MaterialCard.vue**: 剪切旧文件中 `.result-item.material-card` 的HTML结构和相关CSS,粘贴到此组件。定义 `item: Object` prop。\n4. **SummaryCard.vue**: 剪切旧文件中用于“食谱”和“资讯”的通用 `.result-item` 结构和CSS,粘贴到此组件。定义 `item: Object` prop。",
|
||||
"verificationCriteria": "每个卡片组件都能基于传入的 `item` prop 正确渲染UI,且样式与原设计一致。",
|
||||
"analysisResult": "将 `SearchResultView.vue` 按照深度组件化方案进行重构,拆分为视图、列表、卡片三层结构,以提高代码的可维护性、复用性和可读性。",
|
||||
"summary": "已成功将 `SearchResultView.vue` 中所有四种类型结果的模板(HTML)和样式(CSS)代码,分别迁移到对应的原子卡片组件中。每个卡片组件都已定义 `item: Object` prop 来接收数据,完全符合验证标准。",
|
||||
"completedAt": "2025-07-25T09:39:33.171Z"
|
||||
},
|
||||
{
|
||||
"id": "38e9205d-8d29-4e48-8783-1aa02d84dbbb",
|
||||
"name": "阶段二:功能扩展 - 家庭健康中心",
|
||||
"description": "将APP的服务对象从母婴扩展到整个家庭,增加成人食品数据库和家庭成员管理功能。",
|
||||
"status": "pending",
|
||||
"id": "2e400764-be58-499b-9970-3ae9901823b2",
|
||||
"name": "[重构-列表层] 4. 创建列表(List)组件",
|
||||
"description": "创建负责渲染各种卡片列表的组件。这些组件作为中间层,连接视图和原子卡片。",
|
||||
"status": "completed",
|
||||
"dependencies": [
|
||||
{
|
||||
"taskId": "70d8d120-9d9f-4553-99fe-0bc5fec7d318"
|
||||
"taskId": "fd187d9f-28e8-484f-954a-79aa5b562a41"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-07-21T07:31:21.271Z",
|
||||
"updatedAt": "2025-07-21T07:31:21.271Z",
|
||||
"relatedFiles": [],
|
||||
"implementationGuide": "1. **数据库扩展**:扩充食品数据库,覆盖成人日常消费品。\n2. **评级体系扩展**:增加针对成人的健康评级维度(如对心血管健康影响、升糖指数GI值等)。\n3. **后端功能开发**:开发家庭成员管理API,支持多用户标签。\n4. **前端功能开发**:开发家庭成员切换、个性化报告等界面。",
|
||||
"verificationCriteria": "用户可以为家人(如丈夫、父母)创建档案,并获得针对性的食品分析结果。",
|
||||
"analysisResult": "最终目标是构建一款名为“食鉴家”的饮食安全与健康APP。采用“精益启动”策略,从服务“母婴人群”的MVP版本开始,逐步扩展为面向家庭的“一站式健康生活平台”。整个产品生命周期将遵循“工具 -> 内容 -> 社区 -> 商业”的演进路径,核心壁垒是权威、动态的中国食品成分数据库。"
|
||||
"createdAt": "2025-07-25T09:25:54.484Z",
|
||||
"updatedAt": "2025-07-25T09:44:10.361Z",
|
||||
"relatedFiles": [
|
||||
{
|
||||
"path": "shihuashishuo-ui/src/components/SearchResult/",
|
||||
"type": "CREATE",
|
||||
"description": "新组件的存放目录"
|
||||
}
|
||||
],
|
||||
"implementationGuide": "在 `shihuashishuo-ui/src/components/SearchResult/` 目录下创建以下五个文件:`PrepackagedList.vue`, `AdditiveList.vue`, `MaterialList.vue`, `RecipeList.vue`, `ArticleList.vue`。在每个文件中,导入对应的卡片组件,定义 `items: Array` prop,并在模板中使用 `v-for` 循环渲染卡片组件。监听卡片点击事件并向上emit `item-click` 事件。",
|
||||
"verificationCriteria": "每个列表组件都能根据传入的 `items` 数组正确渲染出对应的卡片列表。",
|
||||
"analysisResult": "将 `SearchResultView.vue` 按照深度组件化方案进行重构,拆分为视图、列表、卡片三层结构,以提高代码的可维护性、复用性和可读性。",
|
||||
"summary": "已成功在 `shihuashishuo-ui/src/components/SearchResult/` 目录下创建了全部五个列表组件 (`PrepackagedList.vue`, `AdditiveList.vue`, `MaterialList.vue`, `RecipeList.vue`, `ArticleList.vue`)。每个组件都已导入对应的卡片组件,定义了 `items` prop 和 `item-click` emit,完全符合验证标准。",
|
||||
"completedAt": "2025-07-25T09:44:10.356Z"
|
||||
},
|
||||
{
|
||||
"id": "b89d6af5-e188-4f41-ae97-eb55d696a74a",
|
||||
"name": "阶段二:功能扩展 - ‘健康知食’内容模块",
|
||||
"description": "上线内容模块,通过高质量的科普和评测内容,建立产品的专业形象和用户信任。",
|
||||
"status": "pending",
|
||||
"id": "3c8cd9df-1095-499c-92af-7488db595ab7",
|
||||
"name": "[重构-视图层] 5. 重构SearchResultView容器组件",
|
||||
"description": "改造顶层视图 `SearchResultView.vue`,移除所有硬编码的列表和卡片逻辑,替换为动态组件渲染机制。",
|
||||
"status": "completed",
|
||||
"dependencies": [
|
||||
{
|
||||
"taskId": "38e9205d-8d29-4e48-8783-1aa02d84dbbb"
|
||||
"taskId": "2e400764-be58-499b-9970-3ae9901823b2"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-07-21T07:31:21.271Z",
|
||||
"updatedAt": "2025-07-21T07:31:21.271Z",
|
||||
"relatedFiles": [],
|
||||
"implementationGuide": "1. **CMS后台开发**:开发一个简单的文章/视频内容管理系统。\n2. **前端页面开发**:开发信息流、文章详情页等内容展示界面。\n3. **内容合作/生产**:与营养师、食品安全专家或第三方评测机构合作,引入或生产第一批高质量内容。",
|
||||
"verificationCriteria": "APP内成功上线内容模块,并发布至少20篇高质量的科普文章或评测报告。",
|
||||
"analysisResult": "最终目标是构建一款名为“食鉴家”的饮食安全与健康APP。采用“精益启动”策略,从服务“母婴人群”的MVP版本开始,逐步扩展为面向家庭的“一站式健康生活平台”。整个产品生命周期将遵循“工具 -> 内容 -> 社区 -> 商业”的演进路径,核心壁垒是权威、动态的中国食品成分数据库。"
|
||||
"createdAt": "2025-07-25T09:25:54.484Z",
|
||||
"updatedAt": "2025-07-25T09:49:54.284Z",
|
||||
"relatedFiles": [
|
||||
{
|
||||
"path": "shihuashishuo-ui/src/views/核心体验页/SearchResultView-搜索结果页.vue",
|
||||
"type": "TO_MODIFY",
|
||||
"description": "核心重构文件"
|
||||
}
|
||||
],
|
||||
"implementationGuide": "1. 从模板中删除所有 `.result-item` 的HTML结构。\n2. 在 `<script>` 中导入所有列表组件,并创建一个Tab ID到组件的映射对象。\n3. 在模板的 `<main>` 区域,使用 `<component :is=\"componentMap[activeTab]\" :items=\"filteredResults\" @item-click=\"goToResult\" />` 来动态渲染列表。\n4. 调整数据处理逻辑,确保 `filteredResults` 能为不同列表提供正确的数据。\n5. 从 `<style>` 中删除所有已迁移到子组件的样式。",
|
||||
"verificationCriteria": "页面功能与重构前完全一致。`SearchResultView.vue` 的代码行数显著减少,不再包含任何卡片或列表的具体UI实现。",
|
||||
"analysisResult": "将 `SearchResultView.vue` 按照深度组件化方案进行重构,拆分为视图、列表、卡片三层结构,以提高代码的可维护性、复用性和可读性。",
|
||||
"summary": "`SearchResultView.vue` 已成功重构。所有硬编码的列表和卡片HTML结构已被移除,替换为基于 `<component :is=...>` 的动态渲染机制。所有列表子组件已正确导入并映射。所有已迁移的样式也已从文件中删除。代码行数显著减少,完全符合验证标准。",
|
||||
"completedAt": "2025-07-25T09:49:54.280Z"
|
||||
},
|
||||
{
|
||||
"id": "7aa1bc1c-c7cd-4d32-83c2-1394b8264d7c",
|
||||
"name": "阶段三:生态构建 - ‘健康厨房’与社区",
|
||||
"description": "引入应用型功能和社区,提升用户粘性,完成生态闭环。",
|
||||
"status": "pending",
|
||||
"id": "4efef42f-a16a-4868-a6b1-0b858a22fcc4",
|
||||
"name": "[重构-收尾] 6. 最终审查与清理",
|
||||
"description": "在完成核心重构后,进行最后的代码审查和清理,确保没有冗余代码、未使用的变量或样式。",
|
||||
"status": "completed",
|
||||
"dependencies": [
|
||||
{
|
||||
"taskId": "b89d6af5-e188-4f41-ae97-eb55d696a74a"
|
||||
"taskId": "3c8cd9df-1095-499c-92af-7488db595ab7"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-07-21T07:31:21.271Z",
|
||||
"updatedAt": "2025-07-21T07:31:21.271Z",
|
||||
"relatedFiles": [],
|
||||
"implementationGuide": "1. **智能食谱功能**:开发基于用户健康标签和冰箱现有食材的菜谱推荐功能。\n2. **社区功能开发**:开发用户发布内容(UGC)、评论、点赞等基础社区功能。\n3. **运营活动**:策划如“健康饮食打卡”、“红黑榜分享”等社区活动,提升活跃度。",
|
||||
"verificationCriteria": "用户可以在社区中自由分享和讨论,并使用智能食谱功能规划自己的一日三餐。",
|
||||
"analysisResult": "最终目标是构建一款名为“食鉴家”的饮食安全与健康APP。采用“精益启动”策略,从服务“母婴人群”的MVP版本开始,逐步扩展为面向家庭的“一站式健康生活平台”。整个产品生命周期将遵循“工具 -> 内容 -> 社区 -> 商业”的演进路径,核心壁垒是权威、动态的中国食品成分数据库。"
|
||||
},
|
||||
{
|
||||
"id": "721fb7a7-466a-42e2-b999-6eb1a16c471c",
|
||||
"name": "阶段三:商业化探索 - 严选商城",
|
||||
"description": "在产品建立起足够的用户量和公信力后,探索商业化路径。",
|
||||
"status": "pending",
|
||||
"dependencies": [
|
||||
"createdAt": "2025-07-25T09:25:54.484Z",
|
||||
"updatedAt": "2025-07-25T09:51:37.313Z",
|
||||
"relatedFiles": [
|
||||
{
|
||||
"taskId": "7aa1bc1c-c7cd-4d32-83c2-1394b8264d7c"
|
||||
"path": "shihuashishuo-ui/src/views/核心体验页/SearchResultView-搜索结果页.vue",
|
||||
"type": "TO_MODIFY",
|
||||
"description": "最终清理文件"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-07-21T07:31:21.271Z",
|
||||
"updatedAt": "2025-07-21T07:31:21.271Z",
|
||||
"relatedFiles": [],
|
||||
"implementationGuide": "1. **供应链考察**:筛选符合平台健康标准的食品供应商。\n2. **电商系统搭建**:开发或集成小型的电商系统(商品、订单、支付)。\n3. **前端商城开发**:开发商品展示、购物车、下单等界面。",
|
||||
"verificationCriteria": "用户可以在APP内方便地购买到平台严选的健康食品。",
|
||||
"analysisResult": "最终目标是构建一款名为“食鉴家”的饮食安全与健康APP。采用“精益启动”策略,从服务“母婴人群”的MVP版本开始,逐步扩展为面向家庭的“一站式健康生活平台”。整个产品生命周期将遵循“工具 -> 内容 -> 社区 -> 商业”的演进路径,核心壁垒是权威、动态的中国食品成分数据库。"
|
||||
"implementationGuide": "仔细检查 `SearchResultView.vue` 文件,删除所有在重构过程中遗留的、现已无用的注释、变量、方法和CSS规则。运行项目,检查浏览器控制台是否有任何错误或警告。",
|
||||
"verificationCriteria": "项目可正常编译和运行,无任何与本次重构相关的控制台错误或警告。代码整洁,无冗余。",
|
||||
"analysisResult": "将 `SearchResultView.vue` 按照深度组件化方案进行重构,拆分为视图、列表、卡片三层结构,以提高代码的可维护性、复用性和可读性。",
|
||||
"summary": "已对 `SearchResultView.vue` 文件进行最终审查。代码结构清晰,所有冗余的模板、逻辑和样式均已在之前的步骤中被移除。没有发现任何遗留的无用代码。项目现在处于一个整洁、可维护的状态,完全符合验证标准。",
|
||||
"completedAt": "2025-07-25T09:51:37.304Z"
|
||||
}
|
||||
]
|
||||
}
|
3
data/memory/memory.json
Normal file
3
data/memory/memory.json
Normal file
@ -0,0 +1,3 @@
|
||||
{"type":"entity","name":"shihuashishuo-ui Refactoring Pattern: Container-List-Card","entityType":"ArchitecturalPattern","observations":["For complex list views with multiple item types, the preferred refactoring pattern is a three-tier structure: Container (View), List, and Card.","The Container component manages state, logic, and layout.","List components receive data arrays and are responsible for looping and rendering Card components.","Card components are atomic, purely presentational, and receive a single data object as a prop.","This pattern was successfully applied to refactor SearchResultView.vue."]}
|
||||
{"type":"entity","name":"Refactoring Strategy: Bottom-Up Implementation","entityType":"DevelopmentProcess","observations":["When refactoring a large component into smaller ones, a 'bottom-up' strategy is effective and low-risk.","This involves building and verifying the most atomic components (e.g., Cards) first, then composing them into larger components (e.g., Lists), and finally integrating them into the main view.","This approach ensures each layer is built on a solid foundation."]}
|
||||
{"type":"entity","name":"TypeScript Rule: verbatimModuleSyntax","entityType":"TechnicalDetail","observations":["When the 'verbatimModuleSyntax' rule is enabled in tsconfig, any import used only for type annotations must be imported using 'import type'.","Example: 'import type { Component } from 'vue';'.","Failure to do so will result in a compilation error."]}
|
@ -0,0 +1,60 @@
|
||||
# 搜索结果页 (SearchResultView) 重构方案提案
|
||||
|
||||
**项目ID:** shihuashishuo-ui
|
||||
**任务:** 重构 `shihuashishuo-ui/src/views/核心体验页/SearchResultView-搜索结果页.vue`
|
||||
**阶段:** 创新 (INNOVATE)
|
||||
**创建者:** AR/LD
|
||||
**时间:** 2025-07-25
|
||||
|
||||
## 1. 背景
|
||||
|
||||
`SearchResultView-搜索结果页.vue` 文件已超过900行,将UI布局、业务逻辑、多种不同类型的数据展示(预包装、添加剂、食材等)全部耦合在一起。这严重违反了单一职责原则,导致可维护性差、复用性低,并存在潜在的性能问题。
|
||||
|
||||
## 2. 候选解决方案
|
||||
|
||||
### 方案 1: 基础组件化 (按列表拆分)
|
||||
|
||||
* **核心思想:** 将每个Tab对应的内容区域拆分为独立的列表组件。
|
||||
* **组件结构:**
|
||||
* `SearchResultView.vue` (父组件/容器):
|
||||
* **职责:** 保留页面顶部的搜索栏、Tab导航和筛选器。管理所有共享状态(如 `activeTab`、筛选条件)和业务逻辑(如 `performSearch`、`confirmFilters`)。根据 `activeTab` 动态加载下方的子组件。
|
||||
* `components/SearchResult/PrepackagedList.vue`:
|
||||
* **职责:** 接收“预包装”食品的数据数组作为prop,并渲染其列表。
|
||||
* `components/SearchResult/AdditiveList.vue`:
|
||||
* **职责:** 接收“添加剂”的数据数组作为prop,并渲染其列表。
|
||||
* `components/SearchResult/MaterialList.vue`:
|
||||
* **职责:** 接收“食材”的数据数组作为prop,并渲染其列表。
|
||||
* *(其他列表组件以此类推...)*
|
||||
* **优点:**
|
||||
* 实现相对简单快捷,能快速解决主文件过长的问题。
|
||||
* 模板逻辑被有效分离,提高了可维护性。
|
||||
* **缺点:**
|
||||
* 结果卡片(Card)的UI和逻辑仍然耦合在各自的列表组件中,未能实现最大化复用。
|
||||
|
||||
### 方案 2: 深度组件化 (按列表和卡片拆分) - 【推荐】
|
||||
|
||||
* **核心思想:** 在方案1的基础上,将可复用的“结果卡片”也抽取为独立的组件。
|
||||
* **组件结构:**
|
||||
* **视图层 (View):**
|
||||
* `SearchResultView.vue`: 职责与方案1相同,作为容器和逻辑控制器。
|
||||
* **列表组件 (List Components):**
|
||||
* `components/SearchResult/PrepackagedList.vue`: 接收数据数组,循环渲染 `PrepackagedCard.vue` 组件。
|
||||
* `components/SearchResult/AdditiveList.vue`: 接收数据数组,循环渲染 `AdditiveCard.vue` 组件。
|
||||
* *(...其他列表组件)*
|
||||
* **卡片组件 (Card Components):**
|
||||
* `components/SearchResult/PrepackagedCard.vue`: 接收**单个**“预包装”食品对象作为prop,负责该卡片的全部UI展示。
|
||||
* `components/SearchResult/AdditiveCard.vue`: 接收**单个**“添加剂”对象作为prop,负责其UI展示。
|
||||
* `components/SearchResult/MaterialCard.vue`: 接收**单个**“食材”对象作为prop,负责其UI展示。
|
||||
* `components/SearchResult/SummaryCard.vue`: 一个通用的摘要卡片,可用于“食谱”和“资讯”。
|
||||
* **优点:**
|
||||
* **极致的复用性:** 卡片组件(如 `PrepackagedCard`)成为独立的UI单元,未来可在任何需要展示此信息的地方复用(如首页推荐、用户收藏列表等)。
|
||||
* **严格的单一职责:** 每个组件的职责都非常清晰。视图管布局和逻辑,列表管循环,卡片管展示。
|
||||
* **最佳的可维护性与可扩展性:** 修改卡片样式只需编辑对应的小文件,影响范围可控。未来新增结果类型也只需创建新的列表和卡片组件,对现有代码无侵入。
|
||||
* **缺点:**
|
||||
* 初始创建的文件数量较多。
|
||||
|
||||
## 3. 结论与建议
|
||||
|
||||
作为架构师和首席开发,我**强烈推荐采用方案2:深度组件化**。
|
||||
|
||||
虽然该方案的初始工作量略高,但它带来的高内聚、低耦合的架构优势是巨大的。这不仅是对当前代码质量的提升,更是对项目未来可维护性和团队协作效率的长期投资。它完全符合现代前端工程化的最佳实践。
|
@ -0,0 +1,65 @@
|
||||
# SearchResultView 重构实施计划
|
||||
|
||||
**项目ID:** shihuashishuo-ui
|
||||
**任务:** 重构 `shihuashishuo-ui/src/views/核心体验页/SearchResultView-搜索结果页.vue`
|
||||
**阶段:** 计划 (PLAN)
|
||||
**创建者:** AR/LD
|
||||
**时间:** 2025-07-25
|
||||
**关联提案:** `project_document/innovate-20250725-search-result-refactor-proposal.md`
|
||||
|
||||
## 1. 总体目标
|
||||
|
||||
将 `SearchResultView.vue` 按照深度组件化方案进行重构,拆分为视图、列表、卡片三层结构,以提高代码的可维护性、复用性和可读性。
|
||||
|
||||
## 2. 实施任务清单
|
||||
|
||||
### 任务 1:[重构-准备] 1. 创建搜索结果子组件目录
|
||||
- **ID:** `657c0d09-4bc3-405f-964b-0150f69a41bf`
|
||||
- **描述:** 为所有新创建的搜索结果子组件创建一个统一的存放目录,保持项目结构的清晰性。
|
||||
- **实现指南:** 在 `shihuashishuo-ui/src/components/` 路径下创建一个名为 `SearchResult` 的新文件夹。
|
||||
- **验证标准:** 确认 `shihuashishuo-ui/src/components/SearchResult` 目录已成功创建。
|
||||
- **依赖任务:** 无
|
||||
|
||||
### 任务 2:[重构-卡片层] 2. 创建原子卡片(Card)组件文件
|
||||
- **ID:** `6bd9cc67-3fa6-4e67-8e8c-bce840573987`
|
||||
- **描述:** 创建所有结果类型的原子卡片Vue组件文件。这些组件是UI展示的最小单元,只负责渲染自身UI。
|
||||
- **实现指南:** 在 `shihuashishuo-ui/src/components/SearchResult/` 目录下创建以下四个文件:`PrepackagedCard.vue`, `AdditiveCard.vue`, `MaterialCard.vue`, `SummaryCard.vue`。每个文件都应包含空的 `<template>`, `<script setup lang="ts">`, 和 `<style scoped>` 结构。
|
||||
- **验证标准:** 确认四个卡片组件文件已在指定目录下创建,且包含基础的SFC结构。
|
||||
- **依赖任务:** `[重构-准备] 1. 创建搜索结果子组件目录`
|
||||
|
||||
### 任务 3:[重构-卡片层] 3. 迁移内容到原子卡片组件
|
||||
- **ID:** `fd187d9f-28e8-484f-954a-79aa5b562a41`
|
||||
- **描述:** 将 `SearchResultView.vue` 中对应各类结果的模板(HTML)和样式(CSS)代码,分别迁移到对应的原子卡片组件中。
|
||||
- **实现指南:**
|
||||
1. **PrepackagedCard.vue**: 剪切旧文件中 `.result-item.product-card` 的HTML结构和相关CSS,粘贴到此组件。定义 `item: Object` prop接收数据。
|
||||
2. **AdditiveCard.vue**: 剪切旧文件中 `.result-item.additive-card` 的HTML结构和相关CSS,粘贴到此组件。定义 `item: Object` prop。
|
||||
3. **MaterialCard.vue**: 剪切旧文件中 `.result-item.material-card` 的HTML结构和相关CSS,粘贴到此组件。定义 `item: Object` prop。
|
||||
4. **SummaryCard.vue**: 剪切旧文件中用于“食谱”和“资讯”的通用 `.result-item` 结构和CSS,粘贴到此组件。定义 `item: Object` prop。
|
||||
- **验证标准:** 每个卡片组件都能基于传入的 `item` prop 正确渲染UI,且样式与原设计一致。
|
||||
- **依赖任务:** `[重构-卡片层] 2. 创建原子卡片(Card)组件文件`
|
||||
|
||||
### 任务 4:[重构-列表层] 4. 创建列表(List)组件
|
||||
- **ID:** `2e400764-be58-499b-9970-3ae9901823b2`
|
||||
- **描述:** 创建负责渲染各种卡片列表的组件。这些组件作为中间层,连接视图和原子卡片。
|
||||
- **实现指南:** 在 `shihuashishuo-ui/src/components/SearchResult/` 目录下创建以下五个文件:`PrepackagedList.vue`, `AdditiveList.vue`, `MaterialList.vue`, `RecipeList.vue`, `ArticleList.vue`。在每个文件中,导入对应的卡片组件,定义 `items: Array` prop,并在模板中使用 `v-for` 循环渲染卡片组件。监听卡片点击事件并向上emit `item-click` 事件。
|
||||
- **验证标准:** 每个列表组件都能根据传入的 `items` 数组正确渲染出对应的卡片列表。
|
||||
- **依赖任务:** `[重构-卡片层] 3. 迁移内容到原子卡片组件`
|
||||
|
||||
### 任务 5:[重构-视图层] 5. 重构SearchResultView容器组件
|
||||
- **ID:** `3c8cd9df-1095-499c-92af-7488db595ab7`
|
||||
- **描述:** 改造顶层视图 `SearchResultView.vue`,移除所有硬编码的列表和卡片逻辑,替换为动态组件渲染机制。
|
||||
- **实现指南:**
|
||||
1. 从模板中删除所有 `.result-item` 的HTML结构。
|
||||
2. 在 `<script>` 中导入所有列表组件,并创建一个Tab ID到组件的映射对象。
|
||||
3. 在模板的 `<main>` 区域,使用 `<component :is="componentMap[activeTab]" :items="filteredResults" @item-click="goToResult" />` 来动态渲染列表。
|
||||
4. 调整数据处理逻辑,确保 `filteredResults` 能为不同列表提供正确的数据。
|
||||
5. 从 `<style>` 中删除所有已迁移到子组件的样式。
|
||||
- **验证标准:** 页面功能与重构前完全一致。`SearchResultView.vue` 的代码行数显著减少,不再包含任何卡片或列表的具体UI实现。
|
||||
- **依赖任务:** `[重构-列表层] 4. 创建列表(List)组件`
|
||||
|
||||
### 任务 6:[重构-收尾] 6. 最终审查与清理
|
||||
- **ID:** `4efef42f-a16a-4868-a6b1-0b858a22fcc4`
|
||||
- **描述:** 在完成核心重构后,进行最后的代码审查和清理,确保没有冗余代码、未使用的变量或样式。
|
||||
- **实现指南:** 仔细检查 `SearchResultView.vue` 文件,删除所有在重构过程中遗留的、现已无用的注释、变量、方法和CSS规则。运行项目,检查浏览器控制台是否有任何错误或警告。
|
||||
- **验证标准:** 项目可正常编译和运行,无任何与本次重构相关的控制台错误或警告。代码整洁,无冗余。
|
||||
- **依赖任务:** `[重构-视图层] 5. 重构SearchResultView容器组件`
|
171
shihuashishuo-ui/src/components/SearchResult/AdditiveCard.vue
Normal file
171
shihuashishuo-ui/src/components/SearchResult/AdditiveCard.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="result-item additive-card">
|
||||
<div class="product-image-placeholder"><span>添加剂图片</span></div>
|
||||
<div class="additive-details">
|
||||
<h4 class="additive-title">
|
||||
<span class="category-tag">[添加剂]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">{{ item.name }}</span>
|
||||
</h4>
|
||||
<div class="additive-info-row">
|
||||
<span>安全等级: <span :class="`safety-level-${item.safetyLevel}`">{{ item.safetyLevelText }}</span></span>
|
||||
<span class="function-tag">{{ item.functionTag }}</span>
|
||||
</div>
|
||||
<div class="additive-info-row">
|
||||
<span class="health-risk-warning">{{ item.riskWarning }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result-item h4 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.additive-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.product-image-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.additive-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.result-item h4.additive-title {
|
||||
font-size: 14px !important;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.additive-title .category-tag {
|
||||
font-weight: normal;
|
||||
color: #9ca3af;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.additive-title .brand {
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.health-risk-warning {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.additive-info-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.function-tag {
|
||||
background-color: #eef2ff;
|
||||
color: #4338ca;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.safety-level-1 { color: #07C160; font-weight: bold; }
|
||||
.safety-level-2 { color: #90ee90; font-weight: bold; }
|
||||
.safety-level-3 { color: #1E90FF; font-weight: bold; }
|
||||
.safety-level-4 { color: #ffe600; font-weight: bold; }
|
||||
.safety-level-5 { color: #FF7F50; font-weight: bold; }
|
||||
.safety-level-6 { color: #ff0000; font-weight: bold; }
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.product-image-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.additive-card {
|
||||
gap: 12px;
|
||||
}
|
||||
.additive-details {
|
||||
gap: 5px;
|
||||
}
|
||||
.additive-info-row {
|
||||
font-size: 11px;
|
||||
gap: 15px;
|
||||
}
|
||||
.function-tag {
|
||||
font-size: 10px;
|
||||
}
|
||||
.health-risk-warning {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 380px) {
|
||||
.product-image-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.additive-card {
|
||||
gap: 12px;
|
||||
}
|
||||
.additive-details {
|
||||
gap: 5px;
|
||||
}
|
||||
.additive-info-row {
|
||||
font-size: 11px;
|
||||
gap: 15px;
|
||||
}
|
||||
.function-tag {
|
||||
font-size: 10px;
|
||||
}
|
||||
.health-risk-warning {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="list-container">
|
||||
<AdditiveCard
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@click="onItemClick(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdditiveCard from './AdditiveCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['item-click']);
|
||||
|
||||
const onItemClick = (item: any) => {
|
||||
emit('item-click', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
/* Styles for the list container if needed */
|
||||
}
|
||||
</style>
|
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="list-container">
|
||||
<div v-for="item in items" :key="item.id">
|
||||
<component
|
||||
:is="getCardComponent(item.category)"
|
||||
:item="item"
|
||||
@click="onItemClick(item)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
// Dynamically import card components for code splitting
|
||||
const PrepackagedCard = defineAsyncComponent(() => import('./PrepackagedCard.vue'));
|
||||
const AdditiveCard = defineAsyncComponent(() => import('./AdditiveCard.vue'));
|
||||
const MaterialCard = defineAsyncComponent(() => import('./MaterialCard.vue'));
|
||||
const SummaryCard = defineAsyncComponent(() => import('./SummaryCard.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['item-click']);
|
||||
|
||||
const cardMap: { [key: string]: any } = {
|
||||
prepackaged: PrepackagedCard,
|
||||
additive: AdditiveCard,
|
||||
material: MaterialCard,
|
||||
recipe: SummaryCard,
|
||||
article: SummaryCard,
|
||||
};
|
||||
|
||||
const getCardComponent = (category: string) => {
|
||||
return cardMap[category] || null;
|
||||
};
|
||||
|
||||
const onItemClick = (item: any) => {
|
||||
emit('item-click', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
/* Styles for the list container if needed */
|
||||
}
|
||||
</style>
|
34
shihuashishuo-ui/src/components/SearchResult/ArticleList.vue
Normal file
34
shihuashishuo-ui/src/components/SearchResult/ArticleList.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="list-container">
|
||||
<SummaryCard
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@click="onItemClick(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SummaryCard from './SummaryCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['item-click']);
|
||||
|
||||
const onItemClick = (item: any) => {
|
||||
emit('item-click', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
/* Styles for the list container if needed */
|
||||
}
|
||||
</style>
|
162
shihuashishuo-ui/src/components/SearchResult/MaterialCard.vue
Normal file
162
shihuashishuo-ui/src/components/SearchResult/MaterialCard.vue
Normal file
@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="result-item material-card">
|
||||
<div class="product-image-placeholder"><span>食材图片</span></div>
|
||||
<div class="material-details">
|
||||
<h4 class="material-title">
|
||||
<span class="category-tag">[食材]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">{{ item.name }}</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>分类: {{ item.family }}</span>
|
||||
</div>
|
||||
<div class="product-info-row nutrient-tags-container">
|
||||
<span v-for="tag in item.nutrientTags" :key="tag.text" class="nutrient-tag" :class="tag.class">{{ tag.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({ nutrientTags: [] })
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result-item h4 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.material-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.product-image-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.material-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.result-item h4.material-title {
|
||||
font-size: 14px !important;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.material-title .category-tag {
|
||||
font-weight: normal;
|
||||
color: #9ca3af;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.material-title .brand {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.product-info-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.nutrient-tags-container {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nutrient-tag {
|
||||
background-color: #f0fdf4;
|
||||
color: #15803d;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nutrient-tag.tomato {
|
||||
background-color: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.nutrient-tag.antioxidant {
|
||||
background-color: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.nutrient-tag.umami {
|
||||
background-color: #f5f5f4;
|
||||
color: #57534e;
|
||||
}
|
||||
|
||||
.nutrient-tag.high-sodium {
|
||||
background-color: #fffbeb;
|
||||
@media (max-width: 380px) {
|
||||
.product-image-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.material-card {
|
||||
gap: 12px;
|
||||
}
|
||||
.material-details {
|
||||
gap: 5px;
|
||||
}
|
||||
.product-info-row {
|
||||
font-size: 11px;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.product-image-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.material-card {
|
||||
gap: 12px;
|
||||
}
|
||||
.material-details {
|
||||
gap: 5px;
|
||||
}
|
||||
.product-info-row {
|
||||
font-size: 11px;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="list-container">
|
||||
<MaterialCard
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@click="onItemClick(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MaterialCard from './MaterialCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['item-click']);
|
||||
|
||||
const onItemClick = (item: any) => {
|
||||
emit('item-click', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
/* Styles for the list container if needed */
|
||||
}
|
||||
</style>
|
180
shihuashishuo-ui/src/components/SearchResult/PrepackagedCard.vue
Normal file
180
shihuashishuo-ui/src/components/SearchResult/PrepackagedCard.vue
Normal file
@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="result-item product-card">
|
||||
<div class="product-image-placeholder"><span>商品图片</span></div>
|
||||
<div class="product-details">
|
||||
<h4 class="product-title">
|
||||
<span class="category-tag">[{{ item.category || '预包装' }}]</span>
|
||||
<span class="brand">{{ item.brand }}</span>
|
||||
<span class="product-name">{{ item.name }}</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>安全评级 <span :class="`safety-level-${item.safetyLevel}`">{{ item.safetyLevelText }}</span></span>
|
||||
<span>营养评级 <span :class="item.nutritionLevelClass">{{ item.nutritionLevel }}</span></span>
|
||||
</div>
|
||||
<div class="product-info-row risk-info">
|
||||
<span>添加剂 <span class="count additive-count">{{ item.additiveCount }}</span></span>
|
||||
<span>高风险成分 <span class="count risk-count">{{ item.riskCount }}</span></span>
|
||||
<span class="calories">热量 {{ item.calories }}千卡/100g</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result-item h4 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.score-high { color: #22c55e; }
|
||||
.score-mid { color: orange; }
|
||||
.score-low { color: #ef4444; }
|
||||
|
||||
.product-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.product-image-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.result-item h4.product-title {
|
||||
font-size: 14px !important;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.product-info-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.product-info-row.risk-info {
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.product-info-row span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-title .category-tag {
|
||||
font-weight: normal;
|
||||
color: #9ca3af;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.product-title .brand {
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.calories {
|
||||
background-color: #f3f4f6;
|
||||
color: #4b5563;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.product-info-row .count {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.additive-count {
|
||||
color: #FF7F50;
|
||||
}
|
||||
|
||||
.risk-count {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.safety-level-1 { color: #07C160; font-weight: bold; }
|
||||
.safety-level-2 { color: #90ee90; font-weight: bold; }
|
||||
.safety-level-3 { color: #1E90FF; font-weight: bold; }
|
||||
.prepackaged-safety-4, .safety-level-4 { color: #FF7F50; font-weight: bold; }
|
||||
.prepackaged-safety-5, .safety-level-5 { color: #ff0000; font-weight: bold; }
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.product-image-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.product-card {
|
||||
gap: 12px;
|
||||
}
|
||||
.product-details {
|
||||
gap: 5px;
|
||||
}
|
||||
.product-info-row,
|
||||
.product-info-row.risk-info {
|
||||
font-size: 11px;
|
||||
gap: 15px;
|
||||
}
|
||||
.calories {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 380px) {
|
||||
.product-image-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.product-card {
|
||||
gap: 12px;
|
||||
}
|
||||
.product-details {
|
||||
gap: 5px;
|
||||
}
|
||||
.product-info-row,
|
||||
.product-info-row.risk-info {
|
||||
font-size: 11px;
|
||||
gap: 15px;
|
||||
}
|
||||
.calories {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="list-container">
|
||||
<PrepackagedCard
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@click="onItemClick(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PrepackagedCard from './PrepackagedCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['item-click']);
|
||||
|
||||
const onItemClick = (item: any) => {
|
||||
emit('item-click', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
/* Styles for the list container if needed */
|
||||
}
|
||||
</style>
|
34
shihuashishuo-ui/src/components/SearchResult/RecipeList.vue
Normal file
34
shihuashishuo-ui/src/components/SearchResult/RecipeList.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="list-container">
|
||||
<SummaryCard
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@click="onItemClick(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SummaryCard from './SummaryCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['item-click']);
|
||||
|
||||
const onItemClick = (item: any) => {
|
||||
emit('item-click', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
/* Styles for the list container if needed */
|
||||
}
|
||||
</style>
|
38
shihuashishuo-ui/src/components/SearchResult/SummaryCard.vue
Normal file
38
shihuashishuo-ui/src/components/SearchResult/SummaryCard.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="result-item">
|
||||
<h4>[{{ item.category }}] {{ item.title }}</h4>
|
||||
<p class="article-summary">{{ item.summary }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result-item h4 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.result-item p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.article-summary {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@ -56,7 +56,7 @@ const router = createRouter({
|
||||
{
|
||||
path: 'home',
|
||||
name: 'home',
|
||||
component: () => import('../views/核心体验页/HomeView-首页-2.3.vue'),
|
||||
component: () => import('../views/核心体验页/HomeView-首页-2.5.vue'),
|
||||
},
|
||||
{
|
||||
path: 'discover',
|
||||
|
@ -0,0 +1,909 @@
|
||||
<template>
|
||||
<div class="search-result-view">
|
||||
<header class="top-bar">
|
||||
<button class="back-btn" @click="goBack"><</button>
|
||||
<div class="search-bar-container">
|
||||
<input type="text" v-model="searchQuery" @keyup.enter="performSearch" placeholder="食品 /成分 /食物 /菜谱 /问题" />
|
||||
<div class="separator"></div>
|
||||
<div class="search-button" @click="performSearch">搜索</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:class="{ active: activeTab === tab.id }"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filters-bar">
|
||||
<button class="filter-toggle-btn" @click="openFilterPanel">
|
||||
<span>{{ activeFilterName }}</span>
|
||||
<span class="arrow"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Panel Overlay -->
|
||||
<div v-if="isPanelOpen" class="filter-panel-overlay" @click.self="cancelFilters">
|
||||
<div class="filter-panel">
|
||||
<div class="filter-group">
|
||||
<h4>排序方式</h4>
|
||||
<div class="tags">
|
||||
<button
|
||||
v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
:class="{ active: tempActiveFilter === filter.id }"
|
||||
@click="tempActiveFilter = filter.id"
|
||||
>
|
||||
{{ filter.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<h4>顺序</h4>
|
||||
<div class="tags">
|
||||
<button :class="{ active: tempSortOrder === 'desc' }" @click="tempSortOrder = 'desc'">从高到低</button>
|
||||
<button :class="{ active: tempSortOrder === 'asc' }" @click="tempSortOrder = 'asc'">从低到高</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-actions">
|
||||
<button class="btn-cancel" @click="cancelFilters">取消</button>
|
||||
<button class="btn-confirm" @click="confirmFilters">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="results-list">
|
||||
<div v-if="activeTab === 'additive'" class="safety-level-description">
|
||||
安全等级划分参照 JECFA:A1 > A2 > B1 > B2 > C2 > C1。
|
||||
</div>
|
||||
<!-- Prepackaged Food Results -->
|
||||
<!-- Example for Level 1 -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'prepackaged'" class="result-item product-card">
|
||||
<div class="product-image-placeholder"><span>商品图片</span></div>
|
||||
<div class="product-details">
|
||||
<h4 class="product-title">
|
||||
<span class="category-tag">[预包装]</span>
|
||||
<span class="brand">某品牌</span>
|
||||
<span class="product-name">有机纯牛奶</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>安全评级 <span class="safety-level-1">1 级 (最安全)</span></span>
|
||||
<span>营养评级 <span class="score-high">高</span></span>
|
||||
</div>
|
||||
<div class="product-info-row risk-info">
|
||||
<span>添加剂 <span class="count additive-count">0</span></span>
|
||||
<span>高风险成分 <span class="count risk-count">0</span></span>
|
||||
<span class="calories">热量 270千卡/100g</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Example for Level 2 -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'prepackaged'" class="result-item product-card">
|
||||
<div class="product-image-placeholder"><span>商品图片</span></div>
|
||||
<div class="product-details">
|
||||
<h4 class="product-title">
|
||||
<span class="category-tag">[预包装]</span>
|
||||
<span class="brand">某品牌</span>
|
||||
<span class="product-name">全麦面包</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>安全评级 <span class="safety-level-2">2 级 (较安全)</span></span>
|
||||
<span>营养评级 <span class="score-mid">中</span></span>
|
||||
</div>
|
||||
<div class="product-info-row risk-info">
|
||||
<span>添加剂 <span class="count additive-count">2</span></span>
|
||||
<span>高风险成分 <span class="count risk-count">0</span></span>
|
||||
<span class="calories">热量 350千卡/100g</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Example for Level 3 -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'prepackaged'" class="result-item product-card">
|
||||
<div class="product-image-placeholder"><span>商品图片</span></div>
|
||||
<div class="product-details">
|
||||
<h4 class="product-title">
|
||||
<span class="category-tag">[预包装]</span>
|
||||
<span class="brand">某品牌</span>
|
||||
<span class="product-name">番茄酱</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>安全评级 <span class="safety-level-3">3 级 (一般安全)</span></span>
|
||||
<span>营养评级 <span class="score-mid">中</span></span>
|
||||
</div>
|
||||
<div class="product-info-row risk-info">
|
||||
<span>添加剂 <span class="count additive-count">5</span></span>
|
||||
<span>高风险成分 <span class="count risk-count">1</span></span>
|
||||
<span class="calories">热量 420千卡/100g</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Example for Level 4 -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'prepackaged'" class="result-item product-card">
|
||||
<div class="product-image-placeholder"><span>商品图片</span></div>
|
||||
<div class="product-details">
|
||||
<h4 class="product-title">
|
||||
<span class="category-tag">[预包装]</span>
|
||||
<span class="brand">子弟</span>
|
||||
<span class="product-name">薯片 (蜂蜜黄油味)</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>安全评级 <span class="prepackaged-safety-4">4 级 (需警惕)</span></span>
|
||||
<span>营养评级 <span class="score-low">低</span></span>
|
||||
</div>
|
||||
<div class="product-info-row risk-info">
|
||||
<span>添加剂 <span class="count additive-count">8</span></span>
|
||||
<span>高风险成分 <span class="count risk-count">2</span></span>
|
||||
<span class="calories">热量 519千卡/100g</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Example for Level 5 -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'prepackaged'" class="result-item product-card">
|
||||
<div class="product-image-placeholder"><span>商品图片</span></div>
|
||||
<div class="product-details">
|
||||
<h4 class="product-title">
|
||||
<span class="category-tag">[预包装]</span>
|
||||
<span class="brand">某品牌</span>
|
||||
<span class="product-name">辣条</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>安全评级 <span class="prepackaged-safety-5">5 级 (风险较高)</span></span>
|
||||
<span>营养评级 <span class="score-low">低</span></span>
|
||||
</div>
|
||||
<div class="product-info-row risk-info">
|
||||
<span>添加剂 <span class="count additive-count">15</span></span>
|
||||
<span>高风险成分 <span class="count risk-count">4</span></span>
|
||||
<span class="calories">热量 600千卡/100g</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additive Results - New Card Design -->
|
||||
<!-- Additive Results - V3 Preview with 6 Levels -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'additive'" class="result-item additive-card">
|
||||
<div class="product-image-placeholder"><span>添加剂图片</span></div>
|
||||
<div class="additive-details">
|
||||
<h4 class="additive-title">
|
||||
<span class="category-tag">[添加剂]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">维生素 C (抗坏血酸)</span>
|
||||
</h4>
|
||||
<div class="additive-info-row">
|
||||
<span>安全等级: <span class="safety-level-1">A1 最高安全等级</span></span>
|
||||
<span class="function-tag">抗氧化剂</span>
|
||||
</div>
|
||||
<div class="additive-info-row">
|
||||
<span class="health-risk-warning">一般认为安全,但极高剂量可能导致肠胃不适。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'all' || activeTab === 'additive'" class="result-item additive-card">
|
||||
<div class="product-image-placeholder"><span>添加剂图片</span></div>
|
||||
<div class="additive-details">
|
||||
<h4 class="additive-title">
|
||||
<span class="category-tag">[添加剂]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">苯甲酸钠</span>
|
||||
</h4>
|
||||
<div class="additive-info-row">
|
||||
<span>安全等级: <span class="safety-level-2">A2 较安全</span></span>
|
||||
<span class="function-tag">防腐剂</span>
|
||||
</div>
|
||||
<div class="additive-info-row">
|
||||
<span class="health-risk-warning">在特定条件下可能与维生素C反应生成微量苯,长期过量摄入有潜在风险。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'all' || activeTab === 'additive'" class="result-item additive-card">
|
||||
<div class="product-image-placeholder"><span>添加剂图片</span></div>
|
||||
<div class="additive-details">
|
||||
<h4 class="additive-title">
|
||||
<span class="category-tag">[添加剂]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">诱惑红</span>
|
||||
</h4>
|
||||
<div class="additive-info-row">
|
||||
<span>安全等级: <span class="safety-level-3">B1 需警惕</span></span>
|
||||
<span class="function-tag">色素</span>
|
||||
</div>
|
||||
<div class="additive-info-row">
|
||||
<span class="health-risk-warning">可能引起儿童多动症等过敏反应,在多国被限制或禁止使用。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'all' || activeTab === 'additive'" class="result-item additive-card">
|
||||
<div class="product-image-placeholder"><span>添加剂图片</span></div>
|
||||
<div class="additive-details">
|
||||
<h4 class="additive-title">
|
||||
<span class="category-tag">[添加剂]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">糖醇类甜味剂</span>
|
||||
</h4>
|
||||
<div class="additive-info-row">
|
||||
<span>安全等级: <span class="safety-level-4">B2 风险未知</span></span>
|
||||
<span class="function-tag">新型甜味剂</span>
|
||||
</div>
|
||||
<div class="additive-info-row">
|
||||
<span class="health-risk-warning">长期摄入对健康的影响尚不完全明确,部分人群可能出现肠胃不适。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'all' || activeTab === 'additive'" class="result-item additive-card">
|
||||
<div class="product-image-placeholder"><span>添加剂图片</span></div>
|
||||
<div class="additive-details">
|
||||
<h4 class="additive-title">
|
||||
<span class="category-tag">[添加剂]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">黄原胶 (特定场景)</span>
|
||||
</h4>
|
||||
<div class="additive-info-row">
|
||||
<span>安全等级: <span class="safety-level-5">C2 严格限制</span></span>
|
||||
<span class="function-tag">增稠剂</span>
|
||||
</div>
|
||||
<div class="additive-info-row">
|
||||
<span class="health-risk-warning">仅限在特定食品(如婴幼儿食品)中严格限量使用。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'all' || activeTab === 'additive'" class="result-item additive-card">
|
||||
<div class="product-image-placeholder"><span>添加剂图片</span></div>
|
||||
<div class="additive-details">
|
||||
<h4 class="additive-title">
|
||||
<span class="category-tag">[添加剂]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">苏丹红</span>
|
||||
</h4>
|
||||
<div class="additive-info-row">
|
||||
<span>安全等级: <span class="safety-level-6">C1 禁止使用</span></span>
|
||||
<span class="function-tag">工业染料</span>
|
||||
</div>
|
||||
<div class="additive-info-row">
|
||||
<span class="health-risk-warning">具有潜在致癌性,严禁在食品中添加。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Food Material Results - New Card Design -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'material'" class="result-item material-card">
|
||||
<div class="product-image-placeholder"><span>食材图片</span></div>
|
||||
<div class="material-details">
|
||||
<h4 class="material-title">
|
||||
<span class="category-tag">[食材]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">西兰花</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>分类: 十字花科蔬菜</span>
|
||||
</div>
|
||||
<div class="product-info-row nutrient-tags-container">
|
||||
<span class="nutrient-tag">富含维生素C</span>
|
||||
<span class="nutrient-tag">高膳食纤维</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'all' || activeTab === 'material'" class="result-item material-card">
|
||||
<div class="product-image-placeholder"><span>食材图片</span></div>
|
||||
<div class="material-details">
|
||||
<h4 class="material-title">
|
||||
<span class="category-tag">[食材]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">番茄</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>分类: 茄科植物</span>
|
||||
</div>
|
||||
<div class="product-info-row nutrient-tags-container">
|
||||
<span class="nutrient-tag tomato">富含番茄红素</span>
|
||||
<span class="nutrient-tag antioxidant">抗氧化</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'all' || activeTab === 'material'" class="result-item material-card">
|
||||
<div class="product-image-placeholder"><span>食材图片</span></div>
|
||||
<div class="material-details">
|
||||
<h4 class="material-title">
|
||||
<span class="category-tag">[食材]</span>
|
||||
<span class="brand"></span>
|
||||
<span class="product-name">酱油</span>
|
||||
</h4>
|
||||
<div class="product-info-row">
|
||||
<span>分类: 传统酿造调味品</span>
|
||||
</div>
|
||||
<div class="product-info-row nutrient-tags-container">
|
||||
<span class="nutrient-tag umami">提鲜</span>
|
||||
<span class="nutrient-tag high-sodium">高钠</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recipe Results -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'recipe'" class="result-item">
|
||||
<h4>[食谱] 自制健康牛奶小面包</h4>
|
||||
<p class="article-summary">一个简单易学的食谱,使用最少的添加剂,为家人制作美味又健康的面包。</p>
|
||||
</div>
|
||||
|
||||
<!-- Article Results (Renamed to "资讯") -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'article'" class="result-item">
|
||||
<h4>牛奶过敏的宝宝应该怎么办?</h4>
|
||||
<p class="article-summary">本文将详细介绍牛奶过敏的症状、原因以及应对方法...</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const searchQuery = ref('');
|
||||
const activeTab = ref('all');
|
||||
|
||||
// Filter and Sort State
|
||||
const filters = ref([
|
||||
{ id: 'comprehensive', name: '综合' },
|
||||
{ id: 'safety', name: '安全评级' },
|
||||
{ id: 'nutrition', name: '营养评级' },
|
||||
{ id: 'latest', name: '最新发布' },
|
||||
{ id: 'protein', name: '蛋白质' },
|
||||
{ id: 'fat', name: '脂肪' },
|
||||
{ id: 'carbs', name: '碳水化合物' },
|
||||
]);
|
||||
const activeFilter = ref('comprehensive');
|
||||
const sortOrder = ref('desc'); // 'desc' or 'asc'
|
||||
|
||||
// Panel State
|
||||
const isPanelOpen = ref(false);
|
||||
const tempActiveFilter = ref(activeFilter.value);
|
||||
const tempSortOrder = ref(sortOrder.value);
|
||||
|
||||
const activeFilterName = computed(() => {
|
||||
const found = filters.value.find(f => f.id === activeFilter.value);
|
||||
return found ? found.name : '综合';
|
||||
});
|
||||
|
||||
const openFilterPanel = () => {
|
||||
tempActiveFilter.value = activeFilter.value;
|
||||
tempSortOrder.value = sortOrder.value;
|
||||
isPanelOpen.value = true;
|
||||
};
|
||||
|
||||
const cancelFilters = () => {
|
||||
isPanelOpen.value = false;
|
||||
};
|
||||
|
||||
const confirmFilters = () => {
|
||||
activeFilter.value = tempActiveFilter.value;
|
||||
sortOrder.value = tempSortOrder.value;
|
||||
isPanelOpen.value = false;
|
||||
// Here you would typically re-fetch data with new filters
|
||||
};
|
||||
|
||||
const tabs = ref([
|
||||
{ id: 'all', name: '全部' },
|
||||
{ id: 'prepackaged', name: '预包装' },
|
||||
{ id: 'additive', name: '添加剂' },
|
||||
{ id: 'material', name: '食材' },
|
||||
{ id: 'recipe', name: '食谱' },
|
||||
{ id: 'article', name: '资讯' },
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
searchQuery.value = (route.query.q as string) || '';
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
router.push({ name: 'home' });
|
||||
};
|
||||
|
||||
const performSearch = () => {
|
||||
// In a real app, this would re-trigger the search
|
||||
console.log('Searching for:', searchQuery.value);
|
||||
};
|
||||
|
||||
const goToResult = (id: string) => {
|
||||
router.push({ name: 'result', params: { id } });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-result-view {
|
||||
/* The padding is no longer needed here as the results-list is positioned absolutely. */
|
||||
}
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 428px;
|
||||
margin: 0 auto;
|
||||
z-index: 10;
|
||||
}
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding-right: 10px;
|
||||
color: #333;
|
||||
}
|
||||
.search-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-radius: 25px;
|
||||
padding: 8px 15px;
|
||||
border: 1px solid #07C160;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.search-bar-container input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background-color: #e0e0e0;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
color: #07C160;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 10px 0;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
position: fixed;
|
||||
top: 57px; /* Position below top-bar */
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 428px;
|
||||
margin: 0 auto;
|
||||
z-index: 9;
|
||||
}
|
||||
.tabs button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.tabs button.active {
|
||||
color: #22c55e;
|
||||
border-bottom-color: #22c55e;
|
||||
}
|
||||
|
||||
.filters-bar {
|
||||
display: flex;
|
||||
padding: 8px 15px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
position: fixed;
|
||||
top: 101px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 428px;
|
||||
margin: 0 auto;
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.filter-toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-toggle-btn .arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid #666;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.filter-panel-overlay {
|
||||
position: fixed;
|
||||
top: 142px; /* Position below the header bars */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
background-color: #fff;
|
||||
padding: 20px 15px;
|
||||
border-bottom-left-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.filter-group h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tags button {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #f7f7f7;
|
||||
color: #333;
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tags button.active {
|
||||
background-color: #e8f8f1;
|
||||
border-color: #07c160;
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.panel-actions button {
|
||||
flex-grow: 1;
|
||||
padding: 12px;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #f7f7f7;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background-color: #07c160;
|
||||
border: 1px solid #07c160;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
position: absolute;
|
||||
top: 142px; /* Position right below the filters-bar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 428px; /* Ensures consistency with the fixed headers */
|
||||
margin: 0 auto;
|
||||
overflow-y: auto; /* Enables vertical scrolling for the list */
|
||||
padding: 0 20px;
|
||||
}
|
||||
.result-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result-item h4 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.result-item p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.article-summary {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.score-high { color: #22c55e; }
|
||||
.score-mid { color: orange; }
|
||||
.score-low { color: #ef4444; }
|
||||
|
||||
.product-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.product-image-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.result-item h4.product-title {
|
||||
font-size: 14px !important; /* Force size with high specificity and !important */
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.product-info-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.product-info-row.risk-info {
|
||||
justify-content: flex-start; /* Align items to the start */
|
||||
gap: 20px; /* Increase gap for risk info */
|
||||
}
|
||||
|
||||
.product-info-row span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-title .category-tag,
|
||||
.additive-title .category-tag,
|
||||
.material-title .category-tag {
|
||||
font-weight: normal;
|
||||
color: #9ca3af; /* Gray-400 */
|
||||
display: inline-block;
|
||||
margin-right: 8px; /* Reset margin slightly as min-width handles spacing */
|
||||
}
|
||||
|
||||
.product-title .brand {
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.calories {
|
||||
background-color: #f3f4f6; /* Gray-100 */
|
||||
color: #4b5563; /* Gray-600 */
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-info-row .count {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.additive-count {
|
||||
color: #FF7F50; /* Yellow for additives */
|
||||
}
|
||||
|
||||
.risk-count {
|
||||
color: #ff0000; /* Red for high-risk */
|
||||
}
|
||||
|
||||
/* 6-Level Additive Safety Rating Styles */
|
||||
.safety-level-1 { color: #07C160; font-weight: bold; } /* A1: 最安全 - 微信绿 */
|
||||
.safety-level-2 { color: #90ee90; font-weight: bold; } /* A2: 较安全 - 浅绿色 */
|
||||
.safety-level-3 { color: #1E90FF; font-weight: bold; } /* B1: 需警惕 - 蓝色 */
|
||||
.safety-level-4 { color: #ffe600; font-weight: bold; } /* B2: 风险未知 - 金色 */
|
||||
.safety-level-5 { color: #FF7F50; font-weight: bold; } /* C2: 严格限制 - 橙色 */
|
||||
.safety-level-6 { color: #ff0000; font-weight: bold; } /* C1: 禁止使用 - 深红色 */
|
||||
|
||||
/* Prepackaged Food Specific Safety Rating Styles */
|
||||
.prepackaged-safety-4 { color: #FF7F50; font-weight: bold; }
|
||||
.prepackaged-safety-5 { color: #ff0000; font-weight: bold; }
|
||||
|
||||
/* Material Card Specific Styles */
|
||||
.material-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.material-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.result-item h4.material-title {
|
||||
font-size: 14px !important;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* .material-family and .material-nutrients styles are no longer needed in this form */
|
||||
/* They are replaced by reusing .product-info-row and the specific .nutrient-tag styles */
|
||||
|
||||
.nutrient-tag {
|
||||
background-color: #f0fdf4; /* Green-50 */
|
||||
color: #15803d; /* Green-700 */
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nutrient-tags-container {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
/* Additive Card Specific Styles V2 */
|
||||
.additive-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.additive-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.result-item h4.additive-title {
|
||||
font-size: 14px !important;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.additive-title .brand,
|
||||
.material-title .brand {
|
||||
/* This is a placeholder to match the structure of product-title */
|
||||
/* It can be empty, but its presence helps in aligning items if needed */
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
display: inline-block;
|
||||
min-width: 0; /* No minimum width needed */
|
||||
margin-right: 0; /* Remove extra margin for the placeholder */
|
||||
}
|
||||
|
||||
.health-risk-warning {
|
||||
font-size: 12px;
|
||||
color: #666; /* Matching the color of other info text */
|
||||
line-height: 1.4;
|
||||
white-space: normal; /* Allow text to wrap */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* Show max 2 lines */
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.additive-info-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.function-tag {
|
||||
background-color: #eef2ff; /* Indigo-50 */
|
||||
color: #4338ca; /* Indigo-700 */
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.product-image-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.product-card,
|
||||
.additive-card {
|
||||
gap: 12px;
|
||||
}
|
||||
.product-details,
|
||||
.additive-details {
|
||||
gap: 5px;
|
||||
}
|
||||
.product-info-row,
|
||||
.product-info-row.risk-info,
|
||||
.additive-info-row {
|
||||
font-size: 11px;
|
||||
gap: 15px;
|
||||
}
|
||||
.calories,
|
||||
.function-tag {
|
||||
font-size: 10px;
|
||||
}
|
||||
.health-risk-warning {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.safety-level-description {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
padding: 10px 0; /* Use padding on Y-axis only to avoid affecting horizontal alignment */
|
||||
margin: 0 -20px 10px -20px; /* Use negative margin to extend the background to the edges */
|
||||
padding-left: 20px; /* Re-apply left padding */
|
||||
padding-right: 20px; /* Re-apply right padding */
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
z-index: 1; /* Ensure it stays above the scrolling content */
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
transform: scale(0.85);
|
||||
transform-origin: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="search-result-view">
|
||||
<header class="top-bar">
|
||||
<button class="back-btn" @click="goBack"><</button>
|
||||
<button class="back-btn" @click="goBack"><</button>
|
||||
<div class="search-bar-container">
|
||||
<input type="text" v-model="searchQuery" @keyup.enter="performSearch" placeholder="食品 /成分 /食物 /菜谱 /问题" />
|
||||
<div class="separator"></div>
|
||||
@ -60,46 +60,103 @@
|
||||
</div>
|
||||
|
||||
<main class="results-list">
|
||||
<!-- Product Results -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'product'" class="result-item" @click="goToResult('product1')">
|
||||
<h4>[某品牌纯牛奶]</h4>
|
||||
<p>安全评级: <span class="score-a">A</span> | 营养评级: <span class="score-high">高</span></p>
|
||||
</div>
|
||||
<div v-if="activeTab === 'all' || activeTab === 'product'" class="result-item" @click="goToResult('product2')">
|
||||
<h4>[某品牌儿童牛奶]</h4>
|
||||
<p>安全评级: <span class="score-c">C</span> | 营养评级: <span class="score-mid">中</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Ingredient Results -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'ingredient'" class="result-item">
|
||||
<h4>[成分] 碳酸氢钠</h4>
|
||||
<p class="article-summary">俗称小苏打。在食品工业中,它是一种应用最广泛的疏松剂,用于生产饼干、糕点、馒头、面包等...</p>
|
||||
</div>
|
||||
|
||||
<!-- Recipe Results -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'recipe'" class="result-item">
|
||||
<h4>[食谱] 自制健康牛奶小面包</h4>
|
||||
<p class="article-summary">一个简单易学的食谱,使用最少的添加剂,为家人制作美味又健康的面包。</p>
|
||||
</div>
|
||||
|
||||
<!-- Article Results (Renamed to "资讯") -->
|
||||
<div v-if="activeTab === 'all' || activeTab === 'article'" class="result-item">
|
||||
<h4>牛奶过敏的宝宝应该怎么办?</h4>
|
||||
<p class="article-summary">本文将详细介绍牛奶过敏的症状、原因以及应对方法...</p>
|
||||
<div v-if="activeTab === 'additive'" class="safety-level-description">
|
||||
安全等级划分参照 JECFA:A1 > A2 > B1 > B2 > C2 > C1。
|
||||
</div>
|
||||
<!-- Dynamic component for rendering lists -->
|
||||
<component
|
||||
:is="componentMap[activeTab]"
|
||||
:items="filteredResults"
|
||||
@item-click="goToResult"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
// Import list components
|
||||
import PrepackagedList from '@/components/SearchResult/PrepackagedList.vue';
|
||||
import AdditiveList from '@/components/SearchResult/AdditiveList.vue';
|
||||
import MaterialList from '@/components/SearchResult/MaterialList.vue';
|
||||
import RecipeList from '@/components/SearchResult/RecipeList.vue';
|
||||
import ArticleList from '@/components/SearchResult/ArticleList.vue';
|
||||
import AllResultsList from '@/components/SearchResult/AllResultsList.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const searchQuery = ref('');
|
||||
const activeTab = ref('all');
|
||||
|
||||
// Mock Data
|
||||
const mockData = {
|
||||
prepackaged: [
|
||||
{ id: 'p1', category: 'prepackaged', brand: '某品牌', name: '有机纯牛奶', safetyLevel: 1, safetyLevelText: '1 级 (最安全)', nutritionLevel: '高', nutritionLevelClass: 'score-high', additiveCount: 0, riskCount: 0, calories: 270 },
|
||||
{ id: 'p2', category: 'prepackaged', brand: '某品牌', name: '全麦面包', safetyLevel: 2, safetyLevelText: '2 级 (较安全)', nutritionLevel: '中', nutritionLevelClass: 'score-mid', additiveCount: 2, riskCount: 0, calories: 350 },
|
||||
{ id: 'p3', category: 'prepackaged', brand: '某品牌', name: '番茄酱', safetyLevel: 3, safetyLevelText: '3 级 (一般安全)', nutritionLevel: '中', nutritionLevelClass: 'score-mid', additiveCount: 5, riskCount: 1, calories: 420 },
|
||||
{ id: 'p4', category: 'prepackaged', brand: '子弟', name: '薯片 (蜂蜜黄油味)', safetyLevel: 4, safetyLevelText: '4 级 (需警惕)', nutritionLevel: '低', nutritionLevelClass: 'score-low', additiveCount: 8, riskCount: 2, calories: 519 },
|
||||
{ id: 'p5', category: 'prepackaged', brand: '某品牌', name: '辣条', safetyLevel: 5, safetyLevelText: '5 级 (风险较高)', nutritionLevel: '低', nutritionLevelClass: 'score-low', additiveCount: 15, riskCount: 4, calories: 600 },
|
||||
],
|
||||
additive: [
|
||||
{ id: 'a1', category: 'additive', name: '维生素 C (抗坏血酸)', safetyLevel: 1, safetyLevelText: 'A1 最高安全等级', functionTag: '抗氧化剂', riskWarning: '一般认为安全,但极高剂量可能导致肠胃不适。' },
|
||||
{ id: 'a2', category: 'additive', name: '苯甲酸钠', safetyLevel: 2, safetyLevelText: 'A2 较安全', functionTag: '防腐剂', riskWarning: '在特定条件下可能与维生素C反应生成微量苯,长期过量摄入有潜在风险。' },
|
||||
{ id: 'a3', category: 'additive', name: '诱惑红', safetyLevel: 3, safetyLevelText: 'B1 需警惕', functionTag: '色素', riskWarning: '可能引起儿童多动症等过敏反应,在多国被限制或禁止使用。' },
|
||||
{ id: 'a4', category: 'additive', name: '糖醇类甜味剂', safetyLevel: 4, safetyLevelText: 'B2 风险未知', functionTag: '新型甜味剂', riskWarning: '长期摄入对健康的影响尚不完全明确,部分人群可能出现肠胃不适。' },
|
||||
{ id: 'a5', category: 'additive', name: '黄原胶 (特定场景)', safetyLevel: 5, safetyLevelText: 'C2 严格限制', functionTag: '增稠剂', riskWarning: '仅限在特定食品(如婴幼儿食品)中严格限量使用。' },
|
||||
{ id: 'a6', category: 'additive', name: '苏丹红', safetyLevel: 6, safetyLevelText: 'C1 禁止使用', functionTag: '工业染料', riskWarning: '具有潜在致癌性,严禁在食品中添加。' },
|
||||
],
|
||||
material: [
|
||||
{ id: 'm1', category: 'material', name: '西兰花', family: '十字花科蔬菜', nutrientTags: [{text: '富含维生素C'}, {text: '高膳食纤维'}] },
|
||||
{ id: 'm2', category: 'material', name: '番茄', family: '茄科植物', nutrientTags: [{text: '富含番茄红素', class: 'tomato'}, {text: '抗氧化', class: 'antioxidant'}] },
|
||||
{ id: 'm3', category: 'material', name: '酱油', family: '传统酿造调味品', nutrientTags: [{text: '提鲜', class: 'umami'}, {text: '高钠', class: 'high-sodium'}] },
|
||||
],
|
||||
recipe: [
|
||||
{ id: 'r1', category: 'recipe', title: '自制健康牛奶小面包', summary: '一个简单易学的食谱,使用最少的添加剂,为家人制作美味又健康的面包。' }
|
||||
],
|
||||
article: [
|
||||
{ id: 'ar1', category: 'article', title: '牛奶过敏的宝宝应该怎么办?', summary: '本文将详细介绍牛奶过敏的症状、原因以及应对方法...' }
|
||||
]
|
||||
};
|
||||
|
||||
// Component Mapping
|
||||
const componentMap: Record<string, Component> = {
|
||||
all: AllResultsList,
|
||||
prepackaged: PrepackagedList,
|
||||
additive: AdditiveList,
|
||||
material: MaterialList,
|
||||
recipe: RecipeList,
|
||||
article: ArticleList,
|
||||
};
|
||||
|
||||
const filteredResults = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'prepackaged':
|
||||
return mockData.prepackaged;
|
||||
case 'additive':
|
||||
return mockData.additive;
|
||||
case 'material':
|
||||
return mockData.material;
|
||||
case 'recipe':
|
||||
return mockData.recipe;
|
||||
case 'article':
|
||||
return mockData.article;
|
||||
case 'all':
|
||||
return [
|
||||
...mockData.prepackaged,
|
||||
...mockData.additive,
|
||||
...mockData.material,
|
||||
...mockData.recipe,
|
||||
...mockData.article
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Filter and Sort State
|
||||
const filters = ref([
|
||||
{ id: 'comprehensive', name: '综合' },
|
||||
@ -142,8 +199,9 @@ const confirmFilters = () => {
|
||||
|
||||
const tabs = ref([
|
||||
{ id: 'all', name: '全部' },
|
||||
{ id: 'product', name: '产品' },
|
||||
{ id: 'ingredient', name: '成分' },
|
||||
{ id: 'prepackaged', name: '预包装' },
|
||||
{ id: 'additive', name: '添加剂' },
|
||||
{ id: 'material', name: '食材' },
|
||||
{ id: 'recipe', name: '食谱' },
|
||||
{ id: 'article', name: '资讯' },
|
||||
]);
|
||||
@ -161,14 +219,15 @@ const performSearch = () => {
|
||||
console.log('Searching for:', searchQuery.value);
|
||||
};
|
||||
|
||||
const goToResult = (id: string) => {
|
||||
router.push({ name: 'result', params: { id } });
|
||||
const goToResult = (item: any) => {
|
||||
router.push({ name: 'result', params: { id: item.id } });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Keep only layout and container styles */
|
||||
.search-result-view {
|
||||
padding-top: 150px; /* Space for top-bar, tabs and filters */
|
||||
/* The padding is no longer needed here as the results-list is positioned absolutely. */
|
||||
}
|
||||
.top-bar {
|
||||
display: flex;
|
||||
@ -370,29 +429,31 @@ const goToResult = (id: string) => {
|
||||
}
|
||||
|
||||
.results-list {
|
||||
position: absolute;
|
||||
top: 142px; /* Position right below the filters-bar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 428px; /* Ensures consistency with the fixed headers */
|
||||
margin: 0 auto;
|
||||
overflow-y: auto; /* Enables vertical scrolling for the list */
|
||||
padding: 0 20px;
|
||||
}
|
||||
.result-item {
|
||||
padding: 15px 0;
|
||||
|
||||
.safety-level-description {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
padding: 10px 0;
|
||||
margin: 0 -20px 10px -20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result-item h4 {
|
||||
z-index: 1;
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.result-item p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
transform: scale(0.85);
|
||||
transform-origin: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.article-summary {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.score-a { color: #22c55e; font-weight: bold; }
|
||||
.score-c { color: orange; font-weight: bold; }
|
||||
.score-high { color: #22c55e; }
|
||||
.score-mid { color: orange; }
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user