feat(auth): 重构通用基础页面与登录流程
本次提交对用户认证与引导流程进行了全面的重构和功能增强。 核心变更: 1. **文件结构重构:** * 将所有登录、注册、引导相关的页面组件(共9个)统一移动到了新的 \shihuashishuo-ui/src/views/通用基础页/\ 目录下,使项目结构更清晰。 * 删除了旧路径下的相应文件。 2. **新增功能页面:** * **密码登录:** 新增了 \PasswordLoginView-密码登录页.vue\,为用户提供了手机验证码之外的另一种登录选择。 * **忘记密码:** 新增了 \ForgotPasswordView-忘记密码页.vue\,包含一个完整的、带验证码校验的两步式密码重设流程。 * **第三方授权:** 新增了 \ThirdPartyAuthView-授权登录页.vue\ 作为处理社交登录的中转页。 3. **功能增强与修复:** * **登录页 (\LoginView\):** * UI更新:将原有的微信一键登录按钮替换为包含微信、QQ等多种方式的社交登录图标组。 * 交互增强:为获取验证码按钮增加了60秒倒计时功能,并对手机号和验证码输入框增加了长度和字符类型限制。 * **闪屏页 (\SplashView\):** * UI更新:根据新的设计稿,将页面重构为包含Logo、应用名称和Slogan的居中布局,并增加了渐变背景效果。 * **路由 (\ outer/index.ts\):** * 全面更新了所有相关页面的导入路径以匹配新的文件结构。 * 移除了对已废弃的 \HomeView-首页-1.0.vue\ 的引用,修复了编译错误。 4. **文档更新:** * 新增并全面重写了 \1.1.2通用基础页面-原型功能说明文档-2.0.md\,使其与当前的代码实现和功能逻辑完全保持一致,为后续开发提供了准确的参考。
This commit is contained in:
parent
f6f418081a
commit
f2034c6709
151
1-原型设计及功能说明/1.1.2通用基础页面-原型功能说明文档-2.0.md
Normal file
151
1-原型设计及功能说明/1.1.2通用基础页面-原型功能说明文档-2.0.md
Normal file
@ -0,0 +1,151 @@
|
||||
# 1.1.2 通用基础页面 - 功能说明文档 (v2.0)
|
||||
|
||||
**文档目的:** 本文档旨在为技术开发团队提供最终版的页面功能说明、交互反馈及业务规则,作为前端开发的最终参考依据。
|
||||
|
||||
**版本:** 2.0 (同步代码重构,新增登录流程页面)
|
||||
|
||||
---
|
||||
|
||||
## 页面流程总览
|
||||
|
||||
应用启动后,用户将经历以下核心流程:
|
||||
|
||||
`启动页` -> `登录/注册页` -> `用户引导流程 (Onboarding)` -> `应用主页`
|
||||
|
||||
**登录/注册流程分支:**
|
||||
* 用户可在 `登录/注册页` 选择 `密码登录`。
|
||||
* 用户可在 `密码登录页` 选择 `忘记密码`。
|
||||
* 用户可在 `登录/注册页` 选择 `第三方社交账户登录`。
|
||||
* 用户可在 `登录/注册页` 查看 `用户协议/隐私政策`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 启动页 (`SplashView-闪屏页.vue`)
|
||||
|
||||
- **页面名称:** Splash Screen / 启动页
|
||||
- **文件路径:** `shihuashishuo-ui/src/views/通用基础页/SplashView-闪屏页.vue`
|
||||
|
||||
### 1.1. 功能描述
|
||||
- **核心功能:** 作为应用的入口,展示品牌信息,并在短暂延迟后自动导航到登录页。
|
||||
- **显示元素:**
|
||||
- 应用 Logo (居中,位于文字上方)
|
||||
- 应用名称: "食话食说 Talk of Food"
|
||||
- 应用口号: "食品真相,食话食说"
|
||||
|
||||
### 1.2. 交互反馈
|
||||
- **自动跳转:** 页面加载完成 `2.5` 秒后,系统自动跳转到 **登录/注册页** (`/login`)。
|
||||
- **用户交互:** 此页面无任何用户可操作的交互元素。
|
||||
|
||||
### 1.3. 布局与样式
|
||||
- **布局:** 采用 Flexbox 垂直居中布局。
|
||||
- **背景:** 柔和的从上至下线性渐变 (`#fafff5` -> `#f0fdf4`)。
|
||||
- **字体大小:** 应用标题为 `32px` 以适应中英文组合。
|
||||
|
||||
---
|
||||
|
||||
## 2. 登录认证流程
|
||||
|
||||
### 2.1. 登录/注册页 (`LoginView-登录页-2.0.vue`)
|
||||
|
||||
- **页面名称:** Login / Register Page / 登录注册页
|
||||
- **文件路径:** `shihuashishuo-ui/src/views/通用基础页/LoginView-登录页-2.0.vue`
|
||||
|
||||
#### 2.1.1. 功能描述
|
||||
- **核心功能:** 提供用户通过手机验证码注册/登录的主入口,并分流至其他登录方式。
|
||||
- **显示元素:**
|
||||
- 手机号输入框 (11位数字限制)
|
||||
- 验证码输入框 (6位数字限制)
|
||||
- "获取验证码" 按钮
|
||||
- "登录 / 注册" 主按钮
|
||||
- "密码登录" 文字按钮
|
||||
- 第三方社交登录图标 (微信, QQ, 抖音, 微博)
|
||||
- 政策同意复选框及链接
|
||||
|
||||
#### 2.1.2. 交互反馈
|
||||
- **获取验证码:** 点击后,按钮进入60秒倒计时,期间不可再次点击。
|
||||
- **登录/注册:** (临时) 点击后,跳转到 **用户引导流程页** (`/onboarding`)。
|
||||
- **密码登录:** 点击后,跳转到 **密码登录页** (`/password-login`)。
|
||||
- **社交登录:** 点击任一社交图标,跳转到 **第三方授权页** (`/auth`),并通过URL参数传递平台名称。
|
||||
- **政策链接:** 点击 `用户协议` 或 `隐私政策` 链接,页面跳转到 **政策详情页** (`/policy`)。
|
||||
|
||||
### 2.2. 密码登录页 (`PasswordLoginView-密码登录页.vue`)
|
||||
|
||||
- **页面名称:** Password Login Page / 密码登录页
|
||||
- **文件路径:** `shihuashishuo-ui/src/views/通用基础页/PasswordLoginView-密码登录页.vue`
|
||||
|
||||
#### 2.2.1. 功能描述
|
||||
- **核心功能:** 为已注册用户提供通过手机号和密码登录的方式。
|
||||
- **显示元素:**
|
||||
- 手机号输入框
|
||||
- 密码输入框
|
||||
- "登录" 按钮
|
||||
- "← 其它登录方式" 链接
|
||||
- "忘记密码?" 链接
|
||||
|
||||
#### 2.2.2. 交互反馈
|
||||
- **登录:** (临时) 点击后,跳转到 **用户引导流程页** (`/onboarding`)。
|
||||
- **其它登录方式:** 点击后,返回 **登录/注册页** (`/login`)。
|
||||
- **忘记密码?:** 点击后,跳转到 **忘记密码页** (`/forgot-password`)。
|
||||
|
||||
### 2.3. 忘记密码页 (`ForgotPasswordView-忘记密码页.vue`)
|
||||
|
||||
- **页面名称:** Forgot Password Page / 忘记密码页
|
||||
- **文件路径:** `shihuashishuo-ui/src/views/通用基础页/ForgotPasswordView-忘记密码页.vue`
|
||||
|
||||
#### 2.3.1. 功能描述
|
||||
- **核心功能:** 通过两步验证,引导用户安全地重设密码。
|
||||
- **流程步骤:**
|
||||
1. **验证身份:** 输入手机号和验证码,点击 "下一步"。
|
||||
2. **重设密码:** 验证通过后,输入新密码和确认密码,点击 "确认重设"。
|
||||
|
||||
#### 2.3.2. 交互反馈
|
||||
- **获取验证码:** 与登录页逻辑相同,有60秒倒计时。
|
||||
- **下一步:**
|
||||
- (临时) 如果验证码为 "111111",则进入重设密码步骤。
|
||||
- 如果验证码错误,页面下方显示红色错误提示 "验证码错误,请重试"。
|
||||
- **确认重设:**
|
||||
- 如果两次输入的密码不一致,显示错误提示 "两次输入的密码不一致"。
|
||||
- (临时) 如果密码一致,模拟重设成功并跳转回 **登录/注册页** (`/login`)。
|
||||
- **返回登录:** 点击后,返回上一页。
|
||||
|
||||
### 2.4. 第三方授权页 (`ThirdPartyAuthView-授权登录页.vue`)
|
||||
|
||||
- **页面名称:** Third Party Auth Page / 第三方授权页
|
||||
- **文件路径:** `shihuashishuo-ui/src/views/通用基础页/ThirdPartyAuthView-授权登录页.vue`
|
||||
|
||||
#### 2.4.1. 功能描述
|
||||
- **核心功能:** 作为用户点击社交登录图标后的中转页面,模拟向第三方平台请求授权的过程。
|
||||
- **显示元素:**
|
||||
- 加载动画 (Spinner)
|
||||
- 动态文本,如 "正在通过 微信 安全登录..."
|
||||
|
||||
#### 2.4.2. 交互反馈
|
||||
- **自动跳转:** 页面加载 `2` 秒后,模拟授权成功,自动跳转到 **用户引导流程页** (`/onboarding`)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户引导流程 (`OnboardingView.vue` & `OnboardingStep.vue`)
|
||||
|
||||
- **页面名称:** Onboarding / 用户偏好设置
|
||||
- **文件路径:**
|
||||
- `shihuashishuo-ui/src/views/通用基础页/OnboardingView.vue` (流程控制器)
|
||||
- `shihuashishuo-ui/src/views/通用基础页/OnboardingStep.vue` (可复用步骤组件)
|
||||
|
||||
### 3.1. 功能描述
|
||||
- **核心功能:** 在用户首次登录后,通过一个多步骤的流程收集用户的健康和饮食偏好。
|
||||
- **流程步骤:** 关注点 -> 过敏原 -> 基础疾病 -> 饮食偏好。
|
||||
|
||||
### 3.2. 交互反馈
|
||||
- **通用交互:** `跳过`, `下一步`, `返回`, `完成` 按钮逻辑与之前版本一致。
|
||||
- **核心交互:** 搜索、Tag式选择、"无"选项互斥、自定义添加等复杂交互逻辑均已实现。
|
||||
|
||||
---
|
||||
|
||||
## 4. 政策详情页 (`PolicyView.vue`)
|
||||
|
||||
- **页面名称:** Policy Page / 用户协议或隐私政策页
|
||||
- **文件路径:** `shihuashishuo-ui/src/views/通用基础页/PolicyView.vue`
|
||||
|
||||
### 4.1. 功能描述
|
||||
- **核心功能:** 显示静态的法律条款文本。
|
||||
- **交互反馈:** 点击左上角的 `< 返回` 按钮,页面会返回到之前的页面。
|
103
3-工作日志/3.1-工作日志-视图层(Views)快照-v1.0.md
Normal file
103
3-工作日志/3.1-工作日志-视图层(Views)快照-v1.0.md
Normal file
@ -0,0 +1,103 @@
|
||||
# “食话食说”APP - 视图层(Views)工作日志快照
|
||||
|
||||
**最后更新**: 2025-07-23 13:12:29 (UTC+8)
|
||||
**目的**: 为 `shihuashishuo-ui/src/views` 目录下的所有页面组件提供一份简明扼要的功能说明和状态记录,以便于快速理解项目结构和进行后续开发。
|
||||
|
||||
---
|
||||
|
||||
## 设计依据
|
||||
|
||||
本文件中所有页面的设计思路、功能定义及交互逻辑,均源于以下核心文档,并应以此为准:
|
||||
|
||||
* **产品需求文档**: [`0-产品需求文档/`](0-产品需求文档/)
|
||||
* **原型设计及功能说明**: [`1-原型设计及功能说明/`](1-原型设计及功能说明/)
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心体验流程页面
|
||||
|
||||
这些页面构成了用户使用APP最核心的功能路径。
|
||||
|
||||
* **`ScanView-扫码页.vue`**
|
||||
* **用途**: APP的核心功能页面,用于拉起设备摄像头,进行食品包装的二维码或条形码扫描。
|
||||
* **状态**: 结构占位,需实现相机调用、扫码识别及与后端API的交互逻辑。
|
||||
|
||||
* **`SearchView-搜索页.vue`**
|
||||
* **用途**: 提供手动输入关键词搜索食品、成分或品牌的功能,是扫码功能的补充。
|
||||
* **状态**: 结构占位,需实现搜索框、历史记录、搜索建议及API调用逻辑。
|
||||
|
||||
* **`SearchResultView-搜索结果页.vue`**
|
||||
* **用途**: 展示通过关键词搜索返回的结果列表。
|
||||
* **状态**: 结构占位,需实现结果列表的渲染、分页加载和错误状态处理。
|
||||
|
||||
* **`ResultView-结果页.vue`**
|
||||
* **用途**: 展示单个食品的最终分析结果。这是对用户最有价值的页面之一,会包含成分解析、安全评级、营养信息等。
|
||||
* **状态**: 结构占位,需根据后端API返回的数据结构,设计精细化的数据可视化组件。
|
||||
|
||||
---
|
||||
|
||||
## 2. 主要导航页面 (底部Tab栏)
|
||||
|
||||
这些页面是APP的一级模块,通过底部导航栏进行切换。
|
||||
|
||||
* **`HomeView-首页-2.0.vue`**
|
||||
* **用途**: 用户登录后的主入口,也是我们本次迭代的核心。
|
||||
* **状态**: **v2.5.1 (已完成)**。经过多轮迭代,现已包含搜索热词、精巧的扫码按钮、个性化提示、含圆形进度盘的健康看板及内容信息流。功能完善,设计现代。
|
||||
|
||||
* **`DiscoverView-发现页.vue`**
|
||||
* **用途**: 内容中心,以信息流形式展示与食品安全、健康知识相关的文章、资讯和评测。
|
||||
* **状态**: 结构占位,需实现文章列表的渲染和分类筛选功能。
|
||||
|
||||
* **`KitchenView-厨房页.vue`**
|
||||
* **用途**: 健康厨房模块,提供健康菜谱、饮食搭配建议等功能。
|
||||
* **状态**: 结构占位,需实现菜谱的展示和搜索功能。
|
||||
|
||||
* **`MallView-商城页.vue`**
|
||||
* **用途**: 电商模块,用于推荐和销售与健康饮食相关的商品。
|
||||
* **状态**: 结构占位,需实现商品列表、分类及详情页的跳转逻辑。
|
||||
|
||||
* **`MeView-我的.vue`**
|
||||
* **用途**: 个人中心,用于管理用户资料、健康档案、设置、查看收藏等。
|
||||
* **状态**: 结构占位,需实现各功能入口的列表展示。
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户引导与认证页面
|
||||
|
||||
这些页面负责用户的首次进入、注册、登录等流程。
|
||||
|
||||
* **`SplashView-闪屏页.vue`**
|
||||
* **用途**: APP启动时展示的第一个页面,通常用于显示品牌Logo或进行短暂的初始化。
|
||||
* **状态**: 基础实现,通常在几秒后自动跳转到引导页或登录页。
|
||||
|
||||
* **`OnboardingView-引导页.vue`**
|
||||
* **用途**: 用户首次打开APP时的功能介绍和引导流程,通常是可滑动的卡片。
|
||||
* **状态**: 结构占位,需与 `OnboardingStep-引导步骤.vue` 配合实现。
|
||||
|
||||
* **`OnboardingStep-引导步骤.vue`**
|
||||
* **用途**: 单个引导步骤的组件,被 `OnboardingView` 复用。
|
||||
* **状态**: 结构占位。
|
||||
|
||||
* **`LoginView-登录页.vue`**
|
||||
* **用途**: 用户登录/注册页面。
|
||||
* **状态**: 结构占位,需实现输入框、按钮及登录认证逻辑。
|
||||
|
||||
* **`PolicyView-协议页.vue`**
|
||||
* **用途**: 用于展示用户协议和隐私政策的静态文本页面。
|
||||
* **状态**: 结构占位,需填充具体的协议内容。
|
||||
|
||||
---
|
||||
|
||||
## 4. 其他功能与历史版本页面
|
||||
|
||||
* **`MessageView-消息列表页.vue`**
|
||||
* **用途**: 用于展示系统通知、用户互动等消息。
|
||||
* **状态**: 结构占位。
|
||||
|
||||
* **`HomeView-首页-1.0.vue`**
|
||||
* **用途**: 首页的早期版本。
|
||||
* **状态**: **已废弃**。功能和代码与 `2.0` 初始版本一致,可作为历史参考。
|
||||
|
||||
* **`HomeView-首页-2.0.backup.vue`**
|
||||
* **用途**: 在第三轮迭代 (v2.3) 开始前创建的 **v2.2 版本备份**。
|
||||
* **状态**: **备份文件**。可用于回滚或对比。
|
101
3-工作日志/3.2-工作日志-首页迭代开发日志-v1.0.md
Normal file
101
3-工作日志/3.2-工作日志-首页迭代开发日志-v1.0.md
Normal file
@ -0,0 +1,101 @@
|
||||
# “食话食说”APP - 首页迭代开发工作日志
|
||||
|
||||
**最后更新**: 2025-07-23 13:12:00 (UTC+8)
|
||||
**核心负责人**: L.star
|
||||
**目标**: 对产品首页进行多轮迭代设计与开发,以满足用户需求并提升产品体验。
|
||||
|
||||
---
|
||||
|
||||
## 1. 初始状态分析 (RESEARCH)
|
||||
|
||||
* **起始文件**: `shihuashishuo-ui/src/views/HomeView-首页-1.0.vue` & `...-2.0.vue`
|
||||
* **设计文档**: `0-产品需求文档/设计文档-Home页面.md`
|
||||
* **核心问题**:
|
||||
1. **路由错误**: 应用的 `home` 路由指向一个不存在的文件 (`HomeView-首页.vue`)。
|
||||
2. **功能缺失**: 对比设计文档,当前实现**完全缺失“内容信息流 (Feed)”模块**。
|
||||
3. **实现偏差**: 核心扫码按钮尺寸偏小,视觉冲击力不足;底部导航与设计稿不符。
|
||||
4. **版本存疑**: `v1.0` 与 `v2.0` 文件内容完全一致,未发生实质性迭代。
|
||||
|
||||
---
|
||||
|
||||
## 2. 迭代历程 (INNOVATE → EXECUTE → REVIEW)
|
||||
|
||||
### **第一轮迭代: 框架完善 (v2.1 → v2.2)**
|
||||
|
||||
* **用户反馈/目标**:
|
||||
* 修复路由,让页面能正常显示。
|
||||
* 补全缺失的“内容信息流”模块。
|
||||
* 强化核心扫码按钮的视觉效果。
|
||||
* 追求“简单大气”的设计,移除 Slogan。
|
||||
* **解决方案**:
|
||||
* **v2.1 提案**: 提出补全信息流、放大扫码按钮的方案。
|
||||
* **v2.2 提案**: 根据“简单大气”的反馈,移除 Slogan,形成最终方案。
|
||||
* **核心实现**:
|
||||
* **路由修复**: 将 `router/index.ts` 中的 `home` 路由指向 `HomeView-首页-2.0.vue`。
|
||||
* **备份**: 创建了 `HomeView-首页-2.0.backup.vue` 作为安全备份。
|
||||
* **代码修改**:
|
||||
* 在页面底部新增了 `feed-section`,并填充了示例数据。
|
||||
* 显著增大了 `.scan-cta` 的尺寸和样式权重。
|
||||
* 从模板中移除了 Slogan 元素。
|
||||
|
||||
### **第二轮迭代: 功能增强 (v2.3)**
|
||||
|
||||
* **用户反馈/目标**:
|
||||
1. 在搜索栏下方增加搜索热词。
|
||||
2. 将扫码按钮改得小巧一些。
|
||||
3. 将个性化提示框升级为包含“每日营养、热量、喝水记录”的健康看板。
|
||||
* **解决方案**:
|
||||
* **v2.3 提案**: 设计了包含热词、精巧按钮和多功能健康看板的布局。
|
||||
* **核心实现**:
|
||||
* **代码修改**:
|
||||
* 新增 `hot-searches-section` 并填充了关键词数据。
|
||||
* 缩小了 `.scan-cta` 的尺寸。
|
||||
* 将 `.dynamic-card` 替换为全新的 `.health-dashboard`,并使用 `grid` 布局实现三栏式健康数据展示。
|
||||
* **问题修复**: 修复了因复用 `goToSearch` 方法导致的 TypeScript 类型冲突。
|
||||
|
||||
### **第三轮迭代: 布局优化 (v2.4)**
|
||||
|
||||
* **用户反馈/目标**:
|
||||
1. 恢复被替换掉的“个性化使用提示”卡片。
|
||||
2. 解决健康看板三栏式布局“比较奇怪”的问题。
|
||||
* **解决方案**:
|
||||
* **v2.4 提案**: 提出恢复提示卡片,并将健康看板重排为垂直布局的方案。
|
||||
* **核心实现**:
|
||||
* **代码修改**:
|
||||
* 在扫码按钮下方重新加入了 `.dynamic-card` 的 HTML 结构与样式。
|
||||
* 将 `.health-dashboard` 的布局从 `grid` 修改为 `flex-direction: column`,使其内部模块垂直排列,更清晰规整。
|
||||
|
||||
### **第四轮迭代: 视觉升级 (v2.5)**
|
||||
|
||||
* **用户反馈/目标**:
|
||||
1. 将“热量”和“喝水”记录合并到一张卡片里。
|
||||
2. 使用两个并列的圆形图案来记录摄入量。
|
||||
* **解决方案**:
|
||||
* **v2.5 提案**: 设计了包含两个并列圆形进度盘的组合卡片。
|
||||
* **核心实现**:
|
||||
* **代码修改**:
|
||||
* 新增 `.progress-card` 来容纳两个进度盘。
|
||||
* 使用 **SVG** 技术绘制圆形进度盘,通过动态绑定 `stroke-dashoffset` 属性来反映数据进度。
|
||||
* 新增 `computed` 属性 (`calorieProgressOffset`, `waterProgressOffset`) 来处理 SVG 计算。
|
||||
|
||||
### **第五轮迭代: 文本优化 (v2.5.1 - 最终版)**
|
||||
|
||||
* **用户反馈/目标**:
|
||||
* 明确指出“热量”应为“热量摄入”,避免用户误解。
|
||||
* **解决方案**:
|
||||
* 将标签文本从“热量”修改为“热量摄入”。
|
||||
* **核心实现**:
|
||||
* 使用 `apply_diff` 工具,精准地将 `<p>热量</p>` 修改为 `<p>热量摄入</p>`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 最终状态与关键资产
|
||||
|
||||
* **当前版本**: **v2.5.1**
|
||||
* **核心文件**:
|
||||
* `shihuashishuo-ui/src/views/HomeView-首页-2.0.vue` (最终版代码)
|
||||
* `shihuashishuo-ui/src/views/HomeView-首页-2.0.backup.vue` (v2.2 版备份)
|
||||
* `shihuashishuo-ui/src/router/index.ts` (已修复路由)
|
||||
* **本日志**: `3-工作日志/3-工作日志-首页迭代开发日志.md`
|
||||
|
||||
**任务已全部完成。**
|
@ -1,8 +1,8 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import SplashView from '../views/SplashView-闪屏页.vue'
|
||||
import LoginView from '../views/LoginView-登录页.vue'
|
||||
import PolicyView from '../views/PolicyView-协议页.vue'
|
||||
import OnboardingView from '../views/OnboardingView-引导页.vue'
|
||||
import SplashView from '../views/通用基础页/SplashView-闪屏页.vue'
|
||||
import LoginView from '../views/通用基础页/LoginView-登录页-2.0.vue'
|
||||
import PolicyView from '../views/通用基础页/PolicyView-协议页.vue'
|
||||
import OnboardingView from '../views/通用基础页/OnboardingView-引导页.vue'
|
||||
import MainLayout from '../layouts/MainLayout.vue'
|
||||
|
||||
const router = createRouter({
|
||||
@ -32,6 +32,21 @@ const router = createRouter({
|
||||
name: 'onboarding',
|
||||
component: OnboardingView,
|
||||
},
|
||||
{
|
||||
path: '/password-login',
|
||||
name: 'password-login',
|
||||
component: () => import('../views/通用基础页/PasswordLoginView-密码登录页.vue'),
|
||||
},
|
||||
{
|
||||
path: '/auth',
|
||||
name: 'auth',
|
||||
component: () => import('../views/通用基础页/ThirdPartyAuthView-授权登录页.vue'),
|
||||
},
|
||||
{
|
||||
path: '/forgot-password',
|
||||
name: 'forgot-password',
|
||||
component: () => import('../views/通用基础页/ForgotPasswordView-忘记密码页.vue'),
|
||||
},
|
||||
// Main application layout with bottom navigation
|
||||
{
|
||||
path: '/app',
|
||||
@ -91,11 +106,6 @@ const router = createRouter({
|
||||
name: 'messages',
|
||||
component: () => import('../views/MessageView-消息列表页.vue'),
|
||||
},
|
||||
{
|
||||
path: '/home-v1',
|
||||
name: 'home-v1-standalone',
|
||||
component: () => import('../views/HomeView-首页-1.0.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
@ -1,297 +0,0 @@
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<!-- 1. Top Status Bar -->
|
||||
<header class="top-bar">
|
||||
<div class="greeting">晚上好, 王女士</div>
|
||||
<div class="message-icon" @click="goToMessages">
|
||||
<img src="@/assets/message-icon.svg" alt="Messages" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Search Bar Section -->
|
||||
<section class="search-section" @click="goToSearch">
|
||||
<div class="search-bar-container">
|
||||
<input type="text" placeholder="疾病 / 症状 / 药品 / 问题" readonly>
|
||||
<div class="separator"></div>
|
||||
<div class="search-button">搜索</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Banner Section -->
|
||||
<section class="banner-section">
|
||||
<div class="banner-wrapper" :style="bannerStyle">
|
||||
<div v-for="item in bannerItems" :key="item.id" class="banner-slide">
|
||||
<img :src="item.imageUrl" :alt="item.alt" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="banner-dots">
|
||||
<span v-for="(item, index) in bannerItems" :key="item.id"
|
||||
:class="{ active: index === currentIndex }"
|
||||
@click="goToSlide(index)"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Hero Section -->
|
||||
<main class="hero-section">
|
||||
<p class="slogan">食品真相,食话食说</p>
|
||||
<div class="scan-cta" @click="goToScan">
|
||||
<div class="scan-icon">📷</div>
|
||||
<span>扫码/拍照</span>
|
||||
</div>
|
||||
|
||||
<!-- 3. Personalized Dynamics -->
|
||||
<div class="dynamic-card">
|
||||
<div class="card-text">
|
||||
<p>本周您已分析 <strong>8</strong> 种食品,成功为家人避开 <strong>4</strong> 个高风险成分!</p>
|
||||
</div>
|
||||
<div class="card-avatar">
|
||||
<img src="https://images.unsplash.com/photo-1527980965255-d3b416303d12?q=80&w=1780&auto=format&fit=crop" alt="User Avatar" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const bannerItems = ref([
|
||||
{ id: 1, imageUrl: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?q=80&w=2080&auto=format&fit=crop', alt: 'Healthy Salad' },
|
||||
{ id: 2, imageUrl: 'https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?q=80&w=1887&auto=format&fit=crop', alt: 'Fresh Berries' },
|
||||
{ id: 3, imageUrl: 'https://images.unsplash.com/photo-1482049016688-2d3e1b311543?q=80&w=1910&auto=format&fit=crop', alt: 'Avocado Toast' },
|
||||
]);
|
||||
const currentIndex = ref(0);
|
||||
let intervalId: number;
|
||||
|
||||
const bannerStyle = computed(() => ({
|
||||
transform: `translateX(-${currentIndex.value * 100}%)`
|
||||
}));
|
||||
|
||||
const goToSlide = (index: number) => {
|
||||
currentIndex.value = index;
|
||||
// Reset autoplay timer
|
||||
clearInterval(intervalId);
|
||||
intervalId = window.setInterval(nextSlide, 3000);
|
||||
};
|
||||
|
||||
const nextSlide = () => {
|
||||
currentIndex.value = (currentIndex.value + 1) % bannerItems.value.length;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
intervalId = window.setInterval(nextSlide, 3000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const goToMessages = () => {
|
||||
router.push({ name: 'messages' });
|
||||
};
|
||||
|
||||
const goToScan = () => {
|
||||
router.push({ name: 'scan' });
|
||||
};
|
||||
|
||||
const goToSearch = () => {
|
||||
router.push({ name: 'search' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
/* The parent .home-view now handles padding and background */
|
||||
margin-top: 20px; /* Adjust this value to increase/decrease space */
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.search-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-radius: 25px;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #07C160; /* WeChat Green */
|
||||
/* box-shadow: 0 2px 4px rgba(0,0,0,0.05); */ /* Removing shadow for a flatter look */
|
||||
cursor: pointer;
|
||||
max-width: 90%; /* Adjust width */
|
||||
margin: 0 auto; /* Center the search bar */
|
||||
}
|
||||
|
||||
.search-bar-container input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
flex-grow: 1; /* Allow input to take up remaining space */
|
||||
width: 0; /* Necessary for flex-grow to work correctly */
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
pointer-events: none; /* Makes the input not focusable, click is handled by parent */
|
||||
}
|
||||
|
||||
.search-bar-container input::placeholder {
|
||||
color: #ccc; /* Lighter gray for placeholder */
|
||||
font-style: normal; /* Remove italic */
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background-color: #e0e0e0;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
color: #07C160; /* WeChat Green */
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap; /* Prevent text from wrapping */
|
||||
}
|
||||
|
||||
.banner-section {
|
||||
width: 100%;
|
||||
margin-top: 10px; /* Adjust space between search-bar and banner */
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.banner-wrapper {
|
||||
display: flex;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
}
|
||||
.banner-slide {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 150px; /* Set a fixed height for the banner */
|
||||
overflow: hidden;
|
||||
}
|
||||
.banner-slide img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; /* Ensure the image covers the area without distortion */
|
||||
display: block;
|
||||
}
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.banner-dots span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
.banner-dots span.active {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-icon img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start; /* Move content towards the top */
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding-top: 10%; /* Add some padding from the banner */
|
||||
}
|
||||
|
||||
.slogan {
|
||||
font-size: 24px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scan-cta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 180px; /* Smaller size */
|
||||
height: 180px; /* Smaller size */
|
||||
border-radius: 50%;
|
||||
background: var(--color-background-soft); /* Light grey to stand out on white */
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
border: 1px solid #07C160; /* WeChat Green */
|
||||
box-shadow: none; /* Remove shadow */
|
||||
}
|
||||
|
||||
.scan-icon {
|
||||
font-size: 50px; /* Smaller icon */
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dynamic-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-background-soft);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
font-size: 14px;
|
||||
margin-top: 25px; /* Adjust space */
|
||||
width: 90%;
|
||||
max-width: 350px;
|
||||
border: 1px solid #07C160; /* WeChat Green */
|
||||
}
|
||||
|
||||
.card-text {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.card-avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dynamic-card strong {
|
||||
color: var(--color-primary-accent);
|
||||
}
|
||||
</style>
|
@ -46,32 +46,62 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 3. Health Dashboard -->
|
||||
<!-- 3. Personalized Dynamic Card (Restored) -->
|
||||
<section class="dynamic-card-section">
|
||||
<div class="dynamic-card">
|
||||
<div class="card-text">
|
||||
<p>本周您已分析 <strong>8</strong> 种食品,成功为家人避开 <strong>4</strong> 个高风险成分!</p>
|
||||
</div>
|
||||
<div class="card-avatar">
|
||||
<img src="https://images.unsplash.com/photo-1527980965255-d3b416303d12?q=80&w=1780&auto=format&fit=crop" alt="User Avatar" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Health Dashboard (v2.5 Visual Upgrade) -->
|
||||
<section class="health-dashboard">
|
||||
<div class="dashboard-item nutrition-tracker">
|
||||
<h4>每日营养</h4>
|
||||
<div class="nutrition-details">
|
||||
<span>蛋白: {{ healthDashboardData.nutrition.protein }}/{{ healthDashboardData.nutrition.proteinGoal }}g</span>
|
||||
<span>脂肪: {{ healthDashboardData.nutrition.fat }}/{{ healthDashboardData.nutrition.fatGoal }}g</span>
|
||||
<div>
|
||||
<p>蛋白质</p>
|
||||
<strong>{{ healthDashboardData.nutrition.protein }} / {{ healthDashboardData.nutrition.proteinGoal }}g</strong>
|
||||
</div>
|
||||
<div>
|
||||
<p>脂肪</p>
|
||||
<strong>{{ healthDashboardData.nutrition.fat }} / {{ healthDashboardData.nutrition.fatGoal }}g</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-item calorie-tracker">
|
||||
<h4>热量记录</h4>
|
||||
<div class="progress-bar">
|
||||
<div class="progress" :style="{ width: healthDashboardData.calories.progress + '%' }"></div>
|
||||
<div class="dashboard-item progress-card">
|
||||
<!-- Calorie Progress Ring -->
|
||||
<div class="progress-ring">
|
||||
<svg class="progress-ring__svg" :width="ringSize" :height="ringSize">
|
||||
<circle class="progress-ring__circle-bg" :r="radius" :cx="center" :cy="center" />
|
||||
<circle class="progress-ring__circle" :stroke-dasharray="circumference" :stroke-dashoffset="calorieProgressOffset" :r="radius" :cx="center" :cy="center" />
|
||||
</svg>
|
||||
<div class="progress-ring__text">
|
||||
<p>热量摄入</p>
|
||||
<strong>{{ healthDashboardData.calories.current }}</strong>
|
||||
<span>/ {{ healthDashboardData.calories.goal }} kcal</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>{{ healthDashboardData.calories.current }}/{{ healthDashboardData.calories.goal }} kcal</span>
|
||||
</div>
|
||||
<div class="dashboard-item water-tracker">
|
||||
<h4>喝水记录</h4>
|
||||
<div class="water-icons">
|
||||
<span v-for="i in 8" :key="i" :class="{ filled: i <= healthDashboardData.water.cups }">💧</span>
|
||||
<!-- Water Progress Ring -->
|
||||
<div class="progress-ring">
|
||||
<svg class="progress-ring__svg" :width="ringSize" :height="ringSize">
|
||||
<circle class="progress-ring__circle-bg" :r="radius" :cx="center" :cy="center" />
|
||||
<circle class="progress-ring__circle water" :stroke-dasharray="circumference" :stroke-dashoffset="waterProgressOffset" :r="radius" :cx="center" :cy="center" />
|
||||
</svg>
|
||||
<div class="progress-ring__text">
|
||||
<p>饮水</p>
|
||||
<strong>{{ healthDashboardData.water.current / 1000 }}</strong>
|
||||
<span>/ {{ healthDashboardData.water.goal / 1000 }} L</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>{{ healthDashboardData.water.current }}/{{ healthDashboardData.water.goal }} ml</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Content Feed -->
|
||||
<!-- 5. Content Feed -->
|
||||
<section class="feed-section">
|
||||
<div v-for="item in feedItems" :key="item.id" class="feed-card" @click="goTo(item.link)">
|
||||
<div v-if="item.type === 'discover'" class="discover-card">
|
||||
@ -110,10 +140,28 @@ const hotSearches = ref(['无糖酸奶', '酱油', '儿童零食', '高钙牛奶
|
||||
|
||||
const healthDashboardData = ref({
|
||||
nutrition: { protein: 30, proteinGoal: 60, fat: 20, fatGoal: 50 },
|
||||
calories: { current: 800, goal: 1800, progress: computed(() => (800 / 1800) * 100) },
|
||||
water: { current: 1000, goal: 2000, cups: 4 },
|
||||
calories: { current: 800, goal: 1800 },
|
||||
water: { current: 1000, goal: 2000 },
|
||||
});
|
||||
|
||||
// Progress Ring Computations
|
||||
const ringSize = 120;
|
||||
const strokeWidth = 10;
|
||||
const center = ringSize / 2;
|
||||
const radius = center - strokeWidth / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
|
||||
const calorieProgressOffset = computed(() => {
|
||||
const progress = healthDashboardData.value.calories.current / healthDashboardData.value.calories.goal;
|
||||
return circumference * (1 - progress);
|
||||
});
|
||||
|
||||
const waterProgressOffset = computed(() => {
|
||||
const progress = healthDashboardData.value.water.current / healthDashboardData.value.water.goal;
|
||||
return circumference * (1 - progress);
|
||||
});
|
||||
|
||||
|
||||
const feedItems = ref([
|
||||
{ id: 'd1', type: 'discover', title: "警惕!这5种'儿童酱油'其实是钠含量炸弹", summary: '深度评测10款热门儿童酱油,结果令人震惊...', imageUrl: 'https://images.unsplash.com/photo-1598134493282-85b82454f754?q=80&w=1887&auto=format&fit=crop', link: { name: 'discover' } },
|
||||
{ id: 'r1', type: 'recipe', title: '适合减脂期的你:牛油果鸡胸肉沙拉', imageUrl: 'https://images.unsplash.com/photo-1505253716362-af78f6d38348?q=80&w=1887&auto=format&fit=crop', link: { name: 'kitchen' } },
|
||||
@ -295,8 +343,8 @@ const goTo = (link: object) => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160px; /* Adjusted size */
|
||||
height: 160px; /* Adjusted size */
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-background-soft);
|
||||
cursor: pointer;
|
||||
@ -305,74 +353,144 @@ const goTo = (link: object) => {
|
||||
}
|
||||
|
||||
.scan-icon {
|
||||
font-size: 60px; /* Adjusted icon size */
|
||||
font-size: 60px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.scan-cta span {
|
||||
font-size: 16px; /* Adjusted font size */
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dynamic-card-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dynamic-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-background-soft);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
font-size: 14px;
|
||||
width: 90%;
|
||||
max-width: 350px;
|
||||
border: 1px solid #07C160;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.card-avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dynamic-card strong {
|
||||
color: var(--color-primary-accent);
|
||||
}
|
||||
|
||||
.health-dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background-color: var(--color-background-soft);
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.dashboard-item {
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.dashboard-item h4 {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
margin: 0 0 10px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.nutrition-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 5px;
|
||||
.nutrition-details p {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.nutrition-details strong {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background-color: #07C160;
|
||||
border-radius: 4px;
|
||||
.progress-card {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.calorie-tracker span, .water-tracker span {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
.progress-ring {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.water-icons {
|
||||
font-size: 20px;
|
||||
margin-bottom: 5px;
|
||||
.progress-ring__svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.water-icons span {
|
||||
opacity: 0.3;
|
||||
.progress-ring__circle-bg {
|
||||
fill: none;
|
||||
stroke: #e6e6e6;
|
||||
stroke-width: 10;
|
||||
}
|
||||
|
||||
.water-icons span.filled {
|
||||
opacity: 1;
|
||||
.progress-ring__circle {
|
||||
fill: none;
|
||||
stroke: #07C160;
|
||||
stroke-width: 10;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 0.35s;
|
||||
}
|
||||
|
||||
.progress-ring__circle.water {
|
||||
stroke: #007bff;
|
||||
}
|
||||
|
||||
.progress-ring__text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.progress-ring__text p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-ring__text strong {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.progress-ring__text span {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.feed-section {
|
||||
|
202
shihuashishuo-ui/src/views/通用基础页/ForgotPasswordView-忘记密码页.vue
Normal file
202
shihuashishuo-ui/src/views/通用基础页/ForgotPasswordView-忘记密码页.vue
Normal file
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="forgot-password-page">
|
||||
<div class="welcome-section">
|
||||
<h2>找回密码</h2>
|
||||
<p>请通过绑定的手机号重设您的密码</p>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<!-- Step 1: Verify Identity -->
|
||||
<div v-if="step === 1">
|
||||
<div class="input-group">
|
||||
<span class="icon">📱</span>
|
||||
<input type="tel" placeholder="请输入手机号" v-model="phone" maxlength="11" @input="formatInput('phone')" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="icon">✉️</span>
|
||||
<input type="text" placeholder="请输入验证码" v-model="code" maxlength="6" @input="formatInput('code')" />
|
||||
<button class="get-code-btn" @click="getVerificationCode" :disabled="isCountingDown">
|
||||
{{ countdownText }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="submit-btn" @click="verifyCode">下一步</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Reset Password -->
|
||||
<div v-if="step === 2">
|
||||
<div class="input-group">
|
||||
<span class="icon">🔒</span>
|
||||
<input type="password" placeholder="请输入新密码" v-model="newPassword" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="icon">🔒</span>
|
||||
<input type="password" placeholder="请再次输入新密码" v-model="confirmPassword" />
|
||||
</div>
|
||||
<button class="submit-btn" @click="resetPassword">确认重设</button>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||
</div>
|
||||
|
||||
<div class="links-section">
|
||||
<a href="#" @click.prevent="goBack">← 返回登录</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const step = ref(1); // 1: verify, 2: reset
|
||||
const phone = ref('');
|
||||
const code = ref('');
|
||||
const newPassword = ref('');
|
||||
const confirmPassword = ref('');
|
||||
const countdown = ref(0);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const isCountingDown = computed(() => countdown.value > 0);
|
||||
const countdownText = computed(() => {
|
||||
return isCountingDown.value ? `${countdown.value}s 后重试` : '获取验证码';
|
||||
});
|
||||
|
||||
const getVerificationCode = () => {
|
||||
if (isCountingDown.value) return;
|
||||
countdown.value = 60;
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const formatInput = (type: 'phone' | 'code') => {
|
||||
if (type === 'phone') {
|
||||
phone.value = phone.value.replace(/\D/g, '');
|
||||
} else {
|
||||
code.value = code.value.replace(/\D/g, '');
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const verifyCode = () => {
|
||||
errorMessage.value = '';
|
||||
// Mock verification logic
|
||||
if (code.value === '111111') {
|
||||
step.value = 2;
|
||||
} else {
|
||||
errorMessage.value = '验证码错误,请重试';
|
||||
}
|
||||
};
|
||||
|
||||
const resetPassword = () => {
|
||||
// Mock reset logic
|
||||
if (newPassword.value && newPassword.value === confirmPassword.value) {
|
||||
console.log('密码重设成功');
|
||||
router.push('/login');
|
||||
} else {
|
||||
errorMessage.value = '两次输入的密码不一致';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.forgot-password-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80px 40px 20px;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.welcome-section h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.welcome-section p {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
.input-group .icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
border: none;
|
||||
outline: none;
|
||||
flex-grow: 1;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.get-code-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #22c55e;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.get-code-btn:disabled {
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #22c55e;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.links-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.links-section a {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
@ -162,4 +162,4 @@ const wechatLogin = () => {
|
||||
color: #22c55e;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</style>
|
230
shihuashishuo-ui/src/views/通用基础页/LoginView-登录页-2.0.vue
Normal file
230
shihuashishuo-ui/src/views/通用基础页/LoginView-登录页-2.0.vue
Normal file
@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="welcome-section">
|
||||
<h2>食话食说 Talk of Food</h2>
|
||||
<p>守护您和家人的每一餐</p>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="input-group">
|
||||
<span class="icon">📱</span>
|
||||
<input type="tel" placeholder="请输入手机号" v-model="phone" maxlength="11" @input="formatInput('phone')" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="icon">✉️</span>
|
||||
<input type="text" placeholder="请输入验证码" v-model="code" maxlength="6" @input="formatInput('code')" />
|
||||
<button class="get-code-btn" @click="getVerificationCode" :disabled="isCountingDown">
|
||||
{{ countdownText }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="login-btn" @click="mainLogin">登录 / 注册</button>
|
||||
</div>
|
||||
|
||||
<div class="social-login-section">
|
||||
<button class="password-login-btn" @click="goToPasswordLogin">密码登录</button>
|
||||
<div class="social-icons-container">
|
||||
<div class="social-icon" @click="socialLogin('WeChat')">🟢</div>
|
||||
<div class="social-icon" @click="socialLogin('QQ')">🐧</div>
|
||||
<div class="social-icon" @click="socialLogin('TikTok')">🎵</div>
|
||||
<div class="social-icon" @click="socialLogin('Weibo')">🔴</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="policy-section">
|
||||
<input type="checkbox" id="policy-check" checked />
|
||||
<label for="policy-check">
|
||||
我已阅读并同意
|
||||
<router-link to="/policy">用户协议</router-link> 和
|
||||
<router-link to="/policy">隐私政策</router-link>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const phone = ref('');
|
||||
const code = ref('');
|
||||
const countdown = ref(0);
|
||||
|
||||
const isCountingDown = computed(() => countdown.value > 0);
|
||||
const countdownText = computed(() => {
|
||||
return isCountingDown.value ? `${countdown.value}s 后重试` : '获取验证码';
|
||||
});
|
||||
|
||||
const getVerificationCode = () => {
|
||||
if (isCountingDown.value) return;
|
||||
// 在这里可以添加手机号格式校验
|
||||
console.log(`向手机号 ${phone.value} 发送验证码`);
|
||||
|
||||
countdown.value = 60;
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const goToPasswordLogin = () => {
|
||||
router.push('/password-login');
|
||||
};
|
||||
|
||||
const socialLogin = (platform: string) => {
|
||||
router.push({ path: '/auth', query: { platform } });
|
||||
};
|
||||
|
||||
const formatInput = (type: 'phone' | 'code') => {
|
||||
if (type === 'phone') {
|
||||
phone.value = phone.value.replace(/\D/g, '');
|
||||
} else {
|
||||
code.value = code.value.replace(/\D/g, '');
|
||||
}
|
||||
};
|
||||
|
||||
const mainLogin = () => {
|
||||
// TODO: 替换为真实的登录逻辑
|
||||
console.log('执行首次登录,跳转到引导页...');
|
||||
router.push('/onboarding');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 100px 40px 20px;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.welcome-section h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.welcome-section p {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
.input-group .icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
border: none;
|
||||
outline: none;
|
||||
flex-grow: 1;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.get-code-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #22c55e;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.get-code-btn:disabled {
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #22c55e;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.social-login-section {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #9ca3af;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.password-login-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.social-icons-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.policy-section {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.policy-section a {
|
||||
color: #22c55e;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
131
shihuashishuo-ui/src/views/通用基础页/PasswordLoginView-密码登录页.vue
Normal file
131
shihuashishuo-ui/src/views/通用基础页/PasswordLoginView-密码登录页.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="password-login-page">
|
||||
<div class="welcome-section">
|
||||
<h2>密码登录</h2>
|
||||
<p>欢迎回来!</p>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="input-group">
|
||||
<span class="icon">📱</span>
|
||||
<input type="tel" placeholder="请输入手机号" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="icon">🔒</span>
|
||||
<input type="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
<button class="login-btn" @click="passwordLogin">登录</button>
|
||||
</div>
|
||||
|
||||
<div class="links-section">
|
||||
<a href="#" @click.prevent="goToSmsLogin">← 其它登录方式</a>
|
||||
<a href="#" @click.prevent="goToForgotPassword">忘记密码?</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const goToSmsLogin = () => {
|
||||
// 假设您的验证码登录页路由为 /login
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
const passwordLogin = () => {
|
||||
// TODO: 替换为真实的密码登录逻辑
|
||||
console.log('执行首次登录,跳转到引导页...');
|
||||
router.push('/onboarding');
|
||||
};
|
||||
|
||||
const goToForgotPassword = () => {
|
||||
router.push('/forgot-password');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.password-login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 100px 40px 20px;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.welcome-section h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.welcome-section p {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
.input-group .icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
border: none;
|
||||
outline: none;
|
||||
flex-grow: 1;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #22c55e;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.links-section {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.links-section a {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.links-section a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div class="splash-screen">
|
||||
<div class="logo-container">
|
||||
<span class="logo-icon">💬</span>
|
||||
<h1 class="app-title">食话食说</h1>
|
||||
<p class="app-slogan">食品真相, 实话实说</p>
|
||||
<div class="content-wrapper">
|
||||
<img src="@/assets/logo.svg" alt="App Logo" class="main-logo" />
|
||||
<h1 class="app-title">食话食说 Talk of Food</h1>
|
||||
<p class="app-slogan">食品真相,食话食说</p>
|
||||
</div>
|
||||
<p class="version">Version 1.0.0</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -27,42 +26,32 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
.splash-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: #f0fdf4; /* 淡绿色背景 */
|
||||
background-image: linear-gradient(to bottom, #fafff5, #f0fdf4); /* A light, fresh green gradient */
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
.content-wrapper {
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 80px;
|
||||
.main-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 36px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #22c55e; /* 品牌绿色 */
|
||||
color: #22c55e; /* Brand green */
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.app-slogan {
|
||||
font-size: 16px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
color: #4b5563; /* A calm, dark gray */
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="auth-page">
|
||||
<div class="spinner"></div>
|
||||
<h2>正在授权</h2>
|
||||
<p>正在通过 {{ platform }} 安全登录...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const platform = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
// 从路由参数中获取授权平台名称
|
||||
platform.value = route.query.platform as string || '第三方平台';
|
||||
|
||||
// 模拟授权过程
|
||||
setTimeout(() => {
|
||||
// 授权成功后,跳转到引导页
|
||||
router.push('/onboarding');
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f9fafb;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border-left-color: #22c55e;
|
||||
animation: spin 1s ease infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user