feat(mine): 完成我的页面改造

- 实现未登录/已登录两种状态样式
- 添加常用功能入口:我的订单、往期测评、联系我们、邀请新用户
- 添加其他功能入口:关于、用户协议、隐私政策、退出登录
- 实现退出登录二次确认弹窗
- 修复 uni.scss 中 SCSS 导入路径问题
- 整理 .gitignore 文件,移除 unpackage 构建目录
This commit is contained in:
zpc 2026-02-10 00:12:01 +08:00
parent fe9b270571
commit 4387b15de0
269 changed files with 13996 additions and 27140 deletions

View File

@ -0,0 +1,327 @@
# 小程序页面开发设计
## 1. 项目结构
```
uniapp/
├── api/ # API 接口(已有)
│ ├── request.js # 请求封装
│ ├── auth.js # 认证接口
│ ├── user.js # 用户接口
│ ├── home.js # 首页接口(新增)
│ ├── assessment.js # 测评接口(新增)
│ ├── order.js # 订单接口(新增)
│ ├── planner.js # 规划师接口(新增)
│ ├── invite.js # 分销接口(新增)
│ ├── system.js # 系统接口(新增)
│ └── index.js # 统一导出
├── components/ # 公共组件
│ ├── Navbar/ # 自定义导航栏(新增)
│ ├── Loading/ # 加载状态(已有)
│ ├── Empty/ # 空状态(已有)
│ ├── Popup/ # 弹窗(已有)
│ └── index.js
├── composables/ # 组合式函数(新增)
│ ├── useAuth.js # 认证相关
│ ├── usePayment.js # 支付相关
│ └── useNavbar.js # 导航栏相关
├── config/ # 配置(已有)
│ └── index.js
├── pages/ # 页面
│ ├── index/ # 首页(改造)
│ ├── team/ # 团队页(新增)
│ ├── mine/ # 我的页(改造)
│ │ └── profile/ # 个人资料(新增)
│ ├── login/ # 登录页(改造)
│ ├── assessment/ # 测评模块(新增)
│ │ ├── info/ # 信息填写
│ │ ├── questions/ # 答题页
│ │ ├── loading/ # 生成中
│ │ ├── result/ # 结果页
│ │ └── history/ # 往期测评
│ ├── order/ # 订单模块(新增)
│ │ └── list/ # 订单列表
│ ├── business/ # 业务详情(新增)
│ │ └── detail/
│ ├── planner/ # 规划师模块(新增)
│ │ ├── list/ # 规划师列表
│ │ └── book/ # 预约页
│ ├── invite/ # 邀请分销(新增)
│ ├── about/ # 关于页(新增)
│ └── agreement/ # 协议页面(新增)
│ ├── user/ # 用户协议
│ └── privacy/ # 隐私政策
├── store/ # 状态管理(已有)
│ ├── user.js # 用户状态
│ ├── app.js # 应用状态(新增)
│ └── index.js
├── static/ # 静态资源
│ ├── images/ # 图片(新增)
│ └── tabbar/ # TabBar图标已有
├── styles/ # 全局样式(新增)
│ ├── variables.scss # 变量
│ └── common.scss # 通用样式
├── utils/ # 工具函数(已有)
│ ├── storage.js
│ ├── format.js
│ └── validate.js # 验证工具(新增)
├── App.vue
├── main.js
├── pages.json # 页面配置(更新)
└── manifest.json
```
## 2. 页面配置 (pages.json)
```json
{
"pages": [
{ "path": "pages/index/index" },
{ "path": "pages/team/index" },
{ "path": "pages/mine/index" },
{ "path": "pages/login/index" },
{ "path": "pages/mine/profile/index" },
{ "path": "pages/assessment/info/index" },
{ "path": "pages/assessment/questions/index" },
{ "path": "pages/assessment/loading/index" },
{ "path": "pages/assessment/result/index" },
{ "path": "pages/assessment/history/index" },
{ "path": "pages/order/list/index" },
{ "path": "pages/business/detail/index" },
{ "path": "pages/planner/list/index" },
{ "path": "pages/planner/book/index" },
{ "path": "pages/invite/index" },
{ "path": "pages/about/index" },
{ "path": "pages/agreement/user/index" },
{ "path": "pages/agreement/privacy/index" }
],
"tabBar": {
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/team/index", "text": "团队" },
{ "pagePath": "pages/mine/index", "text": "我的" }
]
}
}
```
## 3. 状态管理设计
### 3.1 用户状态 (store/user.js)
```javascript
// 状态
{
token: '',
userInfo: {
userId: 0,
uid: '',
nickname: '',
avatar: '',
phone: '',
userLevel: 1 // 1普通 2合伙人 3渠道合伙人
},
isLoggedIn: false
}
// 方法
- login(data) // 登录
- logout() // 登出
- updateProfile(data) // 更新信息
- restoreFromStorage() // 恢复登录态
```
### 3.2 应用状态 (store/app.js)
```javascript
// 状态
{
statusBarHeight: 20,
navbarHeight: 44,
systemInfo: {}
}
// 方法
- initSystemInfo() // 初始化系统信息
```
## 4. 组合式函数设计
### 4.1 useAuth.js
```javascript
// 功能
- checkLogin() // 检查登录状态,未登录跳转登录页
- requireLogin() // 需要登录的操作装饰器
```
### 4.2 usePayment.js
```javascript
// 功能
- createOrder(params) // 创建订单
- pay(orderId) // 发起支付
- checkPayResult(orderId) // 查询支付结果
```
### 4.3 useNavbar.js
```javascript
// 功能
- statusBarHeight // 状态栏高度
- navbarHeight // 导航栏总高度
- contentPadding // 内容区域顶部padding
```
## 5. 公共组件设计
### 5.1 Navbar 自定义导航栏
```vue
<Navbar
title="页面标题"
:showBack="true"
:transparent="false"
@back="handleBack"
/>
```
### 5.2 已有组件复用
- Loading页面加载、加载更多
- Empty空状态展示
- Popup弹窗确认
## 6. API 模块设计
### 6.1 新增 API 文件
```javascript
// api/home.js
export function getBannerList() { ... }
export function getAssessmentList() { ... }
export function getPromotionList() { ... }
// api/assessment.js
export function getIntro(typeId) { ... }
export function getQuestionList(typeId) { ... }
export function submitAnswers(data) { ... }
export function getResultStatus(recordId) { ... }
export function getResult(recordId) { ... }
export function verifyInviteCode(code) { ... }
export function getHistoryList(params) { ... }
// api/order.js
export function getOrderList(params) { ... }
export function getOrderDetail(orderId) { ... }
export function createOrder(data) { ... }
export function pay(orderId) { ... }
export function getPayResult(orderId) { ... }
// api/planner.js
export function getPlannerList() { ... }
// api/invite.js
export function getInviteInfo() { ... }
export function getQrcode() { ... }
export function getRecordList(params) { ... }
export function getCommission() { ... }
export function applyWithdraw(amount) { ... }
export function getWithdrawList(params) { ... }
// api/system.js
export function getAgreement() { ... }
export function getPrivacy() { ... }
export function getAbout() { ... }
```
## 7. 页面交互流程
### 7.1 测评主流程
```
首页 → 点击测评入口
测评信息填写页
├── 填写基本信息
├── 点击"支付测评" → 创建订单 → 微信支付 → 支付成功
└── 点击"邀请码测评" → 验证邀请码 → 验证成功
测评答题页
├── 展示80道题目
├── 点击提交 → 检测未答题
└── 全部已答 → 提交答案
测评生成中页
├── 轮询查询状态
└── 生成完成 → 自动跳转
测评结果页
├── 展示报告内容
└── 保存PDF调用后端生成
```
### 7.2 登录流程
```
点击需要登录的功能
跳转登录页
用户点击登录按钮
├── wx.login() 获取 code
├── wx.getPhoneNumber() 获取手机号
└── 调用后端登录接口
登录成功 → 保存token → 返回原页面
```
### 7.3 支付流程
```
创建订单 → 获取订单ID
调用支付接口 → 获取微信支付参数
wx.requestPayment() → 拉起微信支付
支付回调 → 查询支付结果 → 跳转下一步
```
## 8. 样式规范
### 8.1 颜色变量
```scss
// styles/variables.scss
$primary-color: #4A90E2; // 主色
$success-color: #52C41A; // 成功
$warning-color: #FAAD14; // 警告
$error-color: #FF4D4F; // 错误
$text-color: #333333; // 主文字
$text-secondary: #666666; // 次要文字
$text-placeholder: #999999; // 占位文字
$border-color: #E8E8E8; // 边框
$bg-color: #F5F5F5; // 背景
$bg-white: #FFFFFF; // 白色背景
```
### 8.2 间距规范
```scss
$spacing-xs: 8rpx;
$spacing-sm: 16rpx;
$spacing-md: 24rpx;
$spacing-lg: 32rpx;
$spacing-xl: 48rpx;
```
### 8.3 字体规范
```scss
$font-size-xs: 22rpx;
$font-size-sm: 24rpx;
$font-size-md: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;
```

View File

@ -0,0 +1,113 @@
# 小程序页面开发需求
## 概述
基于 UniApp + Vue 3 开发微信小程序前端,共 18 个页面API 接口已全部完成。
**所有页面必须严格按照设计图样式开发。**
## 技术栈
- UniApp 3.x + Vue 3 (Composition API)
- Pinia 状态管理
- SCSS 样式
- JavaScript非 TypeScript
## 设计资源
- **Figma 设计图(主要参考)**https://www.figma.com/design/88edYGASUcyID6afiwILdf/项目?node-id=432-1991
- **本地设计图目录**`docs/设计图/`
- **本地切图资源**`docs/切图/`
> **注意**:开发时优先参考 Figma 在线设计图,可获取精确的颜色值、字体大小、间距等样式信息。本地设计图作为离线备份参考。
## 页面清单与设计图对照
### P0 核心页面8个
| 序号 | 页面 | 路由 | 设计图 |
|------|------|------|--------|
| 1 | 首页 | /pages/index/index | `首页.png` |
| 2 | 团队页 | /pages/team/index | `团队.png` |
| 3 | 我的页 | /pages/mine/index | `我的-未登录.png`、`我的-登录页.png`、`我的-退出登录.png` |
| 4 | 登录页 | /pages/login/index | `登录页.png`、`登录页(1).png` |
| 5 | 测评-信息填写 | /pages/assessment/info/index | `测评-个人信息填写.png`、`测评-个人信息填写2.png`、`测评-个人信息填写3.png`、`测评-个人信息填写4.png` |
| 6 | 测评-答题页 | /pages/assessment/questions/index | `测评-题目.png`、`测评-提交题目检验空题.png`、`测评-提交题目检验空题(1).png`、`测评-提交题目检验空题(2).png` |
| 7 | 测评-生成中 | /pages/assessment/loading/index | `测评-等待测评.png`、`测评-测评等待.png` |
| 8 | 测评-结果页 | /pages/assessment/result/index | 暂无设计图(根据需求文档实现) |
### P1 重要页面4个
| 序号 | 页面 | 路由 | 设计图 |
|------|------|------|--------|
| 9 | 个人资料 | /pages/mine/profile/index | `个人资料.png` |
| 10 | 业务详情 | /pages/business/detail/index | `业务详情页.png` |
| 11 | 我的订单 | /pages/order/list/index | `我的订单.png`、`我的订单(1).png`、`我的订单-空状态.png` |
| 12 | 往期测评 | /pages/assessment/history/index | `往期测评-空状态.png` |
### P2 扩展页面3个
| 序号 | 页面 | 路由 | 设计图 |
|------|------|------|--------|
| 13 | 规划师选择 | /pages/planner/list/index | `学业规划.png` |
| 14 | 规划预约 | /pages/planner/book/index | `学业规划2.png`、`学业规划3.png`、`学业规划4.png` |
| 15 | 邀请新用户 | /pages/invite/index | `邀请新用户.png`、`邀请新用户-二维码.png`、`邀请新用户-提现金额.png`、`邀请新用户-提现记录.png`、`邀请新用户-提现记录(1).png` |
### P3 辅助页面3个
| 序号 | 页面 | 路由 | 设计图 |
|------|------|------|--------|
| 16 | 关于页 | /pages/about/index | `关于.png` |
| 17 | 用户协议 | /pages/agreement/user/index | `用户/隐私协议.png` |
| 18 | 隐私政策 | /pages/agreement/privacy/index | `用户/隐私协议.png` |
## API 接口(已完成)
### 首页模块
- GET /api/home/getBannerList - Banner列表
- GET /api/home/getAssessmentList - 测评入口列表
- GET /api/home/getPromotionList - 宣传图列表
### 用户模块
- POST /api/login - 微信登录
- GET /api/user/getProfile - 获取用户信息
- POST /api/user/updateProfile - 更新用户信息
- POST /api/user/updateAvatar - 更新头像
### 测评模块
- GET /api/assessment/getIntro - 测评介绍
- GET /api/assessment/getQuestionList - 题目列表
- POST /api/assessment/submitAnswers - 提交答案
- GET /api/assessment/getResultStatus - 报告状态
- GET /api/assessment/getResult - 测评结果
- POST /api/assessment/verifyInviteCode - 验证邀请码
- GET /api/assessment/getHistoryList - 往期测评
### 订单模块
- GET /api/order/getList - 订单列表
- GET /api/order/getDetail - 订单详情
- POST /api/order/create - 创建订单
- POST /api/order/pay - 发起支付
- GET /api/order/getPayResult - 支付结果
### 其他模块
- GET /api/business/getDetail - 业务详情
- GET /api/planner/getList - 规划师列表
- GET /api/team/getInfo - 团队介绍
- GET /api/invite/getInfo - 邀请信息
- GET /api/invite/getQrcode - 邀请二维码
- GET /api/invite/getRecordList - 邀请记录
- GET /api/invite/getCommission - 佣金信息
- POST /api/invite/applyWithdraw - 申请提现
- GET /api/invite/getWithdrawList - 提现记录
- GET /api/system/getAgreement - 用户协议
- GET /api/system/getPrivacy - 隐私政策
- GET /api/system/getAbout - 关于我们
## 设计资源
- 设计图目录docs/设计图/
- 切图资源docs/切图/
## 开发规范
参考:.kiro/steering/development-standards.md

View File

