feat:设备概括-添加筛选条件及下拉刷新

master
黄伟杰 6 days ago
parent f47c203896
commit 9d3ab2553b

@ -1,4 +1,4 @@
import request from '@/utils/request'
import request from '@/utils/request'
export function getIotDevicePage(params = {}) {
return request({
@ -15,3 +15,11 @@ export function getDeviceRunOverview(params = {}) {
params
})
}
export function getRunOverviewOrganizationList(params = {}) {
return request({
url: '/admin-api/mes/organization/list',
method: 'get',
params
})
}

@ -3,6 +3,7 @@
<NavBar :title="t('deviceOverview.title')" />
<view class="summary-wrap">
<view class="section-heading">设备状态</view>
<view class="summary-grid">
<view
v-for="item in statisticCards"
@ -20,14 +21,74 @@
<text class="stat-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="refresh-row" @click="fetchOverview">
<uni-icons type="refresh" size="18" color="#64748b"></uni-icons>
<text class="refresh-text">刷新数据</text>
<uni-popup ref="filterPopupRef" class="overview-filter-popup" type="right" background-color="transparent" :animation="false">
<view class="filter-drawer">
<view class="drawer-header">
<text class="drawer-title">更多筛选</text>
</view>
<scroll-view scroll-y class="drawer-body">
<view class="drawer-section drawer-fields">
<view class="drawer-field">
<text class="drawer-label">设备分组</text>
<picker class="drawer-picker-host" :range="groupPickerLabels" :value="groupPickerIndex" @change="onGroupChange">
<view class="drawer-picker">
<text :class="['drawer-picker-text', runFilter.groupId === '' ? 'placeholder' : '']">{{ selectedGroupLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<view class="drawer-field">
<text class="drawer-label">设备名称</text>
<picker class="drawer-picker-host" :range="devicePickerLabels" :value="devicePickerIndex" @change="onDeviceFilterChange">
<view class="drawer-picker">
<text :class="['drawer-picker-text', runFilter.deviceId === '' ? 'placeholder' : '']">{{ selectedDeviceLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<view class="drawer-field drawer-field-wide">
<text class="drawer-label">时间范围</text>
<view class="drawer-date">
<uni-datetime-picker
v-model="runFilter.timeRange"
type="datetimerange"
:clear-icon="true"
start-placeholder="开始时间"
end-placeholder="结束时间"
/>
</view>
</view>
<view class="drawer-field drawer-field-column">
<text class="drawer-label">快捷日期</text>
<view class="quick-range-row">
<view
v-for="item in quickRanges"
:key="item.value"
:class="['quick-range-btn', runFilter.quickRange === item.value ? 'active' : '']"
@click="handleQuickRangeChange(item.value)"
>
{{ item.label }}
</view>
</view>
</view>
</view>
</scroll-view>
<view class="drawer-actions">
<view class="drawer-action reset" @click="resetRunFilter"></view>
<view class="drawer-action confirm" @click="confirmFilterDrawer"></view>
</view>
</view>
</view>
</uni-popup>
<view class="operation-wrap">
<view class="section-heading-row">
<view class="section-heading operation-heading">设备运行</view>
<view class="operation-filter-btn" @click="openFilterDrawer">
<uni-icons type="settings" size="22" color="#7b8491"></uni-icons>
</view>
</view>
<view class="metric-grid">
<view v-for="item in operationMetrics" :key="item.key" class="metric-card">
<view class="metric-icon" :style="{ background: item.bgColor }">
@ -83,13 +144,22 @@ import { computed, reactive, ref } from 'vue'
import { onLoad, onPullDownRefresh, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceRunOverview, getIotDevicePage } from '@/api/iot/device'
import { getDeviceRunOverview, getIotDevicePage, getRunOverviewOrganizationList } from '@/api/iot/device'
const { t } = useI18n()
const loading = ref(false)
const loadFailed = ref(false)
const initialized = ref(false)
const filterPopupRef = ref(null)
const organizationList = ref([])
const allDeviceRows = ref([])
const runFilter = reactive({
groupId: '',
deviceId: '',
quickRange: 'today',
timeRange: createDateRangeByQuickKey('today')
})
const statistics = reactive({
total: 0,
@ -120,6 +190,47 @@ const statusConfig = {
offline: { label: '离线', color: '#cfd4df' }
}
const quickRanges = computed(() => [
{ label: '今天', value: 'today' },
{ label: '昨天', value: 'yesterday' },
{ label: '近7天', value: 'last7Days' },
{ label: '近30天', value: 'last30Days' }
])
const groupPickerOptions = computed(() => [
{ id: '', name: '设备分组' },
...organizationList.value
])
const groupPickerLabels = computed(() => groupPickerOptions.value.map((item) => item.name || '-'))
const groupPickerIndex = computed(() => {
const index = groupPickerOptions.value.findIndex((item) => String(item.id) === String(runFilter.groupId))
return index >= 0 ? index : 0
})
const selectedGroupLabel = computed(() => {
const found = groupPickerOptions.value.find((item) => String(item.id) === String(runFilter.groupId))
return found?.name || '设备分组'
})
const devicePickerOptions = computed(() => [
{ value: '', label: '设备名称' },
...collectDeviceOptionsByGroup(runFilter.groupId)
])
const devicePickerLabels = computed(() => devicePickerOptions.value.map((item) => item.label || '-'))
const devicePickerIndex = computed(() => {
const index = devicePickerOptions.value.findIndex((item) => String(item.value) === String(runFilter.deviceId))
return index >= 0 ? index : 0
})
const selectedDeviceLabel = computed(() => {
const found = devicePickerOptions.value.find((item) => String(item.value) === String(runFilter.deviceId))
return found?.label || '设备名称'
})
const statisticCards = computed(() => [
{
key: 'total',
@ -205,7 +316,7 @@ const donutStyle = computed(() => {
return { background: 'conic-gradient(' + segments.join(', ') + ')' }
})
onLoad(fetchOverview)
onLoad(initPage)
onShow(() => {
if (initialized.value) fetchOverview(false)
@ -216,6 +327,11 @@ onPullDownRefresh(async () => {
uni.stopPullDownRefresh()
})
async function initPage() {
await loadOrganizations()
await fetchOverview()
}
async function fetchOverview(showLoading = true) {
if (loading.value) return
if (showLoading) loading.value = true
@ -231,11 +347,14 @@ async function fetchOverview(showLoading = true) {
})
rows = allPage.list
}
setStatistics(rows, total)
await fetchRunOverview(rows)
allDeviceRows.value = rows
const filteredRows = filterRowsByRunFilter(rows)
setStatistics(filteredRows, filteredRows.length)
await fetchRunOverview(filteredRows)
initialized.value = true
} catch (error) {
loadFailed.value = true
allDeviceRows.value = []
resetStatistics()
resetOperationOverview()
} finally {
@ -243,11 +362,32 @@ async function fetchOverview(showLoading = true) {
}
}
async function loadOrganizations() {
try {
const res = await getRunOverviewOrganizationList({})
organizationList.value = normalizeOrganizationList(res)
} catch (error) {
organizationList.value = []
}
}
async function getDeviceRows(params) {
const res = await getIotDevicePage(params)
return normalizePageData(res)
}
function filterRowsByRunFilter(rows) {
if (runFilter.deviceId) {
return rows.filter((row) => String(row?.id) === String(runFilter.deviceId))
}
const deviceIds = getCurrentDeviceIds()
if (runFilter.groupId && deviceIds.length) {
const idSet = new Set(deviceIds.map((id) => String(id)))
return rows.filter((row) => idSet.has(String(row?.id)))
}
return rows
}
function setStatistics(rows, totalCount) {
const next = {
total: Number(totalCount || rows.length || 0),
@ -277,15 +417,12 @@ function resetStatistics() {
async function fetchRunOverview(rows) {
try {
const ids = rows
.map((item) => item?.id)
.filter((id) => id !== undefined && id !== null && String(id) !== '')
.join(',')
const range = buildTodayRange()
const ids = getRunOverviewDeviceIds(rows).join(',')
const range = normalizeTimeRange(runFilter.timeRange)
const res = await getDeviceRunOverview({
ids,
startTime: range.startTime,
endTime: range.endTime,
startTime: range[0],
endTime: range[1],
timelinePageNo: 1,
timelinePageSize: 1
})
@ -360,6 +497,123 @@ function buildFallbackSummary() {
}))
}
function createDateRangeByQuickKey(key) {
const now = new Date()
const start = new Date(now)
const end = new Date(now)
if (key === 'yesterday') {
start.setDate(start.getDate() - 1)
end.setDate(end.getDate() - 1)
} else if (key === 'last7Days') {
start.setDate(start.getDate() - 6)
} else if (key === 'last30Days') {
start.setDate(start.getDate() - 29)
}
start.setHours(0, 0, 0, 0)
end.setHours(23, 59, 59, 999)
return [formatDateTime(start), formatDateTime(end)]
}
function normalizeTimeRange(value) {
return Array.isArray(value) && value.length === 2 && value[0] && value[1]
? value
: createDateRangeByQuickKey('today')
}
function getRunOverviewDeviceIds(rows) {
const selectedIds = getCurrentDeviceIds()
if (selectedIds.length) return selectedIds
return rows
.map((item) => item?.id)
.filter((id) => id !== undefined && id !== null && String(id) !== '')
}
function getCurrentDeviceIds() {
if (runFilter.deviceId) return [runFilter.deviceId]
const options = collectDeviceOptionsByGroup(runFilter.groupId)
return options.map((item) => item.value).filter((value) => value !== undefined && value !== null && String(value) !== '')
}
function collectDeviceOptionsByGroup(groupId) {
const deviceMap = new Map()
const pushDevice = (node) => {
if (!node?.dvId || !String(node.machineName || '').trim()) return
const id = String(node.dvId)
if (!deviceMap.has(id)) {
deviceMap.set(id, { label: String(node.machineName).trim(), value: id })
}
}
if (!groupId) {
organizationList.value.forEach(pushDevice)
return Array.from(deviceMap.values())
}
const childrenMap = organizationList.value.reduce((acc, item) => {
const parentKey = String(item.parentId ?? 0)
if (!acc[parentKey]) acc[parentKey] = []
acc[parentKey].push(item)
return acc
}, {})
const queue = [String(groupId)]
while (queue.length) {
const current = queue.shift()
const node = organizationList.value.find((item) => String(item.id) === current)
pushDevice(node)
;(childrenMap[current] || []).forEach((child) => queue.push(String(child.id)))
}
return Array.from(deviceMap.values())
}
function normalizeOrganizationList(res) {
const root = res && res.data !== undefined ? res.data : res
const list = Array.isArray(root) ? root : Array.isArray(root?.list) ? root.list : Array.isArray(root?.data) ? root.data : []
return list.map((item) => ({
...item,
id: String(item.id ?? ''),
parentId: item.parentId != null ? String(item.parentId) : item.parentId,
name: item.name || item.label || String(item.id || '')
}))
}
function onGroupChange(e) {
const item = groupPickerOptions.value[Number(e?.detail?.value || 0)]
if (!item) return
runFilter.groupId = item.id || ''
runFilter.deviceId = ''
}
function onDeviceFilterChange(e) {
const item = devicePickerOptions.value[Number(e?.detail?.value || 0)]
if (!item) return
runFilter.deviceId = item.value || ''
}
function handleQuickRangeChange(key) {
runFilter.quickRange = key
runFilter.timeRange = createDateRangeByQuickKey(key)
}
function openFilterDrawer() {
filterPopupRef.value?.open()
}
function closeFilterDrawer() {
filterPopupRef.value?.close()
}
async function confirmFilterDrawer() {
closeFilterDrawer()
await fetchOverview()
}
async function resetRunFilter() {
runFilter.groupId = ''
runFilter.deviceId = ''
runFilter.quickRange = 'today'
runFilter.timeRange = createDateRangeByQuickKey('today')
closeFilterDrawer()
await fetchOverview()
}
function buildTodayRange() {
const start = new Date()
start.setHours(0, 0, 0, 0)
@ -429,6 +683,14 @@ function openDeviceList(item) {
padding: 24rpx;
}
.section-heading {
margin-bottom: 18rpx;
font-size: 32rpx;
line-height: 1.3;
color: #0f172a;
font-weight: 800;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@ -508,21 +770,253 @@ function openDeviceList(item) {
background: #eff6ff;
}
.refresh-row {
margin-top: 24rpx;
height: 72rpx;
.section-heading-row {
margin-bottom: 18rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
}
.operation-heading {
margin-bottom: 0;
}
.operation-filter-btn {
width: 66rpx;
height: 66rpx;
flex: 0 0 66rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
background: transparent;
}
.operation-filter-btn:active {
background: #e5e7eb;
}
.filter-drawer {
width: 630rpx;
height: calc(100vh - var(--status-bar-height));
margin-top: var(--status-bar-height);
background: #f5f5f7;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 28rpx 0 0 28rpx;
}
:deep(.overview-filter-popup.right .uni-popup__content-transition) {
transform: none !important;
}
.drawer-header {
height: 104rpx;
padding: 18rpx 34rpx 0;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
color: #64748b;
box-sizing: border-box;
}
.drawer-title {
color: #1f2937;
font-size: 34rpx;
line-height: 1.3;
font-weight: 700;
}
.drawer-body {
flex: 1;
min-height: 0;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.refresh-text {
.drawer-section {
margin-bottom: 18rpx;
padding: 8rpx 28rpx;
border-radius: 24rpx;
background: #ffffff;
box-sizing: border-box;
}
.drawer-fields {
display: flex;
flex-direction: column;
}
.drawer-field {
min-width: 0;
min-height: 98rpx;
display: flex;
align-items: center;
gap: 20rpx;
border-bottom: 1rpx solid #eceff3;
box-sizing: border-box;
}
.drawer-field:last-child {
border-bottom: 0;
}
.drawer-field-column {
align-items: stretch;
flex-direction: column;
gap: 14rpx;
padding: 24rpx 0;
}
.drawer-label {
width: 150rpx;
flex: 0 0 150rpx;
font-size: 24rpx;
line-height: 1.3;
color: #4b5563;
font-weight: 500;
}
.drawer-field-column .drawer-label {
width: auto;
flex: none;
}
.drawer-picker-host {
min-width: 0;
flex: 1;
display: block;
}
.drawer-picker,
.drawer-date {
min-width: 0;
flex: 1;
width: 100%;
min-height: 74rpx;
border: 0;
border-radius: 8rpx;
background: #f7f8fb;
box-sizing: border-box;
}
.drawer-picker {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 18rpx;
gap: 8rpx;
}
.drawer-picker-text {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 26rpx;
line-height: 1;
color: #111827;
text-align: right;
}
.drawer-picker-text.placeholder {
color: #9ca3af;
}
.drawer-date {
display: flex;
align-items: center;
padding: 0 12rpx;
}
.drawer-date :deep(.uni-date),
.drawer-date :deep(.uni-date-editor),
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) {
width: 100%;
}
.drawer-date :deep(.uni-date-editor),
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) {
min-height: 74rpx;
}
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) {
border: 0;
padding: 0;
background: transparent;
}
.drawer-date :deep(.uni-date-range) {
display: flex;
align-items: center;
}
.drawer-date :deep(.uni-date__x-input) {
text-align: center;
font-size: 26rpx;
color: #111827;
}
.quick-range-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
}
.quick-range-btn {
height: 62rpx;
border-radius: 8rpx;
background: #f1f5f9;
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 700;
box-sizing: border-box;
}
.quick-range-btn.active {
background: #174b78;
color: #ffffff;
}
.drawer-actions {
height: 126rpx;
padding: 18rpx 28rpx 24rpx;
box-sizing: border-box;
display: flex;
align-items: center;
background: #ffffff;
box-shadow: 0 -8rpx 24rpx rgba(17, 24, 39, 0.06);
}
.drawer-action {
flex: 1;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
border: 2rpx solid #174b78;
box-sizing: border-box;
}
.drawer-action.reset {
border-radius: 12rpx 0 0 12rpx;
background: #ffffff;
color: #174b78;
}
.drawer-action.confirm {
border-radius: 0 12rpx 12rpx 0;
background: #174b78;
color: #ffffff;
}
.hint {

Loading…
Cancel
Save