|
|
@@ -1,32 +1,41 @@
|
|
|
<template>
|
|
|
<view class="device-list-container">
|
|
|
+ <!-- 站点选择 -->
|
|
|
+ <picker
|
|
|
+ v-if="stationList.length > 0"
|
|
|
+ mode="selector"
|
|
|
+ :range="stationList"
|
|
|
+ range-key="stationName"
|
|
|
+ :value="selectedStationIndex"
|
|
|
+ @change="onStationChange"
|
|
|
+ >
|
|
|
+ <view class="station-picker">
|
|
|
+ <view class="station-picker-row">
|
|
|
+ <AppIcon name="building" :size="18" color="#C6171E" />
|
|
|
+ <text class="station-picker-name">{{ currentStation?.stationName || '选择站点' }}</text>
|
|
|
+ <AppIcon name="chevron-down" :size="14" color="#999999" />
|
|
|
+ </view>
|
|
|
+ <text class="station-picker-count" v-if="deviceList.length > 0">共 {{ totalDevices }} 台设备</text>
|
|
|
+ </view>
|
|
|
+ </picker>
|
|
|
+
|
|
|
+ <!-- 搜索栏 -->
|
|
|
<view class="search-bar">
|
|
|
<view class="search-input-wrap">
|
|
|
<AppIcon name="search" size="16" color="#999999" />
|
|
|
<input
|
|
|
type="text"
|
|
|
- placeholder="请输入设备名称或编号"
|
|
|
+ placeholder="搜索设备名称或编号"
|
|
|
v-model="searchKeyword"
|
|
|
@confirm="handleSearch"
|
|
|
/>
|
|
|
- </view>
|
|
|
- <button class="search-btn" @click="handleSearch">搜索</button>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view class="filter-bar">
|
|
|
- <view class="segmented-control">
|
|
|
- <view
|
|
|
- v-for="(option, index) in filterOptions"
|
|
|
- :key="index"
|
|
|
- class="segment-item"
|
|
|
- :class="{ active: activeFilter === index }"
|
|
|
- @click="activeFilter = index; handleFilterChange(index)"
|
|
|
- >
|
|
|
- <text>{{ option.label }}</text>
|
|
|
+ <view v-if="searchKeyword" class="search-clear" @click="searchKeyword = ''; handleSearch()">
|
|
|
+ <AppIcon name="x" size="14" color="#B0B0B0" />
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
+ <!-- 设备列表 -->
|
|
|
<view class="device-list">
|
|
|
<view
|
|
|
v-for="device in deviceList"
|
|
|
@@ -68,12 +77,7 @@
|
|
|
</view>
|
|
|
|
|
|
<view class="device-footer" v-if="isDeviceOnline(getDeviceStatusValue(device))">
|
|
|
- <button
|
|
|
- class="stop-btn"
|
|
|
- @click.stop="handleStopDevice(device)"
|
|
|
- >
|
|
|
- 停止设备
|
|
|
- </button>
|
|
|
+ <button class="stop-btn" @click.stop="handleStopDevice(device)">停止设备</button>
|
|
|
</view>
|
|
|
|
|
|
<view v-if="showConfigDropdown === getDeviceId(device)" class="config-dropdown">
|
|
|
@@ -103,7 +107,7 @@
|
|
|
|
|
|
<view class="load-more" v-if="deviceList.length > 0">
|
|
|
<text v-if="loadMoreStatus === 'loading'">正在加载...</text>
|
|
|
- <text v-else-if="loadMoreStatus === 'noMore'">— 没有更多数据了 —</text>
|
|
|
+ <text v-else-if="loadMoreStatus === 'noMore'">— 没有更多了 —</text>
|
|
|
<text v-else>上拉加载更多</text>
|
|
|
</view>
|
|
|
|
|
|
@@ -118,14 +122,17 @@
|
|
|
<text class="loading-text">加载中...</text>
|
|
|
</view>
|
|
|
|
|
|
+ <CustomTabBar :selected="2" />
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
+import CustomTabBar from '../../components/CustomTabBar.vue'
|
|
|
import { ref, onMounted, computed } from 'vue'
|
|
|
import { onReachBottom } from '@dcloudio/uni-app'
|
|
|
import { getDeviceList, stopDevice, getDeviceConfigList, batchModifyDeviceConfig } from '../../api/device.js'
|
|
|
-import { showToast, fmtDictName, getDictColor } from '../../utils/index.js'
|
|
|
+import { getStationList } from '../../api/station.js'
|
|
|
+import { showToast, storage, fmtDictName, getDictColor } from '../../utils/index.js'
|
|
|
import dictUtil, { loadDicts } from '../../utils/dict.js'
|
|
|
|
|
|
const deviceList = ref([])
|
|
|
@@ -134,20 +141,46 @@ const page = ref(1)
|
|
|
const pageSize = ref(10)
|
|
|
const hasMore = ref(true)
|
|
|
const loadMoreStatus = ref('more')
|
|
|
-const activeFilter = ref(0)
|
|
|
const searchKeyword = ref('')
|
|
|
|
|
|
+const stationList = ref([])
|
|
|
+const currentStation = ref(null)
|
|
|
+const selectedStationIndex = ref(0)
|
|
|
+const totalDevices = ref(0)
|
|
|
+
|
|
|
const configList = ref([])
|
|
|
const showConfigDropdown = ref(null)
|
|
|
const loadingConfig = ref(false)
|
|
|
|
|
|
-const filterOptions = computed(() => dictUtil.getDictFilterOptions('WashDevice.status'))
|
|
|
-
|
|
|
onMounted(async () => {
|
|
|
- await loadDicts()
|
|
|
- loadDeviceList()
|
|
|
+ await Promise.all([loadDicts(), loadStationList()])
|
|
|
})
|
|
|
|
|
|
+const loadStationList = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getStationList({ pageNum: 1, pageSize: 1024 })
|
|
|
+ if (res && res.code === 200 && res.data && res.data.list) {
|
|
|
+ stationList.value = res.data.list
|
|
|
+ if (stationList.value.length > 0) {
|
|
|
+ currentStation.value = stationList.value[0]
|
|
|
+ selectedStationIndex.value = 0
|
|
|
+ storage.set('currentStationId', currentStation.value.stationId)
|
|
|
+ loadDeviceList()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载站点列表失败:', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onStationChange = async (e) => {
|
|
|
+ const index = e.detail.value
|
|
|
+ selectedStationIndex.value = index
|
|
|
+ currentStation.value = stationList.value[index]
|
|
|
+ storage.set('currentStationId', currentStation.value.stationId)
|
|
|
+ loadDeviceList()
|
|
|
+}
|
|
|
+
|
|
|
const loadDeviceList = async (isLoadMore = false) => {
|
|
|
if (!isLoadMore) {
|
|
|
page.value = 1
|
|
|
@@ -159,8 +192,8 @@ const loadDeviceList = async (isLoadMore = false) => {
|
|
|
const params = {
|
|
|
page: page.value,
|
|
|
pageSize: pageSize.value,
|
|
|
- keyword: searchKeyword.value,
|
|
|
- status: activeFilter.value === 0 ? '' : getStatusValue(activeFilter.value)
|
|
|
+ keyword: searchKeyword.value || '',
|
|
|
+ stationId: currentStation.value?.stationId || ''
|
|
|
}
|
|
|
|
|
|
const res = await getDeviceList(params)
|
|
|
@@ -170,16 +203,18 @@ const loadDeviceList = async (isLoadMore = false) => {
|
|
|
const records = data.records || data.list || data
|
|
|
const totalPages = data.pages || data.totalPages || 1
|
|
|
|
|
|
+ totalDevices.value = data.total || (Array.isArray(records) ? records.length : 0)
|
|
|
+
|
|
|
if (isLoadMore) {
|
|
|
deviceList.value = [...deviceList.value, ...records]
|
|
|
} else {
|
|
|
- deviceList.value = records
|
|
|
+ deviceList.value = Array.isArray(records) ? records : []
|
|
|
}
|
|
|
|
|
|
hasMore.value = page.value < totalPages
|
|
|
loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
|
|
|
|
|
|
- if (isLoadMore) page.value++
|
|
|
+ if (!isLoadMore) page.value++
|
|
|
}
|
|
|
} catch (error) {
|
|
|
showToast('获取设备列表失败')
|
|
|
@@ -191,22 +226,14 @@ const loadDeviceList = async (isLoadMore = false) => {
|
|
|
const loadMore = () => {
|
|
|
if (hasMore.value && !loading.value && loadMoreStatus.value !== 'loading') {
|
|
|
loadMoreStatus.value = 'loading'
|
|
|
+ page.value++
|
|
|
loadDeviceList(true)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-onReachBottom(() => {
|
|
|
- loadMore()
|
|
|
-})
|
|
|
-
|
|
|
-const handleSearch = () => {
|
|
|
- loadDeviceList()
|
|
|
-}
|
|
|
+onReachBottom(() => loadMore())
|
|
|
|
|
|
-const handleFilterChange = (index) => {
|
|
|
- activeFilter.value = index
|
|
|
- loadDeviceList()
|
|
|
-}
|
|
|
+const handleSearch = () => loadDeviceList()
|
|
|
|
|
|
const viewDeviceDetail = (deviceId) => {
|
|
|
uni.navigateTo({ url: `/pages/device/detail?id=${deviceId}` })
|
|
|
@@ -277,15 +304,9 @@ const selectConfig = async (deviceId, configId, configName) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-const getDeviceId = (device) => {
|
|
|
- if (!device) return null
|
|
|
- return device.id || device.deviceId || device.device_id || device.shortId || null
|
|
|
-}
|
|
|
+const getDeviceId = (device) => device?.id || device?.deviceId || device?.device_id || device?.shortId || null
|
|
|
|
|
|
-const getDeviceStatusValue = (device) => {
|
|
|
- if (!device) return null
|
|
|
- return device.status ?? device.state ?? device.stateCode ?? device.deviceStatus ?? null
|
|
|
-}
|
|
|
+const getDeviceStatusValue = (device) => device?.status ?? device?.state ?? device?.stateCode ?? device?.deviceStatus ?? null
|
|
|
|
|
|
const getDeviceStatusText = (status) => fmtDictName('WashDevice.status', status)
|
|
|
|
|
|
@@ -307,7 +328,7 @@ const getDeviceBusyStatusClass = (device) => {
|
|
|
|
|
|
const getDeviceStatusStyle = (status) => {
|
|
|
const color = getDictColor('WashDevice.status', status)
|
|
|
- if (color) return { color: color, backgroundColor: `${color}1A` }
|
|
|
+ if (color) return { color, backgroundColor: `${color}1A` }
|
|
|
return {}
|
|
|
}
|
|
|
|
|
|
@@ -315,13 +336,6 @@ const isDeviceOnline = (status) => {
|
|
|
const onlineValue = dictUtil.getDictValue('WashDevice.status', '在线')
|
|
|
return status == onlineValue
|
|
|
}
|
|
|
-
|
|
|
-const getStatusValue = (filterIndex) => {
|
|
|
- if (filterIndex === 0) return ''
|
|
|
- const options = dictUtil.getDictOptions('WashDevice.status')
|
|
|
- const idx = filterIndex - 1
|
|
|
- return options[idx] ? options[idx].value : ''
|
|
|
-}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -330,83 +344,72 @@ const getStatusValue = (filterIndex) => {
|
|
|
background-color: #F5F7FA;
|
|
|
min-height: 100vh;
|
|
|
box-sizing: border-box;
|
|
|
- padding-bottom: 100rpx;
|
|
|
+ padding-bottom: 120rpx;
|
|
|
}
|
|
|
|
|
|
-/* ===== Search Bar ===== */
|
|
|
-.search-bar {
|
|
|
+/* ===== Station Picker ===== */
|
|
|
+.station-picker {
|
|
|
+ background: #FFFFFF;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 24rpx;
|
|
|
display: flex;
|
|
|
- gap: 16rpx;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8rpx;
|
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
|
|
|
|
-.search-input-wrap {
|
|
|
- flex: 1;
|
|
|
+.station-picker:active {
|
|
|
+ background: #F5F7FA;
|
|
|
+}
|
|
|
+
|
|
|
+.station-picker-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 12rpx;
|
|
|
- background: #F5F5F5;
|
|
|
- border-radius: 16rpx;
|
|
|
- padding: 0 24rpx;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 10rpx;
|
|
|
}
|
|
|
|
|
|
-.search-input-wrap input {
|
|
|
- flex: 1;
|
|
|
- height: 72rpx;
|
|
|
- border: none;
|
|
|
- outline: none;
|
|
|
+.station-picker-name {
|
|
|
font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
color: #1A1A1A;
|
|
|
- background: transparent;
|
|
|
-}
|
|
|
-
|
|
|
-.search-btn {
|
|
|
- height: 72rpx;
|
|
|
- line-height: 72rpx;
|
|
|
- padding: 0 36rpx;
|
|
|
- background: #C6171E;
|
|
|
- color: #FFFFFF;
|
|
|
- border: none;
|
|
|
- border-radius: 16rpx;
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: 500;
|
|
|
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
}
|
|
|
|
|
|
-.search-btn:active {
|
|
|
- background: #A81212;
|
|
|
- transform: scale(0.97);
|
|
|
+.station-picker-count {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999999;
|
|
|
}
|
|
|
|
|
|
-/* ===== Filter Bar ===== */
|
|
|
-.filter-bar {
|
|
|
+/* ===== Search Bar ===== */
|
|
|
+.search-bar {
|
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
|
|
|
|
-.segmented-control {
|
|
|
+.search-input-wrap {
|
|
|
display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12rpx;
|
|
|
background: #FFFFFF;
|
|
|
border-radius: 16rpx;
|
|
|
- padding: 6rpx;
|
|
|
- gap: 6rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
|
}
|
|
|
|
|
|
-.segment-item {
|
|
|
+.search-input-wrap input {
|
|
|
flex: 1;
|
|
|
- text-align: center;
|
|
|
- padding: 16rpx 0;
|
|
|
- font-size: 26rpx;
|
|
|
- color: #666666;
|
|
|
- border-radius: 12rpx;
|
|
|
- font-weight: 500;
|
|
|
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
+ height: 72rpx;
|
|
|
+ border: none;
|
|
|
+ outline: none;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #1A1A1A;
|
|
|
+ background: transparent;
|
|
|
}
|
|
|
|
|
|
-.segment-item.active {
|
|
|
- color: #FFFFFF;
|
|
|
- font-weight: 600;
|
|
|
- background: #C6171E;
|
|
|
- box-shadow: 0 4rpx 12rpx rgba(198, 23, 30, 0.3);
|
|
|
+.search-clear {
|
|
|
+ padding: 6rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
|
|
|
/* ===== Device List ===== */
|
|
|
@@ -426,7 +429,7 @@ const getStatusValue = (filterIndex) => {
|
|
|
|
|
|
.device-item:active {
|
|
|
transform: translateY(-2rpx);
|
|
|
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
}
|
|
|
|
|
|
.device-header {
|
|
|
@@ -442,29 +445,12 @@ const getStatusValue = (filterIndex) => {
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
-.device-name-wrap {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 10rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.device-name-text {
|
|
|
+.device-name {
|
|
|
font-size: 30rpx;
|
|
|
font-weight: 600;
|
|
|
color: #1A1A1A;
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.pa-badge-list {
|
|
|
- margin-left: 8rpx;
|
|
|
- padding: 2rpx 10rpx;
|
|
|
- background: #C6171E;
|
|
|
- color: #FFFFFF;
|
|
|
- border-radius: 6rpx;
|
|
|
- font-size: 20rpx;
|
|
|
- font-weight: 700;
|
|
|
- line-height: 1.4;
|
|
|
- flex-shrink: 0;
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 10rpx;
|
|
|
}
|
|
|
|
|
|
.device-meta {
|
|
|
@@ -572,7 +558,7 @@ const getStatusValue = (filterIndex) => {
|
|
|
color: #C6171E;
|
|
|
}
|
|
|
|
|
|
-/* ===== Config Button (header top-right) ===== */
|
|
|
+/* ===== Config Button ===== */
|
|
|
.config-btn {
|
|
|
padding: 8rpx 20rpx;
|
|
|
background: #C6171E;
|
|
|
@@ -671,7 +657,7 @@ const getStatusValue = (filterIndex) => {
|
|
|
margin-top: 24rpx;
|
|
|
}
|
|
|
|
|
|
-/* ===== Empty ===== */
|
|
|
+/* ===== Empty State ===== */
|
|
|
.empty-state {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|