| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723 |
- <template>
- <view class="device-list-container">
- <view class="search-bar">
- <view class="search-input-wrap">
- <AppIcon name="search" size="16" color="#999999" />
- <input
- type="text"
- 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>
- </view>
- </view>
- <view class="device-list">
- <view
- v-for="device in deviceList"
- :key="device.id || device.deviceId"
- class="device-item"
- @click="viewDeviceDetail(getDeviceId(device))"
- >
- <view class="device-header">
- <view class="device-info">
- <text class="device-name">
- {{ device.name }}
- <text v-if="device.hasPa" class="pa-badge-list">PA</text>
- </text>
- <view class="device-meta">
- <text class="device-id">ID: {{ device.shortId }}</text>
- <view class="device-status" :style="getDeviceStatusStyle(getDeviceStatusValue(device))">
- <text class="status-dot"></text>
- <text>{{ getDeviceStatusText(getDeviceStatusValue(device)) }}</text>
- </view>
- <view class="device-busy-tag" :class="getDeviceBusyStatusClass(device)">
- <text>{{ getDeviceBusyStatus(device) }}</text>
- </view>
- </view>
- </view>
- <button
- class="config-btn"
- @click.stop="toggleConfigDropdown(getDeviceId(device))"
- >
- 设备配置
- </button>
- </view>
- <view class="device-body">
- <view class="device-row">
- <text class="row-label">所属站点</text>
- <text class="row-value">{{ device.stationName || '未分配站点' }}</text>
- </view>
- <view class="device-row">
- <text class="row-label">价格配置</text>
- <text class="row-value">{{ device.deviceConfigName || '未绑定配置' }}</text>
- </view>
- </view>
- <view class="device-footer" v-if="isDeviceOnline(getDeviceStatusValue(device))">
- <button
- class="stop-btn"
- @click.stop="handleStopDevice(device)"
- >
- 停止设备
- </button>
- </view>
- <view v-if="showConfigDropdown === getDeviceId(device)" class="config-dropdown">
- <view class="dropdown-header">
- <text class="dropdown-title">选择配置</text>
- <view class="dropdown-close" @click.stop="closeConfigDropdown">
- <AppIcon name="x" size="16" color="#999999" />
- </view>
- </view>
- <view class="dropdown-content">
- <view
- v-for="config in configList"
- :key="config.id"
- class="config-option"
- @click.stop="selectConfig(getDeviceId(device), config.id, config.name)"
- >
- <text class="config-option-name">{{ config.name }}</text>
- <text class="config-option-desc">{{ config.remark || '无描述' }}</text>
- </view>
- <view v-if="configList.length === 0" class="no-config">
- <text>暂无配置,请先添加</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- <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>上拉加载更多</text>
- </view>
- <view class="empty-state" v-if="deviceList.length === 0 && !loading">
- <AppIcon name="smartphone" size="48" color="#BFBFBF" />
- <text class="empty-text">暂无设备数据</text>
- <button class="empty-refresh-btn" @click="loadDeviceList">刷新</button>
- </view>
- <view class="loading-state" v-if="loading && deviceList.length === 0">
- <view class="loading-spinner"></view>
- <text class="loading-text">加载中...</text>
- </view>
- </view>
- </template>
- <script setup>
- 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 dictUtil, { loadDicts } from '../../utils/dict.js'
- const deviceList = ref([])
- const loading = ref(true)
- const page = ref(1)
- const pageSize = ref(10)
- const hasMore = ref(true)
- const loadMoreStatus = ref('more')
- const activeFilter = ref(0)
- const searchKeyword = ref('')
- const configList = ref([])
- const showConfigDropdown = ref(null)
- const loadingConfig = ref(false)
- const filterOptions = computed(() => dictUtil.getDictFilterOptions('WashDevice.status'))
- onMounted(async () => {
- await loadDicts()
- loadDeviceList()
- })
- const loadDeviceList = async (isLoadMore = false) => {
- if (!isLoadMore) {
- page.value = 1
- deviceList.value = []
- loading.value = true
- }
- try {
- const params = {
- page: page.value,
- pageSize: pageSize.value,
- keyword: searchKeyword.value,
- status: activeFilter.value === 0 ? '' : getStatusValue(activeFilter.value)
- }
- const res = await getDeviceList(params)
- if (res && res.code === 200) {
- const data = res.data
- const records = data.records || data.list || data
- const totalPages = data.pages || data.totalPages || 1
- if (isLoadMore) {
- deviceList.value = [...deviceList.value, ...records]
- } else {
- deviceList.value = records
- }
- hasMore.value = page.value < totalPages
- loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
- if (isLoadMore) page.value++
- }
- } catch (error) {
- showToast('获取设备列表失败')
- } finally {
- loading.value = false
- }
- }
- const loadMore = () => {
- if (hasMore.value && !loading.value && loadMoreStatus.value !== 'loading') {
- loadMoreStatus.value = 'loading'
- loadDeviceList(true)
- }
- }
- onReachBottom(() => {
- loadMore()
- })
- const handleSearch = () => {
- loadDeviceList()
- }
- const handleFilterChange = (index) => {
- activeFilter.value = index
- loadDeviceList()
- }
- const viewDeviceDetail = (deviceId) => {
- uni.navigateTo({ url: `/pages/device/detail?id=${deviceId}` })
- }
- const handleStopDevice = (device) => {
- uni.showModal({
- title: '停止设备',
- content: `确定要停止设备「${device.name || device.shortId}」吗?`,
- success: async (res) => {
- if (res.confirm) {
- try {
- await stopDevice(device.shortId)
- showToast('设备已停止', 'success')
- loadDeviceList()
- } catch (error) {
- showToast('停止设备失败')
- }
- }
- }
- })
- }
- const loadConfigList = async () => {
- if (loadingConfig.value) return
- loadingConfig.value = true
- try {
- const res = await getDeviceConfigList({ page: 1, pageSize: 1000 })
- if (res && res.code === 200) {
- const data = res.data
- configList.value = data.list || data.records || (Array.isArray(data) ? data : [])
- }
- } catch (error) {
- showToast('无法连接服务器')
- } finally {
- loadingConfig.value = false
- }
- }
- const toggleConfigDropdown = (deviceId) => {
- if (showConfigDropdown.value === deviceId) {
- closeConfigDropdown()
- } else {
- loadConfigList()
- showConfigDropdown.value = deviceId
- }
- }
- const closeConfigDropdown = () => {
- showConfigDropdown.value = null
- }
- const selectConfig = async (deviceId, configId, configName) => {
- try {
- const res = await batchModifyDeviceConfig({
- deviceIds: [deviceId],
- deviceConfigId: configId
- })
- if (res && res.code === 200) {
- showToast(`成功绑定: ${configName}`, 'success')
- closeConfigDropdown()
- setTimeout(() => loadDeviceList(), 500)
- } else {
- showToast(res.message || '绑定失败')
- }
- } catch (error) {
- showToast('网络连接失败')
- }
- }
- const getDeviceId = (device) => {
- if (!device) return null
- return 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 getDeviceStatusText = (status) => fmtDictName('WashDevice.status', status)
- const getDeviceBusyStatus = (device) => {
- const status = getDeviceStatusValue(device)
- const onlineValue = dictUtil.getDictValue('WashDevice.status', '在线')
- if (status != onlineValue) return ''
- const hasCurrentOrder = device.isBusy || device.currentOrder || device.workingStatus === 'BUSY'
- return hasCurrentOrder || (device.todayOrders > 0)
- ? dictUtil.getDictLabel('deviceBusyStatus', '1')
- : dictUtil.getDictLabel('deviceBusyStatus', '0')
- }
- const getDeviceBusyStatusClass = (device) => {
- const label = getDeviceBusyStatus(device)
- if (!label) return ''
- return label !== dictUtil.getDictLabel('deviceBusyStatus', '0') ? 'busy' : 'idle'
- }
- const getDeviceStatusStyle = (status) => {
- const color = getDictColor('WashDevice.status', status)
- if (color) return { color: color, backgroundColor: `${color}1A` }
- return {}
- }
- 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>
- .device-list-container {
- padding: 20rpx 28rpx;
- background-color: #F5F7FA;
- min-height: 100vh;
- box-sizing: border-box;
- padding-bottom: 100rpx;
- }
- /* ===== Search Bar ===== */
- .search-bar {
- display: flex;
- gap: 16rpx;
- margin-bottom: 20rpx;
- }
- .search-input-wrap {
- flex: 1;
- display: flex;
- align-items: center;
- gap: 12rpx;
- background: #F5F5F5;
- border-radius: 16rpx;
- padding: 0 24rpx;
- }
- .search-input-wrap input {
- flex: 1;
- height: 72rpx;
- border: none;
- outline: none;
- font-size: 28rpx;
- 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);
- }
- /* ===== Filter Bar ===== */
- .filter-bar {
- margin-bottom: 20rpx;
- }
- .segmented-control {
- display: flex;
- background: #FFFFFF;
- border-radius: 16rpx;
- padding: 6rpx;
- gap: 6rpx;
- }
- .segment-item {
- 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);
- }
- .segment-item.active {
- color: #FFFFFF;
- font-weight: 600;
- background: #C6171E;
- box-shadow: 0 4rpx 12rpx rgba(198, 23, 30, 0.3);
- }
- /* ===== Device List ===== */
- .device-list {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- }
- .device-item {
- background: #FFFFFF;
- border-radius: 20rpx;
- padding: 24rpx;
- position: relative;
- transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
- }
- .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);
- }
- .device-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 16rpx;
- margin-bottom: 18rpx;
- }
- .device-info {
- flex: 1;
- min-width: 0;
- }
- .device-name {
- font-size: 30rpx;
- font-weight: 600;
- color: #1A1A1A;
- display: block;
- margin-bottom: 10rpx;
- }
- .pa-badge-list {
- display: inline-block;
- margin-left: 10rpx;
- padding: 2rpx 10rpx;
- background: #C6171E;
- color: #FFFFFF;
- border-radius: 6rpx;
- font-size: 20rpx;
- font-weight: 700;
- vertical-align: middle;
- }
- .device-meta {
- display: flex;
- align-items: center;
- gap: 12rpx;
- flex-wrap: wrap;
- }
- .device-id {
- font-size: 24rpx;
- color: #999999;
- }
- .device-status {
- display: flex;
- align-items: center;
- font-size: 22rpx;
- font-weight: 600;
- padding: 4rpx 16rpx;
- border-radius: 100px;
- white-space: nowrap;
- }
- .status-dot {
- display: inline-block;
- width: 10rpx;
- height: 10rpx;
- border-radius: 50%;
- margin-right: 6rpx;
- background-color: currentColor;
- }
- .device-busy-tag {
- display: flex;
- align-items: center;
- font-size: 20rpx;
- font-weight: 600;
- padding: 4rpx 14rpx;
- border-radius: 100px;
- flex-shrink: 0;
- }
- .device-busy-tag.busy {
- color: #F44336;
- background-color: rgba(244, 67, 54, 0.08);
- }
- .device-busy-tag.idle {
- color: #52C41A;
- background-color: rgba(82, 196, 26, 0.08);
- }
- /* ===== Device Body ===== */
- .device-body {
- padding: 14rpx 0;
- border-top: 1px solid #F0F0F0;
- border-bottom: 1px solid #F0F0F0;
- margin-bottom: 16rpx;
- }
- .device-row {
- display: flex;
- align-items: center;
- padding: 6rpx 0;
- }
- .row-label {
- font-size: 24rpx;
- color: #999999;
- width: 120rpx;
- flex-shrink: 0;
- }
- .row-value {
- font-size: 24rpx;
- color: #1A1A1A;
- font-weight: 500;
- flex: 1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- /* ===== Device Footer ===== */
- .device-footer {
- padding-top: 16rpx;
- border-top: 1px solid #F0F0F0;
- }
- .stop-btn {
- width: 100%;
- padding: 14rpx 0;
- background: #FFFFFF;
- color: #1A1A1A;
- border: 1px solid #E0E0E0;
- border-radius: 12rpx;
- font-size: 24rpx;
- font-weight: 500;
- transition: border-color 0.25s, color 0.25s;
- }
- .stop-btn:active {
- border-color: #C6171E;
- color: #C6171E;
- }
- /* ===== Config Button (header top-right) ===== */
- .config-btn {
- padding: 8rpx 20rpx;
- background: #C6171E;
- color: #FFFFFF;
- border: none;
- border-radius: 12rpx;
- font-size: 22rpx;
- font-weight: 500;
- flex-shrink: 0;
- transition: background 0.25s, transform 0.15s;
- }
- .config-btn:active {
- background: #A81212;
- transform: scale(0.97);
- }
- /* ===== Config Dropdown ===== */
- .config-dropdown {
- position: absolute;
- top: 60rpx;
- right: 0;
- width: 340rpx;
- background: #FFFFFF;
- border-radius: 16rpx;
- box-shadow: 0 10px 15px rgba(0, 0, 0, 0.08), 0 4px 6px rgba(0, 0, 0, 0.05);
- z-index: 100;
- overflow: hidden;
- }
- .dropdown-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 18rpx 22rpx;
- background: #F5F7FA;
- border-bottom: 1px solid #F0F0F0;
- }
- .dropdown-title {
- font-size: 26rpx;
- font-weight: 600;
- color: #1A1A1A;
- }
- .dropdown-close {
- padding: 4rpx;
- }
- .dropdown-content {
- max-height: 360rpx;
- overflow-y: auto;
- }
- .config-option {
- padding: 18rpx 22rpx;
- border-bottom: 1px solid #F0F0F0;
- transition: background 0.2s;
- }
- .config-option:last-child {
- border-bottom: none;
- }
- .config-option:active {
- background: #F5F7FA;
- }
- .config-option-name {
- font-size: 26rpx;
- font-weight: 500;
- color: #1A1A1A;
- display: block;
- margin-bottom: 4rpx;
- }
- .config-option-desc {
- font-size: 22rpx;
- color: #999999;
- display: block;
- }
- .no-config {
- padding: 40rpx 20rpx;
- text-align: center;
- color: #999999;
- font-size: 24rpx;
- }
- /* ===== Load More ===== */
- .load-more {
- text-align: center;
- padding: 32rpx;
- color: #666666;
- font-size: 26rpx;
- margin-top: 24rpx;
- }
- /* ===== Empty ===== */
- .empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 120rpx 0;
- }
- .empty-text {
- font-size: 28rpx;
- color: #999999;
- margin: 24rpx 0 32rpx;
- }
- .empty-refresh-btn {
- padding: 16rpx 48rpx;
- background: #C6171E;
- color: #FFFFFF;
- border: none;
- border-radius: 44rpx;
- font-size: 28rpx;
- font-weight: 500;
- }
- /* ===== Loading ===== */
- .loading-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 120rpx 0;
- }
- .loading-spinner {
- width: 56rpx;
- height: 56rpx;
- border: 4rpx solid #F0F0F0;
- border-top-color: #C6171E;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- .loading-text {
- font-size: 26rpx;
- color: #999999;
- margin-top: 20rpx;
- }
- </style>
|