@ -0,0 +1,285 @@
# 小程序页面开发任务清单
## 概述
小程序前端开发,共 18 个页面,分 4 个阶段完成。
**设计图参考**
- **Figma 在线设计图(主要)**https://www.figma.com/design/88edYGASUcyID6afiwILdf/项目?node-id=432-1991
- **本地设计图**`docs/设计图/`
- **切图资源**`docs/切图/`
**所有页面必须严格按照 Figma 设计图样式开发。**
## 任务列表
### 第一阶段基础框架1-2天
- [x] 1. 项目基础设施
- [x] 1.1 更新 pages.json 配置
- 配置所有页面路由
- 配置 TabBar首页、团队、我的
- 配置全局样式和导航栏
- [x] 1.2 创建全局样式文件
- 创建 styles/variables.scss颜色、间距、字体变量
- 创建 styles/common.scss通用样式类
- 更新 uni.scss 引入变量
- [x] 1.3 创建 API 接口文件
- 创建 api/home.js首页接口
- 创建 api/assessment.js测评接口
- 创建 api/order.js订单接口
- 创建 api/planner.js规划师接口
- 创建 api/invite.js分销接口
- 创建 api/system.js系统接口
- 更新 api/index.js 统一导出
- [x] 1.4 创建状态管理
- 更新 store/user.js添加 userLevel 字段)
- 创建 store/app.js系统信息状态
- 更新 store/index.js
- [x] 1.5 创建组合式函数
- 创建 composables/useAuth.js登录检查
- 创建 composables/usePayment.js支付流程
- 创建 composables/useNavbar.js导航栏高度
- [x] 1.6 创建公共组件
- 创建 components/Navbar/index.vue自定义导航栏
- 更新 components/index.js
- [x] 1.7 创建工具函数
- 创建 utils/validate.js表单验证
- [x] 2. Checkpoint - 基础框架验证
- 确保项目能正常编译运行
- 确保 TabBar 正常显示切换
- 确保 API 请求封装正常工作
### 第二阶段P0 核心页面3-5天
- [x] 3. 登录页
- [x] 3.1 创建 pages/login/index.vue
- **设计图**: `docs/设计图/登录页.png`、`docs/设计图/登录页(1).png`
- 按设计图实现页面布局Logo、登录按钮、协议勾选
- 微信登录流程wx.login + wx.getPhoneNumber
- 调用后端登录接口 POST /api/login
- 登录成功后返回原页面
- [x] 4. 首页
- [x] 4.1 改造 pages/index/index.vue
- **设计图**: `docs/设计图/首页.png`
- 按设计图实现自定义导航栏样式
- Banner 轮播图(调用 GET /api/home/getBannerList
- 测评入口列表(调用 GET /api/home/getAssessmentList
- 底部宣传长图(调用 GET /api/home/getPromotionList
- 点击即将上线测评弹出提示"该测评暂未开放"
- 下拉刷新
- [x] 5. 团队页
- [x] 5.1 创建 pages/team/index.vue
- **设计图**: `docs/设计图/团队.png`
- 按设计图实现自定义导航栏
- 团队介绍图片展示(调用 GET /api/team/getInfo
- [x] 6. 我的页
- [x] 6.1 改造 pages/mine/index.vue
- **设计图**: `docs/设计图/我的-未登录.png`、`docs/设计图/我的-登录页.png`、`docs/设计图/我的-退出登录.png`
- 按设计图实现未登录状态样式
- 按设计图实现已登录状态样式头像、昵称、UID
- 功能入口按设计图布局:我的订单、往期测评、联系我们、邀请新用户(合伙人可见)
- 其他入口:关于、用户协议、隐私政策、退出登录
- 退出登录二次确认弹窗按设计图样式
- [x] 7. Checkpoint - TabBar 页面验证
- 确保三个 TabBar 页面与设计图一致
- 确保登录流程正常
- 确保页面数据正确加载
- [ ] 8. 测评-信息填写页
- [ ] 8.1 创建 pages/assessment/info/index.vue
- **设计图**: `docs/设计图/测评-个人信息填写.png`、`测评-个人信息填写2.png`、`测评-个人信息填写3.png`、`测评-个人信息填写4.png`
- 按设计图实现顶部测评介绍区域
- 按设计图实现表单样式:姓名、手机号、性别、年龄、学业阶段、省市区
- 年龄下拉10-50岁
- 学业阶段下拉6个选项
- 省市区三级联动
- 表单验证:必填项、手机号格式、区县必选
- 按设计图实现两个按钮样式:支付测评、邀请码免费测评
- 未填写完整时按钮灰色不可点击
- 按设计图实现邀请码弹窗样式
- [ ] 9. 测评-答题页
- [ ] 9.1 创建 pages/assessment/questions/index.vue
- **设计图**: `docs/设计图/测评-题目.png`、`测评-提交题目检验空题.png`、`测评-提交题目检验空题(1).png`、`测评-提交题目检验空题(2).png`
- 按设计图实现导航栏样式
- 按设计图实现题目卡片样式
- 按设计图实现选项样式每题10个选项单选
- 按设计图实现底部提交按钮
- 按设计图实现未答题弹窗样式
- 调用 GET /api/assessment/getQuestionList 获取题目
- 调用 POST /api/assessment/submitAnswers 提交答案
- [ ] 10. 测评-生成中页
- [ ] 10.1 创建 pages/assessment/loading/index.vue
- **设计图**: `docs/设计图/测评-等待测评.png`、`docs/设计图/测评-测评等待.png`
- 按设计图实现加载动画样式
- 按设计图实现提示文字样式
- 轮询调用 GET /api/assessment/getResultStatus3秒间隔
- 生成完成自动跳转结果页
- [ ] 11. 测评-结果页
- [ ] 11.1 创建 pages/assessment/result/index.vue
- **设计图**: 暂无参考需求文档第五章第4节
- 自定义导航栏,顶部"保存到本地"按钮
- 基本信息展示
- 八大智能分析展示
- 个人特质分析展示
- 40项细分能力展示
- 其他分析模块展示
- 调用 GET /api/assessment/getResult 获取报告数据
- [ ] 12. Checkpoint - 测评流程验证
- 确保完整测评流程可走通
- 确保各页面与设计图一致
- 信息填写 → 答题 → 生成中 → 结果
### 第三阶段P1 重要页面2-3天
- [ ] 13. 个人资料页
- [ ] 13.1 创建 pages/mine/profile/index.vue
- **设计图**: `docs/设计图/个人资料.png`
- 按设计图实现页面布局
- 头像展示和修改(选择图片、上传)
- 昵称展示和修改
- UID 展示(不可修改)
- 调用 GET /api/user/getProfile、POST /api/user/updateProfile、POST /api/user/updateAvatar
- [ ] 14. 业务详情页
- [ ] 14.1 创建 pages/business/detail/index.vue
- **设计图**: `docs/设计图/业务详情页.png`
- 按设计图实现背景长图展示
- 按设计图实现底部"点击参与"按钮样式
- 调用 GET /api/business/getDetail
- [ ] 15. 我的订单页
- [ ] 15.1 创建 pages/order/list/index.vue
- **设计图**: `docs/设计图/我的订单.png`、`我的订单(1).png`、`我的订单-空状态.png`
- 按设计图实现订单卡片样式
- 订单信息:日期、编号、项目、金额、状态
- 按设计图实现状态操作按钮样式
- 按设计图实现空状态样式
- 调用 GET /api/order/getList
- 下拉刷新、上拉加载
- [ ] 16. 往期测评页
- [ ] 16.1 创建 pages/assessment/history/index.vue
- **设计图**: `docs/设计图/往期测评-空状态.png`
- 按设计图实现测评记录卡片样式
- 按设计图实现空状态样式
- 调用 GET /api/assessment/getHistoryList
- 下拉刷新、上拉加载
- [ ] 17. Checkpoint - P1 页面验证
- 确保各页面与设计图一致
- 确保个人资料修改正常
- 确保订单列表和操作正常
### 第四阶段P2/P3 扩展页面2-3天
- [ ] 18. 规划师选择页
- [ ] 18.1 创建 pages/planner/list/index.vue
- **设计图**: `docs/设计图/学业规划.png`
- 按设计图实现规划师卡片样式
- 展示:照片、姓名、介绍、价格
- 调用 GET /api/planner/getList
- [ ] 19. 规划预约页
- [ ] 19.1 创建 pages/planner/book/index.vue
- **设计图**: `docs/设计图/学业规划2.png`、`学业规划3.png`、`学业规划4.png`
- 按设计图实现日期时间选择样式
- 按设计图实现表单样式
- 根据年级动态显示成绩字段
- 按设计图实现预约成功弹窗样式
- 调用 POST /api/order/create、POST /api/order/pay
- [ ] 20. 邀请新用户页
- [ ] 20.1 创建 pages/invite/index.vue
- **设计图**: `docs/设计图/邀请新用户.png`、`邀请新用户-二维码.png`、`邀请新用户-提现金额.png`、`邀请新用户-提现记录.png`、`邀请新用户-提现记录(1).png`
- 权限检查(仅合伙人可见)
- 按设计图实现邀请规则说明弹窗
- 按设计图实现邀请二维码弹窗
- 按设计图实现已提现/待提现金额展示
- 按设计图实现申请提现弹窗
- 按设计图实现提现记录弹窗
- 按设计图实现邀请记录列表
- 调用分销相关接口
- [ ] 21. 关于页
- [ ] 21.1 创建 pages/about/index.vue
- **设计图**: `docs/设计图/关于.png`
- 按设计图实现 Logo 展示
- 按设计图实现版本号展示
- 调用 GET /api/system/getAbout
- [ ] 22. 用户协议页
- [ ] 22.1 创建 pages/agreement/user/index.vue
- **设计图**: `docs/设计图/用户/隐私协议.png`
- 按设计图实现协议内容展示样式
- 调用 GET /api/system/getAgreement
- [ ] 23. 隐私政策页
- [ ] 23.1 创建 pages/agreement/privacy/index.vue
- **设计图**: `docs/设计图/用户/隐私协议.png`
- 按设计图实现政策内容展示样式
- 调用 GET /api/system/getPrivacy
- [ ] 24. Final Checkpoint - 全部页面验证
- 确保所有 18 个页面与设计图一致
- 确保所有交互流程正常
- 确保登录态在各页面正确处理
- 真机测试各功能
## 开发注意事项
### 1. 设计图还原
- **必须严格按照设计图样式开发**
- **优先参考 Figma 在线设计图**https://www.figma.com/design/88edYGASUcyID6afiwILdf/项目?node-id=432-1991
- 从 Figma 获取精确的颜色值、字体大小、间距、圆角等样式
- 本地设计图 `docs/设计图/` 作为离线参考
- 切图资源在 `docs/切图/` 目录
### 2. 登录态处理
- 需要登录的页面在 onLoad 时检查登录态
- 未登录跳转登录页,登录成功后返回
### 3. 页面跳转
- TabBar 页面使用 uni.switchTab
- 普通页面使用 uni.navigateTo
- 返回使用 uni.navigateBack
### 4. 数据加载
- 使用 Loading 组件显示加载状态
- 使用 Empty 组件显示空状态
- 列表页支持下拉刷新和上拉加载
### 5. 表单验证
- 必填项验证
- 格式验证(手机号等)
- 验证失败显示提示
### 6. 错误处理
- API 请求失败显示错误提示
- 网络异常友好提示
## 进度跟踪
| 阶段 | 任务 | 状态 | 完成日期 |
|------|------|------|----------|
| 第一阶段 | 基础框架 | ⬜ 待开发 | |
| 第二阶段 | P0 核心页面 | ⬜ 待开发 | |
| 第三阶段 | P1 重要页面 | ⬜ 待开发 | |
| 第四阶段 | P2/P3 扩展页面 | ⬜ 待开发 | |
**状态说明**:⬜ 待开发 | 🔄 开发中 | ✅ 已完成

View File

@ -24,7 +24,7 @@
| ORM | Entity Framework Core |
| 缓存 | Redis |
| 接口风格 | RPC 风格(仅 GET / POST 请求) |
| 小程序前端 | UniApp + Vue 3 + TypeScript |
| 小程序前端 | UniApp + Vue 3 |
| 后台管理前端 | Vue 3 + TypeScript + Vite |
## 二、项目结构
@ -110,14 +110,13 @@ MiAssessment.Admin.Business/
| 控制器 | 模块名 + Controller | `UserController.cs` |
| DTO 模型 | 功能 + Request/Dto | `CreateUserRequest.cs`, `UserDto.cs` |
### 3.4 前端命名 (Vue/TypeScript)
### 3.4 前端命名 (Vue/JavaScript)
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件文件 | PascalCase | `UserList.vue`, `OrderDetail.vue` |
| 组合式函数 | use + camelCase | `useUserList.ts`, `useAuth.ts` |
| 工具函数 | camelCase | `formatDate.ts`, `request.ts` |
| 类型定义 | PascalCase | `UserInfo`, `OrderStatus` |
| 组合式函数 | use + camelCase | `useUserList.js`, `useAuth.js` |
| 工具函数 | camelCase | `formatDate.js`, `request.js` |
| 变量/函数 | camelCase | `userList`, `handleSubmit` |
| 常量 | UPPER_SNAKE_CASE | `API_BASE_URL`, `MAX_PAGE_SIZE` |
| CSS 类名 | kebab-case | `user-list`, `order-card` |
@ -383,15 +382,18 @@ public class UserController : BusinessControllerBase
public async Task<PagedResult<UserDto>> GetUserListAsync(UserQueryRequest request)
```
### 7.2 JSDoc 注释 (TypeScript)
### 7.2 JSDoc 注释 (JavaScript)
```typescript
```javascript
/**
* 获取用户列表
* @param params 查询参数
* @returns 分页用户列表
* @param {Object} params - 查询参数
* @param {number} [params.page] - 页码
* @param {number} [params.pageSize] - 每页数量
* @param {string} [params.phone] - 手机号
* @returns {Promise<Object>} 分页用户列表
*/
export async function getUserList(params: UserQueryParams): Promise<PagedResult<UserDto>> {
export async function getUserList(params) {
// ...
}
```
@ -534,32 +536,32 @@ feat(user): 添加用户列表分页查询功能
- 添加分页参数验证
```
## 十二、前端开发规范
## 十二、前端开发规范(小程序)
### 12.1 Vue 组件规范
```vue
<script setup lang="ts">
<script setup>
/**
* 用户列表组件
*/
import { ref, onMounted } from 'vue'
import type { UserDto } from '@/types/user'
import { ref, onMounted, defineProps, defineEmits } from 'vue'
import { getUserList } from '@/api/user'
// Props
const props = defineProps<{
status?: number
}>()
const props = defineProps({
status: {
type: Number,
default: null
}
})
// Emits
const emit = defineEmits<{
select: [user: UserDto]
}>()
const emit = defineEmits(['select'])
// State
const loading = ref(false)
const userList = ref<UserDto[]>([])
const userList = ref([])
// Methods
async function fetchData() {
@ -579,12 +581,12 @@ onMounted(() => {
</script>
<template>
<div class="user-list">
<view class="user-list">
<!-- 内容 -->
</div>
</view>
</template>
<style scoped>
<style scoped lang="scss">
.user-list {
/* 样式 */
}
@ -593,46 +595,49 @@ onMounted(() => {
### 12.2 API 请求规范
```typescript
// api/user.ts
import request from '@/utils/request'
import type { UserDto, UserQueryParams, PagedResult } from '@/types'
```javascript
// api/user.js
import { get, post } from '@/api/request'
/**
* 获取用户列表
* @param {Object} params - 查询参数
* @returns {Promise<Object>}
*/
export function getUserList(params: UserQueryParams) {
return request.get<PagedResult<UserDto>>('/api/admin/user/getList', { params })
export function getUserList(params) {
return get('/user/getList', params)
}
/**
* 更新用户状态
* @param {number} id - 用户ID
* @param {number} status - 状态
* @returns {Promise<Object>}
*/
export function updateUserStatus(id: number, status: number) {
return request.post('/api/admin/user/updateStatus', { id, status })
export function updateUserStatus(id, status) {
return post('/user/updateStatus', { id, status })
}
```
### 12.3 类型定义规范
### 12.3 常量定义规范
```typescript
// types/user.ts
```javascript
// constants/user.js
/** 用户信息 */
export interface UserDto {
id: number
uid: string
nickname: string
phone: string
status: number
createTime: string
/** 用户等级 */
export const USER_LEVEL = {
NORMAL: 1, // 普通用户
PARTNER: 2, // 合伙人
CHANNEL: 3 // 渠道合伙人
}
/** 用户查询参数 */
export interface UserQueryParams {
page?: number
pageSize?: number
phone?: string
status?: number
/** 订单状态 */
export const ORDER_STATUS = {
PENDING: 1, // 待支付
PAID: 2, // 已支付
COMPLETED: 3, // 已完成
REFUNDING: 4, // 退款中
REFUNDED: 5, // 已退款
CANCELLED: 6 // 已取消
}
```

46
uniapp/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# 依赖目录
node_modules/
# 构建输出
unpackage/
dist/
# 本地环境配置
.env.local
.env.*.local
# 编辑器目录和文件
.vscode/*
!.vscode/extensions.json
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 日志文件
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# 系统文件
.DS_Store
Thumbs.db
Desktop.ini
# 测试覆盖率
coverage/
# 缓存目录
.cache/
.temp/
*.local
# TypeScript 缓存
*.tsbuildinfo
# HBuilderX 相关
.hbuilderx/

View File

@ -1,17 +1,17 @@
<script>
import { useConfigStore } from './store/config.js'
import { useUserStore } from './store/user.js'
import { useAppStore } from './store/app.js'
export default {
onLaunch: function() {
console.log('App Launch')
//
const appStore = useAppStore()
appStore.initSystemInfo()
//
const userStore = useUserStore()
userStore.restoreFromStorage()
//
const configStore = useConfigStore()
configStore.loadAppConfig()
},
onShow: function() {
console.log('App Show')

View File

@ -9,8 +9,7 @@
| 技术 | 版本 | 说明 |
|------|------|------|
| UniApp | 3.x | 跨平台框架 |
| Vue | 3.x | 前端框架 |
| TypeScript | 5.x | 类型系统 |
| Vue | 3.x | 前端框架 (Composition API) |
| Pinia | 2.x | 状态管理 |
| uni-ui | latest | UI 组件库 |
| Sass | latest | CSS 预处理器 |
@ -32,59 +31,54 @@ npm run build:mp-weixin
```
uniapp/
├── src/
│ ├── api/ # API 接口
│ │ ├── request.ts # 请求封装
│ │ ├── user.ts # 用户接口
│ │ ├── home.ts # 首页接口
│ │ ├── assessment.ts # 测评接口
│ │ ├── order.ts # 订单接口
│ │ ├── planner.ts # 规划师接口
│ │ └── invite.ts # 分销接口
│ ├── components/ # 公共组件
│ │ ├── common/ # 通用组件
│ │ └── business/ # 业务组件
│ ├── composables/ # 组合式函数
│ │ ├── useAuth.ts # 认证相关
│ │ ├── usePayment.ts # 支付相关
│ │ └── useShare.ts # 分享相关
│ ├── pages/ # 页面
│ │ ├── index/ # 首页TabBar
│ │ ├── team/ # 团队TabBar
│ │ ├── mine/ # 我的TabBar
│ │ ├── login/ # 登录
│ │ ├── assessment/ # 测评相关
│ │ ├── order/ # 订单相关
│ │ ├── planner/ # 学业规划
│ │ ├── invite/ # 邀请分销
│ │ └── about/ # 关于/协议
│ ├── static/ # 静态资源
│ │ ├── images/ # 图片
│ │ └── icons/ # 图标
│ ├── stores/ # Pinia 状态管理
│ │ ├── user.ts # 用户状态
│ │ └── app.ts # 应用状态
│ ├── styles/ # 全局样式
│ │ ├── variables.scss # 变量定义
│ │ ├── mixins.scss # 混入
│ │ └── common.scss # 通用样式
│ ├── types/ # 类型定义
│ │ ├── api.d.ts # API 类型
│ │ ├── user.d.ts # 用户类型
│ │ └── assessment.d.ts # 测评类型
│ ├── utils/ # 工具函数
│ │ ├── storage.ts # 本地存储
│ │ ├── format.ts # 格式化
│ │ └── validate.ts # 验证
│ ├── App.vue # 根组件
│ ├── main.ts # 入口文件
│ ├── manifest.json # 应用配置
│ ├── pages.json # 页面配置
│ └── uni.scss # uni-app 样式变量
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
├── api/ # API 接口
│ ├── request.js # 请求封装
│ ├── user.js # 用户接口
│ ├── home.js # 首页接口
│ ├── assessment.js # 测评接口
│ ├── order.js # 订单接口
│ ├── planner.js # 规划师接口
│ ├── invite.js # 分销接口
│ └── index.js # 统一导出
├── components/ # 公共组件
│ ├── common/ # 通用组件
│ └── business/ # 业务组件
├── composables/ # 组合式函数
│ ├── useAuth.js # 认证相关
│ ├── usePayment.js # 支付相关
│ └── useShare.js # 分享相关
├── config/ # 配置
│ └── index.js # 环境配置
├── pages/ # 页面
│ ├── index/ # 首页TabBar
│ ├── team/ # 团队TabBar
│ ├── mine/ # 我的TabBar
│ ├── login/ # 登录
│ ├── assessment/ # 测评相关
│ ├── order/ # 订单相关
│ ├── planner/ # 学业规划
│ ├── invite/ # 邀请分销
│ └── about/ # 关于/协议
├── static/ # 静态资源
│ ├── images/ # 图片
│ └── icons/ # 图标
├── store/ # Pinia 状态管理
│ ├── user.js # 用户状态
│ ├── app.js # 应用状态
│ └── index.js # 统一导出
├── styles/ # 全局样式
│ ├── variables.scss # 变量定义
│ ├── mixins.scss # 混入
│ └── common.scss # 通用样式
├── utils/ # 工具函数
│ ├── storage.js # 本地存储
│ ├── format.js # 格式化
│ └── validate.js # 验证
├── App.vue # 根组件
├── main.js # 入口文件
├── manifest.json # 应用配置
├── pages.json # 页面配置
└── uni.scss # uni-app 样式变量
```
## 相关文档

83
uniapp/api/assessment.js Normal file
View File

@ -0,0 +1,83 @@
/**
* 测评接口模块
*/
import { get, post } from './request'
/**
* 获取测评介绍
* @param {number} typeId - 测评类型ID
* @returns {Promise<Object>}
*/
export function getIntro(typeId) {
return get('/assessment/getIntro', { typeId })
}
/**
* 获取题目列表
* @param {number} typeId - 测评类型ID
* @returns {Promise<Object>}
*/
export function getQuestionList(typeId) {
return get('/assessment/getQuestionList', { typeId })
}
/**
* 提交答案
* @param {Object} data - 提交数据
* @param {number} data.typeId - 测评类型ID
* @param {Object} data.userInfo - 用户信息
* @param {Array} data.answers - 答案列表
* @returns {Promise<Object>}
*/
export function submitAnswers(data) {
return post('/assessment/submitAnswers', data)
}
/**
* 获取报告生成状态
* @param {number} recordId - 测评记录ID
* @returns {Promise<Object>}
*/
export function getResultStatus(recordId) {
return get('/assessment/getResultStatus', { recordId })
}
/**
* 获取测评结果
* @param {number} recordId - 测评记录ID
* @returns {Promise<Object>}
*/
export function getResult(recordId) {
return get('/assessment/getResult', { recordId })
}
/**
* 验证邀请码
* @param {string} code - 邀请码
* @returns {Promise<Object>}
*/
export function verifyInviteCode(code) {
return post('/assessment/verifyInviteCode', { code })
}
/**
* 获取往期测评列表
* @param {Object} params - 查询参数
* @param {number} [params.page] - 页码
* @param {number} [params.pageSize] - 每页数量
* @returns {Promise<Object>}
*/
export function getHistoryList(params = {}) {
return get('/assessment/getHistoryList', params)
}
export default {
getIntro,
getQuestionList,
submitAnswers,
getResultStatus,
getResult,
verifyInviteCode,
getHistoryList
}

18
uniapp/api/business.js Normal file
View File

@ -0,0 +1,18 @@
/**
* 业务接口模块
*/
import { get } from './request'
/**
* 获取业务详情
* @param {number} businessId - 业务ID
* @returns {Promise<Object>}
*/
export function getBusinessDetail(businessId) {
return get('/business/getDetail', { businessId })
}
export default {
getBusinessDetail
}

35
uniapp/api/home.js Normal file
View File

@ -0,0 +1,35 @@
/**
* 首页接口模块
*/
import { get } from './request'
/**
* 获取Banner列表
* @returns {Promise<Object>}
*/
export function getBannerList() {
return get('/home/getBannerList')
}
/**
* 获取测评入口列表
* @returns {Promise<Object>}
*/
export function getAssessmentList() {
return get('/home/getAssessmentList')
}
/**
* 获取宣传图列表
* @returns {Promise<Object>}
*/
export function getPromotionList() {
return get('/home/getPromotionList')
}
export default {
getBannerList,
getAssessmentList,
getPromotionList
}

View File

@ -6,15 +6,39 @@ export * from './request'
export * from './auth'
export * from './user'
export * from './chat'
export * from './home'
export * from './assessment'
export * from './order'
export * from './planner'
export * from './invite'
export * from './system'
export * from './team'
export * from './business'
import request from './request'
import auth from './auth'
import user from './user'
import chat from './chat'
import home from './home'
import assessment from './assessment'
import order from './order'
import planner from './planner'
import invite from './invite'
import system from './system'
import team from './team'
import business from './business'
export default {
request,
auth,
user,
chat
chat,
home,
assessment,
order,
planner,
invite,
system,
team,
business
}

69
uniapp/api/invite.js Normal file
View File

@ -0,0 +1,69 @@
/**
* 分销邀请接口模块
*/
import { get, post } from './request'
/**
* 获取邀请信息
* @returns {Promise<Object>}
*/
export function getInviteInfo() {
return get('/invite/getInfo')
}
/**
* 获取邀请二维码
* @returns {Promise<Object>}
*/
export function getQrcode() {
return get('/invite/getQrcode')
}
/**
* 获取邀请记录列表
* @param {Object} params - 查询参数
* @param {number} [params.page] - 页码
* @param {number} [params.pageSize] - 每页数量
* @returns {Promise<Object>}
*/
export function getRecordList(params = {}) {
return get('/invite/getRecordList', params)
}
/**
* 获取佣金信息
* @returns {Promise<Object>}
*/
export function getCommission() {
return get('/invite/getCommission')
}
/**
* 申请提现
* @param {number} amount - 提现金额
* @returns {Promise<Object>}
*/
export function applyWithdraw(amount) {
return post('/invite/applyWithdraw', { amount })
}
/**
* 获取提现记录列表
* @param {Object} params - 查询参数
* @param {number} [params.page] - 页码
* @param {number} [params.pageSize] - 每页数量
* @returns {Promise<Object>}
*/
export function getWithdrawList(params = {}) {
return get('/invite/getWithdrawList', params)
}
export default {
getInviteInfo,
getQrcode,
getRecordList,
getCommission,
applyWithdraw,
getWithdrawList
}

64
uniapp/api/order.js Normal file
View File

@ -0,0 +1,64 @@
/**
* 订单接口模块
*/
import { get, post } from './request'
/**
* 获取订单列表
* @param {Object} params - 查询参数
* @param {number} [params.page] - 页码
* @param {number} [params.pageSize] - 每页数量
* @param {number} [params.status] - 订单状态
* @returns {Promise<Object>}
*/
export function getOrderList(params = {}) {
return get('/order/getList', params)
}
/**
* 获取订单详情
* @param {number} orderId - 订单ID
* @returns {Promise<Object>}
*/
export function getOrderDetail(orderId) {
return get('/order/getDetail', { orderId })
}
/**
* 创建订单
* @param {Object} data - 订单数据
* @param {number} data.productType - 产品类型
* @param {number} data.productId - 产品ID
* @param {Object} [data.userInfo] - 用户信息
* @returns {Promise<Object>}
*/
export function createOrder(data) {
return post('/order/create', data)
}
/**
* 发起支付
* @param {number} orderId - 订单ID
* @returns {Promise<Object>}
*/
export function pay(orderId) {
return post('/order/pay', { orderId })
}
/**
* 获取支付结果
* @param {number} orderId - 订单ID
* @returns {Promise<Object>}
*/
export function getPayResult(orderId) {
return get('/order/getPayResult', { orderId })
}
export default {
getOrderList,
getOrderDetail,
createOrder,
pay,
getPayResult
}

38
uniapp/api/planner.js Normal file
View File

@ -0,0 +1,38 @@
/**
* 规划师接口模块
*/
import { get } from './request'
/**
* 获取规划师列表
* @returns {Promise<Object>}
*/
export function getPlannerList() {
return get('/planner/getList')
}
/**
* 获取规划师详情
* @param {number} plannerId - 规划师ID
* @returns {Promise<Object>}
*/
export function getPlannerDetail(plannerId) {
return get('/planner/getDetail', { plannerId })
}
/**
* 获取可预约时间
* @param {number} plannerId - 规划师ID
* @param {string} date - 日期 YYYY-MM-DD
* @returns {Promise<Object>}
*/
export function getAvailableTime(plannerId, date) {
return get('/planner/getAvailableTime', { plannerId, date })
}
export default {
getPlannerList,
getPlannerDetail,
getAvailableTime
}

View File

@ -2,13 +2,13 @@
* 请求封装模块
* 封装 uni.request 支持 GET/POST/PUT/DELETE
* 实现 JWT Token 自动携带
* 实现 401 状态码拦截和重新认证
* 实现 401 状态码拦截和自动刷新 Token
* 支持请求重试机制
* 支持请求取消机制
* Requirements: 1.2, 1.4
*/
import { getToken, removeToken, removeUserInfo } from '../utils/storage'
import { getToken, setToken, removeToken, getRefreshToken, setRefreshToken, removeRefreshToken, removeUserInfo } from '../utils/storage'
import config from '../config/index'
// 从统一配置获取 API 基础地址
@ -17,6 +17,11 @@ const BASE_URL = config.API_BASE_URL
// 存储进行中的请求任务,用于取消
const pendingRequests = new Map()
// Token 刷新状态
let isRefreshing = false
// 等待 Token 刷新的请求队列
let refreshSubscribers = []
/**
* 生成请求唯一标识
* @param {Object} options 请求配置
@ -28,12 +33,84 @@ function generateRequestKey(options) {
}
/**
* 处理401错误 - 清除token
* 处理401错误 - 清除token并跳转登录
* Property 2: Auth Redirect on Invalid Token
*/
export function handleUnauthorized() {
removeToken()
removeRefreshToken()
removeUserInfo()
// 跳转到登录页
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentPath = currentPage ? `/${currentPage.route}` : ''
uni.navigateTo({
url: `/pages/login/index?redirect=${encodeURIComponent(currentPath)}`
})
}
/**
* 订阅 Token 刷新完成事件
* @param {Function} callback 回调函数
*/
function subscribeTokenRefresh(callback) {
refreshSubscribers.push(callback)
}
/**
* 通知所有订阅者 Token 已刷新
* @param {string} newToken 新的 Token
*/
function onTokenRefreshed(newToken) {
refreshSubscribers.forEach(callback => callback(newToken))
refreshSubscribers = []
}
/**
* 刷新 Token
* @returns {Promise<string|null>} 新的 Token null
*/
async function refreshToken() {
const refreshTokenValue = getRefreshToken()
if (!refreshTokenValue) {
return null
}
try {
const response = await new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}/refresh`,
method: 'POST',
data: { refreshToken: refreshTokenValue },
header: { 'Content-Type': 'application/json' },
timeout: config.REQUEST_TIMEOUT,
success: (res) => resolve(res),
fail: (err) => reject(err)
})
})
const { statusCode, data: responseData } = response
if (statusCode === 200 && responseData.code === 0 && responseData.data) {
const { token, refreshToken: newRefreshToken } = responseData.data
// 保存新的 Token
setToken(token)
if (newRefreshToken) {
setRefreshToken(newRefreshToken)
}
return token
}
return null
} catch (error) {
console.error('Token refresh failed:', error)
return null
}
}
/**
@ -85,7 +162,7 @@ export function cancelRequestsByPrefix(urlPrefix) {
}
/**
* 发起请求带重试和取消机制
* 发起请求带重试取消和 Token 刷新机制
* @param {Object} options 请求配置
* @param {number} retryCount 当前重试次数
* @returns {Promise}
@ -99,7 +176,8 @@ export function request(options, retryCount = 0) {
needAuth = true,
retry = true,
cancelable = true,
cancelPrevious = false
cancelPrevious = false,
skipRefresh = false // 是否跳过 Token 刷新(用于刷新 Token 请求本身)
} = options
const requestKey = generateRequestKey(options)
@ -126,16 +204,52 @@ export function request(options, retryCount = 0) {
data,
header: requestHeader,
timeout: config.REQUEST_TIMEOUT,
success: (res) => {
success: async (res) => {
if (cancelable) {
pendingRequests.delete(requestKey)
}
const { statusCode, data: responseData } = res
if (statusCode === 401) {
// 处理 401 未授权 - 尝试刷新 Token
if (statusCode === 401 && needAuth && !skipRefresh) {
// 如果正在刷新 Token将请求加入队列等待
if (isRefreshing) {
subscribeTokenRefresh((newToken) => {
// 使用新 Token 重试请求
const newOptions = { ...options, header: { ...header, Authorization: `Bearer ${newToken}` } }
request(newOptions, retryCount)
.then(resolve)
.catch(reject)
})
return
}
isRefreshing = true
try {
const newToken = await refreshToken()
if (newToken) {
isRefreshing = false
onTokenRefreshed(newToken)
// 使用新 Token 重试当前请求
const newOptions = { ...options, header: { ...header, Authorization: `Bearer ${newToken}` } }
const result = await request(newOptions, retryCount)
resolve(result)
return
}
} catch (refreshError) {
console.error('Token refresh error:', refreshError)
}
isRefreshing = false
refreshSubscribers = []
// 刷新失败,清除登录状态并跳转登录页
handleUnauthorized()
reject(new Error('未授权,请重新登录'))
reject(new Error('登录已过期,请重新登录'))
return
}

35
uniapp/api/system.js Normal file
View File

@ -0,0 +1,35 @@
/**
* 系统接口模块
*/
import { get } from './request'
/**
* 获取用户协议
* @returns {Promise<Object>}
*/
export function getAgreement() {
return get('/system/getAgreement')
}
/**
* 获取隐私政策
* @returns {Promise<Object>}
*/
export function getPrivacy() {
return get('/system/getPrivacy')
}
/**
* 获取关于我们
* @returns {Promise<Object>}
*/
export function getAbout() {
return get('/system/getAbout')
}
export default {
getAgreement,
getPrivacy,
getAbout
}

17
uniapp/api/team.js Normal file
View File

@ -0,0 +1,17 @@
/**
* 团队接口模块
*/
import { get } from './request'
/**
* 获取团队介绍
* @returns {Promise<Object>}
*/
export function getTeamInfo() {
return get('/team/getInfo')
}
export default {
getTeamInfo
}

View File

@ -0,0 +1,224 @@
<script setup>
/**
* 自定义导航栏组件
*
* Props:
* - title: 标题文字
* - showBack: 是否显示返回按钮
* - transparent: 是否透明背景
* - textColor: 文字颜色 (light/dark)
* - backgroundColor: 背景颜色
*
* Events:
* - back: 点击返回按钮
*
* Slots:
* - left: 左侧自定义内容
* - right: 右侧自定义内容
* - default: 中间自定义内容替代title
*/
import { computed } from 'vue'
import { useNavbar } from '../../composables/useNavbar.js'
const props = defineProps({
title: {
type: String,
default: ''
},
showBack: {
type: Boolean,
default: true
},
transparent: {
type: Boolean,
default: false
},
textColor: {
type: String,
default: 'dark',
validator: (value) => ['light', 'dark'].includes(value)
},
backgroundColor: {
type: String,
default: '#FFFFFF'
}
})
const emit = defineEmits(['back'])
const { statusBarHeight, navbarHeight, totalNavbarHeight } = useNavbar()
//
const navbarStyle = computed(() => {
const style = {
paddingTop: `${statusBarHeight.value}px`,
height: `${totalNavbarHeight.value}px`
}
if (!props.transparent) {
style.backgroundColor = props.backgroundColor
}
return style
})
//
const contentStyle = computed(() => ({
height: `${navbarHeight.value}px`
}))
//
const placeholderStyle = computed(() => ({
height: `${totalNavbarHeight.value}px`
}))
//
const textColorValue = computed(() => {
return props.textColor === 'light' ? '#FFFFFF' : '#333333'
})
//
const backIconColor = computed(() => {
return props.textColor === 'light' ? '#FFFFFF' : '#333333'
})
/**
* 处理返回
*/
function handleBack() {
emit('back')
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.switchTab({
url: '/pages/index/index'
})
}
}
</script>
<template>
<view class="navbar-wrapper">
<!-- 导航栏 -->
<view
class="navbar"
:class="{ 'navbar--transparent': transparent }"
:style="navbarStyle"
>
<view class="navbar__content" :style="contentStyle">
<!-- 左侧区域 -->
<view class="navbar__left">
<slot name="left">
<view
v-if="showBack"
class="navbar__back"
@click="handleBack"
>
<view class="navbar__back-icon" :style="{ borderColor: backIconColor }"></view>
</view>
</slot>
</view>
<!-- 中间区域 -->
<view class="navbar__center">
<slot>
<text
class="navbar__title"
:style="{ color: textColorValue }"
>{{ title }}</text>
</slot>
</view>
<!-- 右侧区域 -->
<view class="navbar__right">
<slot name="right"></slot>
</view>
</view>
</view>
<!-- 占位元素 -->
<view v-if="!transparent" class="navbar__placeholder" :style="placeholderStyle"></view>
</view>
</template>
<style scoped lang="scss">
.navbar-wrapper {
position: relative;
}
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
&--transparent {
background-color: transparent;
}
}
.navbar__content {
display: flex;
align-items: center;
padding: 0 24rpx;
}
.navbar__left,
.navbar__right {
width: 80rpx;
flex-shrink: 0;
}
.navbar__left {
display: flex;
align-items: center;
justify-content: flex-start;
}
.navbar__right {
display: flex;
align-items: center;
justify-content: flex-end;
}
.navbar__center {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.navbar__title {
font-size: 34rpx;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.navbar__back {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
}
.navbar__back-icon {
width: 20rpx;
height: 20rpx;
border-left: 4rpx solid #333;
border-bottom: 4rpx solid #333;
transform: rotate(45deg);
margin-left: 8rpx;
}
.navbar__placeholder {
width: 100%;
}
</style>

View File

@ -7,6 +7,7 @@
* - Loading: 加载组件
* - EmojiPicker: 表情选择器
* - VoiceRecorder: 语音录制组件
* - Navbar: 自定义导航栏
*/
export { default as Popup } from './Popup/index.vue'
@ -14,3 +15,4 @@ export { default as Empty } from './Empty/index.vue'
export { default as Loading } from './Loading/index.vue'
export { default as EmojiPicker } from './EmojiPicker/index.vue'
export { default as VoiceRecorder } from './VoiceRecorder/index.vue'
export { default as Navbar } from './Navbar/index.vue'

View File

@ -0,0 +1,7 @@
/**
* 组合式函数统一导出
*/
export { useAuth } from './useAuth.js'
export { usePayment } from './usePayment.js'
export { useNavbar } from './useNavbar.js'

View File

@ -0,0 +1,81 @@
/**
* 认证相关组合式函数
*/
import { useUserStore } from '../store/user.js'
/**
* 认证相关功能
* @returns {Object}
*/
export function useAuth() {
const userStore = useUserStore()
/**
* 检查登录状态未登录则跳转登录页
* @param {string} [redirectUrl] - 登录后重定向的页面路径
* @returns {boolean} 是否已登录
*/
function checkLogin(redirectUrl) {
if (userStore.isLoggedIn) {
return true
}
// 保存当前页面路径,登录后返回
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const url = redirectUrl || `/${currentPage.route}`
uni.navigateTo({
url: `/pages/login/index?redirect=${encodeURIComponent(url)}`
})
return false
}
/**
* 需要登录的操作装饰器
* @param {Function} fn - 需要登录才能执行的函数
* @returns {Function}
*/
function requireLogin(fn) {
return function(...args) {
if (!checkLogin()) {
return
}
return fn.apply(this, args)
}
}
/**
* 跳转到登录页
* @param {string} [redirect] - 登录后重定向的页面路径
*/
function goLogin(redirect) {
const url = redirect
? `/pages/login/index?redirect=${encodeURIComponent(redirect)}`
: '/pages/login/index'
uni.navigateTo({ url })
}
/**
* 登出并跳转到登录页
*/
function logoutAndRedirect() {
userStore.logout()
uni.reLaunch({
url: '/pages/login/index'
})
}
return {
checkLogin,
requireLogin,
goLogin,
logoutAndRedirect,
isLoggedIn: () => userStore.isLoggedIn
}
}
export default useAuth

View File

@ -0,0 +1,93 @@
/**
* 导航栏相关组合式函数
*/
import { computed } from 'vue'
import { useAppStore } from '../store/app.js'
/**
* 导航栏相关功能
* @returns {Object}
*/
export function useNavbar() {
const appStore = useAppStore()
// 确保系统信息已初始化
if (!appStore.initialized) {
appStore.initSystemInfo()
}
/**
* 状态栏高度
*/
const statusBarHeight = computed(() => appStore.statusBarHeight)
/**
* 导航栏高度不含状态栏
*/
const navbarHeight = computed(() => appStore.navbarHeight)
/**
* 导航栏总高度状态栏 + 导航栏
*/
const totalNavbarHeight = computed(() => appStore.totalNavbarHeight)
/**
* 内容区域顶部padding
*/
const contentPaddingTop = computed(() => appStore.contentPaddingTop)
/**
* 获取导航栏样式
* @param {boolean} [fixed=true] - 是否固定定位
* @returns {Object}
*/
function getNavbarStyle(fixed = true) {
const style = {
paddingTop: `${statusBarHeight.value}px`,
height: `${totalNavbarHeight.value}px`
}
if (fixed) {
style.position = 'fixed'
style.top = '0'
style.left = '0'
style.right = '0'
style.zIndex = '999'
}
return style
}
/**
* 获取内容区域样式用于自定义导航栏页面
* @returns {Object}
*/
function getContentStyle() {
return {
paddingTop: `${totalNavbarHeight.value}px`
}
}
/**
* 获取占位元素样式
* @returns {Object}
*/
function getPlaceholderStyle() {
return {
height: `${totalNavbarHeight.value}px`
}
}
return {
statusBarHeight,
navbarHeight,
totalNavbarHeight,
contentPaddingTop,
getNavbarStyle,
getContentStyle,
getPlaceholderStyle
}
}
export default useNavbar

View File

@ -0,0 +1,158 @@
/**
* 支付相关组合式函数
*/
import { ref } from 'vue'
import { createOrder, pay, getPayResult } from '../api/order.js'
/**
* 支付相关功能
* @returns {Object}
*/
export function usePayment() {
const loading = ref(false)
const paymentError = ref('')
/**
* 创建订单
* @param {Object} params - 订单参数
* @param {number} params.productType - 产品类型
* @param {number} params.productId - 产品ID
* @param {Object} [params.userInfo] - 用户信息
* @returns {Promise<Object|null>}
*/
async function handleCreateOrder(params) {
loading.value = true
paymentError.value = ''
try {
const res = await createOrder(params)
if (res.code === 0 && res.data) {
return res.data
}
paymentError.value = res.message || '创建订单失败'
return null
} catch (e) {
paymentError.value = e.message || '创建订单失败'
return null
} finally {
loading.value = false
}
}
/**
* 发起微信支付
* @param {number} orderId - 订单ID
* @returns {Promise<boolean>}
*/
async function handlePay(orderId) {
loading.value = true
paymentError.value = ''
try {
// 获取支付参数
const res = await pay(orderId)
if (res.code !== 0 || !res.data) {
paymentError.value = res.message || '获取支付参数失败'
return false
}
const payParams = res.data
// 调用微信支付
return new Promise((resolve) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'MD5',
paySign: payParams.paySign,
success: () => {
resolve(true)
},
fail: (err) => {
if (err.errMsg && err.errMsg.includes('cancel')) {
paymentError.value = '支付已取消'
} else {
paymentError.value = '支付失败'
}
resolve(false)
}
})
})
} catch (e) {
paymentError.value = e.message || '支付失败'
return false
} finally {
loading.value = false
}
}
/**
* 查询支付结果
* @param {number} orderId - 订单ID
* @param {number} [maxRetry=3] - 最大重试次数
* @param {number} [interval=1000] - 重试间隔毫秒
* @returns {Promise<Object|null>}
*/
async function checkPayResult(orderId, maxRetry = 3, interval = 1000) {
for (let i = 0; i < maxRetry; i++) {
try {
const res = await getPayResult(orderId)
if (res.code === 0 && res.data) {
if (res.data.paid) {
return res.data
}
}
// 等待后重试
if (i < maxRetry - 1) {
await new Promise(resolve => setTimeout(resolve, interval))
}
} catch (e) {
console.error('查询支付结果失败:', e)
}
}
return null
}
/**
* 完整支付流程
* @param {Object} orderParams - 订单参数
* @returns {Promise<{success: boolean, orderId?: number, error?: string}>}
*/
async function processPayment(orderParams) {
// 1. 创建订单
const order = await handleCreateOrder(orderParams)
if (!order) {
return { success: false, error: paymentError.value }
}
// 2. 发起支付
const paySuccess = await handlePay(order.orderId)
if (!paySuccess) {
return { success: false, orderId: order.orderId, error: paymentError.value }
}
// 3. 查询支付结果
const result = await checkPayResult(order.orderId)
if (result && result.paid) {
return { success: true, orderId: order.orderId }
}
return { success: false, orderId: order.orderId, error: '支付结果确认失败' }
}
return {
loading,
paymentError,
createOrder: handleCreateOrder,
pay: handlePay,
checkPayResult,
processPayment
}
}
export default usePayment

View File

@ -5,9 +5,9 @@
const ENV = {
development: {
API_BASE_URL: 'http://localhost:5000/api',
STATIC_BASE_URL: 'http://localhost:5000',
SIGNALR_URL: 'ws://localhost:5000/hubs/chat'
API_BASE_URL: 'http://192.168.195.15:2830/api',
STATIC_BASE_URL: 'http://192.168.195.15:2830',
SIGNALR_URL: 'ws://192.168.195.15:2830/hubs/chat'
},
production: {
API_BASE_URL: 'https://your-domain.com/api',

View File

@ -1,6 +1,6 @@
{
"name" : "相宜亲家",
"appid" : "__UNI__39EAECC",
"name" : "学业邑规划",
"appid" : "__UNI__1BAACAB",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
@ -50,9 +50,10 @@
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx21b4110b18b31831",
"appid" : "wx9cd3660bb51f2387",
"setting" : {
"urlCheck" : false
"urlCheck" : false,
"es6" : true
},
"usingComponents" : true
},

10434
uniapp/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "uniapp-chat-template",
"name": "mi-assessment-miniapp",
"version": "1.0.0",
"description": "uni-app 小程序模板,包含聊天功能",
"description": "学业邑规划 - 微信小程序",
"scripts": {
"dev:mp-weixin": "uni -p mp-weixin",
"build:mp-weixin": "uni build -p mp-weixin"
@ -10,7 +10,13 @@
"pinia": "^2.1.7"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4020920240930001",
"@dcloudio/uni-cli-shared": "3.0.0-4020920240930001",
"@dcloudio/uni-mp-weixin": "3.0.0-4020920240930001",
"@dcloudio/vite-plugin-uni": "3.0.0-4020920240930001",
"sass": "^1.69.5",
"sass-loader": "^13.3.2"
"sass-loader": "^13.3.2",
"vite": "^5.2.8"
}
}

View File

@ -5,14 +5,14 @@
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "首页",
"enablePullDownRefresh": false
"enablePullDownRefresh": true
}
},
{
"path": "pages/message/index",
"path": "pages/team/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "消息"
"navigationBarTitleText": "团队"
}
},
{
@ -22,30 +22,112 @@
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/chat/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "聊天"
}
},
{
"path": "pages/login/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/mine/profile/index",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/assessment/info/index",
"style": {
"navigationBarTitleText": "测评信息"
}
},
{
"path": "pages/assessment/questions/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "测评答题"
}
},
{
"path": "pages/assessment/loading/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "生成报告"
}
},
{
"path": "pages/assessment/result/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "测评结果"
}
},
{
"path": "pages/assessment/history/index",
"style": {
"navigationBarTitleText": "往期测评"
}
},
{
"path": "pages/order/list/index",
"style": {
"navigationBarTitleText": "我的订单"
}
},
{
"path": "pages/business/detail/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "业务详情"
}
},
{
"path": "pages/planner/list/index",
"style": {
"navigationBarTitleText": "学业规划"
}
},
{
"path": "pages/planner/book/index",
"style": {
"navigationBarTitleText": "预约规划"
}
},
{
"path": "pages/invite/index",
"style": {
"navigationBarTitleText": "邀请新用户"
}
},
{
"path": "pages/about/index",
"style": {
"navigationBarTitleText": "关于"
}
},
{
"path": "pages/agreement/user/index",
"style": {
"navigationBarTitleText": "用户协议"
}
},
{
"path": "pages/agreement/privacy/index",
"style": {
"navigationBarTitleText": "隐私政策"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "App Template",
"navigationBarTitleText": "学业邑规划",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
"backgroundColor": "#F5F5F5",
"backgroundTextStyle": "dark"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#FF6B6B",
"selectedColor": "#4A90E2",
"borderStyle": "black",
"backgroundColor": "#FFFFFF",
"list": [
@ -56,8 +138,8 @@
"selectedIconPath": "static/tabbar/home_s.png"
},
{
"pagePath": "pages/message/index",
"text": "消息",
"pagePath": "pages/team/index",
"text": "团队",
"iconPath": "static/tabbar/message.png",
"selectedIconPath": "static/tabbar/message_s.png"
},

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 关于页面
*/
</script>
<template>
<view class="about-page">
<view class="placeholder">关于页面</view>
</view>
</template>
<style scoped lang="scss">
.about-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 隐私政策页面
*/
</script>
<template>
<view class="agreement-privacy-page">
<view class="placeholder">隐私政策页面</view>
</view>
</template>
<style scoped lang="scss">
.agreement-privacy-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 用户协议页面
*/
</script>
<template>
<view class="agreement-user-page">
<view class="placeholder">用户协议页面</view>
</view>
</template>
<style scoped lang="scss">
.agreement-user-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 往期测评页面
*/
</script>
<template>
<view class="assessment-history-page">
<view class="placeholder">往期测评页面</view>
</view>
</template>
<style scoped lang="scss">
.assessment-history-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 测评信息填写页面
*/
</script>
<template>
<view class="assessment-info-page">
<view class="placeholder">测评信息填写页面</view>
</view>
</template>
<style scoped lang="scss">
.assessment-info-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 测评生成中页面
*/
</script>
<template>
<view class="assessment-loading-page">
<view class="placeholder">测评生成中页面</view>
</view>
</template>
<style scoped lang="scss">
.assessment-loading-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 测评答题页面
*/
</script>
<template>
<view class="assessment-questions-page">
<view class="placeholder">测评答题页面</view>
</view>
</template>
<style scoped lang="scss">
.assessment-questions-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 测评结果页面
*/
</script>
<template>
<view class="assessment-result-page">
<view class="placeholder">测评结果页面</view>
</view>
</template>
<style scoped lang="scss">
.assessment-result-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 业务详情页面
*/
</script>
<template>
<view class="business-detail-page">
<view class="placeholder">业务详情页面</view>
</view>
</template>
<style scoped lang="scss">
.business-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -1,104 +1,352 @@
<template>
<view class="home-page">
<Loading type="page" :loading="pageLoading" />
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<text class="header-title">首页</text>
<view class="custom-navbar" :style="navbarStyle">
<view class="navbar-content" :style="{ height: navbarHeight + 'px' }">
<text class="navbar-title">首页</text>
</view>
</view>
<!-- 内容区域 -->
<scroll-view
class="page-scroll"
scroll-y
:style="{ paddingTop: (statusBarHeight + 44) + 'px' }"
>
<view class="content-section">
<Empty text="欢迎使用模板" :showButton="false" />
<!-- 导航栏占位 -->
<view class="navbar-placeholder" :style="{ height: totalNavbarHeight + 'px' }"></view>
<!-- 页面内容 -->
<view class="page-content">
<!-- Banner 轮播图 -->
<view class="banner-section" v-if="bannerList.length > 0">
<swiper
class="banner-swiper"
:indicator-dots="bannerList.length > 1"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#FFFFFF"
:autoplay="true"
:interval="4000"
:circular="true"
>
<swiper-item
v-for="(item, index) in bannerList"
:key="index"
@click="handleBannerClick(item)"
>
<image
:src="item.imageUrl"
mode="aspectFill"
class="banner-image"
/>
</swiper-item>
</swiper>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</scroll-view>
<!-- 测评入口列表 -->
<view class="assessment-section" v-if="assessmentList.length > 0">
<view
class="assessment-item"
v-for="(item, index) in assessmentList"
:key="index"
@click="handleAssessmentClick(item)"
>
<image
:src="item.imageUrl"
mode="widthFix"
class="assessment-image"
/>
<!-- 即将上线标签 -->
<view v-if="item.status === 0" class="coming-soon-tag">
<text>即将上线</text>
</view>
</view>
</view>
<!-- 底部宣传长图 -->
<view class="promotion-section" v-if="promotionList.length > 0">
<image
v-for="(item, index) in promotionList"
:key="index"
:src="item.imageUrl"
mode="widthFix"
class="promotion-image"
/>
</view>
<!-- 加载状态 -->
<view class="loading-section" v-if="pageLoading">
<Loading type="inline" :loading="true" />
</view>
<!-- 底部安全区域 -->
<view class="safe-bottom"></view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { onPullDownRefresh } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user.js'
import { useNavbar } from '@/composables/useNavbar.js'
import { getBannerList, getAssessmentList, getPromotionList } from '@/api/home.js'
import Loading from '@/components/Loading/index.vue'
import Empty from '@/components/Empty/index.vue'
const userStore = useUserStore()
const { statusBarHeight, navbarHeight, totalNavbarHeight } = useNavbar()
const statusBarHeight = ref(20)
//
const pageLoading = ref(true)
const bannerList = ref([])
const assessmentList = ref([])
const promotionList = ref([])
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
//
const navbarStyle = computed(() => ({
paddingTop: statusBarHeight.value + 'px',
height: totalNavbarHeight.value + 'px'
}))
/**
* 加载Banner数据
*/
async function loadBannerList() {
try {
const res = await getBannerList()
if (res && res.code === 0 && res.data) {
bannerList.value = res.data.list || res.data || []
}
})
} catch (error) {
console.error('加载Banner失败:', error)
}
}
const initPage = async () => {
/**
* 加载测评入口数据
*/
async function loadAssessmentList() {
try {
const res = await getAssessmentList()
if (res && res.code === 0 && res.data) {
assessmentList.value = res.data.list || res.data || []
}
} catch (error) {
console.error('加载测评入口失败:', error)
}
}
/**
* 加载宣传图数据
*/
async function loadPromotionList() {
try {
const res = await getPromotionList()
if (res && res.code === 0 && res.data) {
promotionList.value = res.data.list || res.data || []
}
} catch (error) {
console.error('加载宣传图失败:', error)
}
}
/**
* 初始化页面数据
*/
async function initPageData() {
pageLoading.value = true
try {
//
userStore.restoreFromStorage()
//
await Promise.all([
loadBannerList(),
loadAssessmentList(),
loadPromotionList()
])
} finally {
pageLoading.value = false
}
}
/**
* 处理Banner点击
*/
function handleBannerClick(item) {
if (!item.linkUrl) return
//
if (item.linkType === 'page') {
//
uni.navigateTo({
url: item.linkUrl,
fail: () => {
// tabBar使switchTab
uni.switchTab({
url: item.linkUrl
})
}
})
} else if (item.linkType === 'webview') {
// webview
uni.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(item.linkUrl)}`
})
}
}
/**
* 处理测评入口点击
*/
function handleAssessmentClick(item) {
// 线
if (item.status === 0) {
uni.showToast({
title: '该测评暂未开放',
icon: 'none',
duration: 2000
})
return
}
//
uni.navigateTo({
url: `/pages/assessment/info/index?typeId=${item.id}&typeName=${encodeURIComponent(item.name || '')}`
})
}
/**
* 下拉刷新
*/
onPullDownRefresh(async () => {
await initPageData()
uni.stopPullDownRefresh()
})
/**
* 页面加载
*/
onMounted(() => {
getSystemInfo()
initPage()
initPageData()
})
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.home-page {
min-height: 100vh;
background-color: #f5f5f5;
background-color: $bg-color;
}
//
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #fff;
z-index: 100;
background-color: $bg-white;
z-index: 999;
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
.header-title {
font-size: 17px;
font-weight: 600;
color: #333;
.navbar-title {
font-size: 34rpx;
font-weight: $font-weight-medium;
color: $text-color;
}
}
}
.page-scroll {
min-height: 100vh;
.navbar-placeholder {
width: 100%;
}
.content-section {
padding: 100px 20px;
//
.page-content {
padding-bottom: env(safe-area-inset-bottom);
}
// Banner
.banner-section {
width: 100%;
padding: 0 $spacing-lg;
box-sizing: border-box;
margin-top: $spacing-md;
.banner-swiper {
width: 100%;
height: 320rpx;
border-radius: $border-radius-lg;
overflow: hidden;
.banner-image {
width: 100%;
height: 100%;
border-radius: $border-radius-lg;
}
}
}
//
.assessment-section {
padding: $spacing-lg;
.assessment-item {
position: relative;
margin-bottom: $spacing-md;
border-radius: $border-radius-lg;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
.assessment-image {
width: 100%;
display: block;
}
.coming-soon-tag {
position: absolute;
top: 20rpx;
right: 20rpx;
background-color: rgba(0, 0, 0, 0.5);
padding: 8rpx 20rpx;
border-radius: $border-radius-round;
text {
font-size: $font-size-xs;
color: $text-white;
}
}
}
}
//
.promotion-section {
padding: 0 $spacing-lg;
.promotion-image {
width: 100%;
display: block;
margin-bottom: $spacing-md;
border-radius: $border-radius-lg;
&:last-child {
margin-bottom: 0;
}
}
}
//
.loading-section {
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.bottom-placeholder {
height: 100px;
//
.safe-bottom {
height: 40rpx;
padding-bottom: env(safe-area-inset-bottom);
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 邀请新用户页面
*/
</script>
<template>
<view class="invite-page">
<view class="placeholder">邀请新用户页面</view>
</view>
</template>
<style scoped lang="scss">
.invite-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -1,52 +1,110 @@
<template>
<view class="login-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
<Navbar title="登录" :showBack="true" backgroundColor="#FFFFFF" />
<!-- 主内容区域 -->
<view class="login-content">
<!-- Logo区域 -->
<view class="logo-section">
<view class="logo-wrapper">
<image src="/static/logo.png" mode="aspectFit" class="logo-img" />
</view>
<text class="navbar-title">登录</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- Logo区域 -->
<view class="logo-section" :style="{ marginTop: (statusBarHeight + 44) + 'px' }">
<view class="logo-wrapper">
<image src="/static/logo.png" mode="aspectFit" class="logo-img" />
<!-- 底部登录区域 -->
<view class="bottom-section">
<!-- 登录按钮 - 使用 open-type="getPhoneNumber" 获取手机号 -->
<button
class="btn-login"
:class="{ 'btn-disabled': !isAgreed }"
:disabled="!isAgreed"
open-type="getPhoneNumber"
@getphonenumber="handleGetPhoneNumber"
>
一键注册/登录
</button>
<!-- 协议勾选 -->
<view class="agreement-section" @click="toggleAgreement">
<view class="checkbox" :class="{ 'checkbox-checked': isAgreed }">
<view v-if="isAgreed" class="checkbox-icon"></view>
</view>
<view class="agreement-text">
<text>我已阅读并同意</text>
<text class="link" @click.stop="goUserAgreement">用户协议</text>
<text></text>
<text class="link" @click.stop="goPrivacyPolicy">隐私政策</text>
</view>
</view>
</view>
</view>
<!-- 底部区域 -->
<view class="bottom-section">
<button class="btn-login" @click="handleLogin">一键登录</button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { login } from '@/api/auth.js'
import { post } from '@/api/request.js'
import Navbar from '@/components/Navbar/index.vue'
const userStore = useUserStore()
const statusBarHeight = ref(20)
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
}
//
const isAgreed = ref(false)
//
const redirectUrl = ref('')
/**
* 切换协议勾选状态
*/
function toggleAgreement() {
isAgreed.value = !isAgreed.value
}
/**
* 跳转用户协议
*/
function goUserAgreement() {
uni.navigateTo({
url: '/pages/agreement/user/index'
})
}
const handleBack = () => {
uni.navigateBack()
/**
* 跳转隐私政策
*/
function goPrivacyPolicy() {
uni.navigateTo({
url: '/pages/agreement/privacy/index'
})
}
const handleLogin = async () => {
/**
* 处理获取手机号回调
* @param {Object} e - 回调事件对象
*/
async function handleGetPhoneNumber(e) {
if (!isAgreed.value) {
uni.showToast({
title: '请先同意用户协议和隐私政策',
icon: 'none'
})
return
}
//
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
uni.showToast({
title: '需要授权手机号才能登录',
icon: 'none'
})
return
}
try {
// 1. code
const loginRes = await new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
@ -55,27 +113,64 @@ const handleLogin = async () => {
})
})
if (loginRes.code) {
uni.showLoading({ title: '登录中...' })
const res = await login(loginRes.code)
uni.hideLoading()
if (!loginRes.code) {
uni.showToast({ title: '获取登录凭证失败', icon: 'none' })
return
}
if (res && res.code === 0 && res.data) {
userStore.login({
token: res.data.token,
userInfo: {
userId: res.data.userId,
nickname: res.data.nickname,
avatar: res.data.avatar
}
})
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.showLoading({ title: '登录中...' })
// 2. code code
const res = await post('/login', {
code: loginRes.code,
phoneCode: e.detail.code // code
}, { needAuth: false })
uni.hideLoading()
if (res && res.code === 0 && res.data) {
// refreshToken
userStore.login({
token: res.data.token,
refreshToken: res.data.refreshToken,
userInfo: {
userId: res.data.userId,
uid: res.data.uid,
nickname: res.data.nickname,
avatar: res.data.avatar,
phone: res.data.phone,
userLevel: res.data.userLevel || 1
}
})
uni.showToast({ title: '登录成功', icon: 'success' })
//
setTimeout(() => {
if (redirectUrl.value) {
//
uni.redirectTo({
url: redirectUrl.value,
fail: () => {
// tabBar 使 switchTab
uni.switchTab({
url: redirectUrl.value,
fail: () => {
uni.navigateBack()
}
})
}
})
} else {
//
uni.navigateBack()
}, 1000)
} else {
uni.showToast({ title: res?.message || '登录失败', icon: 'none' })
}
}
}, 1000)
} else {
uni.showToast({
title: res?.message || '登录失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
@ -84,96 +179,215 @@ const handleLogin = async () => {
}
}
/**
* 处理登录备用方法用于不需要手机号的场景
*/
async function handleLogin() {
if (!isAgreed.value) {
uni.showToast({
title: '请先同意用户协议和隐私政策',
icon: 'none'
})
return
}
try {
// 1. code
const loginRes = await new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject
})
})
if (!loginRes.code) {
uni.showToast({ title: '获取登录凭证失败', icon: 'none' })
return
}
uni.showLoading({ title: '登录中...' })
// 2.
const res = await post('/login', {
code: loginRes.code
}, { needAuth: false })
uni.hideLoading()
if (res && res.code === 0 && res.data) {
// refreshToken
userStore.login({
token: res.data.token,
refreshToken: res.data.refreshToken,
userInfo: {
userId: res.data.userId,
uid: res.data.uid,
nickname: res.data.nickname,
avatar: res.data.avatar,
phone: res.data.phone,
userLevel: res.data.userLevel || 1
}
})
uni.showToast({ title: '登录成功', icon: 'success' })
//
setTimeout(() => {
if (redirectUrl.value) {
uni.redirectTo({
url: redirectUrl.value,
fail: () => {
uni.switchTab({
url: redirectUrl.value,
fail: () => {
uni.navigateBack()
}
})
}
})
} else {
uni.navigateBack()
}
}, 1000)
} else {
uni.showToast({
title: res?.message || '登录失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('登录失败:', error)
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
}
}
/**
* 页面加载
*/
onMounted(() => {
getSystemInfo()
//
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
if (options.redirect) {
redirectUrl.value = decodeURIComponent(options.redirect)
}
})
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.login-page {
min-height: 100vh;
background-color: #f5f5f5;
background-color: $bg-white;
display: flex;
flex-direction: column;
}
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #f5f5f5;
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
.navbar-back {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 24px;
color: #333;
}
}
.navbar-title {
font-size: 17px;
font-weight: 600;
color: #333;
}
.navbar-placeholder {
width: 30px;
}
}
.login-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 54rpx;
}
.logo-section {
flex: 1;
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 60px;
align-items: center;
padding-top: 80rpx;
.logo-wrapper {
width: 180px;
height: 180px;
background: #fff;
border-radius: 12px;
width: 494rpx;
height: 494rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
.logo-img {
width: 100px;
height: 100px;
width: 100%;
height: 100%;
}
}
}
.bottom-section {
padding: 30px 24px 40px;
padding-bottom: 120rpx;
.btn-login {
width: 100%;
height: 48px;
line-height: 48px;
background: linear-gradient(135deg, #FFBDC2 0%, #FF8A93 100%);
border-radius: 24px;
font-size: 17px;
color: #fff;
height: 72rpx;
line-height: 72rpx;
background-color: $primary-color;
border-radius: 36rpx;
font-size: 30rpx;
color: $text-white;
border: none;
font-weight: 500;
font-weight: $font-weight-medium;
&::after {
border: none;
}
&:active {
opacity: 0.8;
}
}
.btn-disabled {
background-color: #CCCCCC;
pointer-events: none;
}
}
.agreement-section {
display: flex;
align-items: flex-start;
justify-content: center;
margin-top: 32rpx;
padding: 0 20rpx;
.checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid $border-color;
border-radius: 4rpx;
margin-right: 12rpx;
margin-top: 4rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
&-checked {
background-color: $primary-color;
border-color: $primary-color;
}
.checkbox-icon {
width: 16rpx;
height: 10rpx;
border-left: 3rpx solid $text-white;
border-bottom: 3rpx solid $text-white;
transform: rotate(-45deg);
margin-top: -4rpx;
}
}
.agreement-text {
font-size: 24rpx;
color: $text-secondary;
line-height: 1.6;
flex-wrap: wrap;
.link {
color: $primary-color;
}
}
}
</style>

View File

@ -1,41 +1,121 @@
<template>
<view class="mine-page">
<Loading type="page" :loading="pageLoading" />
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="custom-navbar" :style="navbarStyle">
<view class="navbar-content" :style="{ height: navbarHeight + 'px' }">
<text class="navbar-title">我的</text>
</view>
</view>
<!-- 内容区域 -->
<view class="content" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<!-- 未登录状态 -->
<view class="login-card" v-if="!isLoggedIn && !pageLoading">
<text class="login-tip">登录后体验更多功能</text>
<button class="btn-login" @click="handleLogin">立即登录</button>
</view>
<!-- 导航栏占位 -->
<view class="navbar-placeholder" :style="{ height: totalNavbarHeight + 'px' }"></view>
<!-- 已登录状态 -->
<view class="user-section" v-else-if="isLoggedIn && !pageLoading">
<view class="user-info-row">
<image
class="user-avatar"
:src="userInfo.avatar || '/static/logo.png'"
mode="aspectFill"
/>
<view class="user-detail">
<text class="user-nickname">{{ userInfo.nickname || '用户' }}</text>
<!-- 页面内容 -->
<view class="page-content">
<!-- 用户信息区域 -->
<view class="user-section">
<!-- 未登录状态 -->
<view v-if="!isLoggedIn" class="user-card not-logged">
<view class="avatar-wrapper">
<image class="avatar" src="/static/logo.png" mode="aspectFill" />
</view>
<view class="user-info">
<text class="login-tip">登录后体验更多功能</text>
<button class="btn-login" @click="handleLogin">立即登录</button>
</view>
</view>
<!-- 已登录状态 -->
<view v-else class="user-card logged" @click="goProfile">
<view class="avatar-wrapper">
<image
class="avatar"
:src="userInfo.avatar || '/static/logo.png'"
mode="aspectFill"
/>
</view>
<view class="user-info">
<text class="nickname">{{ userInfo.nickname || '用户' }}</text>
<text class="uid">UID: {{ userInfo.uid || '--' }}</text>
</view>
<view class="arrow-icon">
<text class="iconfont"></text>
</view>
</view>
</view>
<!-- 菜单列表 -->
<!-- 常用功能区域 -->
<view class="menu-section" v-if="isLoggedIn">
<view class="menu-item" @click="handleLogout">
<text class="menu-title">退出登录</text>
<text class="menu-arrow"></text>
<view class="section-title">常用功能</view>
<view class="menu-grid">
<view class="menu-grid-item" @click="goOrderList">
<view class="menu-icon-wrapper order">
<text class="menu-icon-text">📋</text>
</view>
<text class="menu-text">我的订单</text>
</view>
<view class="menu-grid-item" @click="goAssessmentHistory">
<view class="menu-icon-wrapper history">
<text class="menu-icon-text">📊</text>
</view>
<text class="menu-text">往期测评</text>
</view>
<view class="menu-grid-item" @click="handleContactUs">
<view class="menu-icon-wrapper contact">
<text class="menu-icon-text">📞</text>
</view>
<text class="menu-text">联系我们</text>
</view>
<view class="menu-grid-item" v-if="isPartner" @click="goInvite">
<view class="menu-icon-wrapper invite">
<text class="menu-icon-text">👥</text>
</view>
<text class="menu-text">邀请新用户</text>
</view>
</view>
</view>
<!-- 其他功能区域 -->
<view class="menu-section">
<view class="section-title">其他</view>
<view class="menu-list">
<view class="menu-item" @click="goAbout">
<text class="menu-title">关于</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="goUserAgreement">
<text class="menu-title">用户协议</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="goPrivacyPolicy">
<text class="menu-title">隐私政策</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item logout-item" v-if="isLoggedIn" @click="showLogoutPopup">
<text class="menu-title logout-text">退出登录</text>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<!-- 底部安全区域 -->
<view class="safe-bottom"></view>
</view>
<!-- 退出登录确认弹窗 -->
<view v-if="logoutPopupVisible" class="popup-mask" @click="hideLogoutPopup">
<view class="popup-container" @click.stop>
<view class="popup-content">
<text class="popup-title">提示</text>
<text class="popup-message">确定要退出登录吗</text>
</view>
<view class="popup-buttons">
<view class="popup-btn cancel" @click="hideLogoutPopup">
<text>取消</text>
</view>
<view class="popup-btn confirm" @click="handleLogout">
<text>确定</text>
</view>
</view>
</view>
</view>
@ -43,179 +123,483 @@
</template>
<script setup>
/**
* 我的页面
* 展示用户信息和功能入口
*/
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user.js'
import Loading from '@/components/Loading/index.vue'
import { useNavbar } from '@/composables/useNavbar.js'
const userStore = useUserStore()
const { statusBarHeight, navbarHeight, totalNavbarHeight } = useNavbar()
const pageLoading = ref(true)
const statusBarHeight = ref(20)
// 退
const logoutPopupVisible = ref(false)
//
const isLoggedIn = computed(() => userStore.isLoggedIn)
const isPartner = computed(() => userStore.isPartner)
const userInfo = computed(() => ({
userId: userStore.userId,
uid: userStore.uid,
nickname: userStore.nickname,
avatar: userStore.avatar
}))
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
}
//
const navbarStyle = computed(() => ({
paddingTop: statusBarHeight.value + 'px',
height: totalNavbarHeight.value + 'px'
}))
/**
* 跳转登录页
*/
function handleLogin() {
uni.navigateTo({
url: '/pages/login/index'
})
}
const initPage = async () => {
pageLoading.value = true
try {
userStore.restoreFromStorage()
} finally {
pageLoading.value = false
}
/**
* 跳转个人资料页
*/
function goProfile() {
uni.navigateTo({
url: '/pages/mine/profile/index'
})
}
const handleLogin = () => {
uni.navigateTo({ url: '/pages/login/index' })
/**
* 跳转我的订单
*/
function goOrderList() {
uni.navigateTo({
url: '/pages/order/list/index'
})
}
const handleLogout = () => {
/**
* 跳转往期测评
*/
function goAssessmentHistory() {
uni.navigateTo({
url: '/pages/assessment/history/index'
})
}
/**
* 联系我们
*/
function handleContactUs() {
//
//
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
userStore.logout()
uni.showToast({ title: '已退出登录', icon: 'success' })
}
}
title: '联系我们',
content: '如有问题请联系客服微信xxxxxx',
showCancel: false,
confirmText: '我知道了'
})
}
/**
* 跳转邀请新用户
*/
function goInvite() {
uni.navigateTo({
url: '/pages/invite/index'
})
}
/**
* 跳转关于页
*/
function goAbout() {
uni.navigateTo({
url: '/pages/about/index'
})
}
/**
* 跳转用户协议
*/
function goUserAgreement() {
uni.navigateTo({
url: '/pages/agreement/user/index'
})
}
/**
* 跳转隐私政策
*/
function goPrivacyPolicy() {
uni.navigateTo({
url: '/pages/agreement/privacy/index'
})
}
/**
* 显示退出登录弹窗
*/
function showLogoutPopup() {
logoutPopupVisible.value = true
}
/**
* 隐藏退出登录弹窗
*/
function hideLogoutPopup() {
logoutPopupVisible.value = false
}
/**
* 处理退出登录
*/
function handleLogout() {
userStore.logout()
logoutPopupVisible.value = false
uni.showToast({
title: '已退出登录',
icon: 'success'
})
}
/**
* 页面显示时恢复用户状态
*/
onShow(() => {
userStore.restoreFromStorage()
})
/**
* 页面加载
*/
onMounted(() => {
getSystemInfo()
initPage()
userStore.restoreFromStorage()
})
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.mine-page {
min-height: 100vh;
background-color: #f5f5f5;
background-color: $bg-color;
}
//
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #fff;
z-index: 100;
background-color: $bg-white;
z-index: 999;
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
.navbar-title {
font-size: 17px;
font-weight: 600;
color: #333;
font-size: 34rpx;
font-weight: $font-weight-medium;
color: $text-color;
}
}
}
.content {
padding: 16px;
.navbar-placeholder {
width: 100%;
}
.login-card {
background: #fff;
border-radius: 12px;
padding: 24px 20px;
text-align: center;
.login-tip {
display: block;
font-size: 16px;
color: #333;
margin-bottom: 16px;
}
.btn-login {
width: 140px;
height: 40px;
line-height: 40px;
background: linear-gradient(135deg, #FFBDC2 0%, #FF8A93 100%);
border-radius: 20px;
font-size: 15px;
color: #fff;
border: none;
}
//
.page-content {
padding: $spacing-lg;
padding-bottom: env(safe-area-inset-bottom);
}
//
.user-section {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
.user-info-row {
margin-bottom: $spacing-lg;
.user-card {
background-color: $bg-white;
border-radius: $border-radius-lg;
padding: $spacing-lg;
display: flex;
align-items: center;
.user-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
margin-right: 16px;
.avatar-wrapper {
margin-right: $spacing-lg;
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: $bg-gray;
}
}
.user-detail {
.user-nickname {
font-size: 18px;
font-weight: 600;
color: #333;
.user-info {
flex: 1;
}
//
&.not-logged {
.user-info {
display: flex;
flex-direction: column;
align-items: flex-start;
.login-tip {
font-size: $font-size-md;
color: $text-secondary;
margin-bottom: $spacing-sm;
}
.btn-login {
width: 180rpx;
height: 64rpx;
line-height: 64rpx;
background-color: $primary-color;
border-radius: 32rpx;
font-size: $font-size-sm;
color: $text-white;
border: none;
padding: 0;
margin: 0;
&::after {
border: none;
}
&:active {
opacity: 0.8;
}
}
}
}
//
&.logged {
.user-info {
display: flex;
flex-direction: column;
.nickname {
font-size: $font-size-lg;
font-weight: $font-weight-medium;
color: $text-color;
margin-bottom: 8rpx;
}
.uid {
font-size: $font-size-sm;
color: $text-placeholder;
}
}
.arrow-icon {
.iconfont {
font-size: 40rpx;
color: $text-placeholder;
}
}
}
}
}
//
.menu-section {
background: #fff;
border-radius: 12px;
.menu-item {
background-color: $bg-white;
border-radius: $border-radius-lg;
margin-bottom: $spacing-lg;
overflow: hidden;
.section-title {
font-size: $font-size-md;
font-weight: $font-weight-medium;
color: $text-color;
padding: $spacing-lg $spacing-lg $spacing-sm;
}
//
.menu-grid {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
flex-wrap: wrap;
padding: $spacing-sm $spacing-lg $spacing-lg;
.menu-grid-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding: $spacing-md 0;
&:active {
opacity: 0.7;
}
.menu-icon-wrapper {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-xs;
&.order {
background-color: rgba(74, 144, 226, 0.1);
}
&.history {
background-color: rgba(82, 196, 26, 0.1);
}
&.contact {
background-color: rgba(250, 173, 20, 0.1);
}
&.invite {
background-color: rgba(255, 77, 79, 0.1);
}
.menu-icon-text {
font-size: 40rpx;
}
}
.menu-icon {
width: 56rpx;
height: 56rpx;
margin-bottom: $spacing-xs;
}
.menu-text {
font-size: $font-size-xs;
color: $text-color;
}
}
&:active {
background-color: #f8f8f8;
}
//
.menu-list {
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-lg;
border-bottom: 1rpx solid $border-light;
&:last-child {
border-bottom: none;
}
&:active {
background-color: $bg-gray;
}
.menu-title {
font-size: $font-size-md;
color: $text-color;
}
.menu-arrow {
font-size: 32rpx;
color: $text-placeholder;
}
&.logout-item {
.logout-text {
color: $error-color;
}
}
}
}
}
.menu-title {
font-size: 15px;
color: #333;
//
.safe-bottom {
height: 40rpx;
padding-bottom: env(safe-area-inset-bottom);
}
// 退
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: $bg-mask;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup-container {
width: 560rpx;
background-color: $bg-white;
border-radius: $border-radius-lg;
overflow: hidden;
.popup-content {
padding: 60rpx 40rpx 40rpx;
text-align: center;
.popup-title {
display: block;
font-size: $font-size-lg;
font-weight: $font-weight-medium;
color: $text-color;
margin-bottom: $spacing-md;
}
.menu-arrow {
font-size: 16px;
color: #ccc;
.popup-message {
display: block;
font-size: $font-size-md;
color: $text-secondary;
}
}
.popup-buttons {
display: flex;
border-top: 1rpx solid $border-light;
.popup-btn {
flex: 1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
background-color: $bg-gray;
}
text {
font-size: $font-size-lg;
}
&.cancel {
border-right: 1rpx solid $border-light;
text {
color: $text-secondary;
}
}
&.confirm {
text {
color: $primary-color;
}
}
}
}
}

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 个人资料页面
*/
</script>
<template>
<view class="profile-page">
<view class="placeholder">个人资料页面</view>
</view>
</template>
<style scoped lang="scss">
.profile-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 我的订单页面
*/
</script>
<template>
<view class="order-list-page">
<view class="placeholder">我的订单页面</view>
</view>
</template>
<style scoped lang="scss">
.order-list-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 规划预约页面
*/
</script>
<template>
<view class="planner-book-page">
<view class="placeholder">规划预约页面</view>
</view>
</template>
<style scoped lang="scss">
.planner-book-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,27 @@
<script setup>
/**
* 规划师列表页面
*/
</script>
<template>
<view class="planner-list-page">
<view class="placeholder">规划师列表页面</view>
</view>
</template>
<style scoped lang="scss">
.planner-list-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 32rpx;
color: #999;
}
</style>

207
uniapp/pages/team/index.vue Normal file
View File

@ -0,0 +1,207 @@
<template>
<view class="team-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="navbarStyle">
<view class="navbar-content" :style="{ height: navbarHeight + 'px' }">
<text class="navbar-title">团队</text>
</view>
</view>
<!-- 导航栏占位 -->
<view class="navbar-placeholder" :style="{ height: totalNavbarHeight + 'px' }"></view>
<!-- 页面内容 -->
<view class="page-content">
<!-- 加载状态 -->
<Loading v-if="pageLoading" type="page" :loading="true" />
<!-- 团队介绍图片 -->
<view v-else class="team-content">
<!-- 有数据时显示图片列表 -->
<template v-if="teamImages.length > 0">
<image
v-for="(item, index) in teamImages"
:key="index"
:src="item.imageUrl"
mode="widthFix"
class="team-image"
@load="handleImageLoad(index)"
@error="handleImageError(index)"
/>
</template>
<!-- 无数据时显示空状态 -->
<view v-else class="empty-state">
<image
src="/static/ic_empty.png"
mode="aspectFit"
class="empty-icon"
/>
<text class="empty-text">暂无团队介绍</text>
</view>
</view>
<!-- 底部安全区域 -->
<view class="safe-bottom"></view>
</view>
</view>
</template>
<script setup>
/**
* 团队页面
* 展示团队介绍图片
*/
import { ref, computed, onMounted } from 'vue'
import { useNavbar } from '@/composables/useNavbar.js'
import { getTeamInfo } from '@/api/team.js'
import Loading from '@/components/Loading/index.vue'
const { statusBarHeight, navbarHeight, totalNavbarHeight } = useNavbar()
//
const pageLoading = ref(true)
const teamImages = ref([])
//
const navbarStyle = computed(() => ({
paddingTop: statusBarHeight.value + 'px',
height: totalNavbarHeight.value + 'px'
}))
/**
* 加载团队介绍数据
*/
async function loadTeamInfo() {
pageLoading.value = true
try {
const res = await getTeamInfo()
if (res && res.code === 0 && res.data) {
//
if (Array.isArray(res.data)) {
teamImages.value = res.data
} else if (res.data.list && Array.isArray(res.data.list)) {
teamImages.value = res.data.list
} else if (res.data.images && Array.isArray(res.data.images)) {
teamImages.value = res.data.images
} else if (res.data.imageUrl) {
//
teamImages.value = [{ imageUrl: res.data.imageUrl }]
} else {
teamImages.value = []
}
}
} catch (error) {
console.error('加载团队介绍失败:', error)
uni.showToast({
title: '加载失败,请稍后重试',
icon: 'none'
})
} finally {
pageLoading.value = false
}
}
/**
* 图片加载成功
*/
function handleImageLoad(index) {
console.log(`团队图片 ${index + 1} 加载成功`)
}
/**
* 图片加载失败
*/
function handleImageError(index) {
console.error(`团队图片 ${index + 1} 加载失败`)
}
/**
* 页面加载
*/
onMounted(() => {
loadTeamInfo()
})
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.team-page {
min-height: 100vh;
background-color: $bg-color;
}
//
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: $bg-white;
z-index: 999;
.navbar-content {
display: flex;
align-items: center;
justify-content: center;
.navbar-title {
font-size: 34rpx;
font-weight: $font-weight-medium;
color: $text-color;
}
}
}
.navbar-placeholder {
width: 100%;
}
//
.page-content {
padding-bottom: env(safe-area-inset-bottom);
}
//
.team-content {
padding: $spacing-lg;
.team-image {
width: 100%;
display: block;
margin-bottom: $spacing-md;
border-radius: $border-radius-lg;
&:last-child {
margin-bottom: 0;
}
}
}
//
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: $spacing-lg;
}
.empty-text {
font-size: $font-size-md;
color: $text-placeholder;
}
}
//
.safe-bottom {
height: 40rpx;
padding-bottom: env(safe-area-inset-bottom);
}
</style>

77
uniapp/store/app.js Normal file
View File

@ -0,0 +1,77 @@
/**
* 应用状态管理模块
* 管理系统信息导航栏高度等全局状态
*/
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
// 状态栏高度
statusBarHeight: 20,
// 导航栏高度(不含状态栏)
navbarHeight: 44,
// 系统信息
systemInfo: {},
// 是否已初始化
initialized: false
}),
getters: {
/**
* 导航栏总高度状态栏 + 导航栏
*/
totalNavbarHeight: (state) => state.statusBarHeight + state.navbarHeight,
/**
* 内容区域顶部padding
*/
contentPaddingTop: (state) => state.statusBarHeight + state.navbarHeight,
/**
* 是否是iOS系统
*/
isIOS: (state) => state.systemInfo.platform === 'ios',
/**
* 是否是Android系统
*/
isAndroid: (state) => state.systemInfo.platform === 'android'
},
actions: {
/**
* 初始化系统信息
*/
initSystemInfo() {
if (this.initialized) return
try {
const systemInfo = uni.getSystemInfoSync()
this.systemInfo = systemInfo
this.statusBarHeight = systemInfo.statusBarHeight || 20
// 获取胶囊按钮位置信息(微信小程序)
// #ifdef MP-WEIXIN
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
if (menuButtonInfo) {
// 导航栏高度 = 胶囊底部距离 - 状态栏高度 + 胶囊与底部的间距
this.navbarHeight = (menuButtonInfo.bottom - this.statusBarHeight) + (menuButtonInfo.top - this.statusBarHeight)
}
// #endif
this.initialized = true
} catch (e) {
console.error('获取系统信息失败:', e)
}
},
/**
* 获取安全区域底部高度
* @returns {number}
*/
getSafeAreaBottom() {
return this.systemInfo.safeAreaInsets?.bottom || 0
}
}
})

View File

@ -2,5 +2,6 @@
* Store 模块导出
*/
export { useUserStore } from './user.js'
export { useUserStore, USER_LEVEL } from './user.js'
export { useAppStore } from './app.js'
export { useChatStore, MessageType, MessageStatus } from './chat.js'

View File

@ -5,34 +5,65 @@
import { defineStore } from 'pinia'
import {
getToken, setToken, removeToken,
getRefreshToken, setRefreshToken, removeRefreshToken,
getUserInfo, setUserInfo, removeUserInfo
} from '../utils/storage.js'
/** 用户等级常量 */
export const USER_LEVEL = {
NORMAL: 1, // 普通用户
PARTNER: 2, // 合伙人
CHANNEL: 3 // 渠道合伙人
}
export const useUserStore = defineStore('user', {
state: () => ({
token: getToken() || '',
refreshToken: getRefreshToken() || '',
userId: 0,
uid: '',
nickname: '',
avatar: ''
avatar: '',
phone: '',
userLevel: USER_LEVEL.NORMAL
}),
getters: {
/**
* 是否已登录
*/
isLoggedIn: (state) => !!state.token && state.token.length > 0
isLoggedIn: (state) => !!state.token && state.token.length > 0,
/**
* 是否是合伙人合伙人或渠道合伙人
*/
isPartner: (state) => state.userLevel >= USER_LEVEL.PARTNER,
/**
* 是否是渠道合伙人
*/
isChannelPartner: (state) => state.userLevel === USER_LEVEL.CHANNEL
},
actions: {
/**
* 登录 - 设置token和用户信息
* @param {Object} loginData - 登录数据
* @param {string} loginData.token - 访问令牌
* @param {string} loginData.refreshToken - 刷新令牌
* @param {Object} loginData.userInfo - 用户信息
*/
login(loginData) {
const { token, userInfo } = loginData
const { token, refreshToken, userInfo } = loginData
this.token = token
setToken(token)
if (refreshToken) {
this.refreshToken = refreshToken
setRefreshToken(refreshToken)
}
if (userInfo) {
this.updateUserInfo(userInfo)
}
@ -43,28 +74,40 @@ export const useUserStore = defineStore('user', {
*/
logout() {
this.token = ''
this.refreshToken = ''
removeToken()
removeRefreshToken()
this.userId = 0
this.uid = ''
this.nickname = ''
this.avatar = ''
this.phone = ''
this.userLevel = USER_LEVEL.NORMAL
removeUserInfo()
},
/**
* 更新用户信息
* @param {Object} userInfo - 用户信息
*/
updateUserInfo(userInfo) {
if (!userInfo) return
if (userInfo.userId !== undefined) this.userId = userInfo.userId
if (userInfo.uid !== undefined) this.uid = userInfo.uid
if (userInfo.nickname !== undefined) this.nickname = userInfo.nickname
if (userInfo.avatar !== undefined) this.avatar = userInfo.avatar
if (userInfo.phone !== undefined) this.phone = userInfo.phone
if (userInfo.userLevel !== undefined) this.userLevel = userInfo.userLevel
setUserInfo({
userId: this.userId,
uid: this.uid,
nickname: this.nickname,
avatar: this.avatar
avatar: this.avatar,
phone: this.phone,
userLevel: this.userLevel
})
},
@ -73,16 +116,24 @@ export const useUserStore = defineStore('user', {
*/
restoreFromStorage() {
const token = getToken()
const refreshToken = getRefreshToken()
const userInfo = getUserInfo()
if (token) {
this.token = token
}
if (refreshToken) {
this.refreshToken = refreshToken
}
if (userInfo) {
this.userId = userInfo.userId || 0
this.uid = userInfo.uid || ''
this.nickname = userInfo.nickname || ''
this.avatar = userInfo.avatar || ''
this.phone = userInfo.phone || ''
this.userLevel = userInfo.userLevel || USER_LEVEL.NORMAL
}
}
}

346
uniapp/styles/common.scss Normal file
View File

@ -0,0 +1,346 @@
/**
* 通用样式类
* 常用布局文字按钮等样式
*/
@import './variables.scss';
// ==================== Flex 布局 ====================
.flex {
display: flex;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-col {
display: flex;
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-1 {
flex: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.items-end {
align-items: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.justify-end {
justify-content: flex-end;
}
// ==================== 文字样式 ====================
.text-xs {
font-size: $font-size-xs;
}
.text-sm {
font-size: $font-size-sm;
}
.text-md {
font-size: $font-size-md;
}
.text-lg {
font-size: $font-size-lg;
}
.text-xl {
font-size: $font-size-xl;
}
.text-primary {
color: $primary-color;
}
.text-secondary {
color: $text-secondary;
}
.text-placeholder {
color: $text-placeholder;
}
.text-white {
color: $text-white;
}
.text-success {
color: $success-color;
}
.text-warning {
color: $warning-color;
}
.text-error {
color: $error-color;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.font-medium {
font-weight: $font-weight-medium;
}
.font-bold {
font-weight: $font-weight-bold;
}
// 文字省略
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-ellipsis-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
// ==================== 间距样式 ====================
// Margin
.m-xs { margin: $spacing-xs; }
.m-sm { margin: $spacing-sm; }
.m-md { margin: $spacing-md; }
.m-lg { margin: $spacing-lg; }
.mt-xs { margin-top: $spacing-xs; }
.mt-sm { margin-top: $spacing-sm; }
.mt-md { margin-top: $spacing-md; }
.mt-lg { margin-top: $spacing-lg; }
.mb-xs { margin-bottom: $spacing-xs; }
.mb-sm { margin-bottom: $spacing-sm; }
.mb-md { margin-bottom: $spacing-md; }
.mb-lg { margin-bottom: $spacing-lg; }
.ml-xs { margin-left: $spacing-xs; }
.ml-sm { margin-left: $spacing-sm; }
.ml-md { margin-left: $spacing-md; }
.ml-lg { margin-left: $spacing-lg; }
.mr-xs { margin-right: $spacing-xs; }
.mr-sm { margin-right: $spacing-sm; }
.mr-md { margin-right: $spacing-md; }
.mr-lg { margin-right: $spacing-lg; }
// Padding
.p-xs { padding: $spacing-xs; }
.p-sm { padding: $spacing-sm; }
.p-md { padding: $spacing-md; }
.p-lg { padding: $spacing-lg; }
.pt-xs { padding-top: $spacing-xs; }
.pt-sm { padding-top: $spacing-sm; }
.pt-md { padding-top: $spacing-md; }
.pt-lg { padding-top: $spacing-lg; }
.pb-xs { padding-bottom: $spacing-xs; }
.pb-sm { padding-bottom: $spacing-sm; }
.pb-md { padding-bottom: $spacing-md; }
.pb-lg { padding-bottom: $spacing-lg; }
.pl-xs { padding-left: $spacing-xs; }
.pl-sm { padding-left: $spacing-sm; }
.pl-md { padding-left: $spacing-md; }
.pl-lg { padding-left: $spacing-lg; }
.pr-xs { padding-right: $spacing-xs; }
.pr-sm { padding-right: $spacing-sm; }
.pr-md { padding-right: $spacing-md; }
.pr-lg { padding-right: $spacing-lg; }
// ==================== 背景样式 ====================
.bg-white {
background-color: $bg-white;
}
.bg-gray {
background-color: $bg-gray;
}
.bg-primary {
background-color: $primary-color;
}
// ==================== 边框样式 ====================
.border {
border: 1rpx solid $border-color;
}
.border-top {
border-top: 1rpx solid $border-color;
}
.border-bottom {
border-bottom: 1rpx solid $border-color;
}
.rounded-sm {
border-radius: $border-radius-sm;
}
.rounded-md {
border-radius: $border-radius-md;
}
.rounded-lg {
border-radius: $border-radius-lg;
}
.rounded-full {
border-radius: $border-radius-round;
}
// ==================== 按钮样式 ====================
.btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
border-radius: $border-radius-lg;
font-size: $font-size-lg;
font-weight: $font-weight-medium;
transition: opacity $transition-fast;
&:active {
opacity: 0.8;
}
}
.btn-primary {
background-color: $primary-color;
color: $text-white;
}
.btn-outline {
background-color: transparent;
border: 2rpx solid $primary-color;
color: $primary-color;
}
.btn-disabled {
background-color: #CCCCCC;
color: $text-white;
pointer-events: none;
}
.btn-sm {
height: 64rpx;
font-size: $font-size-md;
border-radius: $border-radius-md;
}
.btn-lg {
height: 96rpx;
font-size: $font-size-xl;
}
// ==================== 卡片样式 ====================
.card {
background-color: $bg-white;
border-radius: $border-radius-lg;
padding: $spacing-lg;
box-shadow: $shadow-sm;
}
// ==================== 分割线 ====================
.divider {
height: 1rpx;
background-color: $border-color;
margin: $spacing-md 0;
}
// ==================== 安全区域 ====================
.safe-area-bottom {
padding-bottom: $safe-area-bottom;
}
// ==================== 宽高 ====================
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
// ==================== 定位 ====================
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.fixed {
position: fixed;
}
// ==================== 溢出 ====================
.overflow-hidden {
overflow: hidden;
}
.overflow-auto {
overflow: auto;
}

View File

@ -0,0 +1,83 @@
/**
* 全局样式变量
* 颜色间距字体等设计规范
*/
// ==================== 颜色变量 ====================
// 主色调
$primary-color: #4A90E2;
$primary-light: #6BA3E8;
$primary-dark: #3A7BC8;
// 功能色
$success-color: #52C41A;
$warning-color: #FAAD14;
$error-color: #FF4D4F;
$info-color: #1890FF;
// 文字颜色
$text-color: #333333;
$text-secondary: #666666;
$text-placeholder: #999999;
$text-disabled: #CCCCCC;
$text-white: #FFFFFF;
// 背景颜色
$bg-color: #F5F5F5;
$bg-white: #FFFFFF;
$bg-gray: #F8F8F8;
$bg-mask: rgba(0, 0, 0, 0.5);
// 边框颜色
$border-color: #E8E8E8;
$border-light: #F0F0F0;
// ==================== 间距变量 ====================
$spacing-xs: 8rpx;
$spacing-sm: 16rpx;
$spacing-md: 24rpx;
$spacing-lg: 32rpx;
$spacing-xl: 48rpx;
// ==================== 字体变量 ====================
$font-size-xs: 22rpx;
$font-size-sm: 24rpx;
$font-size-md: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;
$font-size-xxl: 40rpx;
// 字重
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-bold: 600;
// ==================== 圆角变量 ====================
$border-radius-xs: 4rpx;
$border-radius-sm: 8rpx;
$border-radius-md: 12rpx;
$border-radius-lg: 16rpx;
$border-radius-xl: 24rpx;
$border-radius-round: 9999rpx;
// ==================== 阴影变量 ====================
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
$shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
$shadow-lg: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
// ==================== 动画变量 ====================
$transition-fast: 0.15s ease;
$transition-normal: 0.3s ease;
$transition-slow: 0.5s ease;
// ==================== 布局变量 ====================
$navbar-height: 88rpx;
$tabbar-height: 100rpx;
$safe-area-bottom: env(safe-area-inset-bottom);

View File

@ -12,6 +12,9 @@
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量同时无需 import 这个文件
*/
// 引入自定义变量
@import '@/styles/variables.scss';
/* 颜色变量 */
/* 行为相关颜色 */

View File

@ -1 +0,0 @@
{"version":3,"file":"agreement.js","sources":["api/agreement.js"],"sourcesContent":["import { get } from './request.js'\r\n\r\n/**\r\n * 获取用户协议\r\n */\r\nexport const getUserAgreement = () => {\r\n return get('/config/userAgreement')\r\n}\r\n\r\n/**\r\n * 获取隐私协议\r\n */\r\nexport const getPrivacyPolicy = () => {\r\n return get('/config/privacyPolicy')\r\n}"],"names":["get"],"mappings":";;AAKY,MAAC,mBAAmB,MAAM;AACpC,SAAOA,YAAAA,IAAI,uBAAuB;AACpC;AAKY,MAAC,mBAAmB,MAAM;AACpC,SAAOA,YAAAA,IAAI,uBAAuB;AACpC;;;"}

View File

@ -1 +0,0 @@
{"version":3,"file":"auth.js","sources":["api/auth.js"],"sourcesContent":["/**\r\n * 认证接口模块\r\n * Requirements: 1.1, 1.2, 1.3\r\n */\r\n\r\nimport { post } from './request'\r\nimport { setToken, setUserInfo } from '../utils/storage'\r\n\r\n/**\r\n * 微信登录\r\n * WHEN a user opens the XiangYi_MiniApp for the first time, \r\n * THE XiangYi_MiniApp SHALL call WeChat login API and obtain authorization code\r\n * Requirements: 1.1, 1.2\r\n * \r\n * @param {string} code - 微信登录授权码\r\n * @returns {Promise<Object>} 登录响应包含token和用户信息\r\n */\r\nexport async function login(code) {\r\n const response = await post('/auth/login', { code }, { needAuth: false })\r\n \r\n // 存储token和用户信息\r\n if (response.data) {\r\n const { token, userId, nickname, avatar, xiangQinNo, isProfileCompleted, isMember, memberLevel, isRealName } = response.data\r\n \r\n if (token) {\r\n setToken(token)\r\n }\r\n \r\n setUserInfo({\r\n userId,\r\n nickname,\r\n avatar,\r\n xiangQinNo,\r\n isProfileCompleted,\r\n isMember,\r\n memberLevel,\r\n isRealName\r\n })\r\n }\r\n \r\n return response\r\n}\r\n\r\n/**\r\n * 绑定手机号\r\n * WHEN a user needs to bind phone number, \r\n * THE XiangYi_MiniApp SHALL use WeChat getPhoneNumber API and send the code to endpoint\r\n * Requirements: 1.3\r\n * \r\n * @param {string} code - 微信获取手机号的code\r\n * @returns {Promise<Object>} 绑定结果\r\n */\r\nexport async function bindPhone(code) {\r\n const response = await post('/auth/bindPhone', { code })\r\n return response\r\n}\r\n\r\nexport default {\r\n login,\r\n bindPhone\r\n}\r\n"],"names":["post","setToken","setUserInfo"],"mappings":";;;AAiBO,eAAe,MAAM,MAAM;AAChC,QAAM,WAAW,MAAMA,YAAAA,KAAK,eAAe,EAAE,QAAQ,EAAE,UAAU,OAAO;AAGxE,MAAI,SAAS,MAAM;AACjB,UAAM,EAAE,OAAO,QAAQ,UAAU,QAAQ,YAAY,oBAAoB,UAAU,aAAa,WAAY,IAAG,SAAS;AAExH,QAAI,OAAO;AACTC,oBAAAA,SAAS,KAAK;AAAA,IACf;AAEDC,8BAAY;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACN,CAAK;AAAA,EACF;AAED,SAAO;AACT;;"}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"config.js","sources":["api/config.js"],"sourcesContent":["/**\r\n * 配置接口模块\r\n * 小程序启动时调用统一配置接口,一次获取所有配置\r\n */\r\n\r\nimport { get } from './request'\r\n\r\n/**\r\n * 获取小程序统一配置(启动时调用)\r\n * 包含Banner、金刚位、默认头像、搜索页Banner、弹窗配置\r\n * \r\n * @returns {Promise<Object>} 统一配置\r\n */\r\nexport async function getAppConfig() {\r\n const response = await get('/config/app', {}, { needAuth: false })\r\n return response\r\n}\r\n\r\n/**\r\n * 获取用户协议\r\n * \r\n * @returns {Promise<Object>} 用户协议内容\r\n */\r\nexport async function getUserAgreement() {\r\n const response = await get('/config/userAgreement', {}, { needAuth: false })\r\n return response\r\n}\r\n\r\n/**\r\n * 获取隐私协议\r\n * \r\n * @returns {Promise<Object>} 隐私协议内容\r\n */\r\nexport async function getPrivacyPolicy() {\r\n const response = await get('/config/privacyPolicy', {}, { needAuth: false })\r\n return response\r\n}\r\n\r\nexport default {\r\n getAppConfig,\r\n getUserAgreement,\r\n getPrivacyPolicy\r\n}\r\n"],"names":["get"],"mappings":";;AAaO,eAAe,eAAe;AACnC,QAAM,WAAW,MAAMA,gBAAI,eAAe,CAAA,GAAI,EAAE,UAAU,OAAO;AACjE,SAAO;AACT;;"}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"member.js","sources":["api/member.js"],"sourcesContent":["/**\r\n * 会员接口模块\r\n * Requirements: 10.1, 10.2\r\n */\r\n\r\nimport { get, post, del } from './request'\r\n\r\n/**\r\n * 获取会员信息\r\n * WHEN a user visits member page, THE XiangYi_MiniApp SHALL display three membership tiers\r\n * Requirements: 10.1\r\n * \r\n * @returns {Promise<Object>} 会员信息\r\n */\r\nexport async function getMemberInfo() {\r\n const response = await get('/member/info')\r\n return response\r\n}\r\n\r\n/**\r\n * 购买会员\r\n * WHEN a user selects a membership tier and clicks purchase, THE XiangYi_MiniApp SHALL call endpoint\r\n * Requirements: 10.2\r\n * \r\n * @param {number} memberLevel - 会员等级1不限时会员(1299) 2诚意会员(1999) 3家庭版(2999)\r\n * @returns {Promise<Object>} 订单信息和支付参数\r\n */\r\nexport async function purchase(memberLevel) {\r\n const response = await post('/member/purchase', { memberLevel })\r\n return response\r\n}\r\n\r\n/**\r\n * 绑定家庭成员(家庭版会员功能)\r\n * \r\n * @param {number} bindUserId - 要绑定的用户ID\r\n * @returns {Promise<Object>} 绑定结果\r\n */\r\nexport async function bindFamily(bindUserId) {\r\n const response = await post('/member/bindFamily', { bindUserId })\r\n return response\r\n}\r\n\r\n/**\r\n * 获取家庭成员列表\r\n * \r\n * @returns {Promise<Object>} 家庭成员列表\r\n */\r\nexport async function getFamilyMembers() {\r\n const response = await get('/member/familyMembers')\r\n return response\r\n}\r\n\r\n/**\r\n * 解绑家庭成员\r\n * \r\n * @param {number} bindUserId - 被绑定用户ID\r\n * @returns {Promise<Object>} 解绑结果\r\n */\r\nexport async function unbindFamilyMember(bindUserId) {\r\n const response = await del(`/member/familyMembers/${bindUserId}`)\r\n return response\r\n}\r\n\r\nexport default {\r\n getMemberInfo,\r\n purchase,\r\n bindFamily,\r\n getFamilyMembers,\r\n unbindFamilyMember\r\n}\r\n"],"names":["get"],"mappings":";;AAcO,eAAe,gBAAgB;AACpC,QAAM,WAAW,MAAMA,YAAG,IAAC,cAAc;AACzC,SAAO;AACT;;"}

View File

@ -1 +0,0 @@
{"version":3,"file":"message.js","sources":["api/message.js"],"sourcesContent":["/**\r\n * 消息相关 API\r\n */\r\n\r\nimport { get, post } from './request.js'\r\n\r\n/**\r\n * 获取系统消息列表\r\n * @param {Object} params - 查询参数\r\n * @param {number} params.pageIndex - 页码\r\n * @param {number} params.pageSize - 每页数量\r\n * @returns {Promise}\r\n */\r\nexport const getSystemMessages = (params) => {\r\n return get('/notification/list', {\r\n pageIndex: params.pageIndex,\r\n pageSize: params.pageSize\r\n })\r\n}\r\n\r\n/**\r\n * 标记消息已读\r\n * @param {number} messageId - 消息ID\r\n * @returns {Promise}\r\n */\r\nexport const markMessageRead = (messageId) => {\r\n return post('/notification/read', { notificationId: messageId })\r\n}\r\n\r\n/**\r\n * 标记所有系统消息已读\r\n * @returns {Promise}\r\n */\r\nexport const markAllSystemMessagesRead = () => {\r\n return post('/notification/read', { markAll: true })\r\n}\r\n\r\n/**\r\n * 获取未读消息数量\r\n * @returns {Promise}\r\n */\r\nexport const getUnreadCount = () => {\r\n return get('/notification/unreadCount')\r\n}\r\n"],"names":["get"],"mappings":";;AAaY,MAAC,oBAAoB,CAAC,WAAW;AAC3C,SAAOA,YAAAA,IAAI,sBAAsB;AAAA,IAC/B,WAAW,OAAO;AAAA,IAClB,UAAU,OAAO;AAAA,EACrB,CAAG;AACH;;"}

View File

@ -1 +0,0 @@
{"version":3,"file":"order.js","sources":["api/order.js"],"sourcesContent":["/**\r\n * 订单接口模块\r\n * Requirements: 10.2\r\n */\r\n\r\nimport { get, post } from './request'\r\n\r\n/**\r\n * 创建订单\r\n * WHEN order is created, THE XiangYi_MiniApp SHALL invoke WeChat payment with returned payment parameters\r\n * Requirements: 10.2\r\n * \r\n * @param {Object} data - 订单数据\r\n * @param {number} data.orderType - 订单类型1会员 2实名认证\r\n * @param {number} [data.memberLevel] - 会员等级订单类型为1时必填1不限时会员 2诚意会员 3家庭版\r\n * @returns {Promise<Object>} 订单信息和支付参数\r\n */\r\nexport async function createOrder(data) {\r\n const response = await post('/order/create', data)\r\n return response\r\n}\r\n\r\n/**\r\n * 获取订单列表\r\n * \r\n * @param {Object} [params] - 查询参数\r\n * @param {number} [params.orderType=0] - 订单类型0全部 1会员 2实名认证\r\n * @param {number} [params.status=0] - 订单状态0全部 1待支付 2已支付 3已取消 4已退款\r\n * @param {number} [params.pageIndex=1] - 页码\r\n * @param {number} [params.pageSize=10] - 每页数量\r\n * @returns {Promise<Object>} 订单列表\r\n */\r\nexport async function getOrderList(params = {}) {\r\n const { orderType = 0, status = 0, pageIndex = 1, pageSize = 10 } = params\r\n const response = await get('/order/list', { orderType, status, pageIndex, pageSize })\r\n return response\r\n}\r\n\r\n/**\r\n * 获取订单详情\r\n * \r\n * @param {number} orderId - 订单ID\r\n * @returns {Promise<Object>} 订单详情\r\n */\r\nexport async function getOrderDetail(orderId) {\r\n const response = await get(`/order/${orderId}`)\r\n return response\r\n}\r\n\r\n/**\r\n * 取消订单\r\n * \r\n * @param {number} orderId - 订单ID\r\n * @returns {Promise<Object>} 取消结果\r\n */\r\nexport async function cancelOrder(orderId) {\r\n const response = await post(`/order/${orderId}/cancel`)\r\n return response\r\n}\r\n\r\nexport default {\r\n createOrder,\r\n getOrderList,\r\n getOrderDetail,\r\n cancelOrder\r\n}\r\n"],"names":["post"],"mappings":";;AAiBO,eAAe,YAAY,MAAM;AACtC,QAAM,WAAW,MAAMA,iBAAK,iBAAiB,IAAI;AACjD,SAAO;AACT;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"user.js","sources":["api/user.js"],"sourcesContent":["/**\r\n * 用户接口模块\r\n * Requirements: 2.3, 5.1, 11.2\r\n */\r\n\r\nimport { get, post } from './request'\r\n\r\n/**\r\n * 获取每日推荐列表\r\n * WHEN a user visits the home page, THE XiangYi_MiniApp SHALL display recommended user list\r\n * Requirements: 2.3\r\n * \r\n * @param {number} pageIndex - 页码\r\n * @param {number} pageSize - 每页数量\r\n * @param {number} gender - 筛选性别1=男2=女0=不限)\r\n * @returns {Promise<Object>} 推荐用户列表\r\n */\r\nexport async function getRecommend(pageIndex = 1, pageSize = 10, gender = 0) {\r\n // 如果用户已登录发送token以便后端排除当前用户\r\n const token = uni.getStorageSync('token')\r\n const params = { pageIndex, pageSize }\r\n if (gender > 0) {\r\n params.gender = gender\r\n }\r\n const response = await get('/users/recommend', params, { needAuth: !!token })\r\n return response\r\n}\r\n\r\n/**\r\n * 获取用户详情\r\n * WHEN a user views a profile detail page, THE XiangYi_MiniApp SHALL call endpoint and display full profile information\r\n * Requirements: 5.1\r\n * \r\n * @param {number} userId - 用户ID\r\n * @returns {Promise<Object>} 用户详情\r\n */\r\nexport async function getUserDetail(userId) {\r\n const response = await post('/users/detail', { userId })\r\n return response\r\n}\r\n\r\n/**\r\n * 搜索用户\r\n * WHEN a user submits search, THE XiangYi_MiniApp SHALL call endpoint with filter parameters\r\n * Requirements: 11.2\r\n * \r\n * @param {Object} params - 搜索参数\r\n * @param {number} [params.ageMin] - 最小年龄\r\n * @param {number} [params.ageMax] - 最大年龄\r\n * @param {number} [params.heightMin] - 最小身高\r\n * @param {number} [params.heightMax] - 最大身高\r\n * @param {number[]} [params.education] - 学历列表\r\n * @param {string} [params.province] - 省份\r\n * @param {string} [params.city] - 城市\r\n * @param {number} [params.monthlyIncomeMin] - 最低月收入\r\n * @param {number} [params.monthlyIncomeMax] - 最高月收入\r\n * @param {number[]} [params.houseStatus] - 房产状态列表\r\n * @param {number[]} [params.carStatus] - 车辆状态列表\r\n * @param {number[]} [params.marriageStatus] - 婚姻状态列表\r\n * @param {number} [params.pageIndex] - 页码\r\n * @param {number} [params.pageSize] - 每页数量\r\n * @returns {Promise<Object>} 搜索结果\r\n */\r\nexport async function search(params = {}) {\r\n const response = await post('/users/search', params)\r\n return response\r\n}\r\n\r\n/**\r\n * 更新用户头像\r\n * \r\n * @param {string} avatar - 头像URL\r\n * @returns {Promise<Object>} 更新结果\r\n */\r\nexport async function updateAvatar(avatar) {\r\n const response = await post('/users/avatar', { avatar })\r\n return response\r\n}\r\n\r\n/**\r\n * 更新用户昵称\r\n * \r\n * @param {string} nickname - 昵称\r\n * @returns {Promise<Object>} 更新结果\r\n */\r\nexport async function updateNickname(nickname) {\r\n const response = await post('/users/nickname', { nickname })\r\n return response\r\n}\r\n\r\n/**\r\n * 解密微信手机号\r\n * \r\n * @param {string} code - 微信返回的 code\r\n * @returns {Promise<Object>} 解密结果,包含 phone 字段\r\n */\r\nexport async function decryptPhone(code) {\r\n const response = await post('/users/phone/decrypt', { code })\r\n return response\r\n}\r\n\r\nexport default {\r\n getRecommend,\r\n getUserDetail,\r\n search,\r\n updateAvatar,\r\n updateNickname,\r\n decryptPhone\r\n}\r\n"],"names":["uni","get","post"],"mappings":";;;AAiBO,eAAe,aAAa,YAAY,GAAG,WAAW,IAAI,SAAS,GAAG;AAE3E,QAAM,QAAQA,cAAAA,MAAI,eAAe,OAAO;AACxC,QAAM,SAAS,EAAE,WAAW,SAAU;AACtC,MAAI,SAAS,GAAG;AACd,WAAO,SAAS;AAAA,EACjB;AACD,QAAM,WAAW,MAAMC,YAAAA,IAAI,oBAAoB,QAAQ,EAAE,UAAU,CAAC,CAAC,OAAO;AAC5E,SAAO;AACT;AAUO,eAAe,cAAc,QAAQ;AAC1C,QAAM,WAAW,MAAMC,YAAAA,KAAK,iBAAiB,EAAE,OAAM,CAAE;AACvD,SAAO;AACT;AAwBO,eAAe,OAAO,SAAS,IAAI;AACxC,QAAM,WAAW,MAAMA,iBAAK,iBAAiB,MAAM;AACnD,SAAO;AACT;AAQO,eAAe,aAAa,QAAQ;AACzC,QAAM,WAAW,MAAMA,YAAAA,KAAK,iBAAiB,EAAE,OAAM,CAAE;AACvD,SAAO;AACT;AAQO,eAAe,eAAe,UAAU;AAC7C,QAAM,WAAW,MAAMA,YAAAA,KAAK,mBAAmB,EAAE,SAAQ,CAAE;AAC3D,SAAO;AACT;AAQO,eAAe,aAAa,MAAM;AACvC,QAAM,WAAW,MAAMA,YAAAA,KAAK,wBAAwB,EAAE,KAAI,CAAE;AAC5D,SAAO;AACT;;;;;;;"}

View File

@ -1 +0,0 @@
{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["<script>\r\n\timport { useConfigStore } from './store/config.js'\r\n\timport { useUserStore } from './store/user.js'\r\n\t\r\n\texport default {\r\n\t\tonLaunch: function() {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t\t// 恢复用户状态\r\n\t\t\tconst userStore = useUserStore()\r\n\t\t\tuserStore.restoreFromStorage()\r\n\t\t\t\r\n\t\t\t// 加载所有配置(一次请求)\r\n\t\t\tconst configStore = useConfigStore()\r\n\t\t\tconfigStore.loadAppConfig()\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n</style>\r\n","import App from './App'\r\n\r\n// #ifndef VUE3\r\nimport Vue from 'vue'\r\nimport './uni.promisify.adaptor'\r\nVue.config.productionTip = false\r\nApp.mpType = 'app'\r\nconst app = new Vue({\r\n ...App\r\n})\r\napp.$mount()\r\n// #endif\r\n\r\n// #ifdef VUE3\r\nimport { createSSRApp } from 'vue'\r\nimport { createPinia } from 'pinia'\r\n\r\nexport function createApp() {\r\n const app = createSSRApp(App)\r\n const pinia = createPinia()\r\n app.use(pinia)\r\n return {\r\n app\r\n }\r\n}\r\n// #endif\r\n"],"names":["uni","useUserStore","useConfigStore","createSSRApp","App","createPinia"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAExB,UAAM,YAAYC,WAAAA,aAAa;AAC/B,cAAU,mBAAmB;AAG7B,UAAM,cAAcC,aAAAA,eAAe;AACnC,gBAAY,cAAc;AAAA,EAC1B;AAAA,EACD,QAAQ,WAAW;AAClBF,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACJM,SAAS,YAAY;AAC1B,QAAM,MAAMG,cAAY,aAACC,SAAG;AAC5B,QAAM,QAAQC,cAAAA,YAAa;AAC3B,MAAI,IAAI,KAAK;AACb,SAAO;AAAA,IACL;AAAA,EACD;AACH;;;"}

View File

@ -1 +0,0 @@
{"version":3,"file":"assets.js","sources":["static/recommend_title.png","static/title_bg.png","static/ic_seen.png","static/ic_collection.png","static/ic_unlock.png","static/ic_empty.png","static/logo.png","static/butler.png","static/real_name.png","static/ic_agreement1.png","static/ic_agreement2.png","static/ic_exit.png","../../../static/vip-icon.png","static/ic_real_name.png","../../../static/ic_service.png","static/real_name_card_1.png","static/real_name_card_2.png"],"sourcesContent":["export default \"__VITE_ASSET__6534df53__\"","export default \"__VITE_ASSET__84ffc1c9__\"","export default \"__VITE_ASSET__30f23d07__\"","export default \"__VITE_ASSET__31329d26__\"","export default \"__VITE_ASSET__e29e011c__\"","export default \"__VITE_ASSET__b1430526__\"","export default \"__VITE_ASSET__d16cce6e__\"","export default \"__VITE_ASSET__bcd91d0f__\"","export default \"__VITE_ASSET__176444a1__\"","export default \"__VITE_ASSET__a4908586__\"","export default \"__VITE_ASSET__b2d2860e__\"","export default \"__VITE_ASSET__eb408b97__\"","export default \"/static/vip-icon.png\"","export default \"__VITE_ASSET__9a94c78e__\"","export default \"/static/ic_service.png\"","export default \"__VITE_ASSET__26dceb6d__\"","export default \"__VITE_ASSET__36b18ce1__\""],"names":[],"mappings":";AAAA,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,eAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;ACAf,MAAe,aAAA;;;;;;;;;;;;;;;;;;"}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"index.js","sources":["components/EmojiPicker/index.vue","../../../软件/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/RDovb3V0c291cmNlL3hpYW5neWl4aWFuZ3Fpbi9taW5pYXBwL2NvbXBvbmVudHMvRW1vamlQaWNrZXIvaW5kZXgudnVl"],"sourcesContent":["<template>\r\n <view class=\"emoji-picker\" v-if=\"visible\">\r\n <view class=\"emoji-mask\" @click=\"handleClose\"></view>\r\n <view class=\"emoji-panel\">\r\n <!-- 分类标签 -->\r\n <view class=\"emoji-tabs\">\r\n <view \r\n v-for=\"category in categories\" \r\n :key=\"category.key\"\r\n class=\"emoji-tab\"\r\n :class=\"{ active: currentCategory === category.key }\"\r\n @click=\"handleCategoryChange(category.key)\"\r\n >\r\n {{ category.name }}\r\n </view>\r\n </view>\r\n \r\n <!-- 表情列表 -->\r\n <scroll-view class=\"emoji-list\" scroll-y>\r\n <view class=\"emoji-grid\">\r\n <view \r\n v-for=\"(emoji, index) in currentEmojis\" \r\n :key=\"index\"\r\n class=\"emoji-item\"\r\n @click=\"handleEmojiClick(emoji)\"\r\n >\r\n <text class=\"emoji-icon\">{{ emoji.code }}</text>\r\n </view>\r\n </view>\r\n </scroll-view>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script setup>\r\nimport { ref, computed } from 'vue'\r\nimport { emojiCategories } from '@/utils/emoji.js'\r\n\r\nconst props = defineProps({\r\n visible: {\r\n type: Boolean,\r\n default: false\r\n }\r\n})\r\n\r\nconst emit = defineEmits(['close', 'select'])\r\n\r\nconst categories = ref(emojiCategories)\r\nconst currentCategory = ref('common')\r\n\r\n// 当前分类的表情列表\r\nconst currentEmojis = computed(() => {\r\n const category = categories.value.find(c => c.key === currentCategory.value)\r\n return category ? category.emojis : []\r\n})\r\n\r\n// 切换分类\r\nconst handleCategoryChange = (key) => {\r\n currentCategory.value = key\r\n}\r\n\r\n// 选择表情\r\nconst handleEmojiClick = (emoji) => {\r\n emit('select', emoji.code)\r\n}\r\n\r\n// 关闭面板\r\nconst handleClose = () => {\r\n emit('close')\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.emoji-picker {\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n bottom: 0;\r\n z-index: 999;\r\n \r\n .emoji-mask {\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n bottom: 0;\r\n background-color: rgba(0, 0, 0, 0.3);\r\n }\r\n \r\n .emoji-panel {\r\n position: absolute;\r\n bottom: 0;\r\n left: 0;\r\n right: 0;\r\n height: 500rpx;\r\n background-color: #fff;\r\n border-radius: 24rpx 24rpx 0 0;\r\n display: flex;\r\n flex-direction: column;\r\n \r\n .emoji-tabs {\r\n display: flex;\r\n border-bottom: 1rpx solid #eee;\r\n padding: 0 24rpx;\r\n \r\n .emoji-tab {\r\n flex: 1;\r\n text-align: center;\r\n padding: 24rpx 0;\r\n font-size: 28rpx;\r\n color: #666;\r\n position: relative;\r\n \r\n &.active {\r\n color: #ff6b6b;\r\n font-weight: 600;\r\n \r\n &::after {\r\n content: '';\r\n position: absolute;\r\n bottom: 0;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n width: 40rpx;\r\n height: 4rpx;\r\n background-color: #ff6b6b;\r\n border-radius: 2rpx;\r\n }\r\n }\r\n }\r\n }\r\n \r\n .emoji-list {\r\n flex: 1;\r\n padding: 24rpx;\r\n \r\n .emoji-grid {\r\n display: flex;\r\n flex-wrap: wrap;\r\n \r\n .emoji-item {\r\n width: 14.28%;\r\n aspect-ratio: 1;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n \r\n .emoji-icon {\r\n font-size: 48rpx;\r\n }\r\n \r\n &:active {\r\n background-color: #f5f5f5;\r\n border-radius: 8rpx;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n</style>\r\n","import Component from 'D:/outsource/xiangyixiangqin/miniapp/components/EmojiPicker/index.vue'\nwx.createComponent(Component)"],"names":["ref","emojiCategories","computed"],"mappings":";;;;;;;;;;;;;AA6CA,UAAM,OAAO;AAEb,UAAM,aAAaA,cAAG,IAACC,2BAAe;AACtC,UAAM,kBAAkBD,cAAG,IAAC,QAAQ;AAGpC,UAAM,gBAAgBE,cAAQ,SAAC,MAAM;AACnC,YAAM,WAAW,WAAW,MAAM,KAAK,OAAK,EAAE,QAAQ,gBAAgB,KAAK;AAC3E,aAAO,WAAW,SAAS,SAAS,CAAE;AAAA,IACxC,CAAC;AAGD,UAAM,uBAAuB,CAAC,QAAQ;AACpC,sBAAgB,QAAQ;AAAA,IAC1B;AAGA,UAAM,mBAAmB,CAAC,UAAU;AAClC,WAAK,UAAU,MAAM,IAAI;AAAA,IAC3B;AAGA,UAAM,cAAc,MAAM;AACxB,WAAK,OAAO;AAAA,IACd;;;;;;;;;;;;;;;;;;;;;;;;;;ACpEA,GAAG,gBAAgB,SAAS;"}

View File

@ -1 +0,0 @@
{"version":3,"file":"index.js","sources":["components/Empty/index.vue","../../../软件/HBuilderX/plugins/uniapp-cli-vite/uniComponent:/RDovb3V0c291cmNlL3hpYW5neWl4aWFuZ3Fpbi9taW5pYXBwL2NvbXBvbmVudHMvRW1wdHkvaW5kZXgudnVl"],"sourcesContent":["<template>\r\n <view class=\"empty-container\">\r\n <view v-if=\"!image && !hasDefaultImage\" class=\"empty-icon\">\r\n <text class=\"icon-text\">📭</text>\r\n </view>\r\n <image \r\n v-else\r\n class=\"empty-image\" \r\n :src=\"image || defaultImage\" \r\n mode=\"aspectFit\"\r\n @error=\"handleImageError\"\r\n />\r\n <text class=\"empty-text\">{{ text || '暂无数据' }}</text>\r\n <button \r\n v-if=\"showButton\" \r\n class=\"empty-btn\" \r\n @click=\"handleButtonClick\"\r\n >\r\n {{ buttonText || '去相亲' }}\r\n </button>\r\n </view>\r\n</template>\r\n\r\n<script>\r\nexport default {\r\n name: 'Empty',\r\n props: {\r\n image: {\r\n type: String,\r\n default: ''\r\n },\r\n text: {\r\n type: String,\r\n default: '暂无数据'\r\n },\r\n showButton: {\r\n type: Boolean,\r\n default: true\r\n },\r\n buttonText: {\r\n type: String,\r\n default: '去相亲'\r\n },\r\n buttonUrl: {\r\n type: String,\r\n default: '/pages/index/index'\r\n }\r\n },\r\n emits: ['click'],\r\n data() {\r\n return {\r\n defaultImage: '/static/ic_empty.png',\r\n hasDefaultImage: true // 使用空占位图\r\n }\r\n },\r\n methods: {\r\n handleImageError() {\r\n // 图片加载失败时使用emoji替代\r\n this.hasDefaultImage = false\r\n },\r\n handleButtonClick() {\r\n this.$emit('click')\r\n \r\n // 如果有指定跳转URL则跳转\r\n if (this.buttonUrl) {\r\n // 判断是否是tabbar页面\r\n const tabbarPages = ['/pages/index/index', '/pages/message/index', '/pages/mine/index']\r\n if (tabbarPages.includes(this.buttonUrl)) {\r\n uni.switchTab({ url: this.buttonUrl })\r\n } else {\r\n uni.navigateTo({ url: this.buttonUrl })\r\n }\r\n }\r\n }\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.empty-container {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 100rpx 40rpx;\r\n \r\n .empty-icon {\r\n width: 200rpx;\r\n height: 200rpx;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n margin-bottom: 30rpx;\r\n \r\n .icon-text {\r\n font-size: 120rpx;\r\n }\r\n }\r\n \r\n .empty-image {\r\n width: 300rpx;\r\n height: 300rpx;\r\n margin-bottom: 30rpx;\r\n }\r\n \r\n .empty-text {\r\n font-size: 28rpx;\r\n color: #999;\r\n margin-bottom: 40rpx;\r\n text-align: center;\r\n }\r\n \r\n .empty-btn {\r\n min-width: 240rpx;\r\n height: 72rpx;\r\n line-height: 72rpx;\r\n padding: 0 40rpx;\r\n background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);\r\n color: #fff;\r\n font-size: 28rpx;\r\n border-radius: 36rpx;\r\n border: none;\r\n \r\n &::after {\r\n border: none;\r\n }\r\n }\r\n}\r\n</style>\r\n","import Component from 'D:/outsource/xiangyixiangqin/miniapp/components/Empty/index.vue'\nwx.createComponent(Component)"],"names":["uni"],"mappings":";;AAwBA,MAAK,YAAU;AAAA,EACb,MAAM;AAAA,EACN,OAAO;AAAA,IACL,OAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IACV;AAAA,IACD,MAAM;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,IACV;AAAA,IACD,YAAY;AAAA,MACV,MAAM;AAAA,MACN,SAAS;AAAA,IACV;AAAA,IACD,YAAY;AAAA,MACV,MAAM;AAAA,MACN,SAAS;AAAA,IACV;AAAA,IACD,WAAW;AAAA,MACT,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,EACD;AAAA,EACD,OAAO,CAAC,OAAO;AAAA,EACf,OAAO;AACL,WAAO;AAAA,MACL,cAAc;AAAA,MACd,iBAAiB;AAAA;AAAA,IACnB;AAAA,EACD;AAAA,EACD,SAAS;AAAA,IACP,mBAAmB;AAEjB,WAAK,kBAAkB;AAAA,IACxB;AAAA,IACD,oBAAoB;AAClB,WAAK,MAAM,OAAO;AAGlB,UAAI,KAAK,WAAW;AAElB,cAAM,cAAc,CAAC,sBAAsB,wBAAwB,mBAAmB;AACtF,YAAI,YAAY,SAAS,KAAK,SAAS,GAAG;AACxCA,wBAAAA,MAAI,UAAU,EAAE,KAAK,KAAK,WAAW;AAAA,eAChC;AACLA,wBAAAA,MAAI,WAAW,EAAE,KAAK,KAAK,WAAW;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;;;;;;;;;;;;;;AC1EA,GAAG,gBAAgB,SAAS;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"index.js","sources":["config/index.js"],"sourcesContent":["/**\r\n * 统一配置管理\r\n * 所有环境相关的配置都在这里管理\r\n * 部署时只需修改此文件即可\r\n */\r\n\r\n// 环境配置\r\nconst ENV = {\r\n\t// 开发环境\r\n\tdevelopment: {\r\n\t\tAPI_BASE_URL: 'http://localhost:5000/api/app',\r\n\t\tSTATIC_BASE_URL: 'http://localhost:5000',\r\n\t\tADMIN_API_BASE_URL: 'http://localhost:5001/api',\r\n\t\tSIGNALR_URL: 'ws://localhost:5000/hubs/chat'\r\n\t},\r\n\t// 生产环境 - 部署时修改这里的地址\r\n\tproduction: {\r\n\t\tAPI_BASE_URL: 'https://app.zpc-xy.com/xyqj/api/api/app',\r\n\t\tSTATIC_BASE_URL: 'https://app.zpc-xy.com',\r\n\t\tADMIN_API_BASE_URL: 'https://app.zpc-xy.com/xyqj/admin/',\r\n\t\tSIGNALR_URL: 'wss://app.zpc-xy.com/xyqj/api/hubs/chat'\r\n\t}\r\n}\r\n\r\n// 当前环境 - 开发时使用 development打包时改为 production\r\nconst CURRENT_ENV = 'production'\r\n\r\n// 导出配置\r\nexport const config = {\r\n\t// API 基础地址\r\n\tAPI_BASE_URL: ENV[CURRENT_ENV].API_BASE_URL,\r\n\r\n\t// 静态资源服务器地址(图片等)\r\n\tSTATIC_BASE_URL: ENV[CURRENT_ENV].STATIC_BASE_URL,\r\n\r\n\t// 管理后台 API 地址\r\n\tADMIN_API_BASE_URL: ENV[CURRENT_ENV].ADMIN_API_BASE_URL,\r\n\r\n\t// SignalR Hub 地址\r\n\tSIGNALR_URL: ENV[CURRENT_ENV].SIGNALR_URL,\r\n\r\n\t// 请求超时时间(毫秒)\r\n\tREQUEST_TIMEOUT: 30000,\r\n\r\n\t// 请求重试次数\r\n\tREQUEST_RETRY_COUNT: 2,\r\n\r\n\t// 请求重试延迟(毫秒)\r\n\tREQUEST_RETRY_DELAY: 1000\r\n}\r\n\r\n/**\r\n * 获取API基础地址\r\n * @returns {string}\r\n */\r\nexport function getApiBaseUrl() {\r\n\treturn ENV[CURRENT_ENV].API_BASE_URL\r\n}\r\n\r\n/**\r\n * 获取静态资源基础地址\r\n * @returns {string}\r\n */\r\nexport function getStaticBaseUrl() {\r\n\treturn ENV[CURRENT_ENV].STATIC_BASE_URL\r\n}\r\n\r\nexport default config"],"names":[],"mappings":";AAOA,MAAM,MAAM;AAAA;AAAA,EAEX,aAAa;AAAA,IACZ,cAAc;AAAA,IACd,iBAAiB;AAAA,IACjB,oBAAoB;AAAA,IACpB,aAAa;AAAA,EACb;AAAA;AAAA,EAED,YAAY;AAAA,IACX,cAAc;AAAA,IACd,iBAAiB;AAAA,IACjB,oBAAoB;AAAA,IACpB,aAAa;AAAA,EACb;AACF;AAGA,MAAM,cAAc;AAGR,MAAC,SAAS;AAAA;AAAA,EAErB,cAAc,IAAI,WAAW,EAAE;AAAA;AAAA,EAG/B,iBAAiB,IAAI,WAAW,EAAE;AAAA;AAAA,EAGlC,oBAAoB,IAAI,WAAW,EAAE;AAAA;AAAA,EAGrC,aAAa,IAAI,WAAW,EAAE;AAAA;AAAA,EAG9B,iBAAiB;AAAA;AAAA,EAGjB,qBAAqB;AAAA;AAAA,EAGrB,qBAAqB;AACtB;AAMO,SAAS,gBAAgB;AAC/B,SAAO,IAAI,WAAW,EAAE;AACzB;;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More