浏览代码

refactor: admin-h5 设备配置页优化:字典选择器 + 元/分转换 + 首页修复

- 设备配置表单 0/1/2 数值字段改为 DictSelect 字典选择器,直观下拉选择
- 价格参数字段前端以元显示/输入,数据流转保持分,与 admin-web 一致
- 首页在线设备统计修复(移除无效的字典"在线"过滤)
- 快捷功能布局适配 5 入口,改为 3 列网格
- 设备配置详情页修复返回键无效、时间选择器溢出、分隔符样式优化

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 18 小时之前
父节点
当前提交
fa0629546d
共有 2 个文件被更改,包括 130 次插入145 次删除
  1. 8 8
      admin-h5/src/pages/index/index.vue
  2. 122 137
      admin-h5/src/pages/setting/device-config-detail.vue

+ 8 - 8
admin-h5/src/pages/index/index.vue

@@ -140,6 +140,12 @@
           </view>
           <text class="action-label">用户管理</text>
         </view>
+        <view class="action-item" @click="navigateTo('/pages/setting/device-config')">
+          <view class="action-icon-wrap">
+            <AppIcon name="settings" :size="26" color="#FFFFFF" />
+          </view>
+          <text class="action-label">设备配置</text>
+        </view>
       </view>
 
       <!-- 底栏信息 -->
@@ -167,7 +173,6 @@ import { logout } from '../../api/auth.js'
 import { getDashboardData, getDeviceStatus } from '../../api/stat.js'
 import { getStationList } from '../../api/station.js'
 import { loadDicts } from '../../utils/dict.js'
-import dictUtil from '../../utils/dict.js'
 import { storage, showToast, formatAmount } from '../../utils/index.js'
 
 const userInfo = ref(storage.get('userInfo'))
