Files
intc-vue-h5/src/pages/work/health/medicationPlan/list.vue
bot5 d12bf90050 feat(health): 新增用药计划模块
- 新增用药计划和用药记录API接口
- 新增用药计划列表/详情/新增编辑页面
- 新增用药记录列表页面(含今日待服药快捷卡片)
- 支持服药/跳过操作
- 更新pages.json路由配置
2026-03-18 20:39:45 +08:00

569 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<u-sticky offsetTop="0rpx" customNavHeight="0rpx">
<view class="search-view">
<u--input v-model="queryParams.keys" border="false" placeholder="搜索计划/药品名称" class="search-input"
@blur="searchBlur" suffixIcon="search" suffixIconStyle="color: #909399">
</u--input>
<view class="filter-btn" @click="filterPanel = !filterPanel">
<uni-icons type="list" size="18" color="#667eea"></uni-icons>
<text>筛选</text>
</view>
<u-transition :show="filterPanel" mode="fade">
<view class="filter-panel" :style="{ height: `${windowHeight - 42}px` }">
<view class="filter-panel-content">
<view class="filter-title">成员</view>
<view class="state-list">
<view v-for="item in memberList" :key="item.id" class="state-item"
:class="item.selected ? 'active' : ''" @click="selectMember(item)">{{ item.name }}</view>
</view>
<view class="filter-title">状态</view>
<view class="state-list">
<view v-for="item in statusList" :key="item.value" class="state-item"
:class="item.selected ? 'active' : ''" @click="selectStatus(item)">{{ item.label }}</view>
</view>
</view>
<view class="btn-box">
<view class="btn-reset" @click="resetQuery()">
<uni-icons type="reload" size="16" color="#909399"></uni-icons>
<text>重置</text>
</view>
<view class="btn-confirm" @click="searchSubmit()">
<uni-icons type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
<text>确定</text>
</view>
</view>
</view>
</u-transition>
</view>
</u-sticky>
<u-list @scrolltolower="loadmore" :spaceHeight="116" lowerThreshold="100">
<u-list-item v-for="(item, index) in listData" :key="index">
<view class="list-item" @click="enterDetails(item)">
<view class="item-header">
<view class="header-row">
<view class="plan-icon">
<uni-icons type="calendar" size="20" color="#ffffff"></uni-icons>
</view>
<text class="plan-name">{{ item.planName || item.medicineDisplayName || '用药计划' }}</text>
<text class="status-tag" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</text>
</view>
<view class="info-row">
<text class="member-name">{{ item.personName || '--' }}</text>
<text class="medicine-name">{{ item.shortName }}-{{ item.brand }}</text>
</view>
</view>
<view class="card-body">
<view class="info-row">
<view class="info-item">
<text class="info-label">剂量</text>
<text class="info-value">{{ item.dosage }}{{ item.dosageUnit }} × {{ item.frequency }}/</text>
</view>
<view class="info-item">
<text class="info-label">今日进度</text>
<text class="info-value" v-if="item.status === '1'">{{ item.todayTakenCount || 0 }}/{{ item.todayTotalCount || 0 }}</text>
<text class="info-value" v-else>--</text>
</view>
<view class="info-item">
<text class="info-label">开始日期</text>
<text class="info-value">{{ formatDate(item.startDate) }}</text>
</view>
<view class="info-item">
<text class="info-label">结束日期</text>
<text class="info-value">{{ item.endDate ? formatDate(item.endDate) : '长期' }}</text>
</view>
</view>
</view>
<view class="operate" v-if="item.status === '1'" @click.stop>
<view class="btn-today" @click="handleTodayRecords(item)">
<uni-icons type="checkmarkempty" size="16" color="#52c41a"></uni-icons>
<text>今日记录</text>
</view>
<view class="btn-pause" @click="handlePause(item)">
<uni-icons type="hand-up" size="16" color="#faad14"></uni-icons>
<text>暂停</text>
</view>
</view>
</view>
</u-list-item>
<u-loadmore :status="status" loadingIcon="semicircle" height="88" fontSize="32rpx" @loadmore="loadmore" />
</u-list>
<!-- 新增按钮 -->
<view class="add-btn" @click="handleAdd">
<uni-icons type="plus" size="24" color="#ffffff"></uni-icons>
</view>
</view>
<suspend></suspend>
<refresh></refresh>
<u-toast ref="uToast"></u-toast>
</template>
<script setup>
import { listMedicationPlan, listPerson, pauseMedicationPlan, resumeMedicationPlan, endMedicationPlan, delMedicationPlan } from '@/api/health/medicationPlan'
import { onLoad, onShow } from "@dcloudio/uni-app"
import { reactive, toRefs, ref, computed } from "vue"
const pageNum = ref(1)
const listData = ref([])
const isShow = ref(false)
const status = ref('loadmore')
const memberList = ref([])
const filterPanel = ref(false)
const statusList = ref([
{ value: '1', label: '进行中', selected: false },
{ value: '2', label: '已结束', selected: false },
{ value: '3', label: '已暂停', selected: false }
])
const data = reactive({
queryParams: {
keys: null,
personId: null,
status: null
}
})
const { queryParams } = toRefs(data)
const windowHeight = computed(() => {
return uni.getSystemInfoSync().windowHeight - 50
})
onLoad(() => {
getMemberList()
getList()
})
onShow(() => {
if (isShow.value) {
listData.value = []
pageNum.value = 1
getList()
isShow.value = false
}
})
function loadmore() {
pageNum.value += 1
if (status.value == 'loadmore') {
getList()
}
}
function getList() {
status.value = 'loading'
listMedicationPlan({ pageSize: 10, pageNum: pageNum.value, ...queryParams.value }).then(res => {
listData.value = listData.value.concat(res.rows)
if (listData.value.length < res.total) {
status.value = 'loadmore'
} else {
status.value = 'nomore'
}
}).catch(() => {
status.value = 'nomore'
})
}
function getMemberList() {
listPerson({ pageNum: 1, pageSize: 100 }).then(res => {
memberList.value = res.rows.map(item => ({
...item,
selected: false
}))
})
}
function selectMember(item) {
queryParams.value.personId = item.selected ? null : item.id
memberList.value.forEach(ele => {
ele.selected = ele.id === item.id && !item.selected
})
}
function selectStatus(item) {
queryParams.value.status = item.selected ? null : item.value
statusList.value.forEach(ele => {
ele.selected = ele.value === item.value && !item.selected
})
}
function searchSubmit() {
pageNum.value = 1
listData.value = []
getList()
filterPanel.value = false
}
function searchBlur() {
pageNum.value = 1
listData.value = []
getList()
}
function resetQuery() {
queryParams.value.keys = null
queryParams.value.personId = null
queryParams.value.status = null
memberList.value.forEach(ele => ele.selected = false)
statusList.value.forEach(ele => ele.selected = false)
}
function formatDate(date) {
if (!date) return '--'
return date.substring(0, 10)
}
function getStatusClass(status) {
const classes = { '1': 'status-active', '2': 'status-ended', '3': 'status-paused' }
return classes[status] || ''
}
function getStatusText(status) {
const texts = { '1': '进行中', '2': '已结束', '3': '已暂停' }
return texts[status] || '--'
}
function enterDetails(item) {
uni.navigateTo({ url: `/pages/work/health/medicationPlan/details?id=${item.id}` })
}
function handleAdd() {
uni.navigateTo({ url: '/pages/work/health/medicationPlan/addEdit' })
isShow.value = true
}
function handleTodayRecords(item) {
uni.navigateTo({ url: `/pages/work/health/medicationRecord/index?planId=${item.id}` })
}
function handlePause(item) {
uni.showModal({
title: '确认暂停',
content: '确定暂停该用药计划?',
confirmText: '暂停',
cancelText: '取消',
confirmColor: '#faad14',
success: function (res) {
if (res.confirm) {
pauseMedicationPlan(item.id).then(() => {
uni.showToast({ title: '已暂停', icon: 'success' })
listData.value = []
pageNum.value = 1
getList()
})
}
}
})
}
function handleResume(item) {
resumeMedicationPlan(item.id).then(() => {
uni.showToast({ title: '已恢复', icon: 'success' })
listData.value = []
pageNum.value = 1
getList()
})
}
</script>
<style lang="scss" scoped>
page {
height: 100%;
overflow: auto;
}
.search-view {
padding: 12rpx 32rpx;
background-color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 100;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.search-input {
background: rgba(102, 126, 234, 0.08);
color: #333333;
flex: 1;
margin-right: 16rpx;
border-radius: 24rpx;
border: 2rpx solid rgba(102, 126, 234, 0.3);
height: 66rpx !important;
display: flex;
align-items: center;
}
.filter-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 12rpx 24rpx;
background: rgba(102, 126, 234, 0.08);
border-radius: 24rpx;
border: 2rpx solid rgba(102, 126, 234, 0.3);
text {
color: #667eea;
font-size: 28rpx;
font-weight: 600;
}
}
.filter-panel {
width: 100%;
position: absolute;
left: 0;
top: 96rpx;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
.filter-panel-content {
background-color: #ffffff;
padding: 0 30rpx 30rpx;
border-radius: 16rpx 16rpx 0 0;
.filter-title {
color: #2c3e50;
font-size: 32rpx;
font-weight: 600;
padding: 32rpx 0 24rpx 20rpx;
position: relative;
display: flex;
align-items: center;
&::before {
content: '';
width: 6rpx;
height: 32rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 3rpx;
margin-right: 12rpx;
}
}
.state-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.state-item {
padding: 0 32rpx;
height: 68rpx;
border: 2rpx solid #e8edf3;
border-radius: 34rpx;
text-align: center;
line-height: 68rpx;
font-size: 28rpx;
color: #666666;
background: #ffffff;
}
.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: 2rpx solid transparent;
color: #ffffff;
font-weight: 600;
}
}
}
.btn-box {
display: flex;
gap: 20rpx;
padding: 24rpx 30rpx;
background-color: #fff;
.btn-reset, .btn-confirm {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
height: 88rpx;
border-radius: 12rpx;
font-size: 30rpx;
font-weight: 600;
flex: 1;
}
.btn-reset {
background: #f5f7fa;
border: 2rpx solid #dcdfe6;
text { color: #606266; }
}
.btn-confirm {
background: #667eea;
text { color: #ffffff; }
}
}
}
}
.list-item {
margin: 10rpx 24rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.item-header {
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.header-row {
display: flex;
align-items: center;
gap: 10rpx;
}
.plan-icon {
width: 40rpx;
height: 40rpx;
background: rgba(255, 255, 255, 0.25);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.plan-name {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-tag {
font-size: 22rpx;
font-weight: 500;
padding: 4rpx 16rpx;
border-radius: 16rpx;
&.status-active {
background: rgba(82, 196, 26, 0.25);
color: #52c41a;
}
&.status-ended {
background: rgba(140, 140, 140, 0.25);
color: #bfbfbf;
}
&.status-paused {
background: rgba(250, 173, 20, 0.25);
color: #faad14;
}
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12rpx;
padding-top: 12rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
margin-top: 12rpx;
}
.member-name {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.85);
}
.medicine-name {
font-size: 24rpx;
color: #ffffff;
font-weight: 500;
}
}
.card-body {
padding: 24rpx;
background: #fafbfc;
border: 2rpx solid #f0f2f5;
margin: 15rpx 16rpx;
border-radius: 12rpx;
.info-row {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
.info-item {
flex: 0 0 calc(50% - 12rpx);
display: flex;
flex-direction: column;
gap: 4rpx;
.info-label {
font-size: 24rpx;
color: #667eea;
font-weight: 500;
background: rgba(102, 126, 234, 0.08);
padding: 6rpx 12rpx;
border-radius: 8rpx;
align-self: flex-start;
}
.info-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
}
}
.operate {
display: flex;
justify-content: flex-end;
padding: 16rpx 24rpx;
gap: 16rpx;
.btn-today, .btn-pause {
display: flex;
align-items: center;
gap: 6rpx;
padding: 12rpx 24rpx;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: 500;
}
.btn-today {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
}
.btn-pause {
background: rgba(250, 173, 20, 0.1);
color: #faad14;
}
}
}
.add-btn {
position: fixed;
right: 32rpx;
bottom: 120rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
z-index: 100;
}
</style>