Browse Source

正式环境 提审版本

master
kola-web 3 days ago
parent
commit
6714a715c8
  1. 250
      api-doc/PSVT协议模块接口文档.md
  2. 46
      api-doc/charlie.md
  3. 969
      api-doc/psvt-patient-api-doc.md
  4. 2
      dist.ps1
  5. 2
      project.config.json
  6. 11
      project.private.config.json
  7. 1
      src/app.json
  8. 47
      src/app.ts
  9. 33
      src/custom-tab-bar/index.ts
  10. 11
      src/custom-tab-bar/index.wxml
  11. BIN
      src/images/icon28-active.png
  12. BIN
      src/images/icon9-active.png
  13. BIN
      src/images/logo1.png
  14. BIN
      src/images/logo2.png
  15. BIN
      src/images/none.png
  16. 47
      src/pages/articleDetail/index.scss
  17. 113
      src/pages/articleDetail/index.ts
  18. 55
      src/pages/articleDetail/index.wxml
  19. 7
      src/pages/articleVideoDetail/index.json
  20. 156
      src/pages/articleVideoDetail/index.scss
  21. 262
      src/pages/articleVideoDetail/index.ts
  22. 82
      src/pages/articleVideoDetail/index.wxml
  23. 5
      src/pages/changeTel/index.json
  24. 23
      src/pages/changeTel/index.scss
  25. 4
      src/pages/changeTel/index.ts
  26. 12
      src/pages/changeTel/index.wxml
  27. 4
      src/pages/confirmInfo/index.ts
  28. 3
      src/pages/index/index.json
  29. 8
      src/pages/index/index.scss
  30. 184
      src/pages/index/index.ts
  31. 103
      src/pages/index/index.wxml
  32. 7
      src/pages/login/index.ts
  33. 6
      src/pages/my/index.ts
  34. 72
      src/pages/my/index.wxml
  35. 5
      src/pages/myCollection/index.json
  36. 17
      src/pages/myCollection/index.scss
  37. 184
      src/pages/myCollection/index.ts
  38. 53
      src/pages/myCollection/index.wxml
  39. 11
      src/pages/userInfo/index.ts
  40. 24
      src/pages/videoDetail/index.scss
  41. 346
      src/pages/videoDetail/index.ts
  42. 70
      src/pages/videoDetail/index.wxml
  43. 3
      src/pages/videoList/index.json
  44. 19
      src/pages/videoList/index.scss
  45. 145
      src/pages/videoList/index.ts
  46. 69
      src/pages/videoList/index.wxml
  47. 4
      src/pages/wechatWork/index.scss
  48. 7
      src/pages/wechatWork/index.ts
  49. 4
      src/pages/wechatWork/index.wxml
  50. 6
      src/utils/page.ts

250
api-doc/PSVT协议模块接口文档.md