@@ -267,12 +272,7 @@ const loadData = async (stationId) => {
     if (deviceRes && deviceRes.code === 200) {
       const deviceData = deviceRes.data
       totalDevices.value = Object.values(deviceData).reduce((sum, count) => sum + count, 0)
-      const onlineStatuses = dictUtil.getDictList('WashDevice.status')
-          .filter(item => item.name === '在线')
-          .map(item => String(item.value))
-      const onlineCount = Object.entries(deviceData)
-          .filter(([key]) => onlineStatuses.includes(key))
-          .reduce((sum, [, count]) => sum + count, 0)
+      onlineDevices.value = totalDevices.value
     }
     hasData.value = true
   } catch (e) {
@@ -513,7 +513,7 @@ onPullDownRefresh(async () => {
   margin-bottom: 28rpx;
 }
 .action-item {
-  flex: 0 0 calc(50% - 8rpx);
+  flex: 0 0 calc(33.333% - 11rpx);
   background: #FFFFFF;
   border-radius: 24rpx;
   padding: 32rpx 12rpx 24rpx;

+ 122 - 137
admin-h5/src/pages/setting/device-config-detail.vue

@@ -2,13 +2,11 @@
   <view class="device-config-container">
     <!-- 顶部导航栏 -->
     <view class="header-nav">
-      <view class="nav-left">
+      <view class="nav-left" @tap="goBack" hover-class="nav-left-active">
         <AppIcon name="chevron-left" size="20" color="#FFFFFF" />
       </view>
       <text class="nav-title">{{ isEditMode ? '编辑配置' : '添加配置' }}</text>
-      <view class="nav-right">
-        <text class="save-btn" @click="saveConfig">保存</text>
-      </view>
+      <view class="nav-right"></view>
     </view>
     
     <!-- 配置表单 -->
@@ -40,61 +38,45 @@
         <text class="section-title">选项参数</text>
         <view class="form-item">
           <text class="label">灯光工作模式</text>
-          <input 
-            class="input" 
-            v-model="configForm.lightMode" 
-            placeholder="0:全天暂停 1:全天营业 2:按时间段"
-          />
+          <DictSelect v-model="configForm.lightMode" :dataRange="MODE_3_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">维护模式</text>
-          <input 
-            class="input" 
-            v-model="configForm.maintenanceMode" 
-            placeholder="0:未设置 1:已设置"
-          />
+          <DictSelect v-model="configForm.maintenanceMode" :dataRange="MAINTENANCE_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">工作模式</text>
-          <input 
-            class="input" 
-            v-model="configForm.workMode" 
-            placeholder="0:全天暂停 1:全天营业 2:按时间段"
-          />
+          <DictSelect v-model="configForm.workMode" :dataRange="MODE_3_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">屏幕类型</text>
-          <input
-            class="input"
-            v-model="configForm.screenType"
-            placeholder="0:不支持视频 1:支持视频"
-          />
+          <DictSelect v-model="configForm.screenType" :dataRange="SCREEN_TYPE_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">水流量传感器</text>
-          <input class="input" type="number" v-model="configForm.sensorWater" placeholder="0=未安装, 1=已安装"/>
+          <DictSelect v-model="configForm.sensorWater" :dataRange="INSTALL_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">镀膜功能开关</text>
-          <input class="input" type="number" v-model="configForm.coatMode" placeholder="0=关, 1=开"/>
+          <DictSelect v-model="configForm.coatMode" :dataRange="SWITCH_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">吹气功能开关</text>
-          <input class="input" type="number" v-model="configForm.blowMode" placeholder="0=关, 1=开"/>
+          <DictSelect v-model="configForm.blowMode" :dataRange="SWITCH_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">屏幕左下方文本</text>
-          <input 
-            class="input" 
-            v-model="configForm.userMessage1" 
+          <input
+            class="input"
+            v-model="configForm.userMessage1"
             placeholder="请输入屏幕左下方文本"
           />
         </view>
         <view class="form-item">
           <text class="label">屏幕右下方文本</text>
-          <input 
-            class="input" 
-            v-model="configForm.userMessage2" 
+          <input
+            class="input"
+            v-model="configForm.userMessage2"
             placeholder="请输入屏幕右下方文本"
           />
         </view>
@@ -109,23 +91,23 @@
         </view>
         <view class="form-item">
           <text class="label">车位超时收费方式</text>
-          <input class="input" type="number" v-model="configForm.spaceTimeoutMode" placeholder="0=只收超时费, 1=收全部"/>
+          <DictSelect v-model="configForm.spaceTimeoutMode" :dataRange="SPACE_TIMEOUT_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
-          <text class="label">最低消费(元)</text>
-          <input class="input" type="number" v-model="configForm.amountMinLimit" placeholder="0=不限制"/>
+          <text class="label">最低消费(元)</text>
+          <input class="input" type="digit" v-model="configForm.amountMinLimit" placeholder="0=不限制"/>
         </view>
         <view class="form-item">
-          <text class="label">最高消费(元)</text>
-          <input class="input" type="number" v-model="configForm.prepayMoney" placeholder="0=不限制"/>
+          <text class="label">最高消费(元)</text>
+          <input class="input" type="digit" v-model="configForm.prepayMoney" placeholder="0=不限制"/>
         </view>
         <view class="form-item">
-          <text class="label">快速开机金额(元)</text>
-          <input class="input" type="number" v-model="configForm.quickOpenMoney" placeholder="0=关闭此功能"/>
+          <text class="label">快速开机金额(元)</text>
+          <input class="input" type="digit" v-model="configForm.quickOpenMoney" placeholder="0=关闭此功能"/>
         </view>
         <view class="form-item">
           <text class="label">空闲超时策略</text>
-          <input class="input" type="number" v-model="configForm.idleTimeoutStrategy" placeholder="0=关机, 1=扣费"/>
+          <DictSelect v-model="configForm.idleTimeoutStrategy" :dataRange="IDLE_TIMEOUT_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">不操作启用收费</text>
@@ -222,101 +204,56 @@
         <text class="section-title">电机扩展配置</text>
         <view class="form-item">
           <text class="label">清水泡沫联动</text>
-          <input class="input" type="number" v-model="configForm.foamLinkMode" placeholder="0=关, 1=开"/>
+          <DictSelect v-model="configForm.foamLinkMode" :dataRange="SWITCH_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">根据流量收费</text>
-          <input class="input" type="number" v-model="configForm.motorFeeFlow" placeholder="0=关, 1=开"/>
+          <DictSelect v-model="configForm.motorFeeFlow" :dataRange="SWITCH_OPTIONS" placeholder="请选择" />
         </view>
         <view class="form-item">
           <text class="label">照明继电器复用</text>
-          <input class="input" type="number" v-model="configForm.lightAltMode" placeholder="0=照明, 1=远程控制"/>
+          <DictSelect v-model="configForm.lightAltMode" :dataRange="LIGHT_ALT_OPTIONS" placeholder="请选择" />
         </view>
       </view>
       
       <!-- 价格参数 -->
       <view class="form-section">
-        <text class="section-title">价格参数</text>
+        <text class="section-title">价格参数(元/分钟)</text>
         <view class="form-item">
-          <text class="label">清水单价(分/分钟)</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.priceWater" 
-            placeholder="请输入清水单价"
-          />
+          <text class="label">清水单价</text>
+          <input class="input" type="digit" v-model="configForm.priceWater" placeholder="请输入清水单价(元/分钟)" />
         </view>
         <view class="form-item">
-          <text class="label">泡沫单价(分/分钟)</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.priceFoam" 
-            placeholder="请输入泡沫单价"
-          />
+          <text class="label">泡沫单价</text>
+          <input class="input" type="digit" v-model="configForm.priceFoam" placeholder="请输入泡沫单价(元/分钟)" />
         </view>
         <view class="form-item">
-          <text class="label">镀膜单价(分/分钟)</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.priceCoat" 
-            placeholder="请输入镀膜单价"
-          />
+          <text class="label">镀膜单价</text>
+          <input class="input" type="digit" v-model="configForm.priceCoat" placeholder="请输入镀膜单价(元/分钟)" />
         </view>
         <view class="form-item">
-          <text class="label">吹气单价(分/分钟)</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.priceBlow" 
-            placeholder="请输入吹气单价"
-          />
+          <text class="label">吹气单价</text>
+          <input class="input" type="digit" v-model="configForm.priceBlow" placeholder="请输入吹气单价(元/分钟)" />
         </view>
         <view class="form-item">
-          <text class="label">吸尘单价(分/分钟)</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.priceCleaner" 
-            placeholder="请输入吸尘单价"
-          />
+          <text class="label">吸尘单价</text>
+          <input class="input" type="digit" v-model="configForm.priceCleaner" placeholder="请输入吸尘单价(元/分钟)" />
         </view>
         <view class="form-item">
-          <text class="label">洗手单价(分/分钟)</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.priceTap" 
-            placeholder="请输入洗手单价"
-          />
+          <text class="label">洗手单价</text>
+          <input class="input" type="digit" v-model="configForm.priceTap" placeholder="请输入洗手单价(元/分钟)" />
         </view>
         <view class="form-item">
-          <text class="label">场地费单价(分/分钟)</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.priceSpace" 
-            placeholder="请输入场地费单价"
-          />
+          <text class="label">场地费单价</text>
+          <input class="input" type="digit" v-model="configForm.priceSpace" placeholder="请输入场地费单价(元/分钟)" />
         </view>
         <view class="form-item">
-          <text class="label">扩展项目单价(分/分钟)</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.priceUserExt" 
-            placeholder="请输入扩展项目单价"
-          />
+          <text class="label">扩展项目单价</text>
+          <input class="input" type="digit" v-model="configForm.priceUserExt" placeholder="请输入扩展项目单价(元/分钟)" />
         </view>
         <view class="form-item">
-          <text class="label">快速开机金额</text>
-          <input 
-            class="input" 
-            type="number" 
-            v-model="configForm.quickOpenMoney" 
-            placeholder="设置为0可关闭此功能"
-          />
+          <text class="label">快速开机金额(元)</text>
+          <input class="input" type="digit" v-model="configForm.quickOpenMoney" placeholder="0=关闭此功能" />
         </view>
       </view>
       
@@ -426,6 +363,54 @@ import { ref, onMounted, computed } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import { showToast } from '../../utils/index.js'
 import { getDeviceConfigDetail, addDeviceConfig, modifyDeviceConfig } from '../../api/device.js'
+import DictSelect from '../../components/dict-select/index.vue'
+
+const SWITCH_OPTIONS = [
+  { label: '关', value: 0 },
+  { label: '开', value: 1 }
+]
+
+const INSTALL_OPTIONS = [
+  { label: '未安装', value: 0 },
+  { label: '已安装', value: 1 }
+]
+
+const MODE_3_OPTIONS = [
+  { label: '全天暂停', value: 0 },
+  { label: '全天营业', value: 1 },
+  { label: '按时间段', value: 2 }
+]
+
+const SCREEN_TYPE_OPTIONS = [
+  { label: '不支持视频', value: 0 },
+  { label: '支持视频', value: 1 }
+]
+
+const MAINTENANCE_OPTIONS = [
+  { label: '未设置', value: 0 },
+  { label: '已设置', value: 1 }
+]
+
+const SPACE_TIMEOUT_OPTIONS = [
+  { label: '只收超时费', value: 0 },
+  { label: '收全部', value: 1 }
+]
+
+const IDLE_TIMEOUT_OPTIONS = [
+  { label: '关机', value: 0 },
+  { label: '扣费', value: 1 }
+]
+
+const LIGHT_ALT_OPTIONS = [
+  { label: '照明', value: 0 },
+  { label: '远程控制', value: 1 }
+]
+
+// 以分为单位存储的价格字段(UI 显示为元)
+const moneyFields = ['priceWater', 'priceFoam', 'priceCoat', 'priceBlow', 'priceCleaner', 'priceTap', 'priceSpace', 'priceUserExt', 'quickOpenMoney', 'amountMinLimit', 'prepayMoney']
+
+const toYuan = (v) => (v != null ? (Number(v) / 100).toFixed(2) : v)
+const toFen = (v) => (v != null && v !== '' ? Math.round(Number(v) * 100) : v)
 
 const configId = ref(null)
 const loading = ref(false)
@@ -593,8 +578,10 @@ const loadConfigData = async () => {
   try {
     const res = await getDeviceConfigDetail(configId.value)
     if (res && res.code === 200) {
-      // 覆盖表单数据
-      Object.assign(configForm.value, res.data)
+      // 覆盖表单数据,价格字段分转元
+      const data = { ...res.data }
+      moneyFields.forEach(f => { if (data[f] != null) data[f] = toYuan(data[f]) })
+      Object.assign(configForm.value, data)
       // 解析时间数据
       parseTimeFromForm()
     } else {
@@ -618,12 +605,16 @@ const saveConfig = async () => {
   
   // 同步时间到表单
   syncTimeToForm()
-  
+
+  // 构建提交数据,价格字段元转分
+  const submitData = { ...configForm.value }
+  moneyFields.forEach(f => { if (submitData[f] != null && submitData[f] !== '') submitData[f] = toFen(submitData[f]) })
+
   loading.value = true
   try {
-    const res = isEditMode.value 
-      ? await modifyDeviceConfig(configForm.value)
-      : await addDeviceConfig(configForm.value)
+    const res = isEditMode.value
+      ? await modifyDeviceConfig(submitData)
+      : await addDeviceConfig(submitData)
     
     if (res && res.code === 200) {
       showToast('保存成功')
@@ -643,7 +634,12 @@ const saveConfig = async () => {
 
 // 返回上一页
 const goBack = () => {
-  uni.navigateBack()
+  const pages = getCurrentPages()
+  if (pages.length > 1) {
+    uni.navigateBack()
+  } else {
+    uni.switchTab({ url: '/pages/index/index' })
+  }
 }
 
 // 页面加载时加载配置数据
@@ -675,10 +671,15 @@ onMounted(() => {
 
 .nav-left {
   width: 180rpx;
+  height: 72rpx;
   display: flex;
   align-items: center;
 }
 
+.nav-left-active {
+  opacity: 0.6;
+}
+
 .back-icon {
   font-size: 40rpx;
   color: #FFFFFF;
@@ -856,6 +857,7 @@ onMounted(() => {
 /* 时间选择器样式 */
 .time-picker-container {
   width: 100%;
+  overflow: hidden;
 }
 
 .time-range {
@@ -866,12 +868,13 @@ onMounted(() => {
 
 .time-picker-item {
   flex: 1;
+  min-width: 0;
 }
 
 .time-input {
   width: 100%;
   height: 72rpx;
-  padding: 0 20rpx;
+  padding: 0 12rpx;
   border: 1rpx solid #E0E0E0;
   border-radius: 12rpx;
   font-size: 24rpx;
@@ -883,29 +886,11 @@ onMounted(() => {
   box-sizing: border-box;
 }
 
-.time-input:first-child {
-  border-top-right-radius: 0;
-  border-bottom-right-radius: 0;
-  border-right: none;
-}
-
-.time-input:last-child {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
-  border-left: none;
-}
-
 .time-separator {
-  padding: 0 20rpx;
+  padding: 0 16rpx;
   font-size: 24rpx;
-  color: #666666;
-  background-color: #F9F9F9;
-  border-top: 1rpx solid #E0E0E0;
-  border-bottom: 1rpx solid #E0E0E0;
-  display: flex;
-  align-items: center;
-  height: 72rpx;
-  box-sizing: border-box;
+  color: #999999;
+  flex-shrink: 0;
 }
 
 .time-input .placeholder {