729 lines
17 KiB
Plaintext
729 lines
17 KiB
Plaintext
<!-- 养老管理系统 - 用药管理 -->
|
||
<template>
|
||
<view class="medication-management">
|
||
<view class="header">
|
||
<text class="title">用药管理</text>
|
||
<button class="add-btn" @click="showAddMedication">添加用药</button>
|
||
</view>
|
||
|
||
<!-- 筛选区域 -->
|
||
<view class="filter-section">
|
||
<view class="filter-item">
|
||
<text class="filter-label">老人:</text>
|
||
<picker-view class="picker" :value="selectedElderIndex" @change="onElderChange">
|
||
<picker-view-column>
|
||
<view v-for="(elder, index) in elderOptions" :key="elder.id" class="picker-item">
|
||
{{ elder.name }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
</view>
|
||
<view class="filter-item">
|
||
<text class="filter-label">状态:</text>
|
||
<picker-view class="picker" :value="selectedStatusIndex" @change="onStatusChange">
|
||
<picker-view-column>
|
||
<view v-for="(status, index) in statusOptions" :key="index" class="picker-item">
|
||
{{ status.label }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
</view>
|
||
<button class="search-btn" @click="searchMedications">搜索</button>
|
||
</view>
|
||
|
||
<!-- 用药列表 -->
|
||
<view class="medications-list">
|
||
<view v-for="medication in medications" :key="medication.id" class="medication-item" @click="viewMedicationDetail(medication)">
|
||
<view class="medication-header">
|
||
<text class="medication-name">{{ medication.medication_name }}</text>
|
||
<view class="status-badge" :class="getStatusClass(medication.status)">
|
||
<text class="status-text">{{ getStatusText(medication.status) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="medication-info">
|
||
<text class="elder-name">{{ getElderName(medication.elder_id) }}</text>
|
||
<text class="dosage">剂量: {{ medication.dosage ?? '未设置' }}</text>
|
||
<text class="frequency">频率: {{ medication.frequency ?? '未设置' }}</text>
|
||
</view>
|
||
<view class="medication-dates">
|
||
<text class="date-text">开始: {{ formatDate(medication.start_date) }}</text>
|
||
<text class="date-text">结束: {{ formatDate(medication.end_date) }}</text>
|
||
</view>
|
||
<view class="medication-actions">
|
||
<button class="action-btn edit-btn" @click.stop="editMedication(medication)">编辑</button>
|
||
<button class="action-btn log-btn" @click.stop="viewMedicationLogs(medication)">用药记录</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 添加/编辑用药弹窗 -->
|
||
<view v-if="showMedicationModal" class="modal-overlay" @click="closeMedicationModal">
|
||
<view class="modal-content" @click.stop="">
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ isEditMode ? '编辑用药' : '添加用药' }}</text>
|
||
<button class="close-btn" @click="closeMedicationModal">×</button>
|
||
</view>
|
||
<view class="modal-body">
|
||
<view class="form-group">
|
||
<text class="form-label">老人:</text>
|
||
<picker-view class="form-picker" :value="formData.elderIndex" @change="onFormElderChange">
|
||
<picker-view-column>
|
||
<view v-for="(elder, index) in elderOptions" :key="elder.id" class="picker-item">
|
||
{{ elder.name }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="form-label">药品名称:</text>
|
||
<input class="form-input" v-model="formData.medication_name" placeholder="请输入药品名称" />
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="form-label">剂量:</text>
|
||
<input class="form-input" v-model="formData.dosage" placeholder="如:500mg" />
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="form-label">频率:</text>
|
||
<input class="form-input" v-model="formData.frequency" placeholder="如:每日3次" />
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="form-label">给药途径:</text>
|
||
<picker-view class="form-picker" :value="formData.routeIndex" @change="onFormRouteChange">
|
||
<picker-view-column>
|
||
<view v-for="(route, index) in routeOptions" :key="index" class="picker-item">
|
||
{{ route.label }}
|
||
</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="form-label">开始日期:</text>
|
||
<lime-date-time-picker v-model="formData.medication_date" type="date" :placeholder="'选择用药日期'" />
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="form-label">结束日期:</text>
|
||
<lime-date-time-picker v-model="formData.next_medication_date" type="date" :placeholder="'选择下次用药日期'" />
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="form-label">用药说明:</text>
|
||
<textarea class="form-textarea" v-model="formData.instructions" placeholder="请输入用药说明"></textarea>
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="form-label">副作用注意:</text>
|
||
<textarea class="form-textarea" v-model="formData.side_effects" placeholder="请输入副作用注意事项"></textarea>
|
||
</view>
|
||
</view>
|
||
<view class="modal-footer">
|
||
<button class="cancel-btn" @click="closeMedicationModal">取消</button>
|
||
<button class="save-btn" @click="saveMedication">保存</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import type { Medication, Elder } from '../types.uts'
|
||
import { formatDate, getStatusClass } from '../types.uts'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
|
||
// 响应式数据
|
||
const medications = ref<Medication[]>([])
|
||
const elderOptions = ref<Elder[]>([])
|
||
const eldersMap = ref<Map<string, string>>(new Map())
|
||
|
||
// 筛选相关
|
||
const selectedElderIndex = ref([0])
|
||
const selectedStatusIndex = ref([0])
|
||
const statusOptions = [
|
||
{ value: 'all', label: '全部状态' },
|
||
{ value: 'active', label: '使用中' },
|
||
{ value: 'completed', label: '已完成' },
|
||
{ value: 'discontinued', label: '已停用' }
|
||
]
|
||
|
||
// 弹窗相关
|
||
const showMedicationModal = ref(false)
|
||
const isEditMode = ref(false)
|
||
const currentMedicationId = ref<string | null>(null)
|
||
|
||
// 表单数据
|
||
const formData = ref({
|
||
elderIndex: [0],
|
||
medication_name: '',
|
||
dosage: '',
|
||
frequency: '',
|
||
routeIndex: [0],
|
||
medication_date: '',
|
||
next_medication_date: '',
|
||
instructions: '',
|
||
side_effects: ''
|
||
})
|
||
|
||
const routeOptions = [
|
||
{ value: 'oral', label: '口服' },
|
||
{ value: 'injection', label: '注射' },
|
||
{ value: 'topical', label: '外用' }
|
||
]
|
||
|
||
// 页面加载
|
||
onLoad(() => {
|
||
loadData()
|
||
})
|
||
|
||
// 加载数据
|
||
async function loadData(): Promise<void> {
|
||
try {
|
||
await Promise.all([
|
||
loadElders(),
|
||
loadMedications()
|
||
])
|
||
} catch (error) {
|
||
console.error('加载数据失败:', error)
|
||
uni.showToast({
|
||
title: '加载数据失败',
|
||
icon: 'error'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 加载老人列表
|
||
async function loadElders(): Promise<void> {
|
||
const result = await supa.executeAs<Elder>('eldercare_admin', `
|
||
SELECT id, name FROM ec_elders
|
||
WHERE status = 'active'
|
||
ORDER BY name
|
||
`)
|
||
elderOptions.value = [{ id: '', name: '全部老人' } as Elder, ...result]
|
||
|
||
// 建立映射
|
||
const map = new Map<string, string>()
|
||
for (let i: Int = 0; i < result.length; i++) {
|
||
const elder = result[i]
|
||
map.set(elder.id, elder.name)
|
||
}
|
||
eldersMap.value = map
|
||
}
|
||
|
||
// 加载用药记录
|
||
async function loadMedications(): Promise<void> {
|
||
let whereClause = "WHERE 1=1"
|
||
|
||
// 老人筛选
|
||
if (selectedElderIndex.value[0] > 0) {
|
||
const selectedElder = elderOptions.value[selectedElderIndex.value[0]]
|
||
whereClause += ` AND elder_id = '${selectedElder.id}'`
|
||
}
|
||
|
||
// 状态筛选
|
||
if (selectedStatusIndex.value[0] > 0) {
|
||
const selectedStatus = statusOptions[selectedStatusIndex.value[0]]
|
||
whereClause += ` AND status = '${selectedStatus.value}'`
|
||
}
|
||
|
||
const result = await supa.executeAs<Medication>('eldercare_admin', `
|
||
SELECT * FROM ec_medications
|
||
${whereClause}
|
||
ORDER BY created_at DESC
|
||
`)
|
||
medications.value = result
|
||
}
|
||
|
||
// 获取老人姓名
|
||
function getElderName(elderId: string): string {
|
||
return eldersMap.value.get(elderId) ?? '未知老人'
|
||
}
|
||
|
||
// 获取状态文本
|
||
function getStatusText(status: string): string {
|
||
const statusMap: Record<string, string> = {
|
||
'active': '使用中',
|
||
'completed': '已完成',
|
||
'discontinued': '已停用'
|
||
}
|
||
return statusMap[status] ?? status
|
||
}
|
||
|
||
// 筛选事件
|
||
function onElderChange(e: any): void {
|
||
selectedElderIndex.value = e.detail.value
|
||
}
|
||
|
||
function onStatusChange(e: any): void {
|
||
selectedStatusIndex.value = e.detail.value
|
||
}
|
||
|
||
// 搜索用药记录
|
||
function searchMedications(): void {
|
||
loadMedications()
|
||
}
|
||
|
||
// 查看用药详情
|
||
function viewMedicationDetail(medication: Medication): void {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/medication/detail?id=${medication.id}`
|
||
})
|
||
}
|
||
|
||
// 编辑用药
|
||
function editMedication(medication: Medication): void {
|
||
isEditMode.value = true
|
||
currentMedicationId.value = medication.id
|
||
|
||
// 填充表单数据
|
||
const elderIndex = elderOptions.value.findIndex(elder => elder.id === medication.elder_id)
|
||
const routeIndex = routeOptions.findIndex(route => route.value === medication.route)
|
||
|
||
formData.value = {
|
||
elderIndex: [elderIndex > 0 ? elderIndex : 0],
|
||
medication_name: medication.medication_name,
|
||
dosage: medication.dosage ?? '',
|
||
frequency: medication.frequency ?? '',
|
||
routeIndex: [routeIndex > 0 ? routeIndex : 0],
|
||
medication_date: medication.start_date ?? '',
|
||
next_medication_date: medication.end_date ?? '',
|
||
instructions: medication.instructions ?? '',
|
||
side_effects: medication.side_effects ?? ''
|
||
}
|
||
|
||
showMedicationModal.value = true
|
||
}
|
||
|
||
// 查看用药日志
|
||
function viewMedicationLogs(medication: Medication): void {
|
||
uni.navigateTo({
|
||
url: `/pages/ec/medication/logs?id=${medication.id}`
|
||
})
|
||
}
|
||
|
||
// 显示添加用药弹窗
|
||
function showAddMedication(): void {
|
||
isEditMode.value = false
|
||
currentMedicationId.value = null
|
||
|
||
// 重置表单
|
||
formData.value = {
|
||
elderIndex: [0],
|
||
medication_name: '',
|
||
dosage: '',
|
||
frequency: '',
|
||
routeIndex: [0],
|
||
medication_date: '',
|
||
next_medication_date: '',
|
||
instructions: '',
|
||
side_effects: ''
|
||
}
|
||
|
||
showMedicationModal.value = true
|
||
}
|
||
|
||
// 关闭弹窗
|
||
function closeMedicationModal(): void {
|
||
showMedicationModal.value = false
|
||
}
|
||
|
||
// 表单事件
|
||
function onFormElderChange(e: any): void {
|
||
formData.value.elderIndex = e.detail.value
|
||
}
|
||
|
||
function onFormRouteChange(e: any): void {
|
||
formData.value.routeIndex = e.detail.value
|
||
}
|
||
|
||
// 保存用药记录
|
||
async function saveMedication(): Promise<void> {
|
||
// 验证表单
|
||
if (formData.value.medication_name.trim() === '') {
|
||
uni.showToast({
|
||
title: '请输入药品名称',
|
||
icon: 'error'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (formData.value.elderIndex[0] === 0) {
|
||
uni.showToast({
|
||
title: '请选择老人',
|
||
icon: 'error'
|
||
})
|
||
return
|
||
}
|
||
|
||
try {
|
||
const selectedElder = elderOptions.value[formData.value.elderIndex[0]]
|
||
const selectedRoute = routeOptions[formData.value.routeIndex[0]]
|
||
|
||
if (isEditMode.value && currentMedicationId.value !== null) {
|
||
// 更新用药记录
|
||
await supa.executeAs('eldercare_admin', `
|
||
UPDATE ec_medications SET
|
||
elder_id = '${selectedElder.id}',
|
||
medication_name = '${formData.value.medication_name}',
|
||
dosage = '${formData.value.dosage}',
|
||
frequency = '${formData.value.frequency}',
|
||
route = '${selectedRoute.value}',
|
||
start_date = ${formData.value.medication_date ? `'${formData.value.medication_date}'` : 'NULL'},
|
||
end_date = ${formData.value.next_medication_date ? `'${formData.value.next_medication_date}'` : 'NULL'},
|
||
instructions = '${formData.value.instructions}',
|
||
side_effects = '${formData.value.side_effects}',
|
||
updated_at = NOW()
|
||
WHERE id = '${currentMedicationId.value}'
|
||
`)
|
||
} else {
|
||
// 新增用药记录
|
||
await supa.executeAs('eldercare_admin', `
|
||
INSERT INTO ec_medications (
|
||
elder_id, medication_name, dosage, frequency, route,
|
||
start_date, end_date, instructions, side_effects, status
|
||
) VALUES (
|
||
'${selectedElder.id}',
|
||
'${formData.value.medication_name}',
|
||
'${formData.value.dosage}',
|
||
'${formData.value.frequency}',
|
||
'${selectedRoute.value}',
|
||
${formData.value.medication_date ? `'${formData.value.medication_date}'` : 'NULL'},
|
||
${formData.value.next_medication_date ? `'${formData.value.next_medication_date}'` : 'NULL'},
|
||
'${formData.value.instructions}',
|
||
'${formData.value.side_effects}',
|
||
'active'
|
||
)
|
||
`)
|
||
}
|
||
|
||
uni.showToast({
|
||
title: '保存成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
closeMedicationModal()
|
||
loadMedications()
|
||
} catch (error) {
|
||
console.error('保存失败:', error)
|
||
uni.showToast({
|
||
title: '保存失败',
|
||
icon: 'error'
|
||
})
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.medication-management {
|
||
padding: 20px;
|
||
background-color: #f5f5f5;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.title {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.add-btn {
|
||
background-color: #2196f3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 10px 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.filter-section {
|
||
background-color: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-right: 20px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.picker {
|
||
width: 120px;
|
||
height: 40px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.picker-item {
|
||
padding: 10px;
|
||
text-align: center;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.search-btn {
|
||
background-color: #4caf50;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 10px 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.medications-list {
|
||
background-color: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.medication-item {
|
||
padding: 15px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.medication-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.medication-name {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.status-text {
|
||
color: white;
|
||
}
|
||
|
||
.status-active {
|
||
background-color: #4caf50;
|
||
}
|
||
|
||
.status-completed {
|
||
background-color: #2196f3;
|
||
}
|
||
|
||
.status-discontinued {
|
||
background-color: #f44336;
|
||
}
|
||
|
||
.medication-info {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.elder-name, .dosage, .frequency {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.medication-dates {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.date-text {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.medication-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.action-btn {
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.edit-btn {
|
||
background-color: #ff9800;
|
||
color: white;
|
||
}
|
||
|
||
.log-btn {
|
||
background-color: #2196f3;
|
||
color: white;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0,0,0,0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
border-radius: 12px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
max-height: 80%;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
color: #999;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 14px;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
display: block;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
height: 40px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
padding: 0 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-picker {
|
||
width: 100%;
|
||
height: 40px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.form-date-picker {
|
||
width: 100%;
|
||
}
|
||
|
||
.form-textarea {
|
||
width: 100%;
|
||
height: 80px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
padding: 20px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.cancel-btn, .save-btn {
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 10px 20px;
|
||
font-size: 14px;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.cancel-btn {
|
||
background-color: #f5f5f5;
|
||
color: #666;
|
||
}
|
||
|
||
.save-btn {
|
||
background-color: #2196f3;
|
||
color: white;
|
||
}
|
||
|
||
/* 小屏幕适配 */
|
||
@media (max-width: 768px) {
|
||
.medication-management {
|
||
padding: 15px;
|
||
}
|
||
|
||
.filter-section {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.filter-item {
|
||
margin-right: 0;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.picker {
|
||
width: 150px;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 95%;
|
||
}
|
||
|
||
.medication-actions {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.action-btn {
|
||
margin-bottom: 5px;
|
||
}
|
||
}
|
||
</style>
|