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 }} + - - - 刷新数据 + + + + 更多筛选 + + + + + 设备分组 + + + {{ selectedGroupLabel }} + + + + + + 设备名称 + + + {{ selectedDeviceLabel }} + + + + + + 时间范围 + + + + + + 快捷日期 + + + {{ 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 {