SmartLightH5RealtimeTab.vue 8.21 KB
<template>
  <div class="smart-light-h5-realtime">
    <div class="h5-search">
      <el-input
        v-model="searchKeyword"
        placeholder="输入设备名称搜索"
        clearable
        @keyup.enter="onSearch"
        @clear="onSearch"
      >
        <template #append>
          <el-button :loading="loading" @click="onSearch">搜索</el-button>
        </template>
      </el-input>
    </div>

    <div class="h5-filters">
      <button
        v-for="item in filterItems"
        :key="item.key"
        :class="['filter-chip', { active: lampStateFilter === item.key }]"
        type="button"
        @click="onFilter(item.key)"
      >
        <span class="dot" :style="{ backgroundColor: item.dotColor }"></span>
        <span class="label">{{ item.label }}</span>
        <span class="count">{{ item.count }}</span>
      </button>
    </div>

    <el-empty v-if="!loading && deviceList.length === 0" description="暂无设备" />

    <div v-for="device in deviceList" :key="device.id" class="device-card">
      <div class="card-top">
        <div class="device-name">{{ device.name }}</div>
        <div class="device-status">
          <span class="status-dot" :style="{ backgroundColor: device.statusColor }"></span>
          <span class="status-text">{{ device.statusLabel }}</span>
        </div>
      </div>

      <div class="card-body">
        <div class="row">
          <div class="k">稼动率</div>
          <div class="v">{{ device.utilization }}%</div>
        </div>
        <div class="row">
          <div class="k">{{ device.statusLabel }}时长</div>
          <div class="v">{{ device.lightTime }}</div>
        </div>
      </div>

      <div class="card-actions">
        <el-button size="small" @click="goOeeTimeline(device)">OEE时序</el-button>
        <el-button size="small" type="primary" @click="goUtilization(device)">稼动率</el-button>
      </div>
    </div>

    <div class="h5-footer">
      <el-button
        v-if="canLoadMore"
        type="primary"
        :loading="loadingMore"
        style="width: 100%;"
        @click="loadMore"
      >
        加载更多
      </el-button>
      <div v-else-if="deviceList.length > 0" class="no-more">没有更多了</div>
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { apiFetch } from '../../config/api.js'

const route = useRoute()
const router = useRouter()

const searchKeyword = ref('')
const lampStateFilter = ref('')
const currentPage = ref(1)
const PAGE_SIZE = 20

const deviceList = ref([])
const totalDevices = ref(0)
const loading = ref(false)
const loadingMore = ref(false)

const totalCounts = reactive({ all: 0, red: 0, yellow: 0, green: 0, blue: 0, gray: 0 })
const canLoadMore = computed(() => deviceList.value.length < totalDevices.value)

function mapLampStatus(lampState) {
  if (lampState === '3') return 'green'
  if (lampState === '1') return 'red'
  if (lampState === '2') return 'yellow'
  if (lampState === '4') return 'blue'
  return 'gray'
}

const LAMP_LABEL_MAP = { green: '绿灯', yellow: '黄灯', red: '红灯', blue: '蓝灯', gray: '灭灯' }
const LAMP_COLOR_MAP = { green: '#2ecc71', yellow: '#f1c40f', red: '#e74c3c', blue: '#3498db', gray: '#95a5a6' }

function toDeviceViewModel(item) {
  const status = mapLampStatus(item.lampState)
  return {
    id: item.id,
    name: item.deviceName || item.dtuSn,
    dtuSn: item.dtuSn || item.deviceSn || item.sn || '',
    utilization: parseFloat(item.utilizationRate) || 0,
    lightTime: item.duration || '0分',
    statusLabel: LAMP_LABEL_MAP[status] || '灭灯',
    statusColor: LAMP_COLOR_MAP[status] || '#95a5a6',
    _raw: item
  }
}

const filterItems = computed(() => [
  { key: '', label: '全部', count: `${totalCounts.all}台`, dotColor: '#2c3e50' },
  { key: '1', label: '红', count: `${totalCounts.red}台`, dotColor: LAMP_COLOR_MAP.red },
  { key: '2', label: '黄', count: `${totalCounts.yellow}台`, dotColor: LAMP_COLOR_MAP.yellow },
  { key: '3', label: '绿', count: `${totalCounts.green}台`, dotColor: LAMP_COLOR_MAP.green },
  { key: '4', label: '蓝', count: `${totalCounts.blue}台`, dotColor: LAMP_COLOR_MAP.blue },
  { key: '0', label: '灭', count: `${totalCounts.gray}台`, dotColor: LAMP_COLOR_MAP.gray }
])

