EnergyH5RealtimeTab.vue 7.91 KB
<template>
  <div class="energy-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: runStatusFilter === 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.evalue }} kw·h</div>
        </div>
        <div class="row">
          <div class="k">{{ device.statusLabel }}时长</div>
          <div class="v">{{ device.duration }}</div>
        </div>
      </div>

      <div class="card-actions">
        <el-button size="small" @click="goRunStatus(device)">运行状态</el-button>
        <el-button size="small" type="primary" @click="goUsage(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 runStatusFilter = 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, offline: 0, stop: 0, standby: 0, run: 0 })
const canLoadMore = computed(() => deviceList.value.length < totalDevices.value)

const STATUS_LABEL_MAP = { '0': '离线', '1': '停机', '2': '待机', '3': '运行' }
const STATUS_COLOR_MAP = { '0': '#95a5a6', '1': '#e74c3c', '2': '#67c23a', '3': '#3498db' }

function toDeviceViewModel(item) {
  const runStatus = String(item.runStatus ?? '0')
  return {
    id: item.id,
    name: item.deviceName || item.dtuSn,
    dtuSn: item.dtuSn || item.deviceSn || item.sn || '',
    evalue: parseFloat(item.evalue) || 0,
    duration: item.duration || '0秒',
    runStatus,
    statusLabel: STATUS_LABEL_MAP[runStatus] || '未知',
    statusColor: STATUS_COLOR_MAP[runStatus] || '#95a5a6',
    _raw: item
  }
}

const filterItems = computed(() => [
  { key: '', label: '全部', count: `${totalCounts.all}台`, dotColor: '#2c3e50' },
  { key: '3', label: '运行', count: `${totalCounts.run}台`, dotColor: STATUS_COLOR_MAP['3'] },
  { key: '2', label: '待机', count: `${totalCounts.standby}台`, dotColor: STATUS_COLOR_MAP['2'] },
  { key: '1', label: '停机', count: `${totalCounts.stop}台`, dotColor: STATUS_COLOR_MAP['1'] },
  { key: '0', label: '离线', count: `${totalCounts.offline}台`, dotColor: STATUS_COLOR_MAP['0'] }
])

async function fetchStats() {
  const res = await apiFetch('/api/energy/stats')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  const data = await res.json()
  totalCounts.all = data.total || 0
  totalCounts.offline = parseInt(data['0']) || 0
  totalCounts.stop = parseInt(data['1']) || 0
  totalCounts.standby = parseInt(data['2']) || 0
  totalCounts.run = parseInt(data['3']) || 0
}

async function fetchDeviceList({ append }) {
  const params = new URLSearchParams({
    pageNo: String(currentPage.value),
    pageSize: String(PAGE_SIZE),
    projectState: '1'
  })
  if (searchKeyword.value) params.append('deviceName', searchKeyword.value)
  if (runStatusFilter.value !== '') params.append('runStatus', runStatusFilter.value)

  const res = await apiFetch(`/api/energy/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 (runStatusFilter.value === key) return
  runStatusFilter.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 goRunStatus(device) {
  router.push({
    path: '/energy-h5/run-status',
    query: { ...route.query, dtuSn: device.dtuSn || '', deviceName: device.name || '' }
  })
}

function goUsage(device) {
  router.push({
    path: '/energy-h5/usage',
    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-bottom: 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>