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

1439 lines
37 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="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>