2026-07-03 04:10:46 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="home-page">
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<!-- Top Bar: Org Switcher + Scan -->
|
|
|
|
|
|
<view class="top-bar">
|
|
|
|
|
|
<view class="org-section" @click="showOrgPicker = true">
|
|
|
|
|
|
<text class="org-name">{{ currentOrgName || '选择组织' }}</text>
|
|
|
|
|
|
<text class="org-arrow">▾</text>
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</view>
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<view class="scan-btn" @click="handleScan">
|
|
|
|
|
|
<svg class="scan-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
|
<path d="M3 7V5a2 2 0 0 1 2-2h2"/>
|
|
|
|
|
|
<path d="M17 3h2a2 2 0 0 1 2 2v2"/>
|
|
|
|
|
|
<path d="M21 17v2a2 2 0 0 1-2 2h-2"/>
|
|
|
|
|
|
<path d="M7 21H5a2 2 0 0 1-2-2v-2"/>
|
|
|
|
|
|
<line x1="7" y1="12" x2="17" y2="12"/>
|
|
|
|
|
|
</svg>
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<!-- Org Picker Modal -->
|
|
|
|
|
|
<view v-if="showOrgPicker" class="org-overlay" @click="showOrgPicker = false">
|
|
|
|
|
|
<view class="org-picker" @click.stop>
|
|
|
|
|
|
<view class="org-picker-header">
|
|
|
|
|
|
<text class="org-picker-title">切换组织</text>
|
|
|
|
|
|
<text class="org-picker-close" @click="showOrgPicker = false">✕</text>
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</view>
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<view
|
|
|
|
|
|
class="org-option"
|
|
|
|
|
|
:class="{ active: selectedOrgId === null }"
|
|
|
|
|
|
@click="switchOrgAll"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text>全部</text>
|
|
|
|
|
|
<text v-if="selectedOrgId === null" class="org-check">✓</text>
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</view>
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<view
|
|
|
|
|
|
v-for="org in orgs"
|
|
|
|
|
|
:key="org.id"
|
|
|
|
|
|
class="org-option"
|
|
|
|
|
|
:class="{ active: selectedOrgId === org.id }"
|
|
|
|
|
|
@click="switchOrg(org)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text>{{ org.name }}</text>
|
|
|
|
|
|
<text v-if="selectedOrgId === org.id" class="org-check">✓</text>
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<!-- Project Filter -->
|
|
|
|
|
|
<scroll-view class="project-filter" scroll-x enable-flex>
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="filter-tag"
|
|
|
|
|
|
:class="{ active: selectedProjectId === null }"
|
|
|
|
|
|
@click="selectProject(null)"
|
|
|
|
|
|
>
|
|
|
|
|
|
全部
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</view>
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<view
|
|
|
|
|
|
v-for="p in projects"
|
|
|
|
|
|
:key="p.id"
|
|
|
|
|
|
class="filter-tag"
|
|
|
|
|
|
:class="{ active: selectedProjectId === p.id }"
|
|
|
|
|
|
@click="selectProject(p.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ p.name }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
2026-07-03 04:10:46 +08:00
|
|
|
|
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<!-- 6 Stats -->
|
|
|
|
|
|
<view class="stats-row">
|
|
|
|
|
|
<view class="stat-item">
|
|
|
|
|
|
<text class="stat-value blue">{{ stats.online_cabinets ?? 0 }}</text>
|
|
|
|
|
|
<text class="stat-label">设备</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-item">
|
|
|
|
|
|
<text class="stat-value gray">{{ stats.total_channels ?? 0 }}</text>
|
|
|
|
|
|
<text class="stat-label">通道</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-item">
|
|
|
|
|
|
<text class="stat-value green">{{ stats.idle_channels ?? 0 }}</text>
|
|
|
|
|
|
<text class="stat-label">空闲</text>
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</view>
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<view class="stat-item">
|
|
|
|
|
|
<text class="stat-value blue">{{ stats.charging_count ?? 0 }}</text>
|
|
|
|
|
|
<text class="stat-label">充电</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-item">
|
|
|
|
|
|
<text class="stat-value purple">{{ stats.full_count ?? 0 }}</text>
|
|
|
|
|
|
<text class="stat-label">充满</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-item">
|
|
|
|
|
|
<text class="stat-value red">{{ stats.fault_count ?? 0 }}</text>
|
|
|
|
|
|
<text class="stat-label">故障</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Device Cards -->
|
|
|
|
|
|
<view v-if="cabinets.length === 0 && !loading" class="empty">
|
|
|
|
|
|
<text class="empty-text">暂无设备</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-else class="device-list">
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="cab in cabinets"
|
|
|
|
|
|
:key="cab.id"
|
|
|
|
|
|
class="device-card"
|
|
|
|
|
|
@click="goDetail(cab.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- Header: abstract_id + signal strength -->
|
|
|
|
|
|
<view class="card-header">
|
|
|
|
|
|
<text class="card-id">{{ cab.abstract_id }}</text>
|
|
|
|
|
|
<view class="signal-chip" :style="{ color: signalColor(cab.rssi) }">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
|
<path v-if="(cab.rssi ?? -1) >= 30" d="M7 20v-4"/>
|
|
|
|
|
|
<path v-if="(cab.rssi ?? -1) >= 40" d="M12 20v-8"/>
|
|
|
|
|
|
<path v-if="(cab.rssi ?? -1) >= 55" d="M17 20V8"/>
|
|
|
|
|
|
<path v-if="(cab.rssi ?? -1) >= 70" d="M22 4v16"/>
|
|
|
|
|
|
<path d="M2 20h.01"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<text class="signal-num">{{ cab.rssi ?? '-' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Name -->
|
|
|
|
|
|
<text class="card-name">{{ cab.name || '未命名' }}</text>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Channel stats (like PC CabinetGrid) -->
|
|
|
|
|
|
<view class="card-stats">
|
|
|
|
|
|
<view class="stat-chip total">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 17H7A5 5 0 0 1 7 7h2"/><path d="M15 7h2a5 5 0 1 1 0 10h-2"/><line x1="8" x2="16" y1="12" y2="12"/></svg>
|
|
|
|
|
|
{{ cab.channel_count ?? 0 }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-chip idle">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 7h1a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-2"/><path d="M6 7H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h1"/><path d="M11 10v2"/><path d="M11 14v.5"/><line x1="22" x2="22" y1="11" y2="13"/></svg>
|
|
|
|
|
|
{{ cab.idle_count ?? 0 }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-chip charge">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 7h1a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-2"/><path d="M6 7H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h1"/><path d="M11 7l-3 5h4l-3 5"/><line x1="22" x2="22" y1="11" y2="13"/></svg>
|
|
|
|
|
|
{{ cab.charging_count ?? 0 }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-chip full">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 7h1a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-2"/><path d="M6 7H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h1"/><path d="M7 10v4"/><path d="M11 10v4"/><path d="M15 10v4"/><line x1="22" x2="22" y1="11" y2="13"/></svg>
|
|
|
|
|
|
{{ cab.full_count ?? 0 }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-chip fault">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
|
|
|
|
{{ cab.fault_count ?? 0 }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-07-04 04:03:02 +08:00
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
|
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
|
|
|
|
|
|
import { h5Api, type DashboardData, type CabinetItem, type OrgItem } from '../../api'
|
2026-07-03 04:10:46 +08:00
|
|
|
|
import { useAuthStore } from '../../store/auth'
|
2026-07-04 04:03:02 +08:00
|
|
|
|
import jsQR from 'jsqr'
|
2026-07-03 04:10:46 +08:00
|
|
|
|
|
|
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
const user = authStore.user
|
|
|
|
|
|
|
2026-07-04 04:03:02 +08:00
|
|
|
|
/** RSSI → 信号颜色 */
|
|
|
|
|
|
function signalColor(rssi?: number): string {
|
|
|
|
|
|
if (!rssi || rssi < 30) return '#9ca3af' // <30 离线
|
|
|
|
|
|
if (rssi >= 70) return '#16a34a' // 强
|
|
|
|
|
|
if (rssi >= 55) return '#2563eb' // 良好
|
|
|
|
|
|
if (rssi >= 40) return '#f59e0b' // 中等
|
|
|
|
|
|
return '#f97316' // 弱
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const stats = ref<DashboardData>({
|
|
|
|
|
|
online_cabinets: 0, total_channels: 0, charging_count: 0, idle_channels: 0,
|
|
|
|
|
|
full_count: 0, fault_channels: 0, fault_count: 0,
|
|
|
|
|
|
today_charge_count: 0, today_energy_kwh: 0, today_charge_hours: 0,
|
|
|
|
|
|
alerts: [], projects: [],
|
|
|
|
|
|
})
|
|
|
|
|
|
const projects = computed(() => stats.value.projects || [])
|
|
|
|
|
|
const cabinets = ref<CabinetItem[]>([])
|
|
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const selectedProjectId = ref<number | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
// Org switcher state
|
|
|
|
|
|
const orgs = ref<OrgItem[]>([])
|
|
|
|
|
|
const showOrgPicker = ref(false)
|
|
|
|
|
|
const selectedOrgId = ref<number | null>(null)
|
|
|
|
|
|
const currentOrgName = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
// Init org from user info
|
|
|
|
|
|
async function initOrg() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await h5Api.getOrganizations()
|
|
|
|
|
|
orgs.value = res || []
|
|
|
|
|
|
// Default to 全部 (no org filter)
|
|
|
|
|
|
switchOrgAll()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
currentOrgName.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function switchOrgAll() {
|
|
|
|
|
|
showOrgPicker.value = false
|
|
|
|
|
|
selectedOrgId.value = null
|
|
|
|
|
|
currentOrgName.value = '全部'
|
|
|
|
|
|
selectedProjectId.value = null
|
|
|
|
|
|
fetchDashboard()
|
|
|
|
|
|
fetchCabinets()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function switchOrg(org: OrgItem) {
|
|
|
|
|
|
showOrgPicker.value = false
|
|
|
|
|
|
selectedOrgId.value = org.id
|
|
|
|
|
|
currentOrgName.value = org.name
|
|
|
|
|
|
selectedProjectId.value = null
|
|
|
|
|
|
fetchDashboard()
|
|
|
|
|
|
fetchCabinets()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onShow(async () => {
|
|
|
|
|
|
// 扫码入口检测:从全局store取参数
|
|
|
|
|
|
const scanId = authStore.scanAbstractId
|
|
|
|
|
|
if (scanId) {
|
|
|
|
|
|
authStore.clearScanId()
|
|
|
|
|
|
if (!authStore.isAuthenticated) {
|
|
|
|
|
|
uni.navigateTo({ url: '/pages/login/index' })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 已登录则直接查设备跳转
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await h5Api.getCabinetByAbstract(scanId)
|
|
|
|
|
|
uni.navigateTo({ url: `/pages/device-detail/index?id=${res.data.id}` })
|
|
|
|
|
|
return
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
uni.showToast({ title: '设备不存在', icon: 'none' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 04:10:46 +08:00
|
|
|
|
if (!authStore.isAuthenticated) {
|
|
|
|
|
|
uni.navigateTo({ url: '/pages/login/index' })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
initOrg()
|
2026-07-03 04:10:46 +08:00
|
|
|
|
fetchDashboard()
|
2026-07-04 04:03:02 +08:00
|
|
|
|
fetchCabinets()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 下拉刷新
|
|
|
|
|
|
onPullDownRefresh(async () => {
|
|
|
|
|
|
await Promise.all([fetchDashboard(), fetchCabinets()])
|
|
|
|
|
|
uni.stopPullDownRefresh()
|
2026-07-03 04:10:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchDashboard() {
|
|
|
|
|
|
try {
|
2026-07-04 04:03:02 +08:00
|
|
|
|
const params: Record<string, string> = {}
|
|
|
|
|
|
if (selectedOrgId.value) params.org_id = String(selectedOrgId.value)
|
|
|
|
|
|
const res = await h5Api.getDashboard(params)
|
|
|
|
|
|
stats.value = res.data
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 静默失败,显示零值
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchCabinets() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params: Record<string, any> = {}
|
|
|
|
|
|
const pid = selectedProjectId.value
|
|
|
|
|
|
if (pid !== null) params.project_id = pid
|
|
|
|
|
|
if (selectedOrgId.value) params.org_id = selectedOrgId.value
|
|
|
|
|
|
const res = await h5Api.getCabinets(Object.keys(params).length > 0 ? params : undefined)
|
|
|
|
|
|
cabinets.value = res.data || []
|
2026-07-03 04:10:46 +08:00
|
|
|
|
} catch {
|
2026-07-04 04:03:02 +08:00
|
|
|
|
cabinets.value = []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 04:03:02 +08:00
|
|
|
|
function selectProject(id: number | null) {
|
|
|
|
|
|
selectedProjectId.value = id
|
|
|
|
|
|
fetchCabinets()
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 04:03:02 +08:00
|
|
|
|
function goDetail(id: number) {
|
|
|
|
|
|
uni.navigateTo({ url: `/pages/device-detail/index?id=${id}` })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleScan() {
|
|
|
|
|
|
if (!navigator.mediaDevices?.getUserMedia) return showScanInput()
|
|
|
|
|
|
|
|
|
|
|
|
const video = document.createElement('video')
|
|
|
|
|
|
video.setAttribute('autoplay', '')
|
|
|
|
|
|
video.setAttribute('playsinline', '')
|
|
|
|
|
|
video.style.width = '100%'
|
|
|
|
|
|
video.style.maxWidth = '400px'
|
|
|
|
|
|
video.style.borderRadius = '8px'
|
|
|
|
|
|
|
|
|
|
|
|
const overlay = document.createElement('div')
|
|
|
|
|
|
overlay.style.cssText = 'position:fixed;inset:0;background:#000;z-index:9999;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:24px'
|
|
|
|
|
|
const closeBtn = document.createElement('button')
|
|
|
|
|
|
closeBtn.textContent = '取消'
|
|
|
|
|
|
closeBtn.style.cssText = 'padding:10px 32px;border:none;border-radius:8px;background:#fff;font-size:16px;cursor:pointer'
|
|
|
|
|
|
const hint = document.createElement('p')
|
|
|
|
|
|
hint.textContent = '将二维码对准摄像头'
|
|
|
|
|
|
hint.style.cssText = 'color:#fff;font-size:14px;margin:0'
|
|
|
|
|
|
|
|
|
|
|
|
overlay.appendChild(hint)
|
|
|
|
|
|
overlay.appendChild(video)
|
|
|
|
|
|
overlay.appendChild(closeBtn)
|
|
|
|
|
|
document.body.appendChild(overlay)
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = document.createElement('canvas')
|
|
|
|
|
|
const ctx = canvas.getContext('2d')!
|
|
|
|
|
|
let stream: MediaStream | null = null
|
|
|
|
|
|
let scanning = true
|
|
|
|
|
|
|
|
|
|
|
|
const stop = () => {
|
|
|
|
|
|
scanning = false
|
|
|
|
|
|
stream?.getTracks().forEach(t => t.stop())
|
|
|
|
|
|
overlay.remove()
|
|
|
|
|
|
}
|
|
|
|
|
|
closeBtn.onclick = stop
|
|
|
|
|
|
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }).then(async (s) => {
|
|
|
|
|
|
stream = s
|
|
|
|
|
|
video.srcObject = s
|
|
|
|
|
|
await video.play()
|
|
|
|
|
|
|
|
|
|
|
|
const scan = () => {
|
|
|
|
|
|
if (!scanning) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (video.videoWidth > 0 && video.videoHeight > 0) {
|
|
|
|
|
|
canvas.width = video.videoWidth
|
|
|
|
|
|
canvas.height = video.videoHeight
|
|
|
|
|
|
ctx.drawImage(video, 0, 0)
|
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
|
|
|
|
const result = jsQR(imageData.data, imageData.width, imageData.height)
|
|
|
|
|
|
if (result) {
|
|
|
|
|
|
const code = result.data.trim()
|
|
|
|
|
|
stop()
|
|
|
|
|
|
const match = code.match(/\/(\d{2}-\d{8})$/) || code.match(/[?&]id=(\d{2}-\d{8})/)
|
|
|
|
|
|
const abstractId = match ? match[1] : code
|
|
|
|
|
|
if (/^\d{2}-\d{8}$/.test(abstractId)) {
|
|
|
|
|
|
goToDeviceByScan(abstractId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uni.showToast({ title: '无效的二维码', icon: 'none' })
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
setTimeout(scan, 300)
|
|
|
|
|
|
}
|
|
|
|
|
|
scan()
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
stop()
|
|
|
|
|
|
showScanInput()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function goToDeviceByScan(code: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await h5Api.scanCabinet(code)
|
|
|
|
|
|
uni.navigateTo({ url: `/pages/device-detail/index?id=${result.data.id}` })
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
const msg = err?.message || ''
|
|
|
|
|
|
if (msg.includes('没有权限') || msg.includes('403') || msg.includes('Forbidden')) {
|
|
|
|
|
|
uni.showToast({ title: '没有权限访问该设备', icon: 'none' })
|
|
|
|
|
|
} else if (msg.includes('不存在')) {
|
|
|
|
|
|
uni.showToast({ title: '设备不存在', icon: 'none' })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uni.showToast({ title: '扫码失败: ' + msg, icon: 'none' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showScanInput() {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '扫码',
|
|
|
|
|
|
content: '请输入或粘贴二维码中的设备编号',
|
|
|
|
|
|
editable: true,
|
|
|
|
|
|
placeholderText: '如: 10-00000000',
|
|
|
|
|
|
success: async (res) => {
|
|
|
|
|
|
if (!res.confirm || !res.content?.trim()) return
|
|
|
|
|
|
const code = res.content.trim()
|
|
|
|
|
|
goToDeviceByScan(code)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.home-page {
|
2026-07-04 04:03:02 +08:00
|
|
|
|
padding: 12px 12px 0;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
|
|
|
|
|
|
/* Top Bar */
|
|
|
|
|
|
.top-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.org-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
background: #f3f4f6;
|
|
|
|
|
|
border-radius: 6px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.org-name {
|
|
|
|
|
|
font-size: 14px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #111;
|
|
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.org-arrow {
|
|
|
|
|
|
font-size: 12px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
color: #6b7280;
|
2026-07-04 04:03:02 +08:00
|
|
|
|
margin-left: 4px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.scan-btn {
|
2026-07-03 04:10:46 +08:00
|
|
|
|
display: flex;
|
2026-07-04 04:03:02 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
color: #6b7280;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.scan-icon {
|
|
|
|
|
|
width: 22px;
|
|
|
|
|
|
height: 22px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
|
|
|
|
|
|
/* Org Picker Modal */
|
|
|
|
|
|
.org-overlay {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
|
|
|
|
background: rgba(0,0,0,0.4);
|
|
|
|
|
|
z-index: 100;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.org-picker {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
width: 85%;
|
|
|
|
|
|
max-height: 60vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.org-picker-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-bottom: 1px solid #f3f4f6;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.org-picker-title {
|
|
|
|
|
|
font-size: 16px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #111;
|
|
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.org-picker-close {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.org-option {
|
2026-07-03 04:10:46 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
2026-07-04 04:03:02 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
color: #374151;
|
|
|
|
|
|
border-bottom: 1px solid #f9fafb;
|
|
|
|
|
|
}
|
|
|
|
|
|
.org-option.active {
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
background: #eff6ff;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.org-option:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.org-check {
|
|
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Project filter */
|
|
|
|
|
|
.project-filter {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
padding-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.filter-tag {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
padding: 5px 14px;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
background-color: #f3f4f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.filter-tag.active {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background-color: #2563eb;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 5 Stats */
|
|
|
|
|
|
.stats-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-item {
|
2026-07-03 04:10:46 +08:00
|
|
|
|
flex: 1;
|
2026-07-04 04:03:02 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 8px 2px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.stat-value {
|
|
|
|
|
|
font-size: 18px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.stat-value.blue { color: #2563eb; }
|
|
|
|
|
|
.stat-value.gray { color: #6b7280; }
|
|
|
|
|
|
.stat-value.green { color: #16a34a; }
|
|
|
|
|
|
.stat-value.purple { color: #7c3aed; }
|
|
|
|
|
|
.stat-value.red { color: #dc2626; }
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
|
font-size: 11px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
color: #6b7280;
|
2026-07-04 04:03:02 +08:00
|
|
|
|
margin-top: 4px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
|
|
|
|
|
|
/* Empty */
|
|
|
|
|
|
.empty {
|
|
|
|
|
|
padding: 40px 0;
|
|
|
|
|
|
text-align: center;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.empty-text {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Device Cards */
|
|
|
|
|
|
.device-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding-bottom: 12px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.device-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.device-card:active {
|
|
|
|
|
|
background-color: #f9fafb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Card header */
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card-id {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
color: #111;
|
|
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.badge {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.signal-chip {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 2px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.signal-chip svg {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.signal-num {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Card name */
|
|
|
|
|
|
.card-name {
|
|
|
|
|
|
font-size: 15px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
color: #374151;
|
2026-07-04 04:03:02 +08:00
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Channel stats */
|
|
|
|
|
|
.card-stats {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
|
border-top: 1px solid #f3f4f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-chip {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-chip svg {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
2026-07-03 04:10:46 +08:00
|
|
|
|
}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
.stat-chip.total { color: #6b7280; }
|
|
|
|
|
|
.stat-chip.idle { color: #16a34a; }
|
|
|
|
|
|
.stat-chip.charge { color: #2563eb; }
|
|
|
|
|
|
.stat-chip.full { color: #7c3aed; }
|
|
|
|
|
|
.stat-chip.fault { color: #dc2626; }
|
2026-07-03 04:10:46 +08:00
|
|
|
|
</style>
|