Files
akmon/pages/ec/medication/management.uvue
2026-01-20 08:04:15 +08:00

729 lines
17 KiB
Plaintext
Raw Permalink 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="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>