@ -1,250 +0,0 @@
# PSVT协议模块接口文档
## 概述
协议模块支持4种协议类型:隐私协议、用户协议、法律声明、健康敏感信息处理与AI服务授权告知书。初始4条数据,无创建接口,仅支持修改更新文案。
所有接口返回格式统一为:
```json
{
"code": 0,
"message": "success",
"data": {}
}
```
协议类型常量值:
| 常量值 | 含义 |
|--------|------|
| 1 | 隐私协议 |
| 2 | 用户协议 |
| 3 | 法律声明 |
| 4 | 健康敏感信息处理与AI服务授权告知书 |
---
## 一、小程序端接口
### 1. 按类型获取协议详情
- **路由**: `?r=psvt/agreement/get-by-type`
- **请求方式**: GET
- **是否需要登录**: 否(白名单路由)
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| type | int | 是 | 协议类型:1-隐私协议,2-用户协议,3-法律声明,4-健康敏感信息处理与AI服务授权告知书 |
#### 响应示例
```json
{
"code": 0,
"message": "success",
"data": {
"id": 1,
"type": 1,
"title": "隐私协议",
"content": "隐私协议内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
}
}
```
#### data字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | int | 协议ID |
| type | int | 协议类型 |
| title | string | 协议标题 |
| content | string | 协议内容(富文本) |
| version | string | 版本号 |
| updateTime | string | 最后更新时间 |
#### 错误码
| code | 说明 |
|------|------|
| 1 | 协议类型参数无效 |
| 1 | 协议不存在 |
---
### 2. 获取协议列表
- **路由**: `?r=psvt/agreement/get-list`
- **请求方式**: GET
- **是否需要登录**: 否(白名单路由)
#### 请求参数
#### 响应示例
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": 1,
"type": 1,
"title": "隐私协议",
"content": "隐私协议内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
},
{
"id": 2,
"type": 2,
"title": "用户协议",
"content": "用户协议内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
},
{
"id": 3,
"type": 3,
"title": "法律声明",
"content": "法律声明内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
},
{
"id": 4,
"type": 4,
"title": "健康敏感信息处理与AI服务授权告知书",
"content": "健康敏感信息处理与AI服务授权告知书内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
}
]
}
```
#### data字段说明
data为数组类型,每个元素字段同"按类型获取协议详情"接口的data字段。
---
## 二、PC后台管理端接口
### 1. 获取协议列表
- **路由**: `?r=agreement/get-list`
- **请求方式**: GET
- **是否需要登录**: 是
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| search | string | 否 | 搜索关键词(按标题模糊搜索) |
| page | int | 否 | 页码,默认1 |
| count | int | 否 | 每页条数,默认10 |
#### 响应示例
```json
{
"code": 0,
"message": "success",
"data": {
"total": 4,
"pages": 1,
"list": [
{
"id": 1,
"type": 1,
"title": "隐私协议",
"content": "隐私协议内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
},
{
"id": 2,
"type": 2,
"title": "用户协议",
"content": "用户协议内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
},
{
"id": 3,
"type": 3,
"title": "法律声明",
"content": "法律声明内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
},
{
"id": 4,
"type": 4,
"title": "健康敏感信息处理与AI服务授权告知书",
"content": "健康敏感信息处理与AI服务授权告知书内容...",
"version": "V1.0",
"updateTime": "2026-07-17 10:00:00"
}
]
}
}
```
#### data字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| total | int | 总记录数 |
| pages | int | 总页数 |
| list | array | 协议列表,每个元素字段同小程序端 |
---
### 2. 更新协议文案
- **路由**: `?r=agreement/update`
- **请求方式**: POST
- **是否需要登录**: 是
- **事务**: 是
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | int | 是 | 协议ID |
| title | string | 否 | 协议标题 |
| content | string | 否 | 协议内容(富文本) |
> 注:title和content至少传一个,传入的字段才会更新
#### 响应示例
```json
{
"code": 0,
"message": "更新成功",
"data": {
"id": 1
}
}
```
#### data字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | int | 更新的协议ID |
#### 错误码
| code | 说明 |
|------|------|
| 1 | 用户未登录 |
| 1 | 协议ID不能为空 |
| 1 | 协议不存在 |
| 1 | 更新协议失败 |

46
api-doc/charlie.md

File diff suppressed because one or more lines are too long

969
api-doc/psvt-patient-api-doc.md

@ -1,969 +0,0 @@
# PSVT患者模块接口文档
## 一、小程序端接口
### 1.1 初始化登录 `GET ?r=psvt/user/init-login`
**说明**: 小程序端通过wx.login获取code,发送到服务端完成登录初始化,返回登录态及用户状态信息。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | ---------------------- |
| code | string | 是 | wx.login获取的登录凭证 |
| channelId | int | 否 | 扫码渠道ID |
**响应示例**:
```json
{
"code": 0,
"data": {
"loginState": "xxx",
"isLogin": true,
"isReg": true,
"userId": "xxx",
"openId": "xxx"
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ---------- | ------ | -------------------------- |
| loginState | string | 登录态凭证,后续接口需携带 |
| isLogin | bool | 是否已登录 |
| userId | string | 用户ID |
| openId | string | 小程序OpenId |
| isReg | bool | 是否已信息提交注册 |
**业务逻辑**:
1. 调用微信接口获取openid和session_key
2. 创建/更新PSVT用户记录(t_psvt_user_info)
3. 若有channelId参数,更新ScanChannelId(最终渠道,每次扫码都更新)
4. 首次打开时记录FirstOpenTime
5. 返回用户状态信息(isPhoneRegistered、isInfoSubmitted)
---
### 1.2 手机号授权注册 `POST ?r=psvt/login/wx-reg-login`
**说明**: 解密微信加密数据获取手机号,完成手机号注册。事务操作。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ------------- | ------ | ---- | -------------------- |
| encryptedData | string | 是 | 微信加密手机号数据 |
| iv | string | 是 | 微信加密iv |
| channelId | int | 否 | 手机号注册时的渠道ID |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"isLogin": true,
"isReg": true
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ------- | ---- | ------------------ |
| isLogin | bool | 是否已登录 |
| isReg | bool | 是否已信息提交注册 |
**业务逻辑**:
1. 解密微信加密数据获取手机号
2. 更新t_psvt_user_info的Telephone、IsPhoneRegistered=1、PhoneRegisterTime
3. 若有channelId,记录TelChannelId(首次手机号注册渠道,仅记录首次,不更新)
4. 若有channelId,记录邀约记录(InviteType=2手机号注册)并更新统计表
5. 更新IsFirstOpenPhoneRegistered(首次打开时间和手机号注册时间是同一天时为1)
6. 更新IsPhoneRegisteredNotLost(手机号注册时间和提交信息时间是同一天时为1)
7. 使用事务
---
### 1.2.1 手机号验证码注册 `POST ?r=psvt/login/reg-login`
**说明**: 解密微信加密数据获取手机号,完成手机号注册。事务操作。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | -------------------- |
| mobile | string | 是 | 手机号 |
| code | string | 是 | 验证码 |
| channelId | int | 否 | 手机号注册时的渠道ID |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"isLogin": true,
"isReg": true
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ------- | ---- | ------------------ |
| isLogin | bool | 是否已登录 |
| isReg | bool | 是否已信息提交注册 |
### 1.3 患者信息提交注册 `POST ?r=psvt/patient/register`
**说明**: 提交年龄段、性别、确诊状态、手术状态等信息完成注册。事务操作。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------------- | ------ | ---- | --------------------------------------------------------------------------------------------- |
| ageRange | int | 是 | 年龄段:1-18~28岁,2-29~40岁,3-41~50岁,4-50岁以上 |
| gender | int | 是 | 性别:1-男,2-女 |
| provinceId | int | 否 | 省份ID |
| provinceName | string | 否 | 省份名称 |
| cityId | int | 否 | 市ID |
| cityName | string | 否 | 市名称 |
| diagnosisStatus | int | 是 | 确诊状态:1-疑似PSVT正在了解中,2-已确诊PSVT,3-心悸/心跳过快 尚未就诊,4-亲友相关 想了解更多 |
| diagnosisTime | string | 否 | 确诊时间(YYYY-MM格式) |
| surgeryStatus | int | 是 | 手术状态:1-已接收射频消融/相关手术,2-尚未接收手术,3-正在评估 |
| channelId | int | 否 | 提交信息渠道ID |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"userId": "xxx",
"nickName": "abcd12345678",
"avatar": "https://psvt.example.com/avatar/3.png",
"isRegistered": true
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ------------ | ------ | ----------------------------- |
| userId | string | 用户ID |
| nickName | string | 随机生成的昵称(4字母+8数字) |
| avatar | string | 随机分配的头像URL |
| isRegistered | bool | 是否已注册 |
**业务逻辑**:
1. 生成随机昵称(4随机字母+8随机数字)
2. 随机分配头像(10张图片池中随机选1)
3. 创建t_psvt_patient_info记录(患者属性)
4. 更新t_psvt_user_info的PatientId、IsInfoSubmitted=1、InfoSubmitTime、SubmitChannelId
5. 更新IsFirstOpenInfoSubmitted(首次打开时间和信息提交时间是同一天时为1)
6. 更新IsPhoneRegisteredNotLost(手机号注册时间和信息提交时间是同一天时为1)
7. 若有channelId,记录邀约记录(InviteType=3信息提交)并更新统计表
8. 使用事务
---
### 1.4 获取我的信息 `GET ?r=psvt/patient/my-info`
**说明**: 返回当前登录用户的完整信息(用户信息+患者属性),小驼峰格式。
**请求参数**: 无(通过loginState获取userId)
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"userId": "xxx",
"openId": "xxx",
"telephone": "138xxxx1234",
"isPhoneRegistered": true,
"isInfoSubmitted": true,
"isHeartRateTips": true,
"firstOpenTime": "2023-01-01 10:00:00",
"phoneRegisterTime": "2023-01-01 10:05:00",
"infoSubmitTime": "2023-01-01 10:10:00",
"nickName": "abcd12345678",
"avatar": "https://psvt.example.com/avatar/3.png",
"ageRange": 1,
"gender": 1,
"provinceName": "北京",
"cityName": "北京",
"diagnosisStatus": 2,
"diagnosisTime": "2023-06",
"surgeryStatus": 1
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ----------------- | ------ | ------------------------------ |
| userId | string | 用户ID |
| openId | string | 小程序OpenId |
| telephone | string | 手机号 |
| isPhoneRegistered | bool | 是否已手机号注册 |
| isInfoSubmitted | bool | 是否已提交信息 |
| isHeartRateTips | bool | 是否开启心率提醒 |
| firstOpenTime | string | 首次打开时间 |
| phoneRegisterTime | string | 手机号注册时间 |
| infoSubmitTime | string | 信息提交时间 |
| nickName | string | 昵称(未提交信息时为null) |
| avatar | string | 头像URL(未提交信息时为null) |
| ageRange | int | 年龄段(未提交信息时为null) |
| gender | int | 性别(未提交信息时为null) |
| provinceName | string | 省份名称(未提交信息时为null) |
| cityName | string | 市名称(未提交信息时为null) |
| diagnosisStatus | int | 确诊状态(未提交信息时为null) |
| diagnosisTime | string | 确诊时间(未提交信息时为null) |
| surgeryStatus | int | 手术状态(未提交信息时为null) |
---
### 1.5 更新我的信息 `POST ?r=psvt/patient/update-my-info`
**说明**: 更新当前登录用户的个人信息。若已确诊PSVT(DiagnosisStatus=2),确诊状态和手术状态不可修改。事务操作。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------------- | ------ | ---- | -------------------------------- |
| avatar | string | 否 | 头像URL |
| nickName | string | 否 | 昵称 |
| telephone | string | 否 | 手机号 |
| ageRange | int | 否 | 年龄段 |
| gender | int | 否 | 性别 |
| provinceId | int | 否 | 省份ID |
| provinceName | string | 否 | 省份名称 |
| cityId | int | 否 | 市ID |
| cityName | string | 否 | 市名称 |
| diagnosisStatus | int | 否 | 确诊状态(已确诊PSVT后不可修改) |
| diagnosisTime | string | 否 | 确诊时间 |
| surgeryStatus | int | 否 | 手术状态(已确诊PSVT后不可修改) |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"userId": "xxx",
"openId": "xxx",
"telephone": "138xxxx1234",
"isPhoneRegistered": true,
"isInfoSubmitted": true,
"isHeartRateTips": true,
"firstOpenTime": "2023-01-01 10:00:00",
"phoneRegisterTime": "2023-01-01 10:05:00",
"infoSubmitTime": "2023-01-01 10:10:00",
"nickName": "newNickname",
"avatar": "https://...",
"ageRange": 2,
"gender": 1,
"provinceName": "上海",
"cityName": "上海",
"diagnosisStatus": 2,
"diagnosisTime": "2023-06",
"surgeryStatus": 1
}
}
```
**响应字段说明**: 同1.4获取我的信息
**业务逻辑**:
1. 若已确诊PSVT(DiagnosisStatus=2),则diagnosisStatus和surgeryStatus不可修改
2. 更新t_psvt_patient_info患者属性
3. 若更新了手机号,同步更新t_psvt_user_info的Telephone
4. 使用事务
---
### 1.6 渠道码扫码记录 `POST ?r=psvt/?r=channel/scan-channel`
**说明**: 记录扫码渠道信息,更新用户的ScanChannelId。事务操作。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ---- | ---- | -------- |
| channelId | int | 是 | 渠道码ID |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"recorded": true
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| -------- | ---- | ------------ |
| recorded | bool | 是否记录成功 |
**业务逻辑**:
1. 验证渠道码存在
2. 更新用户ScanChannelId(最终渠道,每次扫码都更新)
3. 记录邀约记录(InviteType=1用户打开)
4. 更新统计表
5. 使用事务
---
### 1.7 获取基础数据列表 `GET ?r=psvt/patient/base-data`
**说明**: 返回性别、年龄段、确诊状态、手术状态等枚举数据,供前端渲染下拉选项。
**请求参数**: 无
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"genderList": [{ "1": "男" }, { "2": "女" }],
"ageRangeList": [{ "1": "18~28岁" }, { "2": "29~40岁" }, { "3": "41~50岁" }, { "4": "50岁以上" }],
"diagnosisStatusList": [
{ "1": "疑似PSVT正在了解中" },
{ "2": "已确诊PSVT" },
{ "3": "心悸/心跳过快 尚未就诊" },
{ "4": "亲友相关 想了解更多" }
],
"surgeryStatusList": [{ "1": "已接收射频消融/相关手术" }, { "2": "尚未接收手术" }, { "3": "正在评估" }]
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ------------------- | ----- | ------------------------------------- |
| genderList | array | 性别列表,每项为 {value: "label"} |
| ageRangeList | array | 年龄段列表,每项为 {value: "label"} |
| diagnosisStatusList | array | 确诊状态列表,每项为 {value: "label"} |
| surgeryStatusList | array | 手术状态列表,每项为 {value: "label"} |
**genderList值说明**:
| 值 | 说明 |
| --- | ---- |
| 1 | 男 |
| 2 | 女 |
**ageRangeList值说明**:
| 值 | 说明 |
| --- | -------- |
| 1 | 18~28岁 |
| 2 | 29~40岁 |
| 3 | 41~50岁 |
| 4 | 50岁以上 |
**diagnosisStatusList值说明**:
| 值 | 说明 |
| --- | ---------------------- |
| 1 | 疑似PSVT正在了解中 |
| 2 | 已确诊PSVT |
| 3 | 心悸/心跳过快 尚未就诊 |
| 4 | 亲友相关 想了解更多 |
**surgeryStatusList值说明**:
| 值 | 说明 |
| --- | ----------------------- |
| 1 | 已接收射频消融/相关手术 |
| 2 | 尚未接收手术 |
| 3 | 正在评估 |
---
## 二、后台管理端接口
### 2.1 患者列表 `GET ?r=patient/list`
**说明**: 获取PSVT患者列表,含分页、搜索、渠道筛选及汇总统计。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | -------------------------------- |
| search | string | 否 | 搜索关键字(手机号/UserId/省市) |
| channelId | int | 否 | 渠道ID筛选 |
| page | int | 否 | 页码,默认1 |
| count | int | 否 | 每页数量,默认10 |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"page": 1,
"pages": 10,
"count": 100,
"summary": {
"totalOpenCount": 1920,
"phoneRegisteredCount": 1900,
"phoneRegisteredRate": "19.9%",
"infoSubmittedCount": 1082,
"infoSubmittedRate": "56.3%",
"diagnosedPsvtCount": 980,
"diagnosedPsvtRate": "90.2%",
"firstOpenPhoneRegisteredCount": 800,
"firstOpenInfoSubmittedCount": 800,
"phoneRegisteredNotLostCount": 700
},
"list": [
{
"userId": "xxx",
"telephone": "138xxxx1234",
"nickName": "abcd12345678",
"avatar": "https://...",
"ageRange": 1,
"gender": 1,
"provinceName": "北京",
"cityName": "北京",
"diagnosisStatus": 2,
"diagnosisTime": "2023-06",
"surgeryStatus": 1,
"firstOpenTime": "2023-01-01 10:00:00",
"phoneRegisterTime": "2023-01-01 10:05:00",
"infoSubmitTime": "2023-01-01 10:10:00",
"isFirstOpenPhoneRegistered": true,
"isFirstOpenInfoSubmitted": true,
"isPhoneRegisteredNotLost": true,
"scanChannelId": 1,
"scanChannelName": "渠道A",
"telChannelId": 2,
"telChannelName": "渠道B",
"submitChannelId": 3,
"submitChannelName": "渠道C"
}
]
}
}
```
**响应字段说明(data层级)**:
| 字段名 | 类型 | 说明 |
| ------- | ------ | ------------ |
| page | int | 当前页码 |
| pages | int | 总页数 |
| count | int | 总记录数 |
| summary | object | 汇合统计数据 |
| list | array | 患者列表 |
**summary字段说明**:
| 字段名 | 类型 | 说明 |
| ----------------------------- | ------ | ------------------------ |
| totalOpenCount | int | 总打开数 |
| phoneRegisteredCount | int | 手机号注册数 |
| phoneRegisteredRate | string | 手机号注册率(百分比) |
| infoSubmittedCount | int | 信息提交数 |
| infoSubmittedRate | string | 信息提交率(百分比) |
| diagnosedPsvtCount | int | 确诊PSVT数 |
| diagnosedPsvtRate | string | 确诊PSVT率(百分比) |
| firstOpenPhoneRegisteredCount | int | 首次打开完成手机号注册数 |
| firstOpenInfoSubmittedCount | int | 首次打开完成信息填写数 |
| phoneRegisteredNotLostCount | int | 手机号注册后未流失数 |
**list行字段说明**:
| 字段名 | 类型 | 说明 |
| -------------------------- | ------ | ---------------------- |
| userId | string | 用户ID |
| telephone | string | 手机号 |
| nickName | string | 昵称 |
| avatar | string | 头像URL |
| ageRange | int | 年龄段 |
| gender | int | 性别 |
| provinceName | string | 省份名称 |
| cityName | string | 市名称 |
| diagnosisStatus | int | 确诊状态 |
| diagnosisTime | string | 确诊时间 |
| surgeryStatus | int | 手术状态 |
| firstOpenTime | string | 首次打开时间 |
| phoneRegisterTime | string | 手机号注册时间 |
| infoSubmitTime | string | 信息提交时间 |
| isFirstOpenPhoneRegistered | bool | 首次打开完成手机号注册 |
| isFirstOpenInfoSubmitted | bool | 首次打开完成信息填写 |
| isPhoneRegisteredNotLost | bool | 手机号注册后未流失 |
| scanChannelId | int | 扫码渠道ID |
| scanChannelName | string | 扫码渠道名称 |
| telChannelId | int | 手机号注册渠道ID |
| telChannelName | string | 手机号注册渠道名称 |
| submitChannelId | int | 提交信息渠道ID |
| submitChannelName | string | 提交信息渠道名称 |
---
### 2.2 患者详情 `GET ?r=patient/detail`
**说明**: 根据用户信息表ID获取完整的用户+患者详情数据。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ------ | ---- | ---- | ------------ |
| id | int | 是 | 用户信息表ID |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"userId": "xxx",
"openId": "xxx",
"telephone": "138xxxx1234",
"isPhoneRegistered": true,
"isInfoSubmitted": true,
"isHeartRateTips": true,
"firstOpenTime": "2023-01-01 10:00:00",
"phoneRegisterTime": "2023-01-01 10:05:00",
"infoSubmitTime": "2023-01-01 10:10:00",
"nickName": "abcd12345678",
"avatar": "https://...",
"ageRange": 1,
"gender": 1,
"provinceName": "北京",
"cityName": "北京",
"diagnosisStatus": 2,
"diagnosisTime": "2023-06",
"surgeryStatus": 1,
"scanChannelId": 1,
"scanChannelName": "渠道A",
"telChannelId": 2,
"telChannelName": "渠道B",
"submitChannelId": 3,
"submitChannelName": "渠道C"
}
}
```
**响应字段说明**: 同1.4获取我的信息,额外包含渠道信息字段(scanChannelId/scanChannelName、telChannelId/telChannelName、submitChannelId/submitChannelName)
---
### 2.3 渠道码列表 `GET ?r=channel/list`
**说明**: 获取有效渠道码列表,支持分页。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ------ | ---- | ---- | ---------------- |
| page | int | 否 | 页码,默认1 |
| count | int | 否 | 每页数量,默认10 |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"page": 1,
"pages": 5,
"count": 50,
"list": [
{
"channelId": 1,
"channelName": "渠道A",
"channelDesc": "渠道A说明",
"miniCodeUrl": "https://xxx/minicode_1.png",
"createDate": "2023-01-01",
"updateDate": "2023-01-01"
}
]
}
}
```
**响应字段说明(data层级)**:
| 字段名 | 类型 | 说明 |
| ------ | ----- | -------- |
| page | int | 当前页码 |
| pages | int | 总页数 |
| count | int | 总记录数 |
| list | array | 渠道列表 |
**list行字段说明**:
| 字段名 | 类型 | 说明 |
| ----------- | ------ | --------------- |
| channelId | int | 渠道ID |
| channelName | string | 渠道名称 |
| channelDesc | string | 渠道码说明 |
| miniCodeUrl | string | 小程序码图片URL |
| createDate | string | 创建日期 |
| updateDate | string | 更新日期 |
---
### 2.4 创建渠道码 `POST ?r=channel/create`
**说明**: 创建新的渠道码。事务操作。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ----------- | ------ | ---- | ---------- |
| channelName | string | 是 | 渠道名称 |
| channelDesc | string | 否 | 渠道码说明 |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"channelId": 1,
"channelName": "渠道A"
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ----------- | ------ | -------------- |
| channelId | int | 新创建的渠道ID |
| channelName | string | 渠道名称 |
---
### 2.5 更新渠道码 `POST ?r=channel/update`
**说明**: 更新渠道码信息。事务操作。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ----------- | ------ | ---- | ---------- |
| channelId | int | 是 | 渠道ID |
| channelName | string | 否 | 渠道名称 |
| channelDesc | string | 否 | 渠道码说明 |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"channelId": 1,
"channelName": "渠道A(已更新)"
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ----------- | ------ | ---------------- |
| channelId | int | 渠道ID |
| channelName | string | 更新后的渠道名称 |
---
### 2.6 删除渠道码 `POST ?r=channel/delete`
**说明**: 逻辑删除渠道码,将Status置为100(删除状态)。事务操作。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ---- | ---- | ------ |
| channelId | int | 是 | 渠道ID |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"channelId": 1
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| --------- | ---- | -------------- |
| channelId | int | 已删除的渠道ID |
**业务逻辑**:
1. 校验渠道存在
2. 将渠道的Status置为100(删除状态),逻辑删除
3. 更新UpdateBy和UpdateTime
4. 使用事务
---
### 2.7 获取小程序渠道码 `GET ?r=channel/get-minicode`
**说明**: 根据渠道ID获取或生成小程序码图片URL。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ---- | ---- | ------ |
| channelId | int | 是 | 渠道ID |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"channelId": 1,
"channelName": "渠道A",
"miniCodeUrl": "https://xxx/minicode_1.png"
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ----------- | ------ | --------------- |
| channelId | int | 渠道ID |
| channelName | string | 渠道名称 |
| miniCodeUrl | string | 小程序码图片URL |
**业务逻辑**:
1. 若渠道已有小程序码URL(MiniCodeUrl),直接返回
2. 若没有,调用微信接口生成小程序码(scene参数包含渠道ID)
3. 保存生成的MiniCodeUrl到渠道记录
---
### 2.8 渠道码统计 `GET ?r=channel/stat`
**说明**: 根据日期范围和渠道ID获取渠道统计数据,从统计表获取。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------------- | ------ | ---- | --------------------------------- |
| inviteDateBegin | string | 否 | 邀约日期开始(YYYY-MM-DD) |
| inviteDateEnd | string | 否 | 邀约日期结束(YYYY-MM-DD) |
| channelIds | string | 否 | 渠道ID筛选,逗号分隔(如"1,2,3") |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": [
{
"statDate": "2023-01-01",
"channelId": 1,
"channelName": "渠道A",
"channelDesc": "渠道A说明",
"openPatientCount": 100,
"phoneRegisteredCount": 80,
"infoSubmittedCount": 50,
"diagnosedPsvtCount": 30
}
]
}
```
**响应字段说明(每行)**:
| 字段名 | 类型 | 说明 |
| -------------------- | ------ | -------------------- |
| statDate | string | 统计日期 |
| channelId | int | 渠道ID |
| channelName | string | 渠道名称 |
| channelDesc | string | 渠道码说明 |
| openPatientCount | int | 邀约打开患者数 |
| phoneRegisteredCount | int | 邀约注册手机号患者数 |
| infoSubmittedCount | int | 邀约提交信息患者数 |
| diagnosedPsvtCount | int | 确诊PSVT患者数 |
---
### 2.9 渠道码原始数据列表 `GET ?r=channel/raw-list`
**说明**: 获取渠道邀约的原始记录列表,支持分页。通过JOIN t_psvt_user_info和t_psvt_patient_info获取用户属性和患者属性,不冗余存储。
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| --------------- | ------ | ---- | -------------------------------------------------- |
| inviteDateBegin | string | 否 | 邀约日期开始(YYYY-MM-DD) |
| inviteDateEnd | string | 否 | 邀约日期结束(YYYY-MM-DD) |
| channelId | int | 否 | 渠道ID筛选 |
| inviteType | int | 否 | 邀约类型筛选:1-用户打开,2-手机号注册,3-信息提交 |
| page | int | 否 | 页码,默认1 |
| count | int | 否 | 每页数量,默认10 |
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"page": 1,
"pages": 10,
"count": 100,
"list": [
{
"id": 1,
"inviteTime": "2023-01-01 10:00:00",
"inviteDate": "2023-01-01",
"channelId": 1,
"channelName": "渠道A",
"channelDesc": "渠道A说明",
"inviteType": 1,
"inviteTypeName": "用户打开",
"userId": "xxx",
"patientId": 5,
"nickName": "abcd12345678",
"gender": 1,
"genderName": "男",
"ageRange": 1,
"ageRangeName": "18-24岁",
"provinceName": "北京",
"cityName": "北京",
"diagnosisStatus": 2,
"diagnosisStatusName": "确诊",
"diagnosisTime": "2023-06",
"surgeryStatus": 1,
"surgeryStatusName": "已手术",
"telephone": "138xxxx1234"
}
]
}
}
```
**响应字段说明(data层级)**:
| 字段名 | 类型 | 说明 |
| ------ | ----- | ------------ |
| page | int | 当前页码 |
| pages | int | 总页数 |
| count | int | 总记录数 |
| list | array | 原始数据列表 |
**list行字段说明**:
| 字段名 | 类型 | 说明 |
| -------------------------- | ------ | ---------------------- |
| id | int | 记录ID |
| inviteTime | string | 邀约时间 |
| channelId | int | 渠道ID |
| channelName | string | 渠道名称 |
| inviteType | int | 邀约类型 |
| userId | string | 用户ID |
| patientId | int | 患者ID |
| nickName | string | 昵称 |
| gender | int | 性别 |
| ageRange | int | 年龄段 |
| provinceName | string | 省份名称 |
| cityName | string | 市名称 |
| diagnosisStatus | int | 确诊状态 |
| diagnosisTime | string | 确诊时间 |
| surgeryStatus | int | 手术状态 |
| telephone | string | 手机号 |
| firstOpenTime | string | 首次打开时间 |
| phoneRegisterTime | string | 手机号注册时间 |
| infoSubmitTime | string | 信息提交时间 |
| isFirstOpenPhoneRegistered | bool | 首次打开完成手机号注册 |
| isFirstOpenInfoSubmitted | bool | 首次打开完成信息填写 |
| isPhoneRegisteredNotLost | bool | 手机号注册后未流失 |
| scanChannelId | int | 扫码渠道ID |
| scanChannelName | string | 扫码渠道名称 |
| telChannelId | int | 手机号注册渠道ID |
| telChannelName | string | 手机号注册渠道名称 |
| submitChannelId | int | 提交信息渠道ID |
| submitChannelName | string | 提交信息渠道名称 |
---
### 2.10 获取基础数据列表 `GET ?r=patient/base-data`
**说明**: 返回性别、年龄段、确诊状态、手术状态等枚举数据,供后台管理端渲染下拉选项。
**请求参数**: 无
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": {
"genderList": [{ "1": "男" }, { "2": "女" }],
"ageRangeList": [{ "1": "18~28岁" }, { "2": "29~40岁" }, { "3": "41~50岁" }, { "4": "50岁以上" }],
"diagnosisStatusList": [
{ "1": "疑似PSVT正在了解中" },
{ "2": "已确诊PSVT" },
{ "3": "心悸/心跳过快 尚未就诊" },
{ "4": "亲友相关 想了解更多" }
],
"surgeryStatusList": [{ "1": "已接收射频消融/相关手术" }, { "2": "尚未接收手术" }, { "3": "正在评估" }]
}
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
| ------------------- | ----- | ------------------------------------- |
| genderList | array | 性别列表,每项为 {value: "label"} |
| ageRangeList | array | 年龄段列表,每项为 {value: "label"} |
| diagnosisStatusList | array | 确诊状态列表,每项为 {value: "label"} |
| surgeryStatusList | array | 手术状态列表,每项为 {value: "label"} |
---

