Browse Source

feat(统计页面): 重构统计页面功能,支持日期范围筛选和药师统计

refactor(组件): 优化pickerArea组件,支持树形结构数据转换
style(登录表单): 调整输入框和选择器样式,提升用户体验
fix(首页): 修复统计数据展示问题,区分累计和日/月度统计
master
kola-web 6 days ago
parent
commit
f64c9f0238
  1. 303
      .trae/documents/component-reuse-analysis-report.md
  2. 52
      src/components/pickerArea/index.ts
  3. 109
      src/doctor/pages/home/index.ts
  4. 16
      src/doctor/pages/home/index.wxml
  5. 2
      src/doctor/pages/loginForm/index.scss
  6. 99
      src/doctor/pages/loginForm/index.ts
  7. 35
      src/doctor/pages/loginForm/index.wxml
  8. 31
      src/doctor/pages/my/index.ts
  9. 5
      src/doctor/pages/patientList/index.wxml
  10. 15
      src/doctor/pages/stat/index.ts
  11. 3
      src/ground/pages/home/index.ts
  12. 6
      src/ground/pages/my/index.ts
  13. 3
      src/ground/pages/pharmacist/index.ts
  14. 131
      src/ground/pages/stat/index.ts
  15. 1
      src/pages/index/index.wxml
  16. 32
      src/pages/tourists/index.scss
  17. 11
      src/pages/tourists/index.wxml
  18. 1
      src/pages/work/index.wxml

303
.trae/documents/component-reuse-analysis-report.md