async function fetchStats() {
  const res = await apiFetch('/api/device/stats')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  const data = await res.json()
  totalCounts.all = data.all || 0
  totalCounts.red = data.red || 0
  totalCounts.yellow = data.yellow || 0
  totalCounts.green = data.green || 0
  totalCounts.blue = data.blue || 0
  totalCounts.gray = data.off || 0
}

async function fetchDeviceList({ append }) {
  const params = new URLSearchParams({
    pageNo: String(currentPage.value),
    pageSize: String(PAGE_SIZE)
  })
  if (searchKeyword.value) params.append('deviceName', searchKeyword.value)
  if (lampStateFilter.value) params.append('lampState', lampStateFilter.value)

  const res = await apiFetch(`/api/device/list?${params}`)
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  const data = await res.json()

  const mapped = (data.list || []).map(toDeviceViewModel)
  deviceList.value = append ? deviceList.value.concat(mapped) : mapped
  totalDevices.value = data.total || 0
}

async function refresh({ append }) {
  try {
    if (append) loadingMore.value = true
    else loading.value = true

    await Promise.all([fetchDeviceList({ append }), fetchStats()])
  } catch (e) {
    ElMessage.error('获取数据失败,请检查后端接口或代理配置')
    console.warn(e)
  } finally {
    loading.value = false
    loadingMore.value = false
  }
}

function onSearch() {
  currentPage.value = 1
  refresh({ append: false })
}

function onFilter(key) {
  if (lampStateFilter.value === key) return
  lampStateFilter.value = key
  currentPage.value = 1
  refresh({ append: false })
}

function loadMore() {
  if (!canLoadMore.value || loading.value || loadingMore.value) return
  currentPage.value += 1
  refresh({ append: true })
}

function goOeeTimeline(device) {
  router.push({
    path: '/smart-light-h5/oee',
    query: { ...route.query, dtuSn: device.dtuSn || '', deviceName: device.name || '' }
  })
}

function goUtilization(device) {
  router.push({
    path: '/smart-light-h5/utilization',
    query: { ...route.query, dtuSn: device.dtuSn || '', deviceName: device.name || '' }
  })
}

onMounted(() => {
  refresh({ append: false })
})
</script>

<style scoped>
.h5-search :deep(.el-input__wrapper) {
  border-radius: 10px;
}

.h5-filters {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding-top: 10px;
  -webkit-overflow-scrolling: touch;
}

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid rgba(0, 0, 0, 0.08);
  background: #fff;
  border-radius: 999px;
  padding: 6px 10px;
  font-size: 12px;
  color: #334155;
  white-space: nowrap;
}

.filter-chip.active {
  border-color: rgba(64, 158, 255, 0.45);
  background: rgba(64, 158, 255, 0.08);
  color: #1d4ed8;
}

.filter-chip .dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  flex: 0 0 auto;
}

.filter-chip .count {
  opacity: 0.75;
}

.device-card {
  background: #fff;
  border-radius: 12px;
  padding: 12px;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
  margin-top: 10px;
}

.card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.device-name {
  font-weight: 700;
  color: #0f172a;
  font-size: 14px;
  line-height: 1.25;
  word-break: break-all;
}

.device-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: #334155;
  flex: 0 0 auto;
}

.status-dot {
  width: 10px;
  height: 10px;
  border-radius: 999px;
}

.card-body {
  margin-top: 10px;
  display: grid;
  gap: 6px;
}

.card-actions {
  margin-top: 10px;
  display: flex;
  gap: 10px;
}

.card-actions :deep(.el-button) {
  flex: 1 1 auto;
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
}

.row .k {
  color: #64748b;
}

.row .v {
  color: #0f172a;
  font-weight: 600;
}

.h5-footer {
  padding: 10px 0 18px;
}

.no-more {
  text-align: center;
  color: #94a3b8;
  font-size: 12px;
  padding: 8px 0;
}
</style>