2
dist.ps1

@ -7,4 +7,4 @@ svn add . --no-ignore --force
# Commit the changes with a message # Commit the changes with a message
svn ci -m "版本更新" svn ci -m "版本更新"
ssh hb127 "cd /data1/wwwroot/default/wutian_xueyoubing_b/shop/frontend/web/ && svn up && exit" ssh hb127 "cd /data1/wwwroot/default/psvt/shop/frontend/web/ && svn up && exit"

2
project.config.json

@ -66,7 +66,7 @@
], ],
"include": [] "include": []
}, },
"appid": "wx903aa29742dad363", "appid": "wxd9e68fd8ad8cc3f6",
"scripts": { "scripts": {
"beforeCompile": "pnpm run beforeCompile", "beforeCompile": "pnpm run beforeCompile",
"beforePreview": "pnpm run beforeCompile", "beforePreview": "pnpm run beforeCompile",

11
project.private.config.json

@ -23,13 +23,20 @@
"miniprogram": { "miniprogram": {
"list": [ "list": [
{ {
"name": "隐私协议", "name": "修改手机号",
"pathName": "doc/pages/doc/index", "pathName": "pages/changeTel/index",
"query": "", "query": "",
"scene": null, "scene": null,
"launchMode": "default" "launchMode": "default"
}, },
{ {
"name": "隐私协议",
"pathName": "doc/pages/doc/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "个人信息", "name": "个人信息",
"pathName": "pages/userInfo/index", "pathName": "pages/userInfo/index",
"query": "", "query": "",

1
src/app.json

@ -21,6 +21,7 @@
"pages/videoList/index", "pages/videoList/index",
"pages/videoDetail/index", "pages/videoDetail/index",
"pages/articleDetail/index", "pages/articleDetail/index",
"pages/articleVideoDetail/index",
"pages/myWall/index", "pages/myWall/index",
"pages/myComment/index", "pages/myComment/index",
"pages/myCollection/index", "pages/myCollection/index",

47
src/app.ts

@ -25,15 +25,15 @@ App<IAppOption>({
globalData: { globalData: {
// dev // dev
// appid:wx903aa29742dad363 // appid:wx903aa29742dad363
url: 'https://m.psvt.hbraas.com', // url: 'https://m.psvt.hbraas.com',
upFileUrl: 'https://m.psvt.hbraas.com/', // upFileUrl: 'https://m.psvt.hbraas.com/',
imageUrl: 'https://m.psvt.hbraas.com/psvt/', // imageUrl: 'https://m.psvt.hbraas.com/psvt/',
// pro // pro
// appid:wxfc95e2961d481f58 // appid:wxd9e68fd8ad8cc3f6
// url: 'https://m.hbgene.hbsaas.com', url: 'https://m.psvt.hbsaas.com',
// upFileUrl: 'https://m.hbgene.hbsaas.com/', upFileUrl: 'https://m.psvt.hbsaas.com/',
// imageUrl: 'https://m.hbgene.hbsaas.com/GeneB/', imageUrl: 'https://m.psvt.hbsaas.com/psvt/',
Timestamp: new Date().getTime(), Timestamp: new Date().getTime(),
@ -111,21 +111,42 @@ App<IAppOption>({
} }
if (!isLogin) { if (!isLogin) {
wx.reLaunch({ this.redirectToLogin('/pages/login/index')
url: '/pages/login/index',
})
return false return false
} }
if (!isReg) { if (!isReg) {
wx.reLaunch({ this.redirectToLogin('/pages/confirmInfo/index')
url: '/pages/confirmInfo/index',
})
return false return false
} }
return true return true
}, },
redirectToLogin(path) {
// 获取当前页面路径
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentUrl = currentPage ? currentPage.route : ''
// 记录来源页面,登录后返回
if (currentUrl && currentUrl !== 'pages/login/index') {
const options = currentPage?.options || {}
const query = Object.keys(options)
.map((key) => `${key}=${options[key]}`)
.join('&')
this.globalData.loginRedirectUrl = query ? `${currentUrl}?${query}` : currentUrl
}
if (currentUrl.includes('login/index')) return
wx.reLaunch({
url: path,
})
// wx.showModal({
// title: '提示',
// content: '授权登录后,可体验更好的服务',
// success: () => {
// },
// })
},
mpBehavior(data: { PageName: string }) { mpBehavior(data: { PageName: string }) {
const url = '?r=wtx/mp-behavior/add' const url = '?r=wtx/mp-behavior/add'
wx.ajax({ wx.ajax({

33
src/custom-tab-bar/index.ts

@ -16,22 +16,22 @@ Component({
icon: 'tabbar1-1', icon: 'tabbar1-1',
iconActive: 'tabbar1-1-active', iconActive: 'tabbar1-1-active',
}, },
// {
// pagePath: '/pages/wall/index',
// text: '交流墙',
// icon: 'tabbar1-2',
// iconActive: 'tabbar1-2-active',
// },
{ {
pagePath: '/pages/wall/index', pagePath: '',
text: '交流墙',
icon: 'tabbar1-2',
iconActive: 'tabbar1-2-active',
},
{
pagePath: '/pages/knowledge/index',
custom: true, custom: true,
}, },
{ // {
pagePath: '/pages/health/index', // pagePath: '/pages/health/index',
text: '健康管理', // text: '健康管理',
icon: 'tabbar1-2', // icon: 'tabbar1-2',
iconActive: 'tabbar1-2-active', // iconActive: 'tabbar1-2-active',
}, // },
{ {
pagePath: '/pages/my/index', pagePath: '/pages/my/index',
text: '我的', text: '我的',
@ -52,11 +52,16 @@ Component({
}, },
}, },
methods: { methods: {
onChange() {},
handleNav(e) { handleNav(e) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const { list } = this.data const { list } = this.data
const pagePath = list[index].pagePath const pagePath = list[index].pagePath
if (!pagePath) {
wx.showToast({
title: '功能开发中,敬请期待',
icon: 'none',
})
}
wx.switchTab({ wx.switchTab({
url: pagePath, url: pagePath,
}) })

11
src/custom-tab-bar/index.wxml

@ -1,12 +1,7 @@
<van-tabbar <van-tabbar active="{{ active }}" active-color="rgba(47, 130, 169, 1)" inactive-color="rgba(148, 163, 184, 1)">
active="{{ active }}"
active-color="rgba(47, 130, 169, 1)"
bind:change="onChange"
inactive-color="rgba(148, 163, 184, 1)"
>
<block wx:for="{{list}}" wx:key="*this"> <block wx:for="{{list}}" wx:key="*this">
<view class="custom" wx:if="{{item.custom}}"> <view class="custom" wx:if="{{item.custom}}" bind:tap="handleNav" data-index="{{index}}">
<image class="icon" src="/images/tabbar-center.png"></image> <image class="icon" src="{{imageUrl}}/tabbar-center.png"></image>
</view> </view>
<van-tabbar-item <van-tabbar-item
class="tab-item" class="tab-item"

BIN
src/images/icon28-active.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/images/icon9-active.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/images/logo1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src/images/logo2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
src/images/none.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

47
src/pages/articleDetail/index.scss

@ -1,5 +1,8 @@
page {
background: #ffffff;
}
.page { .page {
padding: 32rpx 30rpx; padding: 32rpx 30rpx 200rpx;
.tag-wrap { .tag-wrap {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -167,42 +170,66 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
padding: 30rpx 30rpx calc(env(safe-area-inset-bottom) + 30rpx) 20rpx; padding: 0 30rpx calc(env(safe-area-inset-bottom) + 30rpx) 20rpx;
box-sizing: border-box; box-sizing: border-box;
background-color: #fff; background-color: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 24rpx; gap: 24rpx;
box-shadow: 0 -2rpx 64rpx 0 rgba(0, 99, 132, 0.12);
.op-wrap { .op-wrap {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
.op { .op {
padding: 0 24rpx; padding: 30rpx 24rpx 0;
min-width: 76rpx; min-width: 76rpx;
display: flex;
align-items: center;
gap: 12rpx;
.icon { .icon {
width: 48rpx; width: 48rpx;
height: 48rpx; height: 48rpx;
} }
text-align: center; text-align: center;
.num { .num {
margin-top: 8rpx;
font-size: 28rpx; font-size: 28rpx;
line-height: 32rpx; line-height: 32rpx;
color: rgba(148, 163, 184, 1); color: rgba(148, 163, 184, 1);
&.active1 {
color: rgba(234, 88, 26, 1);
}
&.active2 {
color: rgba(242, 150, 0, 1);
}
}
.share-btn {
padding: 0;
margin: 0;
line-height: normal;
background: transparent;
border: none;
height: 48rpx;
&::after,
&::before {
display: none;
padding: 0;
margin: 0;
}
.icon {
width: 48rpx;
height: 48rpx;
}
} }
} }
} }
.next { .next {
padding-top: 30rpx;
flex: 1; flex: 1;
padding: 32rpx 0; text-align: right;
display: flex;
justify-content: center;
align-items: center;
border-radius: 24rpx; border-radius: 24rpx;
background: var(--, linear-gradient(270deg, #439fc7 0%, #2f82a9 100%));
box-sizing: border-box; box-sizing: border-box;
font-size: 32rpx; font-size: 32rpx;
color: rgba(255, 255, 255, 1); color: rgba(47, 130, 169, 1);
} }
} }
} }

113
src/pages/articleDetail/index.ts

@ -1,10 +1,117 @@
const _app = getApp<IAppOption>() const app = getApp<IAppOption>()
Page({ Page({
data: { data: {
content: '1111', id: '',
detail: {} as any,
content: '',
next: null as any,
},
onLoad(options: any) {
if (options.id) {
this.setData({ id: options.id })
app.waitLogin({ type: 'any' }).then(() => {
this.getDetail(options.id)
this.addViewRecord(options.id)
})
}
},
getDetail(id: string) {
wx.ajax({
method: 'GET',
url: '?r=psvt/pic-text/detail',
data: { Id: id },
}).then((res: any) => {
this.setData({
detail: res || {},
content: res.Content || '',
next: res.Next || null,
})
})
},
addViewRecord(id: string) {
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/view',
data: { ContentId: Number(id) },
})
},
handleCompliment() {
if (!app.checkLoginType(1)) return
const { detail, id } = this.data
if (detail.IsCompliment) return
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/compliment',
data: { ContentId: Number(id) },
}).then(() => {
this.setData({
'detail.IsCompliment': 1,
'detail.ComplimentNum': (detail.ComplimentNum || 0) + 1,
})
})
},
handleCollect() {
if (!app.checkLoginType(1)) return
const { detail, id } = this.data
if (detail.IsCollect) {
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/cancel-collection',
data: { ContentId: Number(id) },
}).then(() => {
this.setData({
'detail.IsCollect': 0,
'detail.CollectionPeopleNum': Math.max(0, (detail.CollectionPeopleNum * 1 || 0) - 1),
})
})
} else {
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/collection',
data: { ContentId: Number(id) },
}).then(() => {
this.setData({
'detail.IsCollect': 1,
'detail.CollectionPeopleNum': (detail.CollectionPeopleNum * 1 || 0) + 1,
})
})
}
},
handleShare() {
const { detail, id } = this.data
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/share',
data: { ContentId: Number(id) },
}).then(() => {
this.setData({
'detail.IsShare': 1,
'detail.ShareNum': (detail.ShareNum * 1 || 0) + 1,
})
})
},
handleNext() {
const { next } = this.data
if (!next) return
wx.redirectTo({
url: `/pages/articleDetail/index?id=${next.Id}`,
})
},
handleRecommendTap(e: any) {
const { id } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/articleDetail/index?id=${id}`,
})
},
onShareAppMessage() {
const { detail } = this.data
this.handleShare()
return {
title: detail.Title || '图文详情',
path: `/pages/articleDetail/index?id=${this.data.id}`,
}
}, },
onLoad() {},
}) })
export {} export {}

55
src/pages/articleDetail/index.wxml

@ -1,45 +1,45 @@
<view class="page"> <view class="page">
<view class="tag-wrap"> <view class="tag-wrap">
<view class="tag" wx:for="{{2}}" wx:key="index">疾病知识</view> <view class="tag" wx:for="{{detail.CateNames}}" wx:key="index">{{item}}</view>
</view> </view>
<view class="title">什么是 PSVT?3 分钟了解阵发性室上性心动过速</view> <view class="title">{{detail.Title}}</view>
<view class="info"> <view class="info">
<view class="date">2026-06-01 18:10</view> <view class="date">{{detail.PublishAt}}</view>
<view class="line">|</view> <view class="line">|</view>
<view class="pv">1209人阅读</view> <view class="pv">{{detail.OpenNum}}人阅读</view>
</view> </view>
<view class="banner"> <view class="banner" wx:if="{{detail.TitlePicLink}}">
<image class="b-img" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="b-img" mode="aspectFill" src="{{detail.TitlePicLink}}"></image>
</view> </view>
<view class="mp-wrap"> <view class="mp-wrap">
<mp-html content="{{content}}"></mp-html> <mp-html content="{{content}}"></mp-html>
</view> </view>
<view class="recommended"> <view class="recommended" wx:if="{{detail.Recommends && detail.Recommends.length}}">
<view class="r-header"> <view class="r-header">
<view class="title">相关推荐</view> <view class="title">相关推荐</view>
</view> </view>
<view class="r-list"> <view class="r-list">
<view class="list-card"> <view class="list-card" wx:for="{{detail.Recommends}}" wx:key="Id" bind:tap="handleRecommendTap" data-id="{{item.Id}}">
<view class="photo-wrap"> <view class="photo-wrap">
<image class="photo" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="photo" mode="aspectFill" src="{{item.TitlePicLink}}"></image>
<!-- <view class="type1">图文</view> --> <view class="type1" wx:if="{{item.Type==='1'}}">图文</view>
<view class="type2"> <view class="type2" wx:if="{{item.Type==='2'}}">
<image class="icon" src="{{imageUrl}}icon10.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon10.png?t={{Timestamp}}"></image>
视频 视频
</view> </view>
</view> </view>
<view class="wrap"> <view class="wrap">
<view class="w-title">什么是 PSVT?3 分钟带您了 解阵发性室上性心动过速</view> <view class="w-title">{{item.Title}}</view>
<view class="status"> <view class="status">
<view class="tag">认识PSVT</view> <view class="tag">{{item.CateName}}</view>
<view class="stat"> <view class="stat">
<view class="s-item"> <view class="s-item">
<image class="icon" src="{{imageUrl}}icon8.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon8.png?t={{Timestamp}}"></image>
2.1万 {{item.OpenNum}}
</view> </view>
<view class="s-item"> <view class="s-item">
<image class="icon" src="{{imageUrl}}icon9.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon9.png?t={{Timestamp}}"></image>
2.1万 {{item.ComplimentNum}}
</view> </view>
</view> </view>
</view> </view>
@ -49,19 +49,26 @@
</view> </view>
<view class="page-footer"> <view class="page-footer">
<view class="op-wrap"> <view class="op-wrap">
<view class="op"> <!-- 点赞 -->
<image class="icon" src="{{imageUrl}}icon9.png?t={{Timestamp}}"></image> <view class="op" bind:tap="handleCompliment">
<view class="num">126</view> <image class="icon" src="{{detail.IsCompliment ? imageUrl + 'icon9-active.png?t=' + Timestamp : imageUrl + 'icon9.png?t=' + Timestamp}}"></image>
<view class="num {{detail.IsCompliment ? 'active1' : ''}}">{{detail.ComplimentNum || 0}}</view>
</view> </view>
<view class="op"> <!-- 收藏 -->
<image class="icon" src="{{imageUrl}}icon28.png?t={{Timestamp}}"></image> <view class="op" bind:tap="handleCollect">
<view class="num">126</view> <image class="icon" src="{{detail.IsCollect ? imageUrl + 'icon28-active.png?t=' + Timestamp : imageUrl + 'icon28.png?t=' + Timestamp}}"></image>
<view class="num {{detail.IsCollect ? 'active2' : ''}}">{{detail.CollectionPeopleNum || 0}}</view>
</view> </view>
<!-- 分享 -->
<view class="op"> <view class="op">
<image class="icon" src="{{imageUrl}}icon29.png?t={{Timestamp}}"></image> <button class="share-btn" open-type="share">
<view class="num">126</view> <image class="icon" src="{{imageUrl}}icon29.png?t={{Timestamp}}"></image>
</button>
<view class="num">{{detail.ShareNum || 0}}</view>
</view> </view>
</view> </view>
<view class="next">下一篇</view> <view class="next" wx:if="{{next.Id}}" bind:tap="handleNext">
下一篇
</view>
</view> </view>
</view> </view>

7
src/pages/articleVideoDetail/index.json

@ -0,0 +1,7 @@
{
"navigationStyle": "custom",
"usingComponents": {
"navbar": "/components/navbar/index",
"van-slider": "@vant/weapp/slider/index"
}
}

156
src/pages/articleVideoDetail/index.scss

@ -0,0 +1,156 @@
.page-title {
font-size: 36rpx;
color: rgba(51, 51, 51, 1);
padding-right: 100rpx;
}
.page {
background-color: #000;
.swiper {
width: 100vw;
height: 100vh;
.swiper-item {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
.video {
width: 100%;
flex: 1;
}
.play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90rpx;
height: 104rpx;
}
.swiper-item-footer {
position: relative;
.info-card {
position: absolute;
top: 0;
left: 0;
transform: translateY(-100%);
background-color: red;
width: 100%;
padding: 24rpx 40rpx 0;
box-sizing: border-box;
background: linear-gradient(0deg, #1e293b 0%, rgba(30, 41, 59, 0.11) 74.25%, rgba(30, 41, 59, 0) 100%);
.rate-select {
position: absolute;
top: -24rpx;
left: 50%;
transform: translate(-50%, -100%);
padding: 0 32rpx;
border-radius: 50rpx;
background: rgba(30, 41, 59, 0.6);
box-sizing: border-box;
display: flex;
.r-item {
padding: 20rpx 32rpx;
font-size: 32rpx;
white-space: nowrap;
color: rgba(255, 255, 255, 1);
&.active {
color: #ff864a;
}
}
}
.tag-wrap {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
.tag {
height: 48rpx;
padding: 4rpx 24rpx;
line-height: 40rpx;
border-radius: 24rpx;
background: #fff;
box-sizing: border-box;
font-size: 24rpx;
color: rgba(30, 41, 59, 1);
}
}
.title {
margin-top: 24rpx;
font-size: 36rpx;
color: rgba(255, 255, 255, 1);
font-weight: bold;
}
.progress {
margin-top: 32rpx;
.slider-btn {
width: 200vw;
height: 80rpx;
}
}
}
.container {
padding: 32rpx 40rpx calc(40rpx + env(safe-area-inset-bottom)) 22rpx;
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
background-color: rgba(30, 41, 59, 1);
.op-wrap {
display: flex;
align-items: center;
.op {
min-width: 88rpx;
padding: 0 18rpx;
text-align: center;
.icon {
width: 48rpx;
height: 48rpx;
}
.num {
margin-top: 8rpx;
font-size: 28rpx;
color: rgba(255, 255, 255, 1);
line-height: 32rpx;
&.active1 {
color: rgba(234, 88, 26, 1);
}
&.active2 {
color: rgba(242, 150, 0, 1);
}
}
.share-btn {
padding: 0;
margin: 0;
line-height: normal;
background: transparent;
border: none;
&::after {
display: none;
}
.icon {
width: 48rpx;
height: 48rpx;
}
}
}
}
.speed {
flex-shrink: 0;
font-size: 28rpx;
color: rgba(255, 255, 255, 1);
padding: 8rpx 16rpx;
display: flex;
align-items: center;
height: 48rpx;
line-height: 32rpx;
border-radius: 8rpx;
border: 1px solid #fff;
box-sizing: border-box;
}
}
}
}
}
}

262
src/pages/articleVideoDetail/index.ts

@ -0,0 +1,262 @@
const app = getApp<IAppOption>()
const PAGE_SIZE = 10
function parseContent(content: any) {
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return null
}
}
return content
}
Page({
data: {
id: '',
current: 0,
videoList: [] as Array<any>,
page: 1,
pages: 1,
showRateSelect: false,
},
videoContexts: {} as any,
onLoad(options: any) {
if (options.id) {
this.setData({ id: options.id })
}
app.waitLogin({ type: 'any' }).then(() => {
this.initData()
})
},
mapItem(item: any) {
const content = parseContent(item.Content)
return {
...item,
ImageUrl: item.TitlePicLink,
SrcUrl: content?.videoUrl || '',
preload: false,
progress: 0,
play: false,
}
},
initData() {
const { id } = this.data
Promise.all([
wx.ajax({ method: 'GET', url: '?r=psvt/pic-text/detail', data: { Id: id } }),
wx.ajax({
method: 'GET',
url: '?r=psvt/pic-text/list-by-content-id',
data: { page: 1, count: PAGE_SIZE, ContentId: id },
}),
]).then(([detail, listRes]: any) => {
const detailItem = this.mapItem(detail)
const list = (listRes.list || []).map((item: any) => this.mapItem(item))
this.setData({
videoList: [detailItem, ...list],
page: listRes.page || 1,
pages: listRes.pages || 1,
})
this.preloadVideos(this.data.current)
this.playCurrentVideo()
if (this.data.videoList.length > 0) {
this.addViewRecord(this.data.videoList[0].Id)
}
})
},
getList(page: number) {
const { id } = this.data
wx.ajax({
method: 'GET',
url: '?r=psvt/pic-text/list-by-content-id',
data: { page, count: PAGE_SIZE, ContentId: id },
}).then((res: any) => {
const list = (res.list || []).map((item: any) => this.mapItem(item))
this.setData({
videoList: this.data.videoList.concat(list),
page: res.page || 1,
pages: res.pages || 1,
})
this.preloadVideos(this.data.current)
})
},
preloadVideos(currentIndex: number) {
const { videoList } = this.data
const newVideoList = [...videoList]
newVideoList.forEach((item: any, index: number) => {
if (Math.abs(index - currentIndex) <= 1) {
item.preload = true
} else {
item.preload = false
}
})
this.setData({ videoList: newVideoList })
},
handleChange(e: any) {
const { current } = e.detail
const { videoList, pages, page } = this.data
this.setData({ current, showRateSelect: false })
this.addViewRecord(videoList[current].Id)
this.preloadVideos(current)
this.playCurrentVideo()
if (current >= videoList.length - 2 && page < pages) {
this.getList(page + 1)
}
},
playCurrentVideo() {
const { current, videoList } = this.data
Object.values(this.videoContexts).forEach((ctx: any) => {
ctx.pause()
})
const currentVideo = videoList[current]
if (currentVideo) {
const videoId = `video-${currentVideo.Id}`
if (!this.videoContexts[videoId]) {
this.videoContexts[videoId] = wx.createVideoContext(videoId)
}
this.videoContexts[videoId].play()
}
},
addViewRecord(contentId: string) {
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/view',
data: { ContentId: Number(contentId) },
})
},
handleTogglePlay(e: any) {
const { index } = e.currentTarget.dataset
const { videoList } = this.data
const video = videoList[index]
const videoId = `video-${video.Id}`
if (!this.videoContexts[videoId]) {
this.videoContexts[videoId] = wx.createVideoContext(videoId)
}
if (video.play) {
this.videoContexts[videoId].pause()
} else {
this.videoContexts[videoId].play()
}
},
handlePlay(e: any) {
const { index } = e.currentTarget.dataset
this.setData({ [`videoList[${index}].play`]: true })
},
handlePause(e: any) {
const { index } = e.currentTarget.dataset
this.setData({ [`videoList[${index}].play`]: false })
},
handleTimeUpdate(e: any) {
const { index } = e.currentTarget.dataset
const { videoList } = this.data
if (videoList[index].drag) return
const { currentTime, duration } = e.detail
const progress = (currentTime / duration) * 100
this.setData({
[`videoList[${index}].progress`]: progress,
[`videoList[${index}].duration`]: duration,
})
},
handleDragStart(e: any) {
const { index } = e.currentTarget.dataset
this.setData({ [`videoList[${index}].drag`]: true })
},
handleDragEnd(e: any) {
const { index } = e.currentTarget.dataset
const { videoList } = this.data
const video = videoList[index]
const duration = video.duration
const dragProgress = e.detail
const currentTime = (dragProgress / 100) * duration
const videoId = `video-${video.Id}`
if (!this.videoContexts[videoId]) {
this.videoContexts[videoId] = wx.createVideoContext(videoId)
}
this.videoContexts[videoId].seek(currentTime)
this.setData({ [`videoList[${index}].drag`]: false })
},
handleCompliment() {
if (!app.checkLoginType(1)) return
const { current, videoList } = this.data
const video = videoList[current]
if (video.IsCompliment) return
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/compliment',
data: { ContentId: Number(video.Id) },
}).then(() => {
this.setData({
[`videoList[${current}].IsCompliment`]: 1,
[`videoList[${current}].ComplimentNum`]: (video.ComplimentNum * 1 || 0) + 1,
})
})
},
handleCollect() {
if (!app.checkLoginType(1)) return
const { current, videoList } = this.data
const video = videoList[current]
const isCollect = video.IsCollect
wx.ajax({
method: 'POST',
url: isCollect ? '?r=psvt/pic-text/cancel-collection' : '?r=psvt/pic-text/collection',
data: { ContentId: Number(video.Id) },
}).then(() => {
this.setData({
[`videoList[${current}].IsCollect`]: isCollect ? 0 : 1,
[`videoList[${current}].CollectionPeopleNum`]: (video.CollectionPeopleNum * 1 || 0) + (isCollect ? -1 : 1),
})
})
},
handleShare() {
const { current, videoList } = this.data
const video = videoList[current]
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/share',
data: { ContentId: Number(video.Id) },
}).then(() => {
this.setData({
[`videoList[${current}].IsShare`]: 1,
[`videoList[${current}].ShareNum`]: (video.ShareNum * 1 || 0) + 1,
})
})
},
handleSpeed(e: any) {
const { speed } = e.currentTarget.dataset
const { current, videoList } = this.data
const video = videoList[current]
const videoId = `video-${video.Id}`
if (!this.videoContexts[videoId]) {
this.videoContexts[videoId] = wx.createVideoContext(videoId)
}
this.videoContexts[videoId].playbackRate(Number(speed))
this.setData({
[`videoList[${current}].playbackRate`]: speed,
showRateSelect: false,
})
},
handleToggleRateSelect() {
this.setData({ showRateSelect: !this.data.showRateSelect })
},
onShareAppMessage() {
const { current, videoList } = this.data
const video = videoList[current]
this.handleShare()
return {
title: video.Title || '视频详情',
path: `/pages/articleVideoDetail/index?id=${video.Id}`,
imageUrl: video.ImageUrl || video.TitlePicLink,
}
},
handleBack() {
wx.navigateBack()
},
})
export {}

82
src/pages/articleVideoDetail/index.wxml

@ -0,0 +1,82 @@
<navbar fixed custom-style="background:{{background}}" back>
<view class="page-title" slot="left" bind:tap="handleBack">
<van-icon name="arrow-left" />
</view>
</navbar>
<view class="page">
<swiper class="swiper" vertical="{{true}}" current="{{current}}" bindchange="handleChange">
<swiper-item class="swiper-item" wx:for="{{videoList}}" wx:key="Id">
<video
id="video-{{item.Id}}"
class="video"
src="{{item.preload ? item.SrcUrl : ''}}"
poster="{{item.ImageUrl}}"
loop
title="{{item.Title}}"
object-fit="contain"
enable-play-gesture
enable-progress-gesture
show-center-play-btn="{{true}}"
controls="{{false}}"
show-fullscreen-btn="{{false}}"
play-btn-position="center"
bindplay="handlePlay"
bindpause="handlePause"
bindtimeupdate="handleTimeUpdate"
data-index="{{index}}"
></video>
<view class="swiper-item-footer">
<view class="info-card">
<view class="rate-select" wx:if="{{showRateSelect && current===index}}">
<view class="r-item {{item.playbackRate==='0.5' && 'active'}}" bind:tap="handleSpeed" data-speed="0.5">0.5倍</view>
<view class="r-item {{(!item.playbackRate || item.playbackRate==='1') && 'active'}}" bind:tap="handleSpeed" data-speed="1">正常</view>
<view class="r-item {{item.playbackRate==='1.5' && 'active'}}" bind:tap="handleSpeed" data-speed="1.5">1.5倍</view>
<view class="r-item {{item.playbackRate==='2' && 'active'}}" bind:tap="handleSpeed" data-speed="2">2.0倍速</view>
</view>
<view class="tag-wrap">
<view class="tag" wx:for="{{item.CateNames}}" wx:key="index" wx:for-item="cate">{{cate}}</view>
</view>
<view class="title">{{item.Title}}</view>
<view class="progress">
<van-slider
value="{{item.progress}}"
active-color="#439FC7"
bar-height="4"
inactive-color="rgba(255, 255, 255, 1)"
bind:drag-start="handleDragStart"
bind:change="handleDragEnd"
use-button-slot
data-index="{{index}}"
>
<view class="slider-btn" slot="button"></view>
</van-slider>
</view>
</view>
<view class="container">
<view class="op-wrap">
<view class="op" bind:tap="handleCompliment">
<image class="icon" src="{{item.IsCompliment ? imageUrl + 'icon9-active.png?t=' + Timestamp : imageUrl + 'icon9-1.png?t=' + Timestamp}}"></image>
<view class="num {{item.IsCompliment ? 'active1' : ''}}">{{item.ComplimentNum || 0}}</view>
</view>
<view class="op">
<image class="icon" src="{{imageUrl}}icon8-1.png?t={{Timestamp}}"></image>
<view class="num">{{item.OpenNum || 0}}</view>
</view>
<view class="op" bind:tap="handleCollect">
<image class="icon" src="{{item.IsCollect ? imageUrl + 'icon28-active.png?t=' + Timestamp : imageUrl + 'icon28-1.png?t=' + Timestamp}}"></image>
<view class="num {{item.IsCollect ? 'active2' : ''}}">{{item.CollectionPeopleNum || 0}}</view>
</view>
<view class="op">
<button class="share-btn" open-type="share">
<image class="icon" src="{{imageUrl}}icon29-1.png?t={{Timestamp}}"></image>
</button>
<view class="num">{{item.ShareNum || 0}}</view>
</view>
</view>
<view class="speed" bind:tap="handleToggleRateSelect">倍速</view>
</view>
</view>
</swiper-item>
</swiper>
</view>

5
src/pages/changeTel/index.json

@ -1,4 +1,7 @@
{ {
"navigationStyle": "custom",
"navigationBarTitleText": "修改手机号", "navigationBarTitleText": "修改手机号",
"usingComponents": {} "usingComponents": {
"navbar": "/components/navbar/index"
}
} }

23
src/pages/changeTel/index.scss

@ -1,5 +1,10 @@
page { page {
background-color: rgba(247, 247, 250, 1); background-color: rgba(247, 250, 252, 1);
}
.page-title {
font-size: 36rpx;
color: rgba(51, 51, 51, 1);
} }
.page { .page {
@ -7,13 +12,13 @@ page {
.form { .form {
padding: 0 32rpx; padding: 0 32rpx;
background-color: #fff; background-color: #fff;
border-radius: 24rpx; border-radius: 32rpx;
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
.label { .label {
font-size: 32rpx; font-size: 32rpx;
color: rgba(1, 1, 5, 1); color: rgba(30, 41, 59, 1);
width: 5em; width: 5em;
} }
.container { .container {
@ -25,27 +30,27 @@ page {
.input { .input {
padding: 32rpx 0; padding: 32rpx 0;
font-size: 32rpx; font-size: 32rpx;
color: rgba(1, 1, 5, 1); color: rgba(30, 41, 59, 1);
} }
.place-input { .place-input {
color: rgba(161, 164, 172, 0.4); color: rgba(148, 163, 184, 1);
} }
.btn { .btn {
flex-shrink: 0; flex-shrink: 0;
font-size: 32rpx; font-size: 32rpx;
color: rgba(246, 74, 58, 1); color: rgba(234, 88, 26, 1);
} }
} }
} }
} }
.login-out { .login-out {
margin-top: 48rpx; margin-top: 48rpx;
padding: 20rpx; padding: 24rpx;
font-size: 32rpx; font-size: 32rpx;
line-height: 48rpx; line-height: 48rpx;
color: #fff; color: #fff;
text-align: center; text-align: center;
background: linear-gradient(356deg, #f23a2f 0%, #fc684f 100%); background: linear-gradient(270deg, #439fc7 0%, #2f82a9 100%);
border-radius: 96rpx; border-radius: 24rpx;
} }
} }

4
src/pages/changeTel/index.ts

@ -35,7 +35,7 @@ Page({
} }
wx.ajax({ wx.ajax({
method: 'POST', method: 'POST',
url: '?r=wtx/login/send-verify-code', url: '?r=psvt/login/send-verify-code',
data: { data: {
mobile, mobile,
}, },
@ -71,7 +71,7 @@ Page({
const { registrationSource, registChannel, regBusinessId } = app.globalData const { registrationSource, registChannel, regBusinessId } = app.globalData
wx.ajax({ wx.ajax({
method: 'POST', method: 'POST',
url: '?r=wtx/account/update-telephone', url: '?r=psvt/account/update-telephone',
data: { data: {
mobile, mobile,
code, code,

12
src/pages/changeTel/index.wxml

@ -1,4 +1,14 @@
<view class="page"> <navbar fixed custom-style="background:{{background}}" back>
<view class="page-title" slot="left" bind:tap="handleBack">
<van-icon name="arrow-left" />
修改手机号
</view>
</navbar>
<view
class="page"
style="background: url('{{imageUrl}}bg6.png?t={{Timestamp}}') no-repeat top center/100% 478rpx;padding-top: {{pageTop+20}}px;"
>
<view class="form"> <view class="form">
<view class="row"> <view class="row">
<view class="label">新手机号</view> <view class="label">新手机号</view>

4
src/pages/confirmInfo/index.ts

@ -151,8 +151,10 @@ Page({
}).then(() => { }).then(() => {
app.globalData.initLoginInfo.isReg = 1 app.globalData.initLoginInfo.isReg = 1
app.globalData.initLoginInfo.isLogin = 1 app.globalData.initLoginInfo.isLogin = 1
const redirectUrl = app.globalData.loginRedirectUrl
app.globalData.loginRedirectUrl = ''
wx.reLaunch({ wx.reLaunch({
url: '/pages/index/index', url: redirectUrl ? `/${redirectUrl}` : '/pages/index/index',
}) })
}) })
}, },

3
src/pages/index/index.json

@ -2,6 +2,7 @@
"navigationStyle": "custom", "navigationStyle": "custom",
"usingComponents": { "usingComponents": {
"popup": "/components/popup/index", "popup": "/components/popup/index",
"navbar": "/components/navbar/index" "navbar": "/components/navbar/index",
"pagination": "/components/pagination/index"
} }
} }

8
src/pages/index/index.scss

@ -166,6 +166,7 @@ page {
display: none; display: none;
} }
.type { .type {
flex-shrink: 0;
padding: 12rpx 32rpx; padding: 12rpx 32rpx;
font-size: 28rpx; font-size: 28rpx;
color: rgba(71, 85, 105, 1); color: rgba(71, 85, 105, 1);
@ -182,6 +183,7 @@ page {
.i-list { .i-list {
margin: 32rpx 30rpx 0; margin: 32rpx 30rpx 0;
.list-card { .list-card {
margin-top: 32rpx;
padding: 32rpx; padding: 32rpx;
background-color: #fff; background-color: #fff;
border-radius: 32rpx; border-radius: 32rpx;
@ -231,6 +233,8 @@ page {
} }
} }
.wrap { .wrap {
flex: 1;
min-width: 0;
.title { .title {
font-size: 28rpx; font-size: 28rpx;
color: rgba(30, 41, 59, 1); color: rgba(30, 41, 59, 1);
@ -254,6 +258,7 @@ page {
color: rgba(148, 163, 184, 1); color: rgba(148, 163, 184, 1);
} }
.stat { .stat {
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 32rpx; gap: 32rpx;
@ -267,6 +272,9 @@ page {
width: 24rpx; width: 24rpx;
height: 24rpx; height: 24rpx;
} }
.active1 {
color: rgba(234, 88, 26, 1);
}
} }
} }
} }

184
src/pages/index/index.ts

@ -7,9 +7,152 @@ Page({
popupParams: { popupParams: {
position: 'bottom', position: 'bottom',
} as any, } as any,
userInfo: {} as any,
days: 0,
cateList: [] as Array<{ CateId: string; CateName: string }>,
activeCateId: '',
picTextList: [] as Array<any>,
pagination: {
page: 1,
pages: 1,
count: 0,
},
videoList: [] as Array<any>,
configList: [] as Array<any>,
sectionList: [] as Array<{ code: string; visible: boolean }>,
moduleVisible: {
health: true,
'spread1': true,
'video-big': true,
'spread2': true,
'qiye-wx': true,
'education': true,
} as Record<string, boolean>,
}, },
onLoad() { onLoad() {
app.waitLogin({ type: 0 }).then(() => {}) app.waitLogin({ type: 'any' }).then(() => {
this.getMyInfo()
this.getCateList()
this.getList()
this.getVideoList()
this.getConfig()
})
},
onShow() {
if (app.globalData.loginState) {
this.getMyInfo()
}
},
getConfig() {
wx.ajax({
method: 'GET',
url: `?r=psvt/mini-conf/get-config`,
data: {},
}).then((res: any) => {
const list = res || []
list.forEach((item: any) => {
if (
(item.code === 'spread1' || item.code === 'spread2' || item.code === 'serviceConf') &&
item.subList
) {
item.subList = item.subList.filter((subItem: any) => subItem.showStatus == 1)
}
})
const moduleVisible: Record<string, boolean> = {} as any
list.forEach((item: any) => {
moduleVisible[item.code] = item.showStatus == 1
})
const sectionCodes = ['health', 'spread1', 'video-big', 'spread2']
const sectionList = sectionCodes
.map((code) => {
const conf = list.find((item: any) => item.code === code)
return { code, visible: conf ? conf.showStatus == 1 : false }
})
.sort((a, b) => {
const aSeq = list.find((item: any) => item.code === a.code)?.sequence || 0
const bSeq = list.find((item: any) => item.code === b.code)?.sequence || 0
return Number(aSeq) - Number(bSeq)
})
this.setData({ configList: list, moduleVisible, sectionList })
})
},
getMyInfo() {
wx.ajax({
method: 'GET',
url: '?r=psvt/patient/my-info',
}).then((res: any) => {
let days = 0
if (res.firstOpenTime) {
const first = new Date(res.firstOpenTime.replace(/-/g, '/'))
const now = new Date()
days = Math.max(1, Math.ceil((now.getTime() - first.getTime()) / (1000 * 60 * 60 * 24)))
}
this.setData({
userInfo: res,
days,
})
})
},
getCateList() {
wx.ajax({
method: 'GET',
url: '?r=psvt/pic-text/cate-list',
}).then((res: any) => {
this.setData({
cateList: res || [],
})
})
},
getList(page = 1, cateId = '', append = false) {
const params: any = {
page,
count: 10,
}
if (cateId) {
params.CateId = cateId
}
wx.ajax({
method: 'GET',
url: '?r=psvt/pic-text/list',
data: params,
}).then((res: any) => {
const list = res.list || []
this.setData({
picTextList: append ? this.data.picTextList.concat(list) : list,
pagination: {
page: res.page || 1,
pages: res.pages || 1,
count: Number(res.count) || 0,
},
})
})
},
getVideoList() {
wx.ajax({
method: 'GET',
url: '?r=psvt/video/list',
data: { page: 1, count: 10 },
}).then((res: any) => {
this.setData({
videoList: res.list || [],
})
})
},
handleCateTap(e: any) {
const { cateid } = e.currentTarget.dataset
this.setData({
activeCateId: cateid || '',
})
this.getList(1, cateid || '')
},
handleLoadMore() {
const { pagination, activeCateId } = this.data
if (pagination.page < pagination.pages) {
this.getList(pagination.page + 1, activeCateId, true)
}
}, },
handlePopupOk() { handlePopupOk() {
const { popupType } = this.data const { popupType } = this.data
@ -74,6 +217,45 @@ Page({
url: '/pages/wechatWork/index', url: '/pages/wechatWork/index',
}) })
}, },
onReachBottom() {
this.handleLoadMore()
},
handleBackTop() {
wx.pageScrollTo({ scrollTop: 0 })
},
handleVideoMore() {
wx.navigateTo({
url: '/pages/videoList/index',
})
},
handleVideoDetail(e) {
const { index } = e.currentTarget.dataset
const { videoList } = this.data
const { Id } = videoList[index]
wx.navigateTo({
url: `/pages/videoDetail/index?id=${Id}&index=${index}`,
})
},
handlePicTextDetail(e) {
const { index } = e.currentTarget.dataset
const { picTextList } = this.data
const { Id, Type } = picTextList[index]
if (Type == 1) {
wx.navigateTo({
url: `/pages/articleDetail/index?id=${Id}`,
})
} else if (Type == 2) {
wx.navigateTo({
url: `/pages/articleVideoDetail/index?id=${Id}`,
})
}
},
handleGift() {
wx.showToast({
title: '功能开发中,敬请期待',
icon: 'none',
})
},
}) })
export {} export {}

103
src/pages/index/index.wxml

@ -2,83 +2,114 @@
<image class="page-title" slot="left" src="{{imageUrl}}logo2.png?t={{Timestamp}}"></image> <image class="page-title" slot="left" src="{{imageUrl}}logo2.png?t={{Timestamp}}"></image>
</navbar> </navbar>
<view class="page" style="background: url('{{imageUrl}}bg3.png?t={{Timestamp}}') no-repeat top center/100%;padding-top: {{pageTop}}px;"> <view
class="page"
style="background: url('{{imageUrl}}bg3.png?t={{Timestamp}}') no-repeat top center/100%;padding-top: {{pageTop}}px;"
>
<view class="user"> <view class="user">
<image class="avatar" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="avatar" mode="aspectFill" src="{{userInfo.avatar || imageUrl + 'bg1.png?t=' + Timestamp}}"></image>
<view class="wrap"> <view class="wrap">
<view class="name">Hi, 伍六七</view> <view class="name">Hi, {{userInfo.nickName || '用户'}}</view>
<view class="content">星动守护您的第 15 天~</view> <view class="content">星动守护您的第 {{days}} 天~</view>
</view> </view>
</view> </view>
<view class="new-gift"> <view class="new-gift" bind:tap="handleGift">
<image class="characters" src="{{imageUrl}}bg5.png?t={{Timestamp}}"></image> <image class="characters" src="{{imageUrl}}bg5.png?t={{Timestamp}}"></image>
<image class="n-img" src="{{imageUrl}}bg4.png?t={{Timestamp}}"></image> <image class="n-img" src="{{imageUrl}}bg4.png?t={{Timestamp}}"></image>
</view> </view>
<view class="kkd"></view> <!-- 动态模块:顺序由接口控制 -->
<view class="video-list"> <block wx:for="{{sectionList}}" wx:key="code">
<view class="v-header"> <!-- code: health 健康动态 -->
<view class="title">视频科普</view> <view class="kkd" wx:if="{{item.code==='health' && item.visible}}"></view>
<view class="more"> <!-- code: spread1 首页banner -->
查看更多 <view class="banner" wx:if="{{item.code==='spread1' && item.visible}}"></view>
<van-icon name="arrow" /> <!-- code: video-big 大咖说 -->
<view class="video-list" wx:if="{{item.code==='video-big' && item.visible}}">
<view class="v-header">
<view class="title">视频科普</view>
<view class="more" bind:tap="handleVideoMore">
查看更多
<van-icon name="arrow" />
</view>
</view> </view>
</view> <view class="scroll">
<view class="scroll"> <view class="card" wx:for="{{videoList}}" wx:key="Id" data-index="{{index}}" bind:tap="handleVideoDetail">
<view class="card" wx:for="{{10}}" wx:key="index"> <image class="poster" mode="aspectFill" src="{{item.ImageUrl}}"></image>
<image class="poster" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <view class="info">
<view class="info"> <view class="title">{{item.Title}}</view>
<view class="title">心脏突然狂飙跳</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> <!-- code: spread2 banner2模块 -->
<view class="info-list"> <view class="banner" wx:if="{{item.code==='spread2' && item.visible}}"></view>
</block>
<!-- code: education 用户教育最底部 -->
<view class="info-list" wx:if="{{moduleVisible.education}}">
<view class="i-header"> <view class="i-header">
<view class="title">知识科普</view> <view class="title">知识科普</view>
<view class="more">
查看更多
<van-icon name="arrow" />
</view>
</view> </view>
<view class="i-types"> <view class="i-types">
<view class="type {{index==0 && 'active'}}" wx:for="{{5}}" wx:key="index">推荐</view> <view class="type {{activeCateId==='' && 'active'}}" bind:tap="handleCateTap" data-cateid="">推荐</view>
<view
class="type {{activeCateId===item.CateId && 'active'}}"
wx:for="{{cateList}}"
wx:key="CateId"
bind:tap="handleCateTap"
data-cateid="{{item.CateId}}"
>
{{item.CateName}}
</view>
</view> </view>
<view class="i-list"> <view class="i-list">
<view class="list-card"> <view
class="list-card"
wx:for="{{picTextList}}"
wx:key="Id"
data-index="{{index}}"
bind:tap="handlePicTextDetail"
>
<view class="photo-wrap"> <view class="photo-wrap">
<image class="photo" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="photo" mode="aspectFill" src="{{item.TitlePicLink}}"></image>
<!-- <view class="type1">图文</view> --> <view class="type1" wx:if="{{item.Type==='1'}}">图文</view>
<view class="type2"> <view class="type2" wx:if="{{item.Type==='2'}}">
<image class="icon" src="{{imageUrl}}icon10.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon10.png?t={{Timestamp}}"></image>
视频 视频
</view> </view>
</view> </view>
<view class="wrap"> <view class="wrap">
<view class="title">什么是 PSVT?3 分钟带您了 解阵发性室上性心动过速</view> <view class="title">{{item.Title}}</view>
<view class="status"> <view class="status">
<view class="tag">认识PSVT</view> <view class="tag">{{item.CateName}}</view>
<view class="stat"> <view class="stat">
<view class="s-item"> <view class="s-item">
<image class="icon" src="{{imageUrl}}icon8.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon8.png?t={{Timestamp}}"></image>
2.1万 {{item.OpenNum}}
</view> </view>
<view class="s-item"> <view class="s-item">
<image class="icon" src="{{imageUrl}}icon9.png?t={{Timestamp}}"></image> <image
2.1万 wx:if="{{item.IsCompliment==1}}"
class="icon"
src="{{imageUrl}}icon9-active.png?t={{Timestamp}}"
></image>
<image wx:else class="icon" src="{{imageUrl}}icon9.png?t={{Timestamp}}"></image>
<view class="num {{item.IsCompliment ? 'active1' : ''}}">{{item.ComplimentNum || 0}}</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<pagination pagination="{{pagination}}" />
</view> </view>
</view> </view>
<view class="back-top"> <view wx:if="{{backTop}}" class="back-top" bind:tap="handleBackTop">
<image class="icon" src="{{imageUrl}}icon11.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon11.png?t={{Timestamp}}"></image>
</view> </view>
<view class="wechat" bind:tap="handleWechatWork"> <!-- code: qiye-wx 企微悬浮按钮 -->
<view class="wechat" wx:if="{{moduleVisible['qiye-wx']}}" bind:tap="handleWechatWork">
<image class="icon" src="{{imageUrl}}icon12.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon12.png?t={{Timestamp}}"></image>
<view class="name">健康助理</view> <view class="name">健康助理</view>
</view> </view>

7
src/pages/login/index.ts

@ -140,13 +140,14 @@ Page({
url: '/pages/confirmInfo/index', url: '/pages/confirmInfo/index',
}) })
} else { } else {
wx.redirectTo({ const redirectUrl = app.globalData.loginRedirectUrl
url: '/pages/index/index', app.globalData.loginRedirectUrl = ''
wx.reLaunch({
url: redirectUrl ? `/${redirectUrl}` : '/pages/index/index',
}) })
} }
}, },
handleVisitors() { handleVisitors() {
app.globalData.anyWhere = true
wx.reLaunch({ wx.reLaunch({
url: '/pages/index/index', url: '/pages/index/index',
}) })

6
src/pages/my/index.ts

@ -31,6 +31,12 @@ Page({
const { url } = e.currentTarget.dataset const { url } = e.currentTarget.dataset
wx.navigateTo({ url }) wx.navigateTo({ url })
}, },
handlePDev() {
wx.showToast({
title: '功能开发中,敬请期待',
icon: 'none',
})
},
}) })
export {} export {}

72
src/pages/my/index.wxml

@ -17,27 +17,27 @@
</view> </view>
</view> </view>
</view> </view>
<view class="kkd"> <!-- <view class="kkd"> -->
<view class="k-item"> <!-- <view class="k-item" bind:tap="handlePDev"> -->
我的帖子 <!-- 我的帖子 -->
<image class="icon" src="{{imageUrl}}icon30.png?t={{Timestamp}}"></image> <!-- <image class="icon" src="{{imageUrl}}icon30.png?t={{Timestamp}}"></image> -->
</view> <!-- </view> -->
<view class="k-item"> <!-- <view class="k-item" bind:tap="handlePDev"> -->
健康档案 <!-- 健康档案 -->
<image class="icon" src="{{imageUrl}}icon31.png?t={{Timestamp}}"></image> <!-- <image class="icon" src="{{imageUrl}}icon31.png?t={{Timestamp}}"></image> -->
</view> <!-- </view> -->
</view> <!-- </view> -->
<view class="list"> <view class="list">
<view class="list-item"> <!-- <view class="list-item" bind:tap="handlePDev"> -->
<image class="icon" src="{{imageUrl}}icon32.png?t={{Timestamp}}"></image> <!-- <image class="icon" src="{{imageUrl}}icon32.png?t={{Timestamp}}"></image> -->
<view class="content">我的评论</view> <!-- <view class="content">我的评论</view> -->
<van-icon class="arrow" name="arrow" /> <!-- <van-icon class="arrow" name="arrow" /> -->
</view> <!-- </view> -->
<view class="list-item"> <!-- <view class="list-item" bind:tap="routerTo" data-url="/pages/myCollection/index"> -->
<image class="icon" src="{{imageUrl}}icon33.png?t={{Timestamp}}"></image> <!-- <image class="icon" src="{{imageUrl}}icon33.png?t={{Timestamp}}"></image> -->
<view class="content">我的收藏</view> <!-- <view class="content">我的收藏</view> -->
<van-icon class="arrow" name="arrow" /> <!-- <van-icon class="arrow" name="arrow" /> -->
</view> <!-- </view> -->
<view class="list-item" bind:tap="routerTo" data-url="/pages/userInfo/index"> <view class="list-item" bind:tap="routerTo" data-url="/pages/userInfo/index">
<image class="icon" src="{{imageUrl}}icon34.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon34.png?t={{Timestamp}}"></image>
<view class="content">个人信息</view> <view class="content">个人信息</view>
@ -49,20 +49,20 @@
<van-icon class="arrow" name="arrow" /> <van-icon class="arrow" name="arrow" />
</view> </view>
</view> </view>
<view class="public-office"> <!-- <view class="public-office"> -->
<view class="wrap"> <!-- <view class="wrap"> -->
<view class="title"> <!-- <view class="title"> -->
<view>关注公众号</view> <!-- <view>关注公众号</view> -->
<view> <!-- <view> -->
及时 <!-- 及时 -->
<view class="high">获取系统消息</view> <!-- <view class="high">获取系统消息</view> -->
</view> <!-- </view> -->
</view> <!-- </view> -->
<view class="content"> <!-- <view class="content"> -->
长按识别二维码 <!-- 长按识别二维码 -->
<image class="icon" src="{{imageUrl}}icon36.png?t={{Timestamp}}"></image> <!-- <image class="icon" src="{{imageUrl}}icon36.png?t={{Timestamp}}"></image> -->
</view> <!-- </view> -->
</view> <!-- </view> -->
<image class="code" src="{{imageUrl}}bg1.png?t={{Timestamp}}" show-menu-by-longpress></image> <!-- <image class="code" src="{{imageUrl}}bg1.png?t={{Timestamp}}" show-menu-by-longpress></image> -->
</view> <!-- </view> -->
</view> </view>

5
src/pages/myCollection/index.json

@ -1,12 +1,13 @@
{ {
"navigationStyle": "custom", "navigationStyle": "custom",
"navigationBarTitleText": "我的评论", "navigationBarTitleText": "我的收藏",
"usingComponents": { "usingComponents": {
"popup": "/components/popup/index", "popup": "/components/popup/index",
"navbar": "/components/navbar/index", "navbar": "/components/navbar/index",
"uploadFile": "/components/uploadFile/index", "uploadFile": "/components/uploadFile/index",
"van-popup": "@vant/weapp/popup/index", "van-popup": "@vant/weapp/popup/index",
"van-tab": "@vant/weapp/tab/index", "van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index" "van-tabs": "@vant/weapp/tabs/index",
"pagination": "/components/pagination/index"
} }
} }

17
src/pages/myCollection/index.scss

@ -123,6 +123,21 @@ page {
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.op-wrap {
margin-top: 12rpx;
padding: 0 12rpx;
.op {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: rgba(242, 150, 0, 1);
.icon {
width: 28rpx;
height: 28rpx;
}
}
}
} }
} }
.body2 { .body2 {
@ -196,7 +211,7 @@ page {
align-items: center; align-items: center;
gap: 8rpx; gap: 8rpx;
font-size: 24rpx; font-size: 24rpx;
color: rgba(234, 88, 26, 1); color: rgba(242, 150, 0, 1);
.icon { .icon {
width: 28rpx; width: 28rpx;
height: 28rpx; height: 28rpx;

184
src/pages/myCollection/index.ts

@ -2,109 +2,139 @@ const app = getApp<IAppOption>()
Page({ Page({
data: { data: {
popupShow: false, tab: 0,
popupType: '', offsetTop: 0,
popupParams: {
position: 'bottom',
} as any,
topicShow: false,
replayShow: true,
fileList: [] as any[], videoList: [] as Array<any>,
videoPagination: {
page: 1,
pages: 1,
count: 0,
},
tab: 1, articleList: [] as Array<any>,
offsetTop: 0, articlePagination: {
page: 1,
pages: 1,
count: 0,
},
}, },
onLoad() { onLoad() {
app.waitLogin({ type: 0 }).then(() => {}) app.waitLogin({ type: 0 }).then(() => {
this.getVideoList()
this.getArticleList()
})
}, },
onReady() { onReady() {
const { statusBarHeight } = wx.getSystemInfoSync() const { statusBarHeight } = wx.getSystemInfoSync()
const navBarHeight = 44 const navBarHeight = 44
this.setData({ offsetTop: statusBarHeight + navBarHeight }) this.setData({ offsetTop: statusBarHeight + navBarHeight })
}, },
handlePopupOk() { getVideoList(page = 1, append = false) {
const { popupType } = this.data wx.ajax({
if (popupType === 'argument') { method: 'GET',
wx.ajax({ url: '?r=psvt/video/collection-list',
method: 'POST', data: { page, count: 10 },
url: '?r=wtx/user/agree-guest-privacy', }).then((res: any) => {
data: { const list = res.list || []
WorkerId: app.globalData.scene?.workerId || '',
},
}).then(() => {
this.setData({
popupShow: false,
popupType: '',
popupParams: {},
})
const waitBindDoctorId = app.globalData.waitBindDoctorId
if (waitBindDoctorId) {
this.handleBindDoctor(waitBindDoctorId)
}
})
}
if (popupType === 'conformBindDoctorConform') {
this.setData({ this.setData({
popupShow: false, videoList: append ? this.data.videoList.concat(list) : list,
}) videoPagination: {
wx.ajax({ page: res.page || 1,
method: 'POST', pages: res.pages || 1,
url: '?r=wtx/account/wait-bind-doctor', count: Number(res.count) || 0,
data: {
doctorId: app.globalData.waitBindDoctorId,
}, },
}).then(() => {
wx.navigateTo({
url: `/patient/pages/login/index`,
})
}) })
} })
}, },
handlePopupCancel() { getArticleList(page = 1, append = false) {
const { popupType } = this.data wx.ajax({
if (popupType === 'argument') { method: 'GET',
wx.exitMiniProgram() url: '?r=psvt/pic-text/collection-list',
} data: { page, count: 10 },
if (popupType === 'conformBindDoctorConform') { }).then((res: any) => {
const list = res.list || []
this.setData({ this.setData({
popupShow: false, articleList: append ? this.data.articleList.concat(list) : list,
}) articlePagination: {
wx.ajax({ page: res.page || 1,
method: 'POST', pages: res.pages || 1,
url: '?r=wtx/account/wait-bind-doctor', count: Number(res.count) || 0,
data: {
doctorId: app.globalData.waitBindDoctorId,
}, },
}).then(() => {
app.globalData.waitBindDoctorId = ''
}) })
} })
}, },
handleWechatWork() { onChange(e: any) {
const { index } = e.detail
this.setData({ tab: index })
},
handleVideoDetail(e: any) {
const { id, index } = e.currentTarget.dataset
wx.navigateTo({ wx.navigateTo({
url: '/pages/wechatWork/index', url: `/pages/videoDetail/index?id=${id}&index=${index}&source=collection`,
}) })
}, },
handleSetData(e) { handleArticleDetail(e: any) {
const { fileList } = this.data const { id, type } = e.currentTarget.dataset
this.setData({ if (type === '2' || type === 2) {
fileList: [...fileList, ...e.detail], wx.navigateTo({
}) url: `/pages/articleVideoDetail/index?id=${id}`,
})
} else {
wx.navigateTo({
url: `/pages/articleDetail/index?id=${id}`,
})
}
}, },
handleDeleteFile(e) { handleCancelVideoCollect(e: any) {
const { index } = e.detail const { index } = e.currentTarget.dataset
const { fileList } = this.data const video = this.data.videoList[index]
this.setData({ wx.ajax({
fileList: fileList.filter((_, i) => i !== index), method: 'POST',
url: '?r=psvt/video/cancel-collection',
data: { ContentId: Number(video.Id) },
}).then(() => {
const videoList = [...this.data.videoList]
videoList.splice(index, 1)
this.setData({
videoList,
'videoPagination.count': this.data.videoPagination.count - 1,
})
}) })
}, },
topicClose() { handleCancelArticleCollect(e: any) {
this.setData({ const { index } = e.currentTarget.dataset
topicShow: false, const article = this.data.articleList[index]
wx.ajax({
method: 'POST',
url: '?r=psvt/pic-text/cancel-collection',
data: { ContentId: Number(article.Id) },
}).then(() => {
const articleList = [...this.data.articleList]
articleList.splice(index, 1)
this.setData({
articleList,
'articlePagination.count': this.data.articlePagination.count - 1,
})
}) })
}, },
onReachBottom() {
const { tab } = this.data
if (tab === 0) {
const { videoPagination } = this.data
if (videoPagination.page < videoPagination.pages) {
this.getVideoList(videoPagination.page + 1, true)
}
} else {
const { articlePagination } = this.data
if (articlePagination.page < articlePagination.pages) {
this.getArticleList(articlePagination.page + 1, true)
}
}
},
handleBack() {
wx.navigateBack()
},
}) })
export {} export {}

53
src/pages/myCollection/index.wxml

@ -1,5 +1,5 @@
<navbar fixed custom-style="background:{{background}}" back> <navbar fixed custom-style="background:{{background}}" back>
<view class="page-title" slot="left"> <view class="page-title" slot="left" bind:tap="handleBack">
<van-icon name="arrow-left" /> <van-icon name="arrow-left" />
我的收藏 我的收藏
</view> </view>
@ -9,7 +9,7 @@
<view class="page-header" id="page-header" style="" style="padding-top: {{pageTop + 21}}px;"> <view class="page-header" id="page-header" style="" style="padding-top: {{pageTop + 21}}px;">
<view class="search"> <view class="search">
<image class="icon" src="{{imageUrl}}icon14.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon14.png?t={{Timestamp}}"></image>
<input class="input" placeholder-class="place-input" type="text" placeholder="搜索视频" /> <input class="input" placeholder-class="place-input" type="text" placeholder="搜索" />
</view> </view>
</view> </view>
<van-tabs <van-tabs
@ -29,46 +29,67 @@
> >
<van-tab title="大咖说"> <van-tab title="大咖说">
<view class="body1"> <view class="body1">
<view class="card" wx:for="{{10}}" wx:key="index"> <view
class="card"
wx:for="{{videoList}}"
wx:key="Id"
bind:tap="handleVideoDetail"
data-id="{{item.Id}}"
data-index="{{index}}"
>
<view class="photo"> <view class="photo">
<image class="p-img" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="p-img" mode="aspectFill" src="{{item.ImageUrl}}"></image>
<view class="stat"> <view class="stat">
<view class="s-item"> <view class="s-item">
<image class="icon" src="{{imageUrl}}icon8-1.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon8-1.png?t={{Timestamp}}"></image>
12 {{item.OpenNum}}
</view> </view>
<view class="s-item"> <view class="s-item">
<image class="icon" src="{{imageUrl}}icon9-1.png?t={{Timestamp}}"></image> <image
12 wx:if="{{item.IsCompliment==1}}"
class="icon"
src="{{imageUrl}}icon9-active.png?t={{Timestamp}}"
></image>
<image wx:else class="icon" src="{{imageUrl}}icon9-1.png?t={{Timestamp}}"></image>
{{item.ComplimentNum || 0}}
</view> </view>
</view> </view>
</view> </view>
<view class="title">PSVT长期管理专家分享:日常忌口、 PSVT长期管理专家分享:日常忌口、</view> <view class="title">{{item.Title}}</view>
</view> </view>
</view> </view>
<pagination pagination="{{videoPagination}}" />
</van-tab> </van-tab>
<van-tab title="科普教育"> <van-tab title="知识科普">
<view class="body2"> <view class="body2">
<view class="list-card"> <view
class="list-card"
wx:for="{{articleList}}"
wx:key="Id"
bind:tap="handleArticleDetail"
data-id="{{item.Id}}"
data-type="{{item.Type}}"
>
<view class="photo-wrap"> <view class="photo-wrap">
<image class="photo" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="photo" mode="aspectFill" src="{{item.TitlePicLink}}"></image>
<!-- <view class="type1">图文</view> --> <view class="type1" wx:if="{{item.Type==='1'}}">图文</view>
<view class="type2"> <view class="type2" wx:if="{{item.Type==='2'}}">
<image class="icon" src="{{imageUrl}}icon10.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon10.png?t={{Timestamp}}"></image>
视频 视频
</view> </view>
</view> </view>
<view class="wrap"> <view class="wrap">
<view class="title">什么是 PSVT?3 分钟带您了 解阵发性室上性心动过速</view> <view class="title">{{item.Title}}</view>
<view class="op-wrap"> <view class="op-wrap">
<view class="op"> <view class="op" catch:tap="handleCancelArticleCollect" data-index="{{index}}">
<image class="icon" src="{{imageUrl}}icon9-active.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon28-active.png?t={{Timestamp}}"></image>
取消收藏 取消收藏
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<pagination pagination="{{articlePagination}}" />
</van-tab> </van-tab>
</van-tabs> </van-tabs>
</view> </view>

11
src/pages/userInfo/index.ts

@ -194,9 +194,14 @@ Page({
content: '确定退出登录吗?', content: '确定退出登录吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
app.globalData.loginState = '' wx.ajax({
app.globalData.initLoginInfo = {} method: 'POST',
wx.reLaunch({ url: '/pages/login/index' }) url: '?r=psvt/user/reg-logout',
data: {},
}).then(() => {
app.globalData.initLoginInfo = {}
wx.reLaunch({ url: '/pages/login/index' })
})
} }
}, },
}) })

24
src/pages/videoDetail/index.scss

@ -1,6 +1,7 @@
.page-title { .page-title {
font-size: 36rpx; font-size: 36rpx;
color: rgba(51, 51, 51, 1); color: rgba(51, 51, 51, 1);
padding-right: 100rpx;
} }
.page { .page {
@ -54,6 +55,9 @@
font-size: 32rpx; font-size: 32rpx;
white-space: nowrap; white-space: nowrap;
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
&.active {
color: #ff864a;
}
} }
} }
.tag-wrap { .tag-wrap {
@ -109,6 +113,26 @@
font-size: 28rpx; font-size: 28rpx;
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
line-height: 32rpx; line-height: 32rpx;
&.active1 {
color: rgba(234, 88, 26, 1);
}
&.active2 {
color: rgba(242, 150, 0, 1);
}
}
.share-btn {
padding: 0;
margin: 0;
line-height: normal;
background: transparent;
border: none;
&::after {
display: none;
}
.icon {
width: 48rpx;
height: 48rpx;
}
} }
} }
} }

346
src/pages/videoDetail/index.ts

@ -1,107 +1,73 @@
import dayjs from 'dayjs'
const licia = require('miniprogram-licia')
const app = getApp<IAppOption>() const app = getApp<IAppOption>()
// 组件说明文档 https://github.com/wechat-miniprogram/miniprogram-component-plus/blob/master/docs/video-swiper.md
const PAGE_SIZE = 10
Page({ Page({
data: { data: {
id: '', id: '',
activeId: '',
LikeTimes: 0,
current: 0, current: 0,
videoList: [ videoList: [] as Array<any>,
{ page: 1,
attachmentId: '292', pages: 1,
activityId: '57', offsetInPage: 0,
sequence: '6', showRateSelect: false,
title: '替妥尤需要全疗程使用吗', source: '',
imgUrl:
'https://picsissiok-10049618.cos.ap-shanghai.myqcloud.com/47a7a99d8a2e8b700e334469eea99d38_17834997791344.png',
videoUrl:
'https://circlehbsaas.oss-cn-beijing.aliyuncs.com/video/20260708492_7.8%E6%9C%B1%E8%99%B9%E7%9B%B4%E6%92%AD-%E6%9B%BF%E5%A6%A5%E5%B0%A4%E9%9C%80%E8%A6%81%E5%85%A8%E7%96%97%E7%A8%8B%E7%94%A8%E8%8D%AF%E5%90%97.mp4',
duration: '140',
},
{
attachmentId: '291',
activityId: '57',
sequence: '5',
title: '用药后血糖升高、皮肤发痒正常吗',
imgUrl:
'https://picsissiok-10049618.cos.ap-shanghai.myqcloud.com/d8cb802839f6ba3e0e6d062fd9ea430c_17834993002066.png',
videoUrl:
'https://circlehbsaas.oss-cn-beijing.aliyuncs.com/video/20260708200_7.8%E6%9C%B1%E8%99%B9%E7%9B%B4%E6%92%AD-%E7%94%A8%E8%8D%AF%E5%90%8E%E8%A1%80%E7%B3%96%E5%8D%87%E9%AB%98%E7%9A%AE%E8%82%A4%E5%8F%91%E7%82%8E%E6%AD%A3%E5%B8%B8%E5%90%97.mp4',
duration: '54',
},
{
attachmentId: '289',
activityId: '57',
sequence: '3',
title: '日常哪些护眼操作能减轻眼部不适',
imgUrl:
'https://picsissiok-10049618.cos.ap-shanghai.myqcloud.com/e7465a80b1546866c45c7b025c05443b_17834992286888.png',
videoUrl:
'https://circlehbsaas.oss-cn-beijing.aliyuncs.com/video/20260708803_7.8%E6%9C%B1%E8%99%B9%E7%9B%B4%E6%92%AD-%E6%97%A5%E5%B8%B8%E6%8A%A4%E7%9C%BC%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.mp4',
duration: '59',
},
{
attachmentId: '288',
activityId: '57',
sequence: '2',
title: '突眼、斜视、眼睑退缩能通过一个手术全部解决吗',
imgUrl:
'https://picsissiok-10049618.cos.ap-shanghai.myqcloud.com/567e806d4ff8acb4b99b5485a0def25d_17834991851738.png',
videoUrl:
'https://circlehbsaas.oss-cn-beijing.aliyuncs.com/video/20260708851_7.8%E6%9C%B1%E8%99%B9-%E7%AA%81%E7%9C%BC%E6%96%9C%E8%A7%86%E3%80%81%E7%9C%BC%E7%9D%91%E9%80%80%E7%BC%A9%E8%83%BD%E4%B8%80%E6%AC%A1%E6%89%8B%E6%9C%AF%E8%A7%A3%E5%86%B3%E5%90%97.mp4',
duration: '82',
},
{
attachmentId: '287',
activityId: '57',
sequence: '1',
title: '甲状腺眼病治疗后会复发吗',
imgUrl:
'https://picsissiok-10049618.cos.ap-shanghai.myqcloud.com/bf25dbb96a3259efd9b76ff62eb2891c_17834991431596.png',
videoUrl:
'https://circlehbsaas.oss-cn-beijing.aliyuncs.com/video/20260708254_7.8%E6%9C%B1%E8%99%B9-%E7%94%B2%E7%8A%B6%E8%85%BA%E7%9C%BC%E7%97%85%E6%B2%BB%E7%96%97%E5%90%8E%E4%BC%9A%E5%A4%8D%E5%8F%91%E5%90%97.mp4',
duration: '94',
},
] as any,
beforeLoading: false,
beforeEnd: false,
afterLoading: false,
afterEnd: false,
starShow: false,
index: 0,
offsetTop: 310,
}, },
videoContexts: {} as any, videoContexts: {} as any,
onLoad(options) { onLoad(options: any) {
this.setData({ const index = Number(options.index) || 0
id: options.id, const startPage = Math.floor(index / PAGE_SIZE) + 1
activeId: options.id, const offsetInPage = index % PAGE_SIZE
index: Number(options.index),
if (options.id) {
this.setData({ id: options.id })
}
if (options.source) {
this.setData({ source: options.source })
}
this.setData({ offsetInPage, current: 0 })
app.waitLogin({ type: 'any' }).then(() => {
this.getList(startPage)
}) })
this.getLightList()
}, },
getLightList() { getList(page: number, append = false) {
this.setData({ const { offsetInPage, source } = this.data
current: this.data.index, const url = source === 'collection' ? '?r=psvt/video/collection-list' : '?r=psvt/video/list'
wx.ajax({
method: 'GET',
url,
data: { page, count: PAGE_SIZE },
}).then((res: any) => {
let list = res.list || []
if (!append && offsetInPage > 0 && list.length > offsetInPage) {
list = list.slice(offsetInPage)
this.setData({ offsetInPage: 0 })
}
list = list.map((item: any) => ({
...item,
preload: false,
progress: 0,
play: false,
}))
this.setData({
videoList: append ? this.data.videoList.concat(list) : list,
page: res.page || 1,
pages: res.pages || 1,
})
this.preloadVideos(this.data.current)
this.playCurrentVideo()
if (this.data.videoList.length > 0) {
this.addViewRecord(this.data.videoList[0].Id)
}
}) })
this.preloadVideos(this.data.index)
// this.playCurrentVideo()
}, },
preloadVideos(currentIndex: number) { preloadVideos(currentIndex: number) {
const { videoList } = this.data const { videoList } = this.data
const newVideoList = [...videoList] const newVideoList = [...videoList]
// 只预加载当前和前后各一个视频
newVideoList.forEach((item: any, index: number) => { newVideoList.forEach((item: any, index: number) => {
if (Math.abs(index - currentIndex) <= 1) { if (Math.abs(index - currentIndex) <= 1) {
item.preload = true item.preload = true
@ -109,91 +75,63 @@ Page({
item.preload = false item.preload = false
} }
}) })
this.setData({ videoList: newVideoList })
this.setData({
videoList: newVideoList,
})
}, },
handleChange(e: any) { handleChange(e: any) {
const { beforeLoading, afterLoading, beforeEnd, afterEnd, videoList } = this.data
const { current } = e.detail const { current } = e.detail
this.setData({ const { videoList, pages, page } = this.data
current, this.setData({ current, showRateSelect: false })
starShow: false,
}) this.addViewRecord(videoList[current].Id)
if (current === 0 && !beforeLoading && beforeEnd) {
this.setData({
beforeLoading: true,
})
// this.getList('before')
}
if (current === videoList.length - 1 && !afterLoading && afterEnd) {
this.setData({
afterLoading: true,
})
// this.getList('after')
}
this.preloadVideos(current) this.preloadVideos(current)
this.playCurrentVideo() this.playCurrentVideo()
if (current >= videoList.length - 2 && page < pages) {
this.getList(page + 1, true)
}
}, },
playCurrentVideo() { playCurrentVideo() {
const { current, videoList } = this.data const { current, videoList } = this.data
// 暂停所有视频
Object.values(this.videoContexts).forEach((ctx: any) => { Object.values(this.videoContexts).forEach((ctx: any) => {
ctx.pause() ctx.pause()
}) })
// 播放当前视频
const currentVideo = videoList[current] const currentVideo = videoList[current]
if (currentVideo) { if (currentVideo) {
const videoId = `video-${currentVideo.attachmentId}` const videoId = `video-${currentVideo.Id}`
if (!this.videoContexts[videoId]) { if (!this.videoContexts[videoId]) {
this.videoContexts[videoId] = wx.createVideoContext(videoId) this.videoContexts[videoId] = wx.createVideoContext(videoId)
} }
this.videoContexts[videoId].play() this.videoContexts[videoId].play()
} }
}, },
handleFullScreenChange(e) { addViewRecord(contentId: string) {
const { index } = e.currentTarget.dataset wx.ajax({
const fullScreenKey = `videoList[${index}].fullScreen` method: 'POST',
this.setData({ url: '?r=psvt/video/view',
[fullScreenKey]: e.detail.fullScreen, data: { ContentId: Number(contentId) },
}) })
}, },
handelFullScreen(e) { handleTogglePlay(e: any) {
const { id } = e.currentTarget.dataset
if (!this.videoContexts[id]) {
this.videoContexts[id] = wx.createVideoContext(id)
}
this.videoContexts[id].requestFullScreen({})
},
handleLoadedMetaData(e) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const offsetTopKey = `videoList[${index}].offsetTop` const { videoList } = this.data
const query = wx.createSelectorQuery() const video = videoList[index]
query const videoId = `video-${video.Id}`
.select(`#${e.target.id}`) if (!this.videoContexts[videoId]) {
.boundingClientRect((rect) => { this.videoContexts[videoId] = wx.createVideoContext(videoId)
this.setData({ }
[offsetTopKey]: rect.height + e.target.offsetTop + 10, if (video.play) {
}) this.videoContexts[videoId].pause()
}) } else {
.exec() this.videoContexts[videoId].play()
}
}, },
handlePlay(e: any) { handlePlay(e: any) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const key = `videoList[${index}].play` this.setData({ [`videoList[${index}].play`]: true })
this.setData({
[key]: true,
})
}, },
handlePause(e: any) { handlePause(e: any) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const key = `videoList[${index}].play` this.setData({ [`videoList[${index}].play`]: false })
this.setData({
[key]: false,
})
}, },
handleTimeUpdate(e: any) { handleTimeUpdate(e: any) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
@ -201,71 +139,105 @@ Page({
if (videoList[index].drag) return if (videoList[index].drag) return
const { currentTime, duration } = e.detail const { currentTime, duration } = e.detail
const progress = (currentTime / duration) * 100 const progress = (currentTime / duration) * 100
const progressKey = `videoList[${index}].progress`
const durationKey = `videoList[${index}].duration`
this.setData({ this.setData({
[progressKey]: progress, [`videoList[${index}].progress`]: progress,
[durationKey]: duration, [`videoList[${index}].duration`]: duration,
}) })
}, },
handleTogglePlay(e) {
const { index } = e.currentTarget.dataset
const { videoList } = this.data
const video = videoList[index]
const videoId = `video-${video.attachmentId}`
if (!this.videoContexts[videoId]) {
this.videoContexts[videoId] = wx.createVideoContext(videoId)
}
if (video.play) {
this.videoContexts[videoId].pause()
} else {
this.videoContexts[videoId].play()
}
},
handleDragStart(e: any) { handleDragStart(e: any) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const key = `videoList[${index}].drag` this.setData({ [`videoList[${index}].drag`]: true })
this.setData({
[key]: true,
})
}, },
handleDragEnd(e: any) { handleDragEnd(e: any) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const { videoList } = this.data const { videoList } = this.data
const video = videoList[index] const video = videoList[index]
const duration = video.duration const duration = video.duration
const drapProgress = e.detail const dragProgress = e.detail
const currentTime = (drapProgress / 100) * duration const currentTime = (dragProgress / 100) * duration
const videoId = `video-${video.attachmentId}` const videoId = `video-${video.Id}`
if (!this.videoContexts[videoId]) { if (!this.videoContexts[videoId]) {
this.videoContexts[videoId] = wx.createVideoContext(videoId) this.videoContexts[videoId] = wx.createVideoContext(videoId)
} }
this.videoContexts[videoId].seek(currentTime) this.videoContexts[videoId].seek(currentTime)
this.setData({ [`videoList[${index}].drag`]: false })
const dragKey = `videoList[${index}].drag` },
handleCompliment() {
if (!app.checkLoginType(1)) return
const { current, videoList } = this.data
const video = videoList[current]
if (video.IsCompliment) return
wx.ajax({
method: 'POST',
url: '?r=psvt/video/compliment',
data: { ContentId: Number(video.Id) },
}).then(() => {
this.setData({
[`videoList[${current}].IsCompliment`]: 1,
[`videoList[${current}].ComplimentNum`]: (video.ComplimentNum * 1 || 0) + 1,
})
})
},
handleCollect() {
if (!app.checkLoginType(1)) return
const { current, videoList } = this.data
const video = videoList[current]
const isCollect = video.IsCollect
wx.ajax({
method: 'POST',
url: isCollect ? '?r=psvt/video/cancel-collection' : '?r=psvt/video/collection',
data: { ContentId: Number(video.Id) },
}).then(() => {
this.setData({
[`videoList[${current}].IsCollect`]: isCollect ? 0 : 1,
[`videoList[${current}].CollectionPeopleNum`]: (video.CollectionPeopleNum * 1 || 0) + (isCollect ? -1 : 1),
})
})
},
handleShare() {
const { current, videoList } = this.data
const video = videoList[current]
wx.ajax({
method: 'POST',
url: '?r=psvt/video/share',
data: { ContentId: Number(video.Id) },
}).then(() => {
this.setData({
[`videoList[${current}].IsShare`]: 1,
[`videoList[${current}].ShareNum`]: (video.ShareNum * 1 || 0) + 1,
})
})
},
handleSpeed(e: any) {
const { speed } = e.currentTarget.dataset
const { current, videoList } = this.data
const video = videoList[current]
const videoId = `video-${video.Id}`
if (!this.videoContexts[videoId]) {
this.videoContexts[videoId] = wx.createVideoContext(videoId)
}
this.videoContexts[videoId].playbackRate(Number(speed))
this.setData({ this.setData({
[dragKey]: false, [`videoList[${current}].playbackRate`]: speed,
showRateSelect: false,
}) })
}, },
handleToggleRateSelect() {
this.setData({ showRateSelect: !this.data.showRateSelect })
},
onShareAppMessage() { onShareAppMessage() {
const { id, current, videoList } = this.data const { current, videoList } = this.data
const video = videoList[current]
this.handleShare()
return { return {
title: videoList[current].Title, title: video.Title || '视频详情',
path: `/patient/pages/liveDetailVideo/index?id=${id}&index=${current}`, path: `/pages/videoDetail/index?id=${video.Id}&index=0&source=${this.data.source}`,
imageUrl: videoList[current].imgUrl, imageUrl: video.ImageUrl,
} }
}, },
handleBack() { handleBack() {
wx.navigateBack({ wx.navigateBack()
fail: () => {
wx.reLaunch({
url: `/patient/pages/liveDetail/index?id=${this.data.id}`,
})
},
})
}, },
}) })

70
src/pages/videoDetail/index.wxml

@ -1,5 +1,5 @@
<navbar fixed custom-style="background:{{background}}" back> <navbar fixed custom-style="background:{{background}}" back>
<view class="page-title" slot="left"> <view class="page-title" slot="left" bind:tap="handleBack">
<van-icon name="arrow-left" /> <van-icon name="arrow-left" />
</view> </view>
</navbar> </navbar>
@ -8,40 +8,48 @@
<swiper class="swiper" vertical="{{true}}" current="{{current}}" bindchange="handleChange"> <swiper class="swiper" vertical="{{true}}" current="{{current}}" bindchange="handleChange">
<swiper-item class="swiper-item" wx:for="{{videoList}}" wx:key="Id"> <swiper-item class="swiper-item" wx:for="{{videoList}}" wx:key="Id">
<video <video
id="video-{{item.attachmentId}}" id="video-{{item.Id}}"
class="video" class="video"
src="{{item.preload ? item.videoUrl : ''}}" src="{{item.preload ? item.SrcUrl : ''}}"
poster="{{item.imgUrl}}" poster="{{item.ImageUrl}}"
loop loop
title="{{item.title}}" title="{{item.Title}}"
object-fit="contain" object-fit="contain"
enable-play-gesture enable-play-gesture
enable-progress-gesture enable-progress-gesture
show-center-play-btn="{{false}}" show-center-play-btn="{{true}}"
controls="{{false}}" controls="{{false}}"
show-fullscreen-btn="{{false}}" show-fullscreen-btn="{{false}}"
play-btn-position="center" play-btn-position="center"
bindloadedmetadata="handleLoadedMetaData"
bindprogress="handleProgress"
bindplay="handlePlay" bindplay="handlePlay"
bindpause="handlePause" bindpause="handlePause"
bindfullscreenchange="handleFullScreenChange"
bindtimeupdate="handleTimeUpdate" bindtimeupdate="handleTimeUpdate"
data-index="{{index}}" data-index="{{index}}"
data-full="{{item.fullScreen}}"
></video> ></video>
<view class="swiper-item-footer"> <view class="swiper-item-footer">
<view class="info-card"> <view class="info-card">
<view class="rate-select"> <view class="rate-select" wx:if="{{showRateSelect && current===index}}">
<view class="r-item">0.5倍</view> <view class="r-item {{item.playbackRate==='0.5' && 'active'}}" bind:tap="handleSpeed" data-speed="0.5">
<view class="r-item">正常</view> 0.5倍
<view class="r-item">1.5倍</view> </view>
<view class="r-item">2.0倍速</view> <view
class="r-item {{(!item.playbackRate || item.playbackRate==='1') && 'active'}}"
bind:tap="handleSpeed"
data-speed="1"
>
正常
</view>
<view class="r-item {{item.playbackRate==='1.5' && 'active'}}" bind:tap="handleSpeed" data-speed="1.5">
1.5倍
</view>
<view class="r-item {{item.playbackRate==='2' && 'active'}}" bind:tap="handleSpeed" data-speed="2">
2.0倍速
</view>
</view> </view>
<view class="tag-wrap"> <view class="tag-wrap">
<view class="tag">患者故事</view> <view class="tag" wx:for="{{item.CateNames}}" wx:key="index" wx:for-item="cate">{{cate}}</view>
</view> </view>
<view class="title">{{item.title}}</view> <view class="title">{{item.Title}}</view>
<view class="progress"> <view class="progress">
<van-slider <van-slider
value="{{item.progress}}" value="{{item.progress}}"
@ -59,24 +67,32 @@
</view> </view>
<view class="container"> <view class="container">
<view class="op-wrap"> <view class="op-wrap">
<view class="op"> <view class="op" bind:tap="handleCompliment">
<image class="icon" src="{{imageUrl}}icon9-1.png?t={{Timestamp}}"></image> <image
<view class="num">126</view> class="icon"
src="{{item.IsCompliment ? imageUrl + 'icon9-active.png?t=' + Timestamp : imageUrl + 'icon9-1.png?t=' + Timestamp}}"
></image>
<view class="num {{item.IsCompliment ? 'active1' : ''}}">{{item.ComplimentNum || 0}}</view>
</view> </view>
<view class="op"> <view class="op">
<image class="icon" src="{{imageUrl}}icon8-1.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon8-1.png?t={{Timestamp}}"></image>
<view class="num">126</view> <view class="num">{{item.OpenNum || 0}}</view>
</view> </view>
<view class="op"> <view class="op" bind:tap="handleCollect">
<image class="icon" src="{{imageUrl}}icon28-1.png?t={{Timestamp}}"></image> <image
<view class="num">126</view> class="icon"
src="{{item.IsCollect ? imageUrl + 'icon28-active.png?t=' + Timestamp : imageUrl + 'icon28-1.png?t=' + Timestamp}}"
></image>
<view class="num {{item.IsCollect ? 'active2' : ''}}">{{item.CollectionPeopleNum || 0}}</view>
</view> </view>
<view class="op"> <view class="op">
<image class="icon" src="{{imageUrl}}icon29-1.png?t={{Timestamp}}"></image> <button class="share-btn" open-type="share">
<view class="num">126</view> <image class="icon" src="{{imageUrl}}icon29-1.png?t={{Timestamp}}"></image>
</button>
<view class="num">{{item.ShareNum || 0}}</view>
</view> </view>
</view> </view>
<view class="speed">倍速</view> <view class="speed" bind:tap="handleToggleRateSelect">倍速</view>
</view> </view>
</view> </view>
</swiper-item> </swiper-item>

3
src/pages/videoList/index.json

@ -2,6 +2,7 @@
"navigationStyle": "custom", "navigationStyle": "custom",
"usingComponents": { "usingComponents": {
"popup": "/components/popup/index", "popup": "/components/popup/index",
"navbar": "/components/navbar/index" "navbar": "/components/navbar/index",
"pagination": "/components/pagination/index"
} }
} }

19
src/pages/videoList/index.scss

@ -3,8 +3,8 @@ page {
} }
.page-title { .page-title {
width: 266rpx; font-size: 36rpx;
height: 58rpx; color: rgba(51, 51, 51, 1);
} }
.page { .page {
@ -34,8 +34,18 @@ page {
} }
} }
} }
.banner {
margin: 32rpx 30rpx 0;
height: 246rpx;
.b-img {
display: block;
width: 100%;
height: 100%;
border-radius: 32rpx;
}
}
.type-navbar { .type-navbar {
padding: 48rpx 0 24rpx 30rpx; padding: 32rpx 0 24rpx 30rpx;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
gap: 12rpx; gap: 12rpx;
@ -104,6 +114,9 @@ page {
width: 32rpx; width: 32rpx;
height: 32rpx; height: 32rpx;
} }
&.active1 {
color: rgba(234, 88, 26, 1);
}
} }
} }
} }

