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:
L.star 2025-07-23 15:37:18 +08:00
parent f6f418081a
commit f2034c6709
16 changed files with 1189 additions and 383 deletions

View 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. 功能描述
- **核心功能:** 显示静态的法律条款文本。
- **交互反馈:** 点击左上角的 `< 返回` 按钮,页面会返回到之前的页面。

View 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 版本备份**
* **状态**: **备份文件**。可用于回滚或对比。

View 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`
**任务已全部完成。**

View File

@ -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'),
},
],
})

View File

@ -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>

View File

@ -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 {

View 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>

View File

@ -162,4 +162,4 @@ const wechatLogin = () => {
color: #22c55e;
text-decoration: none;
}
</style>
</style>

View 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>

View 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>

View File

@ -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>

View File

@ -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>