@ -0,0 +1,303 @@
# 组件复用性分析报告
## 一、项目组件盘点
### 1.1 现有自定义组件(10个)
| 组件名称 | 路径 | 功能描述 | 使用频率 | 状态 |
|---------|------|---------|---------|------|
| navbar | /components/navbar | 自定义导航栏 | 高(15+页面) | ✅ 良好 |
| pagination | /components/pagination | 分页组件 | 中(5页面) | ✅ 良好 |
| popup | /components/popup | 通用弹窗 | 高(8页面) | ⚠ 需优化 |
| calendar | /components/calendar | 日历组件 | 低(未使用) | ❓ 待确认 |
| pickerArea | /components/pickerArea | 省市区选择器 | 低(1页面) | ✅ 可用 |
| uploadFile | /components/uploadFile | 文件上传 | 低(1页面) | ✅ 可用 |
| ec-canvas | /components/ec-canvas | ECharts图表 | 中(2页面) | ✅ 良好 |
| customPoster | /components/customPoster | 海报生成 | 低(未使用) | ❓ 待确认 |
| star | /components/star | 星星动画 | 低(未使用) | ❓ 待确认 |
| popupDoctor | /components/popupDoctor | 医生信息弹窗 | 低(未使用) | ❓ 待确认 |
### 1.2 TabBar组件(2个)
| 组件名称 | 路径 | 使用端 | 使用频率 |
|---------|------|--------|---------|
| doctor-tab-bar | /components/doctor-tab-bar | 药店端 | 4页面 |
| ground-tab-bar | /components/ground-tab-bar | 地推端 | 3页面 |
**复用建议:** 两个组件功能相似,可合并为 `tab-bar` 组件,通过配置区分不同角色。
---
## 二、组件复用价值评估矩阵
### 2.1 高优先级组件(建议立即提取)
| 组件名称 | 使用频率 | 功能独立性 | 通用性 | 复杂度 | 复用价值 | 优先级 |
|---------|---------|-----------|--------|--------|---------|--------|
| **项目选择器** | 3次 | 高 | 高 | 低 | ⭐⭐⭐⭐⭐ | P0 |
| **统计卡片** | 4次 | 高 | 高 | 中 | ⭐⭐⭐⭐⭐ | P0 |
| **日期选择逻辑** | 5次 | 高 | 高 | 中 | ⭐⭐⭐⭐ | P1 |
### 2.2 中优先级组件(建议后续提取)
| 组件名称 | 使用频率 | 功能独立性 | 通用性 | 复杂度 | 复用价值 | 优先级 |
|---------|---------|-----------|--------|--------|---------|--------|
| **状态标签** | 2次 | 高 | 中 | 低 | ⭐⭐⭐ | P2 |
| **个人信息页面** | 6次 | 中 | 中 | 高 | ⭐⭐⭐ | P2 |
| **登录页面** | 2次 | 中 | 中 | 高 | ⭐⭐⭐ | P2 |
| **修改昵称/手机页面** | 4次 | 中 | 中 | 中 | ⭐⭐⭐ | P2 |
### 2.3 低优先级组件(保持现状)
| 组件名称 | 使用频率 | 功能独立性 | 通用性 | 复杂度 | 复用价值 | 优先级 |
|---------|---------|-----------|--------|--------|---------|--------|
| **患者卡片** | 1次 | 中 | 低 | 高 | ⭐⭐ | P3 |
| **上传材料弹窗** | 1次 | 高 | 中 | 中 | ⭐⭐ | P3 |
---
## 三、详细组件分析
### 3.1 项目选择器(ProjectPicker)
**使用位置:**
1. 地推端首页:`/ground/pages/home/index.wxml`
2. 药店端首页:`/doctor/pages/home/index.wxml`
3. 患者列表页:`/doctor/pages/patientList/index.wxml`
**代码相似度:** 90%
**主要差异:**
- 主题色不同(地推端蓝色 #4A8DFF,药店端橙色 #FF8A4C
- 样式细节略有差异
**提取收益:**
- 减少重复代码约 60 行
- 统一项目切换交互体验
- 便于后续维护
**组件接口设计:**
```typescript
interface ProjectPickerProps {
projectList: Array<{ projectId: number; projectName: string }>;
currentProjectName: string;
themeColor: string; // 主题色
onChange: (projectId: number) => void;
}
```
### 3.2 统计卡片(StatCard)
**使用位置:**
1. 地推端首页:`/ground/pages/home/index.wxml`(2处)
2. 药店端首页:`/doctor/pages/home/index.wxml`(2处)
3. 地推端统计页:`/ground/pages/stat/index.wxml`
4. 药店端统计页:`/doctor/pages/stat/index.wxml`
**代码相似度:** 88%
**主要差异:**
- 地推端显示"药店数",药店端不显示
- 数据字段名称略有不同
- 主题色不同
**提取收益:**
- 减少重复代码约 200 行
- 统一统计卡片样式
- 支持配置化展示
**组件接口设计:**
```typescript
interface StatCardProps {
title: string;
tip?: string;
data: Array<{ name: string; value: number }>;
showIndicationStats?: boolean;
indicationStats?: Array<any>;
themeColor: string;
}
```
### 3.3 日期选择逻辑(DatePickerMixin)
**使用位置:**
1. 地推端首页:日期筛选逻辑
2. 药店端首页:日期筛选逻辑
3. 患者列表页:时间筛选
4. 地推端统计页:日期筛选
5. 药店端统计页:日期筛选
**代码相似度:** 85%
**重复逻辑:**
- 日期格式化函数
- 日期范围验证
- 日/月模式切换
- 日期选择器事件处理
**提取收益:**
- 减少重复代码约 150 行
- 统一日期处理逻辑
- 避免日期相关 bug
**Mixin接口设计:**
```typescript
interface DatePickerMixin {
// 数据
startDate: string;
endDate: string;
statType: 'day' | 'month';
// 方法
formatDate(date: string): string;
formatMonth(date: string): string;
validateDateRange(start: string, end: string): boolean;
onDateChange(e: CustomEvent): void;
prevDate(): void;
nextDate(): void;
}
```
### 3.4 状态标签(StatusTag)
**使用位置:**
1. 患者列表页:跳转状态、入组状态、审核状态
**代码相似度:** 80%
**状态定义:**
```typescript
// 跳转/入组状态
const statusOptions = [
{ value: '', label: '全部' },
{ value: 0, label: '未跳转/未入组' },
{ value: 1, label: '已跳转/已入组' },
];
// 审核状态
const auditStatusMap = {
0: { label: '未审核', class: 's2' },
1: { label: '审核中', class: 's2' },
2: { label: '已通过', class: 's1' },
3: { label: '已驳回', class: 's1' },
};
```
**提取收益:**
- 统一状态展示样式
- 便于新增状态类型
### 3.5 个人信息相关页面
**涉及页面:**
- 地推端:my、changeNickname、changeTel(3个页面)
- 药店端:my、changeNickname、changeTel(3个页面)
**代码相似度:** 95%
**主要差异:**
- API接口地址不同
- 数据字段名称不同
- 主题色不同
**提取建议:**
创建通用页面模板,通过配置区分:
```typescript
interface ProfileConfig {
role: 'promoter' | 'pharmacist';
themeColor: string;
apiEndpoints: {
getProfile: string;
updateAvatar: string;
updateName: string;
updatePhone: string;
logout: string;
};
}
```
---
## 四、组件提取实施建议
### 4.1 第一阶段:高优先级组件(1-2周)
1. **项目选择器**
- 创建 `/components/project-picker/`
- 替换 3 个页面的使用
- 编写组件文档
2. **统计卡片**
- 创建 `/components/stat-card/`
- 替换 4 个页面的使用
- 编写组件文档
3. **日期选择Mixin**
- 创建 `/mixins/date-picker.ts`
- 在 5 个页面中引入使用
- 编写使用文档
### 4.2 第二阶段:中优先级组件(2-3周)
1. **状态标签组件**
2. **个人信息页面模板**
3. **登录页面模板**
### 4.3 第三阶段:优化与文档(1周)
1. 完善组件文档
2. 编写使用示例
3. 代码审查与优化
---
## 五、预期收益
### 5.1 代码量减少
| 组件 | 预计减少代码行数 |
|------|----------------|
| 项目选择器 | 60 行 |
| 统计卡片 | 200 行 |
| 日期选择Mixin | 150 行 |
| 状态标签 | 80 行 |
| 个人信息页面 | 300 行 |
| **总计** | **约 800 行** |
### 5.2 维护效率提升
- 统一交互体验
- 减少重复 bug
- 便于功能迭代
- 新功能开发效率提升 30%
### 5.3 代码质量提升
- 提高代码可读性
- 降低耦合度
- 提升可测试性
---
## 六、风险评估与应对措施
| 风险点 | 影响 | 应对措施 |
|--------|------|----------|
| 组件耦合度高 | 提取困难 | 先解耦再提取,保持原有功能 |
| 样式差异大 | 统一困难 | 通过 props 配置主题色和样式 |
| 替换影响功能 | 回归测试 | 分阶段替换,每阶段充分测试 |
| 接口差异 | 逻辑复杂 | 使用配置化方式处理差异 |
---
## 七、下一步行动
1. **立即开始**:项目选择器组件提取
2. **本周完成**:统计卡片组件提取
3. **下周开始**:日期选择Mixin提取
4. **持续进行**:组件文档编写
---
*报告生成时间:2026-03-05*
*分析师:AI Assistant*

52
src/components/pickerArea/index.ts

@ -231,51 +231,37 @@ Component({
wx.ajax({ wx.ajax({
method: 'GET', method: 'GET',
url: '/app/common/common/area-list', url: '/app/common/common/area-list',
}).then((res: any[]) => { }).then((res: any) => {
// 将扁平数据转换为树形结构 // 接口直接返回树形结构数据
const areaTree = this.buildAreaTree(res) const areaList = res.data || res || []
// 转换数据格式以兼容组件
const areaTree = this.convertAreaData(areaList)
this.setData({ this.setData({
area: areaTree, area: areaTree,
}) })
this.getRangeList() this.getRangeList()
}) })
}, },
// 构建省市区树形结构 // 转换接口数据为组件需要的格式
buildAreaTree(areaList: any[]) { convertAreaData(areaList: any[]) {
if (!areaList || !areaList.length) return [] if (!areaList || !areaList.length) return []
// 构建以 parentAreaId 为 key 的映射 const convert = (list: any[]): any[] => {
const parentMap: { [key: string]: any[] } = {} return list.map((item) => {
areaList.forEach((item) => { const result: any = {
const parentId = item.parentAreaId || '0' name: item.label,
if (!parentMap[parentId]) { code: item.value,
parentMap[parentId] = [] value: item.value,
} label: item.label,
parentMap[parentId].push({ }
name: item.areaName, if (item.children && item.children.length > 0) {
code: item.areaId, result.children = convert(item.children)
value: item.areaId,
label: item.areaName,
level: item.level,
parentId: item.parentAreaId,
})
})
// 递归构建树
const buildTree = (parentId: string): any[] => {
const children = parentMap[parentId]
if (!children) return []
return children.map((item) => {
const subChildren = buildTree(item.code)
if (subChildren.length > 0) {
return { ...item, children: subChildren }
} }
return item return result
}) })
} }
return buildTree('0') return convert(areaList)
}, },
handleItem(e: any) { handleItem(e: any) {
const { code, name } = e.currentTarget.dataset const { code, name } = e.currentTarget.dataset

109
src/doctor/pages/home/index.ts

@ -18,13 +18,15 @@ Page({
// 待处理患者数 // 待处理患者数
pendingCount: 0, pendingCount: 0,
jumpPendingCount: 0,
enrollPendingCount: 0,
// 统计数据 // 累计统计数据
invitePatientCount: 0, invitePatientCount: 0,
jumpPatientCount: 0, jumpPatientCount: 0,
enrollPatientCount: 0, enrollPatientCount: 0,
// 适应症统计 // 累计适应症统计
indicationStats: [] as Array<{ indicationStats: [] as Array<{
indicationId: number indicationId: number
indicationName: string indicationName: string
@ -33,8 +35,27 @@ Page({
enrollPatientCount: number enrollPatientCount: number
}>, }>,
// 日/月度统计数据
dailyInvitePatientCount: 0,
dailyJumpPatientCount: 0,
dailyEnrollPatientCount: 0,
// 日/月度适应症统计
dailyIndicationStats: [] as Array<{
indicationId: number
indicationName: string
invitePatientCount: number
jumpPatientCount: number
enrollPatientCount: number
}>,
// 图表数据 // 图表数据
chartData: [] as Array<{ date: string; count: number }>, chartData: [] as Array<{
date: string
invitePatientCount: number
jumpPatientCount: number
enrollPatientCount: number
}>,
// 日期范围 - 邀约患者统计卡片(单日) // 日期范围 - 邀约患者统计卡片(单日)
startDate: '', startDate: '',
@ -131,7 +152,8 @@ Page({
// 获取其他数据 // 获取其他数据
this.getPendingCount() this.getPendingCount()
this.getStatistics() this.getTotalStatistics()
this.getDailyStatistics()
this.getPatientChart() this.getPatientChart()
}) })
}, },
@ -158,7 +180,8 @@ Page({
}) })
// 重新加载数据 // 重新加载数据
this.getPendingCount() this.getPendingCount()
this.getStatistics() this.getTotalStatistics()
this.getDailyStatistics()
this.getPatientChart() this.getPatientChart()
wx.showToast({ wx.showToast({
title: '切换成功', title: '切换成功',
@ -182,19 +205,23 @@ Page({
}) })
.then((res: any) => { .then((res: any) => {
this.setData({ this.setData({
pendingCount: res.count || 0, pendingCount: res.totalPendingCount || 0,
jumpPendingCount: res.jumpPendingCount || 0,
enrollPendingCount: res.enrollPendingCount || 0,
}) })
}) })
.catch(() => { .catch(() => {
// 接口失败时使用模拟数据 // 接口失败时使用默认值
this.setData({ this.setData({
pendingCount: 12, pendingCount: 0,
jumpPendingCount: 0,
enrollPendingCount: 0,
}) })
}) })
}, },
// 获取统计数据看板 // 获取累计统计数据看板
getStatistics() { getTotalStatistics() {
wx.ajax({ wx.ajax({
method: 'GET', method: 'GET',
url: '/app/pharmacist/pharmacist/statistics', url: '/app/pharmacist/pharmacist/statistics',
@ -208,6 +235,31 @@ Page({
}) })
}, },
// 获取日/月度统计数据看板
getDailyStatistics() {
// 根据统计类型格式化日期
const statDate =
this.data.statType === 'month'
? this.data.startMonth // YYYY-MM
: this.data.startDate // YYYY-MM-DD
wx.ajax({
method: 'GET',
url: '/app/pharmacist/pharmacist/patient-statistics',
data: {
statDate,
type: this.data.statType,
},
}).then((res: any) => {
this.setData({
dailyInvitePatientCount: res.invitePatientCount || 0,
dailyJumpPatientCount: res.jumpPatientCount || 0,
dailyEnrollPatientCount: res.enrollPatientCount || 0,
dailyIndicationStats: res.indicationStats || [],
})
})
},
// 获取邀约患者统计图表(使用 chart 的日期范围) // 获取邀约患者统计图表(使用 chart 的日期范围)
getPatientChart() { getPatientChart() {
// 根据统计类型格式化日期 // 根据统计类型格式化日期
@ -232,8 +284,10 @@ Page({
const list = res || [] const list = res || []
// 转换为图表需要的格式 // 转换为图表需要的格式
const chartData = list.map((item: any) => ({ const chartData = list.map((item: any) => ({
date: item.statDate || item.date, date: item.statDate,
count: item.invitePatientCount || item.count || 0, invitePatientCount: item.invitePatientCount || 0,
jumpPatientCount: item.jumpPatientCount || 0,
enrollPatientCount: item.enrollPatientCount || 0,
})) }))
this.setData({ this.setData({
chartData, chartData,
@ -284,7 +338,7 @@ Page({
chartStartMonth, chartStartMonth,
chartEndMonth, chartEndMonth,
}) })
this.getStatistics() this.getDailyStatistics()
this.getPatientChart() this.getPatientChart()
}, },
@ -301,8 +355,8 @@ Page({
startDate: fullDate, startDate: fullDate,
startMonth: monthValue, startMonth: monthValue,
}) })
// 重新加载统计数据 // 重新加载日/月度统计数据
this.getStatistics() this.getDailyStatistics()
}, },
// 切换到上一天/上月 // 切换到上一天/上月
@ -327,8 +381,8 @@ Page({
} }
this.setData({ startDate, startMonth }) this.setData({ startDate, startMonth })
// 重新加载统计数据 // 重新加载日/月度统计数据
this.getStatistics() this.getDailyStatistics()
}, },
// 切换到下一天/下月 // 切换到下一天/下月
@ -375,8 +429,8 @@ Page({
} }
this.setData({ startDate, startMonth }) this.setData({ startDate, startMonth })
// 重新加载统计数据 // 重新加载日/月度统计数据
this.getStatistics() this.getDailyStatistics()
}, },
// 图表日期选择变化(开始日期) // 图表日期选择变化(开始日期)
@ -442,10 +496,14 @@ Page({
}) })
canvas.setChart(chart) canvas.setChart(chart)
const x: string[] = [] const x: string[] = []
const y1: string[] = [] const y1: number[] = []
const y2: number[] = []
const y3: number[] = []
list.forEach((item) => { list.forEach((item) => {
x.push(item.date || item.StatMonth) x.push(item.date)
y1.push(item.count || item.MonthInvitePCount) y1.push(item.enrollPatientCount)
y2.push(item.jumpPatientCount)
y3.push(item.invitePatientCount)
}) })
const option = { const option = {
@ -519,7 +577,7 @@ Page({
stack: 'a', stack: 'a',
color: '#FF8A4C', color: '#FF8A4C',
barWidth: 12, barWidth: 12,
data: y1, data: y2,
}, },
{ {
name: '邀约患者数', name: '邀约患者数',
@ -528,7 +586,7 @@ Page({
width: 4, width: 4,
color: '#FFA64D', color: '#FFA64D',
barWidth: 12, barWidth: 12,
data: y1, data: y3,
}, },
], ],
dataZoom: { dataZoom: {
@ -553,8 +611,9 @@ Page({
}, },
handleStat() { handleStat() {
const { chartStartDate, chartEndDate, statType } = this.data
wx.navigateTo({ wx.navigateTo({
url: '/doctor/pages/stat/index', url: `/doctor/pages/stat/index?startDate=${chartStartDate}&endDate=${chartEndDate}&type=${statType}`,
}) })
}, },

16
src/doctor/pages/home/index.wxml

@ -38,13 +38,13 @@
<view class="col"> <view class="col">
<view class="col-center"> <view class="col-center">
<view class="name">跳转证明\n待上传</view> <view class="name">跳转证明\n待上传</view>
<view class="num">0</view> <view class="num">{{jumpPendingCount}}</view>
</view> </view>
</view> </view>
<view class="col"> <view class="col">
<view class="col-center"> <view class="col-center">
<view class="name">入组证明\n待上传</view> <view class="name">入组证明\n待上传</view>
<view class="num">0</view> <view class="num">{{enrollPendingCount}}</view>
</view> </view>
</view> </view>
</view> </view>
@ -53,7 +53,7 @@
<view class="s-header"> <view class="s-header">
<view class="title" bind:tap="handleFold" data-key="fold1"> <view class="title" bind:tap="handleFold" data-key="fold1">
累计邀约 累计邀约
<view class="fold {{fold1&&'active'}}"> <view class="fold {{fold1&&'active'}}" wx:if="{{indicationStats.length > 0}}">
{{fold1?'展开':'收起'}} {{fold1?'展开':'收起'}}
<van-icon class="icon" name="arrow-down" /> <van-icon class="icon" name="arrow-down" />
</view> </view>
@ -107,7 +107,7 @@
<view class="c-options"> <view class="c-options">
<view class="name" bind:tap="handleFold" data-key="fold2"> <view class="name" bind:tap="handleFold" data-key="fold2">
邀约患者统计 邀约患者统计
<view class="fold {{fold2&&'active'}}"> <view class="fold {{fold2&&'active'}}" wx:if="{{dailyIndicationStats.length > 0}}">
{{fold2?'展开':'收起'}} {{fold2?'展开':'收起'}}
<van-icon class="icon" name="arrow-down" /> <van-icon class="icon" name="arrow-down" />
</view> </view>
@ -124,21 +124,21 @@
<view class="row2"> <view class="row2">
<view class="col"> <view class="col">
<view class="name">邀约患者数</view> <view class="name">邀约患者数</view>
<view class="num">{{invitePatientCount}}</view> <view class="num">{{dailyInvitePatientCount}}</view>
</view> </view>
<view class="line"></view> <view class="line"></view>
<view class="col"> <view class="col">
<view class="name">跳转患者数</view> <view class="name">跳转患者数</view>
<view class="num">{{jumpPatientCount}}</view> <view class="num">{{dailyJumpPatientCount}}</view>
</view> </view>
<view class="line"></view> <view class="line"></view>
<view class="col"> <view class="col">
<view class="name">入组患者数</view> <view class="name">入组患者数</view>
<view class="num">{{enrollPatientCount}}</view> <view class="num">{{dailyEnrollPatientCount}}</view>
</view> </view>
</view> </view>
<view class="card-container {{fold2&&'fold'}}"> <view class="card-container {{fold2&&'fold'}}">
<view wx:for="{{indicationStats}}" wx:key="indicationId" class="row3"> <view wx:for="{{dailyIndicationStats}}" wx:key="indicationId" class="row3">
<view class="col"> <view class="col">
<view class="name">{{item.indicationName}}</view> <view class="name">{{item.indicationName}}</view>
<view class="num">{{item.invitePatientCount}}</view> <view class="num">{{item.invitePatientCount}}</view>

2
src/doctor/pages/loginForm/index.scss

@ -56,7 +56,7 @@ page {
font-size: 32rpx; font-size: 32rpx;
color: #342317; color: #342317;
} }
.picker, .picker-wrap,
.wrap { .wrap {
flex: 1; flex: 1;
display: flex; display: flex;

99
src/doctor/pages/loginForm/index.ts

@ -7,6 +7,13 @@ Page({
pharmacyId: '', pharmacyId: '',
pharmacyName: '', pharmacyName: '',
// 项目选择
projectId: '',
projectName: '',
projectList: [] as Array<{ projectId: number; projectName: string }>,
projectIndex: 0,
projectDisabled: false, // 是否禁用项目选择(扫码进入时)
// 药店选择弹窗 // 药店选择弹窗
show: false, show: false,
@ -36,7 +43,65 @@ Page({
}, },
}, },
onLoad() { onLoad() {
// 页面加载 // 如果通过扫码进入,存在项目ID,则回显并禁用选择
const projectId = app.globalData.projectId
if (projectId) {
this.setData({
projectId,
projectDisabled: true,
})
}
// 获取项目列表
this.getProjectList()
},
// 获取项目列表
getProjectList() {
wx.ajax({
method: 'GET',
url: '/app/common/common/project-list',
}).then((res: any) => {
const list = res.data || res || []
// 转换数据格式:接口返回 id/name,组件使用 projectId/projectName
const projectList = list.map((item: any) => ({
projectId: item.id,
projectName: item.name,
}))
// 如果有预设的项目ID(扫码进入),找到对应索引并回显
let projectIndex = 0
let projectId = ''
let projectName = ''
if (this.data.projectId) {
const index = projectList.findIndex((item: any) => String(item.projectId) === String(this.data.projectId))
if (index !== -1) {
projectIndex = index
projectId = projectList[index].projectId
projectName = projectList[index].projectName
}
}
this.setData({
projectList,
projectIndex,
projectId,
projectName,
})
})
},
// 项目选择变化
onProjectChange(e: WechatMiniprogram.CustomEvent) {
const index = e.detail.value
const project = this.data.projectList[index]
this.setData({
projectIndex: index,
projectId: project?.projectId || '',
projectName: project?.projectName || '',
})
}, },
// 输入姓名 // 输入姓名
handleNameInput(e: WechatMiniprogram.CustomEvent) { handleNameInput(e: WechatMiniprogram.CustomEvent) {
@ -88,13 +153,24 @@ Page({
const currentPage = this.data.page const currentPage = this.data.page
const total = res.total || 0 const total = res.total || 0
const pages = Math.ceil(total / this.data.pageSize) const pages = Math.ceil(total / this.data.pageSize)
const otherPharmacy = {
id: '-1', // 判断是否有筛选条件(关键词或省市区)
name: '其他药店', const hasFilter = this.data.keyword || this.data.provinceId || this.data.cityId || this.data.districtId
address: '没有找到所在药店',
// 只有在没有筛选条件且是最后一页时,才添加"其他药店"
const isLastPage = currentPage >= pages
const newList = [...this.data.pharmacyList, ...list]
if (hasFilter && isLastPage) {
const otherPharmacy = {
id: '-1',
name: '其他药店',
address: '没有找到所在药店',
}
newList.push(otherPharmacy)
} }
this.setData({ this.setData({
pharmacyList: [...this.data.pharmacyList, ...list, otherPharmacy], pharmacyList: newList,
total, total,
page: currentPage + 1, page: currentPage + 1,
hasMore: currentPage < pages, hasMore: currentPage < pages,
@ -161,7 +237,7 @@ Page({
}, },
// 提交注册 // 提交注册
handleSubmit() { handleSubmit() {
const { name, pharmacyId } = this.data const { name, pharmacyId, projectId, projectDisabled } = this.data
if (!name.trim()) { if (!name.trim()) {
wx.showToast({ wx.showToast({
@ -179,6 +255,14 @@ Page({
return return
} }
if (!projectId) {
wx.showToast({
title: '请选择项目',
icon: 'none',
})
return
}
wx.showLoading({ title: '提交中...' }) wx.showLoading({ title: '提交中...' })
wx.ajax({ wx.ajax({
@ -187,6 +271,7 @@ Page({
data: { data: {
name: name.trim(), name: name.trim(),
pharmacyId, pharmacyId,
projectId,
}, },
}) })
.then((res: any) => { .then((res: any) => {

35
src/doctor/pages/loginForm/index.wxml

@ -1,3 +1,5 @@
<page-meta page-style="{{ show ? 'overflow: hidden;' : '' }}" />
<navbar fixed custom-style="background:transparent" back> <navbar fixed custom-style="background:transparent" back>
<view class="page-order" slot="left"> <view class="page-order" slot="left">
<view class="item active">1</view> <view class="item active">1</view>
@ -13,7 +15,14 @@
<view class="row"> <view class="row">
<view class="label">您的姓名</view> <view class="label">您的姓名</view>
<view class="wrap"> <view class="wrap">
<input type="text" class="input" placeholder-class="place-input" placeholder="请输入姓名" value="{{name}}" bindinput="handleNameInput" /> <input
type="text"
class="input"
placeholder-class="place-input"
placeholder="请输入姓名"
value="{{name}}"
bindinput="handleNameInput"
/>
</view> </view>
</view> </view>
<view class="row"> <view class="row">
@ -30,6 +39,30 @@
<image class="icon" src="{{imageUrl}}icon2.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon2.png?t={{Timestamp}}"></image>
</view> </view>
</view> </view>
<view class="row">
<view class="label">选择项目</view>
<view class="picker-wrap">
<picker
class="picker"
mode="selector"
range="{{projectList}}"
range-key="projectName"
value="{{projectIndex}}"
bindchange="onProjectChange"
disabled="{{projectDisabled}}"
>
<input
disabled
type="text"
class="input"
placeholder-class="place-input"
placeholder="请选择项目"
value="{{projectName}}"
/>
</picker>
<image wx:if="{{!projectDisabled}}" class="icon" src="{{imageUrl}}icon2.png?t={{Timestamp}}"></image>
</view>
</view>
</view> </view>
<button class="phone" bind:tap="handleSubmit">立即加入药师端</button> <button class="phone" bind:tap="handleSubmit">立即加入药师端</button>
</view> </view>

31
src/doctor/pages/my/index.ts

@ -11,6 +11,9 @@ Page({
pharmacyName: '', pharmacyName: '',
}, },
// 当前项目信息
currentProjectName: '',
// 邀约人列表 // 邀约人列表
inviterList: [] as Array<{ inviterList: [] as Array<{
promoterName: string promoterName: string
@ -18,10 +21,29 @@ Page({
projectName: string projectName: string
}>, }>,
}, },
onLoad() { onShow() {
// 药店端我的页面,仅允许药店人员访问 // 药店端我的页面,仅允许药店人员访问
app.waitLogin({ types: [4] }).then(() => { app.waitLogin({ types: [4] }).then(() => {
this.getProfile() this.getProfile()
this.getProjectList()
})
},
// 获取项目列表
getProjectList() {
wx.ajax({
method: 'GET',
url: '/app/pharmacist/pharmacist/project-list',
}).then((res: any) => {
const projectList = res.list || []
const currentProjectId = res.currentProjectId || (projectList[0]?.projectId || 0)
const currentProject = projectList.find((item: any) => item.projectId === currentProjectId) || projectList[0]
this.setData({
currentProjectName: currentProject?.projectName || '',
})
// 获取邀约人信息
this.getInviterInfo() this.getInviterInfo()
}) })
}, },
@ -49,7 +71,12 @@ Page({
url: '/app/pharmacist/pharmacist/inviter-info', url: '/app/pharmacist/pharmacist/inviter-info',
}).then((res: any) => { }).then((res: any) => {
const list = res.list || res || [] const list = res.list || res || []
const inviterList = list.map((item: any) => ({ const currentProjectName = this.data.currentProjectName
// 过滤出当前项目的邀约人
const filteredList = list.filter((item: any) => item.projectName === currentProjectName)
const inviterList = filteredList.map((item: any) => ({
promoterName: item.promoterName || '', promoterName: item.promoterName || '',
promoterPhone: item.promoterPhone || '', promoterPhone: item.promoterPhone || '',
projectName: item.projectName || '', projectName: item.projectName || '',

5
src/doctor/pages/patientList/index.wxml

@ -1,3 +1,4 @@
<page-meta page-style="{{ popupShow ? 'overflow: hidden;' : '' }}" />
<navbar fixed custom-style="background: transparent;"> <navbar fixed custom-style="background: transparent;">
<picker <picker
slot="left" slot="left"
@ -75,10 +76,10 @@
<image class="icon" src="{{imageUrl}}icon2.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon2.png?t={{Timestamp}}"></image>
</view> </view>
<view class="row"> <view class="row">
<picker bindchange="handleTimeTypeChange" value="{{timeType}}" range="{{['跳转时间', '入组时间']}}"> <picker bindchange="handleTimeTypeChange" value="{{timeType}}" range="{{['跳转', '入组']}}">
<view class="col"> <view class="col">
<view class="label">时间类型:</view> <view class="label">时间类型:</view>
<view class="content">{{timeType === 0 ? '跳转时间' : '入组时间'}}</view> <view class="content">{{timeType === 0 ? '跳转' : '入组'}}</view>
<image class="icon" src="{{imageUrl}}icon2.png?t={{Timestamp}}"></image> <image class="icon" src="{{imageUrl}}icon2.png?t={{Timestamp}}"></image>
</view> </view>
</picker> </picker>

15
src/doctor/pages/stat/index.ts

@ -37,22 +37,27 @@ Page({
}, },
}, },
onLoad() { onLoad(options: any) {
// 药店端统计页面,仅允许药店人员访问 // 药店端统计页面,仅允许药店人员访问
app.waitLogin({ types: [4] }).then(() => { app.waitLogin({ types: [4] }).then(() => {
this.initDate() this.initDate(options)
}) })
}, },
// 初始化日期 // 初始化日期
initDate() { initDate(options: any) {
const today = this.formatDate(new Date()) const today = this.formatDate(new Date())
const startDate = this.formatDate(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
// 从URL参数获取日期,如果没有则使用默认值
const startDate = options?.startDate || this.formatDate(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
const endDate = options?.endDate || today
const statType = options?.type || 'day'
this.setData({ this.setData({
startDate, startDate,
endDate: today, endDate,
today, today,
statType,
}) })
this.getStatisticsList() this.getStatisticsList()
}, },

3
src/ground/pages/home/index.ts

@ -849,8 +849,9 @@ Page({
}, },
handleInfo() { handleInfo() {
const { chart1StartDate, chart1EndDate, statType } = this.data
wx.navigateTo({ wx.navigateTo({
url: '/ground/pages/stat/index', url: `/ground/pages/stat/index?startDate=${chart1StartDate}&endDate=${chart1EndDate}&type=${statType}`,
}) })
}, },

6
src/ground/pages/my/index.ts

@ -7,16 +7,12 @@ Page({
promoterAvatar: '', promoterAvatar: '',
promoterPhone: '', promoterPhone: '',
}, },
onLoad() { onShow() {
// 地推端我的页面,仅允许地推人员访问 // 地推端我的页面,仅允许地推人员访问
app.waitLogin({ types: [3] }).then(() => { app.waitLogin({ types: [3] }).then(() => {
this.getUserInfo() this.getUserInfo()
}) })
}, },
onShow() {
// 页面显示时刷新用户信息
this.getUserInfo()
},
// 获取个人信息 // 获取个人信息
getUserInfo() { getUserInfo() {
wx.ajax({ wx.ajax({

3
src/ground/pages/pharmacist/index.ts

@ -218,8 +218,9 @@ Page({
// 查看详情 // 查看详情
handleInfo(e: WechatMiniprogram.CustomEvent) { handleInfo(e: WechatMiniprogram.CustomEvent) {
const { id } = e.currentTarget.dataset const { id } = e.currentTarget.dataset
const { startDate, endDate } = this.data
wx.navigateTo({ wx.navigateTo({
url: `/ground/pages/stat/index?id=${id}`, url: `/ground/pages/stat/index?id=${id}&startDate=${startDate}&endDate=${endDate}`,
}) })
}, },
}) })

131
src/ground/pages/stat/index.ts

@ -2,6 +2,9 @@ const app = getApp<IAppOption>()
Page({ Page({
data: { data: {
// 药师ID(从药师列表进入时)
pharmacistId: 0,
// 时间筛选 // 时间筛选
startDate: '', startDate: '',
endDate: '', endDate: '',
@ -24,26 +27,45 @@ Page({
pages: 0, pages: 0,
}, },
}, },
onLoad() { onLoad(options: any) {
// 地推端统计页面,仅允许地推人员访问 // 地推端统计页面,仅允许地推人员访问
app.waitLogin({ types: [3] }).then(() => { app.waitLogin({ types: [3] }).then(() => {
// 设置默认时间范围(2026年3月至今) // 设置参数
this.setDefaultDateRange() this.setOptions(options)
// 获取列表数据(包含顶部统计数据) // 获取列表数据(包含顶部统计数据)
this.getList(true) this.getList(true)
}) })
}, },
// 设置默认时间范围(2026年3月至今)
setDefaultDateRange() { // 设置页面参数
const endDate = new Date() setOptions(options: any) {
const startDate = new Date('2026-03-01') // 设置时间范围
if (options?.startDate && options?.endDate) {
this.setData({ this.setData({
startDate: this.formatDate(startDate), startDate: options.startDate,
endDate: this.formatDate(endDate), endDate: options.endDate,
}) type: options.type || 'day',
})
} else {
// 默认时间范围(2026年3月至今)
const endDate = new Date()
const startDate = new Date('2026-03-01')
this.setData({
startDate: this.formatDate(startDate),
endDate: this.formatDate(endDate),
})
}
// 设置药师ID(从药师列表进入)
if (options?.id) {
this.setData({
pharmacistId: Number.parseInt(options.id),
})
}
}, },
// 格式化日期 // 格式化日期
formatDate(date: Date): string { formatDate(date: Date): string {
const year = date.getFullYear() const year = date.getFullYear()
@ -53,7 +75,7 @@ Page({
}, },
// 获取列表数据(包含顶部统计数据) // 获取列表数据(包含顶部统计数据)
getList(reset = false) { getList(reset = false) {
const { startDate, endDate, type, page, pageSize, pagination } = this.data const { pharmacistId, startDate, endDate, type, page, pageSize, pagination } = this.data
// 如果是重置(如筛选条件变化),先重置状态 // 如果是重置(如筛选条件变化),先重置状态
if (reset) { if (reset) {
@ -70,6 +92,25 @@ Page({
const currentPage = reset ? 1 : page const currentPage = reset ? 1 : page
// 根据是否有 pharmacistId 决定调用哪个接口
if (pharmacistId > 0) {
// 从药师列表进入 - 查看指定药师的统计
this.getPharmacistStatistics(pharmacistId, startDate, endDate, currentPage, pageSize, reset)
} else {
// 从首页进入 - 查看全部统计
this.getPatientStatisticsList(startDate, endDate, type, currentPage, pageSize, reset)
}
},
// 获取患者统计列表(从首页进入时使用)
getPatientStatisticsList(
startDate: string,
endDate: string,
type: string,
page: number,
pageSize: number,
reset: boolean
) {
wx.ajax({ wx.ajax({
method: 'GET', method: 'GET',
url: '/app/promoter/promoter/patient-statistics-list', url: '/app/promoter/promoter/patient-statistics-list',
@ -77,7 +118,7 @@ Page({
startDate, startDate,
endDate, endDate,
type, type,
page: currentPage, page,
pageSize, pageSize,
}, },
}).then((res: any) => { }).then((res: any) => {
@ -109,10 +150,68 @@ Page({
enrollPatientCount: summary.enrollPatientCount || 0, enrollPatientCount: summary.enrollPatientCount || 0,
// 列表数据 // 列表数据
list, list,
page: currentPage + 1, page: page + 1,
pagination: {
count: total,
page,
pages,
},
})
})
},
// 获取药师统计(从药师列表进入时使用)
getPharmacistStatistics(
pharmacistId: number,
startDate: string,
endDate: string,
page: number,
pageSize: number,
reset: boolean
) {
wx.ajax({
method: 'GET',
url: '/app/promoter/promoter/pharmacist-statistics',
data: {
pharmacistId,
startDate,
endDate,
page,
pageSize,
},
}).then((res: any) => {
const summary = res.summary || {}
const newList = res.list || []
const total = res.total || 0
// 转换数据格式
const formattedList = newList.map((item: any) => ({
date: item.statDate,
inviteCount: item.invitePatientCount || 0,
jumpCount: item.jumpPatientCount || 0,
enrollCount: item.enrollPatientCount || 0,
indicationStats: (item.indicationStats || []).map((ind: any) => ({
indicationName: ind.indicationName,
inviteCount: ind.invitePatientCount || 0,
jumpCount: ind.jumpPatientCount || 0,
enrollCount: ind.enrollPatientCount || 0,
})),
}))
const list = reset ? formattedList : [...this.data.list, ...formattedList]
const pages = Math.ceil(total / pageSize)
this.setData({
// 顶部统计数据从 summary 获取
invitePatientCount: summary.invitePatientCount || 0,
jumpPatientCount: summary.jumpPatientCount || 0,
enrollPatientCount: summary.enrollPatientCount || 0,
// 列表数据
list,
page: page + 1,
pagination: { pagination: {
count: total, count: total,
page: currentPage, page,
pages, pages,
}, },
}) })

1
src/pages/index/index.wxml

@ -1,3 +1,4 @@
<page-meta page-style="{{ popupShow ? 'overflow: hidden;' : '' }}" />
<navbar fixed custom-style="background:transparent" back> <navbar fixed custom-style="background:transparent" back>
<picker wx:if="{{projectList.length > 0}}" class="page-switch" slot="left" mode="selector" range="{{projectList}}" range-key="projectName" value="{{projectIndex}}" bindchange="onProjectChange"> <picker wx:if="{{projectList.length > 0}}" class="page-switch" slot="left" mode="selector" range="{{projectList}}" range-key="projectName" value="{{projectIndex}}" bindchange="onProjectChange">
{{currentProjectName}} {{currentProjectName}}

32
src/pages/tourists/index.scss

@ -9,6 +9,38 @@ page {
} }
.page { .page {
.user {
margin-bottom: -40rpx;
padding: 40rpx 40rpx 0;
display: flex;
align-items: center;
gap: 24rpx;
.avatar-wrapper {
margin: 0;
width: 104rpx;
height: 104rpx;
border-radius: 50%;
border: none;
outline: none;
background-color: transparent;
&::after {
border: none;
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.input {
font-size: 36rpx;
color: #ffffff;
}
.input-placeholder {
font-size: 36rpx;
color: #ffffff;
}
}
.page-banner { .page-banner {
width: 100vw; width: 100vw;
height: 1108rpx; height: 1108rpx;

11
src/pages/tourists/index.wxml

@ -2,7 +2,16 @@
<view slot="left" class="page-title">华观健康</view> <view slot="left" class="page-title">华观健康</view>
</navbar> </navbar>
<view class="page" style="background: url('{{imageUrl}}bg11.png?t={{Timestamp}}') no-repeat top center/100% 1624rpx;padding-top: {{pageTop+24}}px;"> <view
class="page"
style="background: url('{{imageUrl}}bg11.png?t={{Timestamp}}') no-repeat top center/100% 1624rpx;padding-top: {{pageTop+24}}px;"
>
<view class="user">
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image class="avatar" src="{{avatarUrl}}"></image>
</button>
<input type="nickname" class="input" placeholder-class="input-placeholder" placeholder="我的昵称" />
</view>
<image class="page-banner" src="{{imageUrl}}start1.png?t={{Timestamp}}"></image> <image class="page-banner" src="{{imageUrl}}start1.png?t={{Timestamp}}"></image>
<view class="page-options"> <view class="page-options">
<view class="o-item" bindtap="goToPharmacist">我是药店工作人员</view> <view class="o-item" bindtap="goToPharmacist">我是药店工作人员</view>

1
src/pages/work/index.wxml

@ -1,3 +1,4 @@
<page-meta page-style="{{ popupShow ? 'overflow: hidden;' : '' }}" />
<navbar fixed custom-style="background:transparent" back></navbar> <navbar fixed custom-style="background:transparent" back></navbar>
<view <view

Loading…
Cancel
Save