diff --git a/src/api/iot/device.js b/src/api/iot/device.js
index edc3e6b..5c15e66 100644
--- a/src/api/iot/device.js
+++ b/src/api/iot/device.js
@@ -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
+ })
+}
diff --git a/src/pages_function/pages/overview/device/index.vue b/src/pages_function/pages/overview/device/index.vue
index f3d0010..f470a19 100644
--- a/src/pages_function/pages/overview/device/index.vue
+++ b/src/pages_function/pages/overview/device/index.vue
@@ -3,6 +3,7 @@
+ 设备状态
{{ item.label }}
+
-
-
- 刷新数据
+
+
+
+ 设备运行
+
+
+
+
@@ -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 {