1439 lines
37 KiB
Plaintext
1439 lines
37 KiB
Plaintext
<!-- 设备管理 - 重构版本 -->
|
||
<template>
|
||
<view class="equipment-management">
|
||
<!-- Header -->
|
||
<view class="header">
|
||
<text class="header-title">设备管理</text>
|
||
<view class="header-actions">
|
||
<button class="action-btn" @click="showAddEquipment">
|
||
<text class="btn-text">➕ 添加设备</text>
|
||
</button>
|
||
<button class="action-btn" @click="showMaintenanceSchedule">
|
||
<text class="btn-text">🔧 维护计划</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Stats Cards -->
|
||
<view class="stats-section">
|
||
<view class="stat-card">
|
||
<view class="stat-icon">📱</view>
|
||
<view class="stat-content">
|
||
<text class="stat-number">{{ stats.total_equipment }}</text>
|
||
<text class="stat-label">设备总数</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card normal">
|
||
<view class="stat-icon">✅</view>
|
||
<view class="stat-content">
|
||
<text class="stat-number">{{ stats.online_equipment }}</text>
|
||
<text class="stat-label">在线设备</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card warning">
|
||
<view class="stat-icon">⚠️</view>
|
||
<view class="stat-content">
|
||
<text class="stat-number">{{ stats.maintenance_needed }}</text>
|
||
<text class="stat-label">需要维护</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card error">
|
||
<view class="stat-icon">❌</view>
|
||
<view class="stat-content">
|
||
<text class="stat-number">{{ stats.faulty_equipment }}</text>
|
||
<text class="stat-label">故障设备</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Filter Section -->
|
||
<view class="filters-section">
|
||
<view class="filter-row">
|
||
<view class="filter-group">
|
||
<text class="filter-label">设备类型</text>
|
||
<picker
|
||
:value="selectedTypeIndex"
|
||
:range="equipmentTypeOptions"
|
||
range-key="label"
|
||
@change="onTypeChange"
|
||
>
|
||
<text class="picker-text">{{ selectedType?.label || '全部类型' }}</text>
|
||
</picker>
|
||
</view>
|
||
<view class="filter-group">
|
||
<text class="filter-label">设备状态</text>
|
||
<picker
|
||
:value="selectedStatusIndex"
|
||
:range="statusOptions"
|
||
range-key="label"
|
||
@change="onStatusChange"
|
||
>
|
||
<text class="picker-text">{{ selectedStatus?.label || '全部状态' }}</text>
|
||
</picker>
|
||
</view>
|
||
<view class="filter-group">
|
||
<text class="filter-label">所在位置</text>
|
||
<picker
|
||
:value="selectedLocationIndex"
|
||
:range="locationOptions"
|
||
range-key="name"
|
||
@change="onLocationChange"
|
||
>
|
||
<text class="picker-text">{{ selectedLocation?.name || '全部位置' }}</text>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
<view class="filter-row">
|
||
<view class="search-group">
|
||
<input
|
||
class="search-input"
|
||
placeholder="搜索设备名称或编号"
|
||
v-model="searchKeyword"
|
||
@input="onSearchInput"
|
||
/>
|
||
<button class="search-btn" @click="performSearch">
|
||
<text class="search-text">🔍</text>
|
||
</button>
|
||
</view>
|
||
<button class="refresh-btn" @click="refreshData">
|
||
<text class="refresh-text">🔄 刷新</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Equipment List -->
|
||
<view class="equipment-section">
|
||
<view class="section-header">
|
||
<text class="section-title">设备列表 ({{ filteredEquipment.length }})</text>
|
||
<view class="view-modes">
|
||
<button
|
||
class="mode-btn"
|
||
:class="{ active: viewMode === 'list' }"
|
||
@click="setViewMode('list')"
|
||
>
|
||
<text class="mode-text">📋</text>
|
||
</button>
|
||
<button
|
||
class="mode-btn"
|
||
:class="{ active: viewMode === 'grid' }"
|
||
@click="setViewMode('grid')"
|
||
>
|
||
<text class="mode-text">⚏</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- List View -->
|
||
<scroll-view
|
||
v-if="viewMode === 'list'"
|
||
class="equipment-list"
|
||
scroll-y="true"
|
||
:style="{ height: '500px' }"
|
||
>
|
||
<view
|
||
v-for="equipment in filteredEquipment"
|
||
:key="equipment.id"
|
||
class="equipment-item"
|
||
:class="getEquipmentStatusClass(equipment)"
|
||
@click="viewEquipmentDetail(equipment)"
|
||
>
|
||
<view class="equipment-header">
|
||
<view class="equipment-info">
|
||
<text class="equipment-name">{{ equipment.name }}</text>
|
||
<text class="equipment-type">{{ equipment.type_name }}</text>
|
||
</view>
|
||
<view class="equipment-status">
|
||
<text class="status-badge" :class="equipment.status">{{ getStatusText(equipment.status) }}</text>
|
||
<text class="equipment-id">#{{ equipment.equipment_id }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="equipment-details">
|
||
<view class="detail-row">
|
||
<text class="detail-label">位置:</text>
|
||
<text class="detail-value">{{ equipment.location_name || '未分配' }}</text>
|
||
</view>
|
||
<view class="detail-row">
|
||
<text class="detail-label">型号:</text>
|
||
<text class="detail-value">{{ equipment.model || 'N/A' }}</text>
|
||
</view>
|
||
<view class="detail-row">
|
||
<text class="detail-label">最后检查:</text>
|
||
<text class="detail-value">{{ formatDate(equipment.last_maintenance) || '未检查' }}</text>
|
||
</view>
|
||
<view class="detail-row">
|
||
<text class="detail-label">下次维护:</text>
|
||
<text class="detail-value" :class="{ overdue: isMaintenanceOverdue(equipment) }">
|
||
{{ formatDate(equipment.next_maintenance) || '未安排' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="equipment-actions">
|
||
<button class="action-btn small" @click.stop="performMaintenance(equipment)">
|
||
<text class="btn-text">🔧</text>
|
||
</button>
|
||
<button class="action-btn small" @click.stop="viewHistory(equipment)">
|
||
<text class="btn-text">📋</text>
|
||
</button>
|
||
<button class="action-btn small" @click.stop="editEquipment(equipment)">
|
||
<text class="btn-text">✏️</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- Grid View -->
|
||
<scroll-view
|
||
v-else
|
||
class="equipment-grid"
|
||
scroll-y="true"
|
||
:style="{ height: '500px' }"
|
||
>
|
||
<view class="grid-container">
|
||
<view
|
||
v-for="equipment in filteredEquipment"
|
||
:key="equipment.id"
|
||
class="equipment-card"
|
||
:class="getEquipmentStatusClass(equipment)"
|
||
@click="viewEquipmentDetail(equipment)"
|
||
>
|
||
<view class="card-header">
|
||
<text class="equipment-icon">{{ getEquipmentIcon(equipment.type) }}</text>
|
||
<text class="status-indicator" :class="equipment.status"></text>
|
||
</view>
|
||
<view class="card-content">
|
||
<text class="card-title">{{ equipment.name }}</text>
|
||
<text class="card-subtitle">{{ equipment.type_name }}</text>
|
||
<text class="card-location">📍 {{ equipment.location_name || '未分配' }}</text>
|
||
</view>
|
||
<view class="card-footer">
|
||
<view class="maintenance-info">
|
||
<text class="maintenance-text" :class="{ overdue: isMaintenanceOverdue(equipment) }">
|
||
{{ getMaintenanceStatus(equipment) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- Add Equipment Modal -->
|
||
<view v-if="showAddModal" class="modal-overlay" @click="hideAddModal">
|
||
<view class="modal-content" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">添加设备</text>
|
||
<button class="close-btn" @click="hideAddModal">
|
||
<text class="close-text">✕</text>
|
||
</button>
|
||
</view>
|
||
|
||
<view class="modal-body">
|
||
<view class="form-group">
|
||
<text class="form-label">设备名称 *</text>
|
||
<input
|
||
class="form-input"
|
||
placeholder="请输入设备名称"
|
||
v-model="newEquipment.name"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">设备编号 *</text>
|
||
<input
|
||
class="form-input"
|
||
placeholder="请输入设备编号"
|
||
v-model="newEquipment.equipment_id"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">设备类型 *</text>
|
||
<picker
|
||
:value="newEquipmentTypeIndex"
|
||
:range="equipmentTypeOptions"
|
||
range-key="label"
|
||
@change="onNewEquipmentTypeChange"
|
||
>
|
||
<text class="picker-text">{{ newEquipmentType?.label || '选择设备类型' }}</text>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">设备型号</text>
|
||
<input
|
||
class="form-input"
|
||
placeholder="请输入设备型号"
|
||
v-model="newEquipment.model"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">安装位置</text>
|
||
<picker
|
||
:value="newLocationIndex"
|
||
:range="locationOptions"
|
||
range-key="name"
|
||
@change="onNewLocationChange"
|
||
>
|
||
<text class="picker-text">{{ newLocation?.name || '选择安装位置' }}</text>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">设备描述</text>
|
||
<textarea
|
||
class="form-textarea"
|
||
placeholder="请输入设备描述"
|
||
v-model="newEquipment.description"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal-footer">
|
||
<button class="cancel-btn" @click="hideAddModal">
|
||
<text class="btn-text">取消</text>
|
||
</button>
|
||
<button class="confirm-btn" @click="saveEquipment" :disabled="!isFormValid">
|
||
<text class="btn-text">保存</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Maintenance Modal -->
|
||
<view v-if="showMaintenanceModal" class="modal-overlay" @click="hideMaintenanceModal">
|
||
<view class="modal-content" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">设备维护</text>
|
||
<button class="close-btn" @click="hideMaintenanceModal">
|
||
<text class="close-text">✕</text>
|
||
</button>
|
||
</view>
|
||
|
||
<view class="modal-body">
|
||
<view class="equipment-summary">
|
||
<text class="summary-title">{{ currentEquipment?.name }}</text>
|
||
<text class="summary-subtitle">{{ currentEquipment?.equipment_id }}</text>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">维护类型 *</text>
|
||
<picker
|
||
:value="maintenanceTypeIndex"
|
||
:range="maintenanceTypeOptions"
|
||
range-key="label"
|
||
@change="onMaintenanceTypeChange"
|
||
>
|
||
<text class="picker-text">{{ maintenanceType?.label || '选择维护类型' }}</text>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">维护描述 *</text>
|
||
<textarea
|
||
class="form-textarea"
|
||
placeholder="请详细描述维护内容"
|
||
v-model="maintenanceRecord.description"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">维护人员 *</text>
|
||
<input
|
||
class="form-input"
|
||
placeholder="请输入维护人员姓名"
|
||
v-model="maintenanceRecord.performed_by"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">下次维护日期</text>
|
||
<picker mode="date" @change="onNextMaintenanceDateChange">
|
||
<text class="picker-text">{{ maintenanceRecord.next_maintenance_date || '选择日期' }}</text>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal-footer">
|
||
<button class="cancel-btn" @click="hideMaintenanceModal">
|
||
<text class="btn-text">取消</text>
|
||
</button>
|
||
<button class="confirm-btn" @click="saveMaintenance" :disabled="!isMaintenanceFormValid">
|
||
<text class="btn-text">保存</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { formatDate } from '../types.uts'
|
||
import type { Equipment, HealthStats, Elder, MaintenanceRecord } from '../types.uts'
|
||
|
||
// Data
|
||
const equipment = ref<Equipment[]>([])
|
||
const stats = ref<HealthStats>({
|
||
total_equipment: 0,
|
||
online_equipment: 0,
|
||
maintenance_needed: 0,
|
||
faulty_equipment: 0
|
||
})
|
||
|
||
// Filters
|
||
const searchKeyword = ref('')
|
||
const selectedTypeIndex = ref(-1)
|
||
const selectedStatusIndex = ref(-1)
|
||
const selectedLocationIndex = ref(-1)
|
||
const viewMode = ref<'list' | 'grid'>('list')
|
||
|
||
// Options
|
||
const equipmentTypeOptions = ref([
|
||
{ value: 'medical', label: '医疗设备' },
|
||
{ value: 'monitoring', label: '监测设备' },
|
||
{ value: 'communication', label: '通讯设备' },
|
||
{ value: 'safety', label: '安全设备' },
|
||
{ value: 'maintenance', label: '维护设备' },
|
||
{ value: 'other', label: '其他设备' }
|
||
])
|
||
|
||
const statusOptions = ref([
|
||
{ value: 'online', label: '在线正常' },
|
||
{ value: 'offline', label: '离线' },
|
||
{ value: 'maintenance', label: '维护中' },
|
||
{ value: 'fault', label: '故障' }
|
||
])
|
||
|
||
const locationOptions = ref<Elder[]>([])
|
||
|
||
const maintenanceTypeOptions = ref([
|
||
{ value: 'routine', label: '常规维护' },
|
||
{ value: 'repair', label: '故障维修' },
|
||
{ value: 'upgrade', label: '升级更新' },
|
||
{ value: 'calibration', label: '校准调试' },
|
||
{ value: 'cleaning', label: '清洁保养' }
|
||
])
|
||
|
||
// Modal states
|
||
const showAddModal = ref(false)
|
||
const showMaintenanceModal = ref(false)
|
||
const currentEquipment = ref<Equipment | null>(null)
|
||
|
||
// Form data
|
||
const newEquipment = ref<Equipment>({
|
||
id: '',
|
||
name: '',
|
||
equipment_id: '',
|
||
type: 'medical',
|
||
type_name: '',
|
||
model: '',
|
||
location_id: '',
|
||
location_name: '',
|
||
status: 'online',
|
||
description: '',
|
||
last_maintenance: '',
|
||
next_maintenance: '',
|
||
created_at: '',
|
||
updated_at: ''
|
||
})
|
||
|
||
const maintenanceRecord = ref<MaintenanceRecord>({
|
||
id: '',
|
||
equipment_id: '',
|
||
type: 'routine',
|
||
description: '',
|
||
performed_by: '',
|
||
performed_at: '',
|
||
next_maintenance_date: '',
|
||
created_at: ''
|
||
})
|
||
|
||
const newEquipmentTypeIndex = ref(-1)
|
||
const newLocationIndex = ref(-1)
|
||
const maintenanceTypeIndex = ref(-1)
|
||
|
||
// Computed
|
||
const selectedType = computed<any>(() => {
|
||
return selectedTypeIndex.value >= 0 ? equipmentTypeOptions.value[selectedTypeIndex.value] : null
|
||
})
|
||
|
||
const selectedStatus = computed<any>(() => {
|
||
return selectedStatusIndex.value >= 0 ? statusOptions.value[selectedStatusIndex.value] : null
|
||
})
|
||
|
||
const selectedLocation = computed<Elder | null>(() => {
|
||
return selectedLocationIndex.value >= 0 ? locationOptions.value[selectedLocationIndex.value] : null
|
||
})
|
||
|
||
const newEquipmentType = computed<any>(() => {
|
||
return newEquipmentTypeIndex.value >= 0 ? equipmentTypeOptions.value[newEquipmentTypeIndex.value] : null
|
||
})
|
||
|
||
const newLocation = computed<Elder | null>(() => {
|
||
return newLocationIndex.value >= 0 ? locationOptions.value[newLocationIndex.value] : null
|
||
})
|
||
|
||
const maintenanceType = computed<any>(() => {
|
||
return maintenanceTypeIndex.value >= 0 ? maintenanceTypeOptions.value[maintenanceTypeIndex.value] : null
|
||
})
|
||
|
||
const filteredEquipment = computed<Equipment[]>(() => {
|
||
let filtered = equipment.value
|
||
|
||
// 搜索过滤
|
||
if (searchKeyword.value.trim() !== '') {
|
||
const keyword = searchKeyword.value.toLowerCase()
|
||
filtered = filtered.filter(item =>
|
||
item.name.toLowerCase().includes(keyword) ||
|
||
item.equipment_id.toLowerCase().includes(keyword) ||
|
||
(item.model && item.model.toLowerCase().includes(keyword))
|
||
)
|
||
}
|
||
|
||
// 类型过滤
|
||
if (selectedType.value) {
|
||
filtered = filtered.filter(item => item.type === selectedType.value.value)
|
||
}
|
||
|
||
// 状态过滤
|
||
if (selectedStatus.value) {
|
||
filtered = filtered.filter(item => item.status === selectedStatus.value.value)
|
||
}
|
||
|
||
// 位置过滤
|
||
if (selectedLocation.value) {
|
||
filtered = filtered.filter(item => item.location_id === selectedLocation.value?.id)
|
||
}
|
||
|
||
return filtered
|
||
})
|
||
|
||
const isFormValid = computed<boolean>(() => {
|
||
return newEquipment.value.name.trim() !== '' &&
|
||
newEquipment.value.equipment_id.trim() !== '' &&
|
||
newEquipmentType.value !== null
|
||
})
|
||
|
||
const isMaintenanceFormValid = computed<boolean>(() => {
|
||
return maintenanceRecord.value.description.trim() !== '' &&
|
||
maintenanceRecord.value.performed_by.trim() !== '' &&
|
||
maintenanceType.value !== null
|
||
})
|
||
|
||
// Methods
|
||
const loadEquipment = async (): Promise<void> => {
|
||
try {
|
||
const response = await supa.executeAs('rpc/get_equipment_list', {})
|
||
if (response.success && response.data) {
|
||
equipment.value = response.data as Equipment[]
|
||
}
|
||
} catch (error) {
|
||
console.error('加载设备列表失败:', error)
|
||
}
|
||
}
|
||
|
||
const loadStats = async (): Promise<void> => {
|
||
try {
|
||
const response = await supa.executeAs('rpc/get_equipment_stats', {})
|
||
if (response.success && response.data && response.data.length > 0) {
|
||
stats.value = response.data[0] as HealthStats
|
||
}
|
||
} catch (error) {
|
||
console.error('加载统计数据失败:', error)
|
||
}
|
||
}
|
||
|
||
const loadLocations = async (): Promise<void> => {
|
||
try {
|
||
const response = await supa.executeAs('select', {
|
||
table: 'rooms',
|
||
select: 'id, name, floor, building',
|
||
order: 'floor, name'
|
||
})
|
||
if (response.success && response.data) {
|
||
locationOptions.value = response.data as Elder[]
|
||
}
|
||
} catch (error) {
|
||
console.error('加载位置列表失败:', error)
|
||
}
|
||
}
|
||
|
||
const getEquipmentStatusClass = (equipment: Equipment): string => {
|
||
switch (equipment.status) {
|
||
case 'online': return 'status-online'
|
||
case 'offline': return 'status-offline'
|
||
case 'maintenance': return 'status-maintenance'
|
||
case 'fault': return 'status-fault'
|
||
default: return ''
|
||
}
|
||
}
|
||
|
||
const getStatusText = (status: string): string => {
|
||
switch (status) {
|
||
case 'online': return '在线'
|
||
case 'offline': return '离线'
|
||
case 'maintenance': return '维护中'
|
||
case 'fault': return '故障'
|
||
default: return '未知'
|
||
}
|
||
}
|
||
|
||
const getEquipmentIcon = (type: string): string => {
|
||
switch (type) {
|
||
case 'medical': return '🏥'
|
||
case 'monitoring': return '📊'
|
||
case 'communication': return '📞'
|
||
case 'safety': return '🛡️'
|
||
case 'maintenance': return '🔧'
|
||
default: return '📱'
|
||
}
|
||
}
|
||
|
||
const isMaintenanceOverdue = (equipment: Equipment): boolean => {
|
||
if (!equipment.next_maintenance) return false
|
||
const nextDate = new Date(equipment.next_maintenance)
|
||
const today = new Date()
|
||
return nextDate < today
|
||
}
|
||
|
||
const getMaintenanceStatus = (equipment: Equipment): string => {
|
||
if (!equipment.next_maintenance) return '未安排'
|
||
if (isMaintenanceOverdue(equipment)) return '逾期'
|
||
|
||
const nextDate = new Date(equipment.next_maintenance)
|
||
const today = new Date()
|
||
const diffDays = Math.ceil((nextDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
||
|
||
if (diffDays <= 7) return `${diffDays}天后`
|
||
return formatDate(equipment.next_maintenance)
|
||
}
|
||
|
||
// Event handlers
|
||
const onTypeChange = (e: any): void => {
|
||
selectedTypeIndex.value = e.detail.value
|
||
}
|
||
|
||
const onStatusChange = (e: any): void => {
|
||
selectedStatusIndex.value = e.detail.value
|
||
}
|
||
|
||
const onLocationChange = (e: any): void => {
|
||
selectedLocationIndex.value = e.detail.value
|
||
}
|
||
|
||
const onSearchInput = (): void => {
|
||
// 实时搜索,无需额外处理
|
||
}
|
||
|
||
const performSearch = (): void => {
|
||
// 手动触发搜索,computed已处理
|
||
}
|
||
|
||
const refreshData = async (): Promise<void> => {
|
||
await Promise.all([
|
||
loadEquipment(),
|
||
loadStats(),
|
||
loadLocations()
|
||
])
|
||
}
|
||
|
||
const setViewMode = (mode: 'list' | 'grid'): void => {
|
||
viewMode.value = mode
|
||
}
|
||
|
||
const viewEquipmentDetail = (equipment: Equipment): void => {
|
||
// 跳转到设备详情页面
|
||
console.log('查看设备详情:', equipment)
|
||
}
|
||
|
||
const performMaintenance = (equipment: Equipment): void => {
|
||
currentEquipment.value = equipment
|
||
maintenanceRecord.value = {
|
||
id: '',
|
||
equipment_id: equipment.id,
|
||
type: 'routine',
|
||
description: '',
|
||
performed_by: '',
|
||
performed_at: '',
|
||
next_maintenance_date: '',
|
||
created_at: ''
|
||
}
|
||
maintenanceTypeIndex.value = -1
|
||
showMaintenanceModal.value = true
|
||
}
|
||
|
||
const viewHistory = (equipment: Equipment): void => {
|
||
// 查看维护历史
|
||
console.log('查看维护历史:', equipment)
|
||
}
|
||
|
||
const editEquipment = (equipment: Equipment): void => {
|
||
// 编辑设备信息
|
||
console.log('编辑设备:', equipment)
|
||
}
|
||
|
||
// Modal methods
|
||
const showAddEquipment = (): void => {
|
||
newEquipment.value = {
|
||
id: '',
|
||
name: '',
|
||
equipment_id: '',
|
||
type: 'medical',
|
||
type_name: '',
|
||
model: '',
|
||
location_id: '',
|
||
location_name: '',
|
||
status: 'online',
|
||
description: '',
|
||
last_maintenance: '',
|
||
next_maintenance: '',
|
||
created_at: '',
|
||
updated_at: ''
|
||
}
|
||
newEquipmentTypeIndex.value = -1
|
||
newLocationIndex.value = -1
|
||
showAddModal.value = true
|
||
}
|
||
|
||
const hideAddModal = (): void => {
|
||
showAddModal.value = false
|
||
}
|
||
|
||
const showMaintenanceSchedule = (): void => {
|
||
// 显示维护计划
|
||
console.log('显示维护计划')
|
||
}
|
||
|
||
const hideMaintenanceModal = (): void => {
|
||
showMaintenanceModal.value = false
|
||
currentEquipment.value = null
|
||
}
|
||
|
||
const onNewEquipmentTypeChange = (e: any): void => {
|
||
newEquipmentTypeIndex.value = e.detail.value
|
||
if (newEquipmentType.value) {
|
||
newEquipment.value.type = newEquipmentType.value.value
|
||
newEquipment.value.type_name = newEquipmentType.value.label
|
||
}
|
||
}
|
||
|
||
const onNewLocationChange = (e: any): void => {
|
||
newLocationIndex.value = e.detail.value
|
||
if (newLocation.value) {
|
||
newEquipment.value.location_id = newLocation.value.id
|
||
newEquipment.value.location_name = newLocation.value.name
|
||
}
|
||
}
|
||
|
||
const onMaintenanceTypeChange = (e: any): void => {
|
||
maintenanceTypeIndex.value = e.detail.value
|
||
if (maintenanceType.value) {
|
||
maintenanceRecord.value.type = maintenanceType.value.value
|
||
}
|
||
}
|
||
|
||
const onNextMaintenanceDateChange = (e: any): void => {
|
||
maintenanceRecord.value.next_maintenance_date = e.detail.value
|
||
}
|
||
|
||
const saveEquipment = async (): Promise<void> => {
|
||
if (!isFormValid.value) return
|
||
|
||
try {
|
||
const response = await supa.executeAs('insert', {
|
||
table: 'equipment',
|
||
data: {
|
||
name: newEquipment.value.name,
|
||
equipment_id: newEquipment.value.equipment_id,
|
||
type: newEquipment.value.type,
|
||
model: newEquipment.value.model,
|
||
location_id: newEquipment.value.location_id || null,
|
||
description: newEquipment.value.description,
|
||
status: 'online'
|
||
}
|
||
})
|
||
|
||
if (response.success) {
|
||
hideAddModal()
|
||
await loadEquipment()
|
||
await loadStats()
|
||
}
|
||
} catch (error) {
|
||
console.error('保存设备失败:', error)
|
||
}
|
||
}
|
||
|
||
const saveMaintenance = async (): Promise<void> => {
|
||
if (!isMaintenanceFormValid.value || !currentEquipment.value) return
|
||
|
||
try {
|
||
// 保存维护记录
|
||
const maintenanceResponse = await supa.executeAs('insert', {
|
||
table: 'equipment_maintenance',
|
||
data: {
|
||
equipment_id: currentEquipment.value.id,
|
||
type: maintenanceRecord.value.type,
|
||
description: maintenanceRecord.value.description,
|
||
performed_by: maintenanceRecord.value.performed_by,
|
||
performed_at: new Date().toISOString()
|
||
}
|
||
})
|
||
|
||
// 更新设备下次维护时间
|
||
if (maintenanceRecord.value.next_maintenance_date) {
|
||
await supa.executeAs('update', {
|
||
table: 'equipment',
|
||
data: {
|
||
last_maintenance: new Date().toISOString(),
|
||
next_maintenance: maintenanceRecord.value.next_maintenance_date
|
||
},
|
||
match: { id: currentEquipment.value.id }
|
||
})
|
||
}
|
||
|
||
if (maintenanceResponse.success) {
|
||
hideMaintenanceModal()
|
||
await loadEquipment()
|
||
await loadStats()
|
||
}
|
||
} catch (error) {
|
||
console.error('保存维护记录失败:', error)
|
||
}
|
||
}
|
||
|
||
// Lifecycle
|
||
onMounted(async () => {
|
||
await refreshData()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.equipment-management {
|
||
padding: 20px;
|
||
background: #f5f7fa;
|
||
min-height: 100vh;
|
||
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
padding: 20px;
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
|
||
.header-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #1a202c;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
|
||
.action-btn {
|
||
padding: 8px 16px;
|
||
background: #4a90e2;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
|
||
&:hover {
|
||
background: #357abd;
|
||
}
|
||
|
||
.btn-text {
|
||
color: white;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.stats-section {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
flex-wrap: wrap;
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
padding: 20px;
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
|
||
&.normal {
|
||
border-left: 4px solid #10b981;
|
||
}
|
||
|
||
&.warning {
|
||
border-left: 4px solid #f59e0b;
|
||
}
|
||
|
||
&.error {
|
||
border-left: 4px solid #ef4444;
|
||
}
|
||
|
||
.stat-icon {
|
||
font-size: 32px;
|
||
}
|
||
|
||
.stat-content {
|
||
.stat-number {
|
||
display: block;
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: #1a202c;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.filters-section {
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 24px;
|
||
|
||
.filter-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.filter-group {
|
||
flex: 1;
|
||
|
||
.filter-label {
|
||
display: block;
|
||
font-size: 14px;
|
||
color: #374151;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.picker-text {
|
||
padding: 10px 16px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 8px;
|
||
background: white;
|
||
color: #374151;
|
||
width: 100%;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.search-group {
|
||
flex: 2;
|
||
display: flex;
|
||
gap: 8px;
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
padding: 10px 16px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.search-btn {
|
||
padding: 10px 16px;
|
||
background: #4a90e2;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
|
||
.search-text {
|
||
color: white;
|
||
}
|
||
}
|
||
}
|
||
|
||
.refresh-btn {
|
||
padding: 10px 16px;
|
||
background: #6b7280;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
|
||
.refresh-text {
|
||
color: white;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.equipment-section {
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1a202c;
|
||
}
|
||
|
||
.view-modes {
|
||
display: flex;
|
||
gap: 8px;
|
||
|
||
.mode-btn {
|
||
padding: 8px 12px;
|
||
background: #f3f4f6;
|
||
border: none;
|
||
border-radius: 6px;
|
||
|
||
&.active {
|
||
background: #4a90e2;
|
||
|
||
.mode-text {
|
||
color: white;
|
||
}
|
||
}
|
||
|
||
.mode-text {
|
||
color: #374151;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.equipment-list {
|
||
padding: 20px;
|
||
|
||
.equipment-item {
|
||
padding: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
margin-bottom: 12px;
|
||
background: white;
|
||
|
||
&.status-online {
|
||
border-left: 4px solid #10b981;
|
||
}
|
||
|
||
&.status-offline {
|
||
border-left: 4px solid #6b7280;
|
||
}
|
||
|
||
&.status-maintenance {
|
||
border-left: 4px solid #f59e0b;
|
||
}
|
||
|
||
&.status-fault {
|
||
border-left: 4px solid #ef4444;
|
||
}
|
||
|
||
.equipment-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 12px;
|
||
|
||
.equipment-info {
|
||
.equipment-name {
|
||
display: block;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1a202c;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.equipment-type {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
}
|
||
|
||
.equipment-status {
|
||
text-align: right;
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
margin-bottom: 4px;
|
||
|
||
&.online {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
|
||
&.offline {
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
}
|
||
|
||
&.maintenance {
|
||
background: #fef3c7;
|
||
color: #92400e;
|
||
}
|
||
|
||
&.fault {
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
}
|
||
|
||
.equipment-id {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
}
|
||
}
|
||
|
||
.equipment-details {
|
||
margin-bottom: 12px;
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 4px;
|
||
|
||
.detail-label {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.detail-value {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
|
||
&.overdue {
|
||
color: #ef4444;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.equipment-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: flex-end;
|
||
|
||
.action-btn {
|
||
padding: 6px 12px;
|
||
background: #f3f4f6;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
|
||
&.small {
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
.btn-text {
|
||
color: #374151;
|
||
}
|
||
|
||
&:hover {
|
||
background: #e5e7eb;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.equipment-grid {
|
||
padding: 20px;
|
||
|
||
.grid-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
|
||
.equipment-card {
|
||
width: 280px;
|
||
padding: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 12px;
|
||
background: white;
|
||
cursor: pointer;
|
||
|
||
&.status-online {
|
||
border-top: 4px solid #10b981;
|
||
}
|
||
|
||
&.status-offline {
|
||
border-top: 4px solid #6b7280;
|
||
}
|
||
|
||
&.status-maintenance {
|
||
border-top: 4px solid #f59e0b;
|
||
}
|
||
|
||
&.status-fault {
|
||
border-top: 4px solid #ef4444;
|
||
}
|
||
|
||
&:hover {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
|
||
.equipment-icon {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.status-indicator {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
|
||
&.online {
|
||
background: #10b981;
|
||
}
|
||
|
||
&.offline {
|
||
background: #6b7280;
|
||
}
|
||
|
||
&.maintenance {
|
||
background: #f59e0b;
|
||
}
|
||
|
||
&.fault {
|
||
background: #ef4444;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-content {
|
||
margin-bottom: 12px;
|
||
|
||
.card-title {
|
||
display: block;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1a202c;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.card-subtitle {
|
||
display: block;
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.card-location {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
}
|
||
|
||
.card-footer {
|
||
.maintenance-info {
|
||
.maintenance-text {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
|
||
&.overdue {
|
||
color: #ef4444;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.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;
|
||
border-radius: 12px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #1a202c;
|
||
}
|
||
|
||
.close-btn {
|
||
padding: 4px;
|
||
background: none;
|
||
border: none;
|
||
font-size: 18px;
|
||
color: #6b7280;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
color: #374151;
|
||
}
|
||
}
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
|
||
.equipment-summary {
|
||
padding: 16px;
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
|
||
.summary-title {
|
||
display: block;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1a202c;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.summary-subtitle {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 16px;
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-input, .form-textarea {
|
||
width: 100%;
|
||
padding: 10px 16px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
color: #374151;
|
||
|
||
&:focus {
|
||
outline: none;
|
||
border-color: #4a90e2;
|
||
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
|
||
}
|
||
}
|
||
|
||
.form-textarea {
|
||
height: 80px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.picker-text {
|
||
padding: 10px 16px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 8px;
|
||
background: white;
|
||
color: #374151;
|
||
width: 100%;
|
||
display: block;
|
||
}
|
||
}
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
padding: 20px;
|
||
border-top: 1px solid #e5e7eb;
|
||
|
||
.cancel-btn, .confirm-btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
|
||
.btn-text {
|
||
color: inherit;
|
||
}
|
||
}
|
||
|
||
.cancel-btn {
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
|
||
&:hover {
|
||
background: #e5e7eb;
|
||
}
|
||
}
|
||
|
||
.confirm-btn {
|
||
background: #4a90e2;
|
||
color: white;
|
||
|
||
&:hover {
|
||
background: #357abd;
|
||
}
|
||
|
||
&:disabled {
|
||
background: #d1d5db;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|