Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,728 @@
<!-- 养老管理系统 - 用药管理 -->
<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>