145
src/pages/videoList/index.ts

@ -2,78 +2,111 @@ const app = getApp<IAppOption>()
Page({ Page({
data: { data: {
popupShow: false, cateList: [] as Array<{ CateId: string; CateName: string }>,
popupType: '', activeCateId: '',
popupParams: { searchValue: '',
position: 'bottom', videoList: [] as Array<any>,
} as any, pagination: {
page: 1,
pages: 1,
count: 0,
},
bannerList: [] as Array<any>,
}, },
onLoad() { onLoad() {
app.waitLogin({ type: 0 }).then(() => {}) app.waitLogin({ type: 'any' }).then(() => {
this.getBannerList()
this.getCateList()
this.getList()
})
}, },
handlePopupOk() { getBannerList() {
const { popupType } = this.data wx.ajax({
if (popupType === 'argument') { method: 'GET',
wx.ajax({ url: '?r=psvt/video/banner-list',
method: 'POST', }).then((res: any) => {
url: '?r=wtx/user/agree-guest-privacy', res = res.filter((item) => item.BannerImageUrl)
data: {
WorkerId: app.globalData.scene?.workerId || '',
},
}).then(() => {
this.setData({
popupShow: false,
popupType: '',
popupParams: {},
})
const waitBindDoctorId = app.globalData.waitBindDoctorId
if (waitBindDoctorId) {
this.handleBindDoctor(waitBindDoctorId)
}
})
}
if (popupType === 'conformBindDoctorConform') {
this.setData({ this.setData({
popupShow: false, bannerList: res || [],
}) })
wx.ajax({ })
method: 'POST', },
url: '?r=wtx/account/wait-bind-doctor', getCateList() {
data: { wx.ajax({
doctorId: app.globalData.waitBindDoctorId, method: 'GET',
}, url: '?r=psvt/video/cate-list',
}).then(() => { }).then((res: any) => {
wx.navigateTo({ this.setData({
url: `/patient/pages/login/index`, cateList: res || [],
})
}) })
} })
}, },
handlePopupCancel() { getList(page = 1, cateId = '', search = '', append = false) {
const { popupType } = this.data const params: any = {
if (popupType === 'argument') { page,
wx.exitMiniProgram() count: 10,
}
if (cateId) {
params.CateId = cateId
}
if (search) {
params.Search = search
} }
if (popupType === 'conformBindDoctorConform') { wx.ajax({
method: 'GET',
url: '?r=psvt/video/list',
data: params,
}).then((res: any) => {
const list = res.list || []
this.setData({ this.setData({
popupShow: false, videoList: append ? this.data.videoList.concat(list) : list,
}) pagination: {
wx.ajax({ page: res.page || 1,
method: 'POST', pages: res.pages || 1,
url: '?r=wtx/account/wait-bind-doctor', count: Number(res.count) || 0,
data: {
doctorId: app.globalData.waitBindDoctorId,
}, },
}).then(() => {
app.globalData.waitBindDoctorId = ''
}) })
})
},
handleCateTap(e: any) {
const { cateid } = e.currentTarget.dataset
this.setData({
activeCateId: cateid || '',
})
this.getList(1, cateid || '', this.data.searchValue)
},
handleSearch(e: any) {
const value = e.detail.value || ''
this.setData({
searchValue: value,
})
this.getList(1, this.data.activeCateId, value)
},
handleLoadMore() {
const { pagination, activeCateId, searchValue } = this.data
if (pagination.page < pagination.pages) {
this.getList(pagination.page + 1, activeCateId, searchValue, true)
} }
}, },
handleDetail() { onReachBottom() {
this.handleLoadMore()
},
handleDetail(e: any) {
const { id, index } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/videoDetail/index?id=${id}&index=${index}`,
})
},
handleBannerTap(e: any) {
const { id } = e.currentTarget.dataset
const idx = this.data.videoList.findIndex((v: any) => v.Id === id)
wx.navigateTo({ wx.navigateTo({
url: '/pages/wallDetail/index', url: `/pages/videoDetail/index?id=${id}&index=${idx >= 0 ? idx : 0}`,
}) })
}, },
handleBack() {
wx.navigateBack()
},
}) })
export {} export {}

69
src/pages/videoList/index.wxml

@ -1,5 +1,7 @@
<navbar fixed custom-style="background:{{background}}" back> <navbar fixed custom-style="background:{{background}}" back>
<image class="page-title" slot="left" src="{{imageUrl}}logo2.png?t={{Timestamp}}"></image> <view class="page-title" slot="left" bind:tap="handleBack">
<van-icon name="arrow-left" />
</view>
</navbar> </navbar>
<view <view
@ -9,28 +11,77 @@
<view class="page-header"> <view class="page-header">
<view class="search"> <view class="search">
<image class="icon" src="{{imageUrl}}icon14.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon14.png?t={{Timestamp}}"></image>
<input class="input" placeholder-class="place-input" type="text" placeholder="搜索帖子 / 话题 / 患者故事" /> <input
class="input"
placeholder-class="place-input"
type="text"
placeholder="搜索视频"
confirm-type="search"
bindconfirm="handleSearch"
value="{{searchValue}}"
/>
</view> </view>
</view> </view>
<swiper
wx:if="{{bannerList.length}}"
class="banner"
autoplay
circular
indicator-dots
indicator-color="rgba(255,255,255,0.4)"
indicator-active-color="#fff"
>
<swiper-item
class="banner-item"
wx:for="{{bannerList}}"
wx:key="Id"
bind:tap="handleBannerTap"
data-id="{{item.Id}}"
>
<image class="b-img" mode="aspectFill" src="{{item.BannerImageUrl}}"></image>
</swiper-item>
</swiper>
<view class="type-navbar"> <view class="type-navbar">
<view class="type {{index==0 && 'active'}}" wx:for="{{9}}" wx:key="index">最新发布</view> <view class="type {{activeCateId==='' && 'active'}}" bind:tap="handleCateTap" data-cateid="">全部</view>
<view
class="type {{activeCateId===item.CateId && 'active'}}"
wx:for="{{cateList}}"
wx:key="CateId"
bind:tap="handleCateTap"
data-cateid="{{item.CateId}}"
>
{{item.CateName}}
</view>
</view> </view>
<view class="list"> <view class="list">
<view class="card" wx:for="{{4}}" wx:key="index"> <view
class="card"
wx:for="{{videoList}}"
wx:key="Id"
bind:tap="handleDetail"
data-id="{{item.Id}}"
data-index="{{index}}"
>
<view class="photo"> <view class="photo">
<image class="p-img" mode="aspectFill" src="{{imageUrl}}bg1.png?t={{Timestamp}}"></image> <image class="p-img" mode="aspectFill" src="{{item.ImageUrl}}"></image>
<view class="stat"> <view class="stat">
<view class="s-item"> <view class="s-item">
<image class="icon" src="{{imageUrl}}icon8-1.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon8-1.png?t={{Timestamp}}"></image>
12 {{item.OpenNum}}
</view> </view>
<view class="s-item"> <view class="s-item">
<image class="icon" src="{{imageUrl}}icon9-1.png?t={{Timestamp}}"></image> <image
12 wx:if="{{item.IsCompliment==1}}"
class="icon"
src="{{imageUrl}}icon9-active.png?t={{Timestamp}}"
></image>
<image wx:else class="icon" src="{{imageUrl}}icon9-1.png?t={{Timestamp}}"></image>
<view class="num {{item.IsCompliment ? 'active1' : ''}}">{{item.ComplimentNum || 0}}</view>
</view> </view>
</view> </view>
</view> </view>
<view class="title">PSVT长期管理专家分享:日常忌口、 PSVT长期管理专家分享:日常忌口、</view> <view class="title">{{item.Title}}</view>
</view> </view>
</view> </view>
<pagination pagination="{{pagination}}" />
</view> </view>

4
src/pages/wechatWork/index.scss

@ -2,8 +2,8 @@ page {
background: #f7fafc; background: #f7fafc;
} }
.page-title { .page-title {
width: 266rpx; font-size: 36rpx;
height: 58rpx; color: rgba(51, 51, 51, 1);
} }
.page { .page {

7
src/pages/wechatWork/index.ts

@ -1,8 +1,11 @@
const _app = getApp<IAppOption>(); const _app = getApp<IAppOption>()
Page({ Page({
data: {}, data: {},
onLoad() {}, onLoad() {},
}); handleBack() {
wx.navigateBack()
},
})
export {} export {}

4
src/pages/wechatWork/index.wxml

@ -1,5 +1,7 @@
<navbar fixed custom-style="background:{{background}}" back> <navbar fixed custom-style="background:{{background}}" back>
<image class="page-title" slot="left" src="{{imageUrl}}logo2.png?t={{Timestamp}}"></image> <view class="page-title" slot="left" bind:tap="handleBack">
<van-icon name="arrow-left" />
</view>
</navbar> </navbar>
<view class="page" style="background: url('{{imageUrl}}bg6.png?t={{Timestamp}}') no-repeat top center/100%;padding-top: {{pageTop}}rpx;"> <view class="page" style="background: url('{{imageUrl}}bg6.png?t={{Timestamp}}') no-repeat top center/100%;padding-top: {{pageTop}}rpx;">

6
src/utils/page.ts

@ -28,11 +28,17 @@ function page(config: WechatMiniprogram.Page.Instance<WechatMiniprogram.IAnyObje
config.onPageScroll = function (options): void { config.onPageScroll = function (options): void {
const scrollTop = options?.scrollTop const scrollTop = options?.scrollTop
let background = 'transparent' let background = 'transparent'
let backTop = false
if (Number(scrollTop) > 20) { if (Number(scrollTop) > 20) {
background = '#ffffff' background = '#ffffff'
} }
if (Number(scrollTop) > 200) {
backTop = true
}
this.setData({ this.setData({
background, background,
backTop,
}) })
if (originalOnPageScroll) { if (originalOnPageScroll) {
originalOnPageScroll.call(this, options) originalOnPageScroll.call(this, options)

Loading…
Cancel
Save