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

956 lines
22 KiB
Plaintext

<template>
<view class="incident-report">
<!-- Header -->
<view class="header">
<view class="header-content">
<text class="header-title">事件报告</text>
<text class="header-subtitle">记录和跟踪护理事件</text>
</view>
</view>
<!-- Quick Stats -->
<view class="stats-section">
<view class="stats-grid">
<view class="stat-card urgent">
<text class="stat-number">{{ stats.urgent_incidents }}</text>
<text class="stat-label">紧急事件</text>
<text class="stat-trend">今日新增</text>
</view>
<view class="stat-card pending">
<text class="stat-number">{{ stats.pending_reports }}</text>
<text class="stat-label">待处理</text>
<text class="stat-trend">需要跟进</text>
</view>
<view class="stat-card resolved">
<text class="stat-number">{{ stats.resolved_today }}</text>
<text class="stat-label">今日解决</text>
<text class="stat-trend">已完成</text>
</view>
</view>
</view>
<!-- New Incident Button -->
<view class="new-incident-section">
<view class="new-incident-btn" @tap="showNewIncidentModal">
<text class="new-incident-icon">📝</text>
<text class="new-incident-text">新建事件报告</text>
</view>
</view>
<!-- Filter Tabs -->
<view class="filter-section">
<scroll-view class="filter-scroll" scroll-x="true">
<view class="filter-tabs">
<view v-for="filter in filterOptions" :key="filter.value"
class="filter-tab" :class="{active: currentFilter === filter.value}"
@tap="setFilter(filter.value)">
<text class="filter-text">{{ filter.label }}</text>
<text v-if="filter.count > 0" class="filter-count">{{ filter.count }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- Incidents List -->
<view class="incidents-section">
<view v-if="filteredIncidents.length === 0" class="empty-state">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无事件记录</text>
<text class="empty-subtitle">点击上方按钮创建新的事件报告</text>
</view>
<view v-else class="incidents-list">
<view v-for="incident in filteredIncidents" :key="incident.id"
class="incident-card" @tap="viewIncidentDetail(incident)">
<view class="incident-header">
<view class="incident-info">
<text class="incident-title">{{ incident.title }}</text>
<text class="incident-elder">{{ incident.elder_name }}</text>
</view>
<view class="incident-priority" :class="getPriorityClass(incident.priority)">
<text class="priority-text">{{ getPriorityText(incident.priority) }}</text>
</view>
</view>
<view class="incident-content">
<text class="incident-description">{{ incident.description }}</text>
</view>
<view class="incident-meta">
<view class="incident-time">
<text class="time-label">发生时间:</text>
<text class="time-value">{{ formatDateTime(incident.incident_time) }}</text>
</view>
<view class="incident-reporter">
<text class="reporter-label">报告人:</text>
<text class="reporter-value">{{ incident.reporter_name }}</text>
</view>
</view>
<view class="incident-footer">
<view class="incident-status" :class="getStatusClass(incident.status)">
<text class="status-text">{{ getStatusText(incident.status) }}</text>
</view>
<view class="incident-actions">
<text class="action-time">{{ formatTime(incident.created_at) }}</text>
<text v-if="incident.status === 'pending'" class="action-follow" @tap.stop="followUpIncident(incident)">跟进</text>
</view>
</view>
</view>
</view>
</view>
<!-- New Incident Modal -->
<view v-if="showModal" class="modal-overlay" @tap="hideModal">
<view class="modal-content" @tap.stop>
<view class="modal-header">
<text class="modal-title">新建事件报告</text>
<text class="modal-close" @tap="hideModal">✕</text>
</view>
<scroll-view class="modal-body">
<view class="form-group">
<text class="form-label">事件标题 *</text>
<input class="form-input" v-model="newIncident.title" placeholder="请输入事件标题" />
</view>
<view class="form-group">
<text class="form-label">相关老人 *</text>
<picker class="form-picker" mode="selector" :value="elderIndex" :range="elderOptions" range-key="name" @change="onElderChange">
<view class="picker-display">
<text class="picker-text">{{ elderOptions[elderIndex]?.name || '请选择老人' }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="form-group">
<text class="form-label">优先级 *</text>
<picker class="form-picker" mode="selector" :value="priorityIndex" :range="priorityOptions" @change="onPriorityChange">
<view class="picker-display">
<text class="picker-text">{{ priorityOptions[priorityIndex] }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="form-group">
<text class="form-label">事件类型</text>
<picker class="form-picker" mode="selector" :value="typeIndex" :range="typeOptions" @change="onTypeChange">
<view class="picker-display">
<text class="picker-text">{{ typeOptions[typeIndex] }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="form-group">
<text class="form-label">发生时间</text>
<picker class="form-picker" mode="datetime" :value="newIncident.incident_time" @change="onTimeChange">
<view class="picker-display">
<text class="picker-text">{{ formatDateTime(newIncident.incident_time) || '选择时间' }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="form-group">
<text class="form-label">事件描述 *</text>
<textarea class="form-textarea" v-model="newIncident.description" placeholder="请详细描述事件经过..." />
</view>
<view class="form-group">
<text class="form-label">处理措施</text>
<textarea class="form-textarea" v-model="newIncident.action_taken" placeholder="请描述已采取的处理措施..." />
</view>
</scroll-view>
<view class="modal-footer">
<button class="btn btn-cancel" @tap="hideModal">取消</button>
<button class="btn btn-submit" @tap="submitIncident" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交报告' }}
</button>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import { IncidentReport, ElderInfo, IncidentStats } from '../types.uts'
export default {
data() {
return {
currentFilter: 'all',
showModal: false,
isSubmitting: false,
elderIndex: 0,
priorityIndex: 0,
typeIndex: 0,
stats: {
urgent_incidents: 0,
pending_reports: 0,
resolved_today: 0
} as IncidentStats,
incidents: [] as IncidentReport[],
elderOptions: [] as ElderInfo[],
newIncident: {
title: '',
elder_id: '',
priority: 'normal',
incident_type: 'medical',
description: '',
action_taken: '',
incident_time: new Date().toISOString()
} as IncidentReport,
filterOptions: [
{ label: '全部', value: 'all', count: 0 },
{ label: '待处理', value: 'pending', count: 0 },
{ label: '处理中', value: 'in_progress', count: 0 },
{ label: '已解决', value: 'resolved', count: 0 },
{ label: '紧急', value: 'urgent', count: 0 }
],
priorityOptions: ['普通', '高', '紧急', '危急'],
typeOptions: ['医疗事件', '安全事件', '行为事件', '设备故障', '其他']
}
},
computed: {
filteredIncidents(): IncidentReport[] {
if (this.currentFilter === 'all') {
return this.incidents
}
if (this.currentFilter === 'urgent') {
return this.incidents.filter(incident => incident.priority === 'urgent' || incident.priority === 'critical')
}
return this.incidents.filter(incident => incident.status === this.currentFilter)
}
},
onLoad() {
this.loadIncidentData()
},
onShow() {
this.loadIncidentData()
},
methods: {
async loadIncidentData() {
try {
await Promise.all([
this.loadIncidentStats(),
this.loadIncidents(),
this.loadElderOptions()
])
this.updateFilterCounts()
} catch (error) {
console.error('加载事件数据失败:', error)
}
},
async loadIncidentStats() {
const result = await supa.executeAs('incident_stats', {})
if (result.success && result.data.length > 0) {
this.stats = result.data[0] as IncidentStats
}
},
async loadIncidents() {
const result = await supa.executeAs('incident_reports', {})
if (result.success) {
this.incidents = result.data as IncidentReport[]
}
},
async loadElderOptions() {
const result = await supa.executeAs('active_elders', {})
if (result.success) {
this.elderOptions = result.data as ElderInfo[]
}
},
updateFilterCounts() {
this.filterOptions.forEach(filter => {
if (filter.value === 'all') {
filter.count = this.incidents.length
} else if (filter.value === 'urgent') {
filter.count = this.incidents.filter(incident =>
incident.priority === 'urgent' || incident.priority === 'critical'
).length
} else {
filter.count = this.incidents.filter(incident => incident.status === filter.value).length
}
})
},
setFilter(filter: string) {
this.currentFilter = filter
},
showNewIncidentModal() {
this.showModal = true
this.resetForm()
},
hideModal() {
this.showModal = false
},
resetForm() {
this.newIncident = {
title: '',
elder_id: '',
priority: 'normal',
incident_type: 'medical',
description: '',
action_taken: '',
incident_time: new Date().toISOString()
} as IncidentReport
this.elderIndex = 0
this.priorityIndex = 0
this.typeIndex = 0
},
onElderChange(e) {
this.elderIndex = e.detail.value
if (this.elderOptions[this.elderIndex]) {
this.newIncident.elder_id = this.elderOptions[this.elderIndex].id
}
},
onPriorityChange(e) {
this.priorityIndex = e.detail.value
const priorityMap = ['normal', 'high', 'urgent', 'critical']
this.newIncident.priority = priorityMap[e.detail.value]
},
onTypeChange(e) {
this.typeIndex = e.detail.value
const typeMap = ['medical', 'safety', 'behavioral', 'equipment', 'other']
this.newIncident.incident_type = typeMap[e.detail.value]
},
onTimeChange(e) {
this.newIncident.incident_time = e.detail.value
},
async submitIncident() {
if (!this.validateForm()) {
return
}
this.isSubmitting = true
try {
const result = await supa.executeAs('create_incident_report', {
...this.newIncident,
status: 'pending'
})
if (result.success) {
uni.showToast({
title: '报告提交成功',
icon: 'success'
})
this.hideModal()
this.loadIncidentData()
} else {
throw new Error(result.error || '提交失败')
}
} catch (error) {
console.error('提交事件报告失败:', error)
uni.showToast({
title: '提交失败',
icon: 'error'
})
} finally {
this.isSubmitting = false
}
},
validateForm(): boolean {
if (!this.newIncident.title.trim()) {
uni.showToast({
title: '请输入事件标题',
icon: 'error'
})
return false
}
if (!this.newIncident.elder_id) {
uni.showToast({
title: '请选择相关老人',
icon: 'error'
})
return false
}
if (!this.newIncident.description.trim()) {
uni.showToast({
title: '请输入事件描述',
icon: 'error'
})
return false
}
return true
},
async followUpIncident(incident: IncidentReport) {
try {
const result = await supa.executeAs('update_incident_status', {
incident_id: incident.id,
status: 'in_progress'
})
if (result.success) {
uni.showToast({
title: '已开始跟进',
icon: 'success'
})
this.loadIncidents()
}
} catch (error) {
console.error('跟进事件失败:', error)
uni.showToast({
title: '操作失败',
icon: 'error'
})
}
},
viewIncidentDetail(incident: IncidentReport) {
uni.navigateTo({
url: `/pages/ec/incident/detail?id=${incident.id}`
})
},
getPriorityClass(priority: string): string {
const priorityMap = {
'normal': 'priority-normal',
'high': 'priority-high',
'urgent': 'priority-urgent',
'critical': 'priority-critical'
}
return priorityMap[priority] || 'priority-normal'
},
getPriorityText(priority: string): string {
const priorityMap = {
'normal': '普通',
'high': '高',
'urgent': '紧急',
'critical': '危急'
}
return priorityMap[priority] || '普通'
},
getStatusClass(status: string): string {
const statusMap = {
'pending': 'status-pending',
'in_progress': 'status-progress',
'resolved': 'status-resolved',
'closed': 'status-closed'
}
return statusMap[status] || 'status-pending'
},
getStatusText(status: string): string {
const statusMap = {
'pending': '待处理',
'in_progress': '处理中',
'resolved': '已解决',
'closed': '已关闭'
}
return statusMap[status] || '未知'
},
formatTime(timestamp: string): string {
if (!timestamp) return ''
const date = new Date(timestamp)
const now = new Date()
const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / (1000 * 60))
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
if (days > 0) {
return `${days}天前`
} else if (hours > 0) {
return `${hours}小时前`
} else if (minutes > 0) {
return `${minutes}分钟前`
} else {
return '刚刚'
}
},
formatDateTime(timestamp: string): string {
if (!timestamp) return ''
const date = new Date(timestamp)
return `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
}
}
</script>
<style lang="scss">
.incident-report {
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40rpx 30rpx 30rpx;
color: white;
.header-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.header-subtitle {
font-size: 26rpx;
opacity: 0.9;
}
}
.stats-section {
padding: 30rpx;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20rpx;
}
.stat-card {
background: white;
padding: 30rpx 20rpx;
border-radius: 15rpx;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
.stat-number {
font-size: 48rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 26rpx;
color: #666;
margin-bottom: 5rpx;
}
.stat-trend {
font-size: 22rpx;
color: #999;
}
&.urgent {
.stat-number {
color: #ff6b6b;
}
}
&.pending {
.stat-number {
color: #4ecdc4;
}
}
&.resolved {
.stat-number {
color: #45b7d1;
}
}
}
.new-incident-section {
padding: 0 30rpx 30rpx;
}
.new-incident-btn {
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
padding: 30rpx;
border-radius: 15rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 15rpx;
color: white;
.new-incident-icon {
font-size: 32rpx;
}
.new-incident-text {
font-size: 28rpx;
font-weight: bold;
}
}
.filter-section {
padding: 0 30rpx;
margin-bottom: 20rpx;
}
.filter-scroll {
white-space: nowrap;
}
.filter-tabs {
display: flex;
gap: 20rpx;
}
.filter-tab {
padding: 20rpx 30rpx;
background: white;
border-radius: 25rpx;
display: flex;
align-items: center;
gap: 10rpx;
white-space: nowrap;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
&.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
.filter-count {
background: rgba(255,255,255,0.3);
color: white;
}
}
.filter-text {
font-size: 26rpx;
}
.filter-count {
background: #f0f0f0;
color: #666;
padding: 5rpx 10rpx;
border-radius: 12rpx;
font-size: 20rpx;
min-width: 30rpx;
text-align: center;
}
}
.incidents-section {
padding: 0 30rpx 30rpx;
}
.empty-state {
text-align: center;
padding: 120rpx 0;
.empty-icon {
font-size: 80rpx;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 15rpx;
}
.empty-subtitle {
font-size: 26rpx;
color: #999;
}
}
.incidents-list {
.incident-card {
background: white;
margin-bottom: 20rpx;
border-radius: 15rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
}
}
.incident-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
}
.incident-info {
flex: 1;
.incident-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.incident-elder {
font-size: 24rpx;
color: #666;
}
}
.incident-priority {
padding: 10rpx 15rpx;
border-radius: 20rpx;
font-size: 22rpx;
&.priority-normal {
background: #e3f2fd;
color: #1976d2;
}
&.priority-high {
background: #fff3e0;
color: #f57c00;
}
&.priority-urgent {
background: #ffebee;
color: #d32f2f;
}
&.priority-critical {
background: #f3e5f5;
color: #7b1fa2;
}
}
.incident-content {
margin-bottom: 20rpx;
.incident-description {
font-size: 26rpx;
color: #555;
line-height: 1.6;
}
}
.incident-meta {
margin-bottom: 20rpx;
.incident-time, .incident-reporter {
display: flex;
align-items: center;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
.time-label, .reporter-label {
font-size: 24rpx;
color: #666;
width: 120rpx;
}
.time-value, .reporter-value {
font-size: 24rpx;
color: #333;
}
}
.incident-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.incident-status {
padding: 8rpx 15rpx;
border-radius: 15rpx;
font-size: 22rpx;
&.status-pending {
background: #fff3e0;
color: #f57c00;
}
&.status-progress {
background: #e8f5e8;
color: #388e3c;
}
&.status-resolved {
background: #e3f2fd;
color: #1976d2;
}
&.status-closed {
background: #f5f5f5;
color: #666;
}
}
.incident-actions {
display: flex;
align-items: center;
gap: 20rpx;
.action-time {
font-size: 22rpx;
color: #999;
}
.action-follow {
font-size: 24rpx;
color: #667eea;
padding: 5rpx 15rpx;
border: 2rpx solid #667eea;
border-radius: 15rpx;
}
}
// Modal styles
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
width: 90%;
max-height: 80vh;
border-radius: 20rpx;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.modal-close {
font-size: 40rpx;
color: #999;
width: 60rpx;
text-align: center;
}
}
.modal-body {
max-height: 60vh;
padding: 30rpx;
}
.form-group {
margin-bottom: 30rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
}
.form-input, .form-textarea {
width: 100%;
border: 2rpx solid #e1e1e1;
border-radius: 10rpx;
padding: 20rpx;
font-size: 28rpx;
background: white;
&:focus {
border-color: #667eea;
}
}
.form-input {
height: 80rpx;
}
.form-textarea {
min-height: 150rpx;
}
.form-picker {
width: 100%;
border: 2rpx solid #e1e1e1;
border-radius: 10rpx;
background: white;
}
.picker-display {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
padding: 0 20rpx;
.picker-text {
font-size: 28rpx;
color: #333;
}
.picker-arrow {
font-size: 24rpx;
color: #999;
}
}
.modal-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 2rpx solid #f0f0f0;
}
.btn {
flex: 1;
height: 80rpx;
border-radius: 10rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
&.btn-cancel {
background: #f5f5f5;
color: #666;
border: 2rpx solid #e1e1e1;
}
&.btn-submit {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
&:disabled {
opacity: 0.6;
}
}
}
</style>