Browse Source

自助洗车运营移动端小程序功能开发

skyline 4 months ago
parent
commit
56bf88c5f4

+ 45 - 0
admin-app/src/api/device.js

@@ -52,4 +52,49 @@ export const removeDevice = (id) => {
  */
 export const stopDevice = (shortId) => {
   return post(`/washDevice/stopDevice/${shortId}`)
+}
+
+/**
+ * 获取设备配置列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} 配置列表
+ */
+export const getDeviceConfigList = (params) => {
+  return post('/device-config/list', params)
+}
+
+/**
+ * 获取设备配置详情
+ * @param {string|number} id - 配置ID
+ * @returns {Promise} 配置详情
+ */
+export const getDeviceConfigDetail = (id) => {
+  return post(`/device-config/${id}`)
+}
+
+/**
+ * 添加设备配置
+ * @param {Object} config - 配置信息
+ * @returns {Promise} 结果
+ */
+export const addDeviceConfig = (config) => {
+  return post('/device-config/add', config)
+}
+
+/**
+ * 修改设备配置
+ * @param {Object} config - 配置信息
+ * @returns {Promise} 结果
+ */
+export const modifyDeviceConfig = (config) => {
+  return post('/device-config/modify', config)
+}
+
+/**
+ * 批量绑定设备配置
+ * @param {Object} params - { deviceIds: [], deviceConfigId: Long }
+ * @returns {Promise} 结果
+ */
+export const batchModifyDeviceConfig = (params) => {
+  return post('/device-config/batchModify', params)
 }

+ 55 - 0
admin-app/src/api/rate.js

@@ -0,0 +1,55 @@
+import { post, get } from '../utils'
+
+/**
+ * 获取平台费率列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} 列表
+ */
+export const getPlatformFeeRateList = (params) => {
+  return post('/platform-fee-rate/list', params)
+}
+
+/**
+ * 添加平台费率
+ * @param {Object} data - 数据
+ * @returns {Promise} 结果
+ */
+export const addPlatformFeeRate = (data) => {
+  return post('/platform-fee-rate/add', data)
+}
+
+/**
+ * 修改平台费率
+ * @param {Object} data - 数据
+ * @returns {Promise} 结果
+ */
+export const modifyPlatformFeeRate = (data) => {
+  return post('/platform-fee-rate/modify', data)
+}
+
+/**
+ * 获取站点费率详情
+ * @param {string} stationId - 站点ID
+ * @returns {Promise} 详情
+ */
+export const getStationFeeRate = (stationId) => {
+  return get(`/station-fee-rate/${stationId}`)
+}
+
+/**
+ * 绑定站点费率
+ * @param {Object} data - 数据
+ * @returns {Promise} 结果
+ */
+export const bindStationFeeRate = (data) => {
+  return post('/station-fee-rate/bind', data)
+}
+
+/**
+ * 获取站点费率列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} 列表
+ */
+export const getStationFeeRateList = (params) => {
+  return post('/station-fee-rate/list', params)
+}

+ 49 - 0
admin-app/src/pages.json

@@ -33,6 +33,13 @@
         "navigationBarTitleText": "设备列表"
       }
     },
+    {
+      "path": "pages/device/detail",
+      "style": {
+        "navigationBarTitleText": "设备详情",
+        "navigationStyle": "custom"
+      }
+    },
     {
       "path": "pages/finance/index",
       "style": {
@@ -44,6 +51,48 @@
       "style": {
         "navigationBarTitleText": "提现管理"
       }
+    },
+    {
+      "path": "pages/setting/index",
+      "style": {
+        "navigationBarTitleText": "平台配置",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/setting/rate-config",
+      "style": {
+        "navigationBarTitleText": "平台费率配置",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/setting/device-config",
+      "style": {
+        "navigationBarTitleText": "设备配置",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/setting/device-config-detail",
+      "style": {
+        "navigationBarTitleText": "设备配置详情",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/setting/rate-config-detail",
+      "style": {
+        "navigationBarTitleText": "费率模板详情",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/setting/device-binding",
+      "style": {
+        "navigationBarTitleText": "设备绑定管理",
+        "navigationStyle": "custom"
+      }
     }
   ],
   "globalStyle": {

+ 635 - 0
admin-app/src/pages/device/detail.vue

@@ -0,0 +1,635 @@
+<template>
+  <view class="device-detail-container">
+    <!-- 顶部紫色导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left">
+        <text class="back-icon" @click="goBack">←</text>
+      </view>
+      <text class="nav-title">设备详情</text>
+      <view class="nav-right"></view>
+    </view>
+    
+    <!-- 设备基本信息卡片 -->
+    <view class="basic-info-card">
+      <view class="device-main-info">
+        <view class="device-name-section">
+          <text class="device-name">{{ deviceDetail.deviceName || '未知设备' }}</text>
+          <text class="device-id">设备编号: {{ deviceDetail.shortId || '无' }}</text>
+        </view>
+        <view class="device-status" :class="getDeviceStatusClass(deviceDetail.state)">
+          <text class="status-dot"></text>
+          <text>{{ getDeviceStatusText(deviceDetail.state) }}</text>
+        </view>
+      </view>
+    </view>
+    
+    <!-- 设备详细信息卡片 -->
+    <view class="detail-info-card">
+      <view class="card-title">基础信息</view>
+      <view class="info-list">
+        <view class="info-item">
+          <text class="item-label">产品Key</text>
+          <text class="item-value">{{ deviceDetail.productKey || '无' }}</text>
+        </view>
+        <view class="info-item">
+          <text class="item-label">工位编号</text>
+          <text class="item-value">{{ deviceDetail.seqName || '无' }}</text>
+        </view>
+        <view class="info-item">
+          <text class="item-label">所属站点</text>
+          <text class="item-value">{{ deviceDetail.stationName || '未分配' }}</text>
+        </view>
+        <view class="info-item">
+          <text class="item-label">设备地址</text>
+          <text class="item-value">{{ deviceDetail.address || '无' }}</text>
+        </view>
+        <view class="info-item">
+          <text class="item-label">参数配置</text>
+          <text class="item-value">{{ deviceDetail.deviceConfigName || '未绑定' }}</text>
+        </view>
+      </view>
+    </view>
+    
+    <!-- 设备运行状态卡片 -->
+    <view class="status-card">
+      <view class="card-title">实时状态</view>
+      <view class="status-list">
+        <view class="status-item">
+          <text class="status-label">核心温度</text>
+          <text class="status-value">{{ deviceDetail.temperatureChip ? deviceDetail.temperatureChip + '°C' : '未知' }}</text>
+        </view>
+        <view class="status-item">
+          <text class="status-label">是否有水</text>
+          <text class="status-value">{{ deviceDetail.hasWater === true ? '有水' : (deviceDetail.hasWater === false ? '无水' : '不支持') }}</text>
+        </view>
+        <view class="status-item">
+          <text class="status-label">是否有泡沫</text>
+          <text class="status-value">{{ deviceDetail.hasFoam === true ? '有' : (deviceDetail.hasFoam === false ? '无' : '不支持') }}</text>
+        </view>
+        <view class="status-item" v-if="deviceDetail.state === 'fault'">
+          <text class="status-label">故障原因</text>
+          <text class="status-value danger-text">{{ deviceDetail.faultReason || '未知故障' }}</text>
+        </view>
+        <view class="status-item">
+          <text class="status-label">运行时长</text>
+          <text class="status-value">{{ formatDuration(deviceDetail.runningTime) || '0小时' }}</text>
+        </view>
+      </view>
+    </view>
+    
+    <!-- 设备操作按钮 -->
+    <view class="action-section">
+      <button 
+        v-if="isOnline" 
+        class="stop-btn"
+        @click="handleStopDevice"
+      >
+        停止设备
+      </button>
+      <button 
+        v-if="!isOnline" 
+        class="start-btn"
+        @click="handleStartDevice"
+      >
+        启动设备
+      </button>
+      <button class="refresh-btn" @click="refreshDeviceInfo">
+        刷新信息
+      </button>
+    </view>
+    
+    <!-- 加载状态 -->
+    <view class="loading-overlay" v-if="loading">
+      <text class="loading-spinner">🔄</text>
+      <text class="loading-text">加载中...</text>
+    </view>
+    
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="!loading && !deviceDetail.id">
+      <text class="empty-icon">📱</text>
+      <text class="empty-text">设备不存在</text>
+      <button class="refresh-btn" @click="loadDeviceDetail">刷新</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { getDeviceDetail, stopDevice } from '../../api/device.js'
+import { formatTime, showToast, storage } from '../../utils/index.js'
+
+const deviceId = ref('')
+const deviceDetail = ref({})
+const loading = ref(true)
+
+// 计算设备是否在线
+const isOnline = computed(() => {
+  const state = deviceDetail.value.state
+  return state === 'init' || state === 'idle' || state === 'busy' || 
+         state === 1 || state === '1' || state === 'ONLINE'
+})
+
+// 获取设备类型文本
+const getDeviceTypeText = (type) => {
+  if (!type) return '无'
+  
+  try {
+    const dicts = storage.get('dicts')
+    // 尝试不同的设备类型字典键名
+    const possibleDictKeys = ['Device.type', 'WashDevice.type', 'Device.deviceType', 'DeviceType']
+    
+    for (const key of possibleDictKeys) {
+      if (dicts && dicts[key]) {
+        const deviceTypeDict = dicts[key]
+        const dictItem = deviceTypeDict.find(item => item.value == type)
+        if (dictItem) {
+          return dictItem.name
+        }
+      }
+    }
+    
+    return String(type)
+  } catch (error) {
+    console.error('获取设备类型文本失败:', error)
+    return String(type)
+  }
+}
+
+// 获取设备状态文本
+const getDeviceStatusText = (state) => {
+  // 添加调试信息
+  console.log('设备状态值:', state, '类型:', typeof state)
+  
+  // 如果状态为空,直接返回离线
+  if (state === null || state === undefined || state === '') {
+    return '离线'
+  }
+  
+  try {
+    const dicts = storage.get('dicts')
+    if (!dicts || !dicts['WashDevice.status']) {
+      console.warn('字典数据未加载或不存在 WashDevice.status')
+      // 降级处理:使用默认映射
+      const statusMap = {
+        'init': '初始化',
+        'idle': '设备空闲',
+        'busy': '设备忙碌',
+        'sleep': '不在营业时间',
+        'maintenance': '维护模式',
+        'fault': '设备故障',
+        0: '离线', 1: '在线', 2: '故障', 3: '维护中',
+        '0': '离线', '1': '在线', '2': '故障', '3': '维护中',
+        'ONLINE': '在线', 'OFFLINE': '离线', 'FAULT': '故障', 'MAINTENANCE': '维护中'
+      }
+      return statusMap[state] || statusMap[String(state)] || String(state)
+    }
+    
+    const deviceStatusDict = dicts['WashDevice.status']
+    const dictItem = deviceStatusDict.find(item => item.value == state)
+    
+    return dictItem ? dictItem.name : String(state)
+  } catch (error) {
+    console.error('获取设备状态文本失败:', error)
+    return String(state)
+  }
+}
+
+// 获取设备状态样式类
+const getDeviceStatusClass = (state) => {
+  if (!state) return ''
+  
+  const statusClassMap = {
+    // 新状态值映射
+    'init': 'status-online',      // 初始化 - 在线样式
+    'idle': 'status-online',      // 设备空闲 - 在线样式
+    'busy': 'status-online',      // 设备忙碌 - 在线样式
+    'sleep': 'status-offline',    // 不在营业时间 - 离线样式
+    'maintenance': 'status-maintenance',  // 维护模式 - 维护样式
+    'fault': 'status-fault',      // 设备故障 - 故障样式
+    // 旧状态值映射(兼容)
+    0: 'status-offline',
+    1: 'status-online',
+    2: 'status-fault',
+    3: 'status-maintenance',
+    '0': 'status-offline',
+    '1': 'status-online',
+    '2': 'status-fault',
+    '3': 'status-maintenance',
+    'ONLINE': 'status-online',
+    'OFFLINE': 'status-offline',
+    'FAULT': 'status-fault',
+    'MAINTENANCE': 'status-maintenance'
+  }
+  
+  // 尝试直接匹配或类型转换后匹配
+  return statusClassMap[state] || statusClassMap[String(state)] || ''
+}
+
+// 格式化运行时长
+const formatDuration = (seconds) => {
+  if (!seconds || isNaN(seconds)) return '0小时'
+  
+  const totalSeconds = parseInt(seconds)
+  const hours = Math.floor(totalSeconds / 3600)
+  const minutes = Math.floor((totalSeconds % 3600) / 60)
+  
+  if (hours > 0) {
+    return `${hours}小时${minutes}分钟`
+  } else {
+    return `${minutes}分钟`
+  }
+}
+
+// 加载设备详情
+const loadDeviceDetail = async () => {
+  if (!deviceId.value) {
+    console.error('设备ID为空')
+    showToast('设备ID不存在')
+    loading.value = false
+    return
+  }
+  
+  loading.value = true
+  try {
+    const res = await getDeviceDetail(deviceId.value)
+    console.log('设备详情响应:', res)
+    
+    if (res && res.code === 200) {
+      deviceDetail.value = res.data || {}
+      console.log('加载的设备详情:', deviceDetail.value)
+    } else {
+      showToast(res.msg || '获取设备详情失败')
+    }
+  } catch (error) {
+    console.error('获取设备详情失败:', error)
+    showToast('获取设备详情失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 停止设备
+const handleStopDevice = async () => {
+  try {
+    await stopDevice(deviceDetail.value.shortId)
+    showToast('设备已停止', 'success')
+    // 刷新设备详情
+    loadDeviceDetail()
+  } catch (error) {
+    console.error('停止设备失败:', error)
+    showToast('停止设备失败')
+  }
+}
+
+// 启动设备(预留功能)
+const handleStartDevice = () => {
+  showToast('启动设备功能开发中')
+}
+
+// 刷新设备信息
+const refreshDeviceInfo = () => {
+  loadDeviceDetail()
+}
+
+// 返回上一页
+const goBack = () => {
+  uni.navigateBack()
+}
+
+// 页面加载时获取设备ID并加载详情
+onMounted(() => {
+  // 获取页面参数
+  const pages = getCurrentPages()
+  const currentPage = pages[pages.length - 1]
+  const receivedDeviceId = currentPage.options.id || ''
+  
+  console.log('设备详情页接收到的参数:', currentPage.options)
+  console.log('设备ID:', receivedDeviceId)
+  
+  if (!receivedDeviceId) {
+    console.error('未接收到设备ID')
+    showToast('设备ID不存在,无法加载详情')
+    loading.value = false
+    return
+  }
+  
+  deviceId.value = receivedDeviceId
+  loadDeviceDetail()
+})
+</script>
+
+<style scoped>
+/* 容器 */
+.device-detail-container {
+  min-height: 100vh;
+  background-color: #F8F9FA;
+  padding-bottom: 120rpx;
+}
+
+/* 顶部紫色导航栏 */
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+  position: relative;
+  z-index: 10;
+}
+
+.nav-left {
+  width: 180rpx;
+  display: flex;
+  align-items: center;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+}
+
+.nav-title {
+  font-size: 34rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+  flex: 1;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.nav-right {
+  width: 180rpx;
+}
+
+.danger-text {
+  color: #F5222D;
+  font-weight: 600;
+}
+
+/* 设备基本信息卡片 */
+.basic-info-card {
+  margin: 20rpx 30rpx;
+  padding: 24rpx 30rpx;
+  background: #FFFFFF;
+  border-radius: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.device-main-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.device-name-section {
+  flex: 1;
+}
+
+.device-name {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  margin-bottom: 8rpx;
+  display: block;
+}
+
+.device-id {
+  font-size: 24rpx;
+  color: #667EEA;
+  font-weight: 600;
+  display: block;
+}
+
+/* 设备状态 */
+.device-status {
+  display: flex;
+  align-items: center;
+  font-size: 24rpx;
+  font-weight: 600;
+  padding: 8rpx 24rpx;
+  border-radius: 30rpx;
+}
+
+.status-dot {
+  display: inline-block;
+  width: 12rpx;
+  height: 12rpx;
+  border-radius: 50%;
+  margin-right: 8rpx;
+  background-color: currentColor;
+}
+
+/* 状态样式 */
+.status-online {
+  color: #52C41A;
+  background: linear-gradient(135deg, #F6FFED 0%, #D9F7BE 100%);
+}
+
+.status-offline {
+  color: #999999;
+  background: linear-gradient(135deg, #F5F5F5 0%, #E0E0E0 100%);
+}
+
+.status-fault {
+  color: #F5222D;
+  background: linear-gradient(135deg, #FFF1F0 0%, #FFCCC7 100%);
+}
+
+.status-maintenance {
+  color: #FAAD14;
+  background: linear-gradient(135deg, #FFF7E6 0%, #FFE7BA 100%);
+}
+
+/* 详情信息卡片 */
+.detail-info-card,
+.status-card {
+  margin: 20rpx 30rpx;
+  background: #FFFFFF;
+  border-radius: 20rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.card-title {
+  padding: 24rpx 30rpx 20rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+
+/* 信息列表 */
+.info-list {
+  padding: 20rpx 30rpx 24rpx;
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #F8F9FA;
+}
+
+.info-item:last-child {
+  border-bottom: none;
+}
+
+.item-label {
+  font-size: 26rpx;
+  color: #666666;
+}
+
+.item-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  font-weight: 500;
+  text-align: right;
+}
+
+/* 状态列表 */
+.status-list {
+  padding: 20rpx 30rpx 24rpx;
+}
+
+.status-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #F8F9FA;
+}
+
+.status-item:last-child {
+  border-bottom: none;
+}
+
+.status-label {
+  font-size: 26rpx;
+  color: #666666;
+}
+
+.status-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  font-weight: 500;
+  text-align: right;
+}
+
+/* 操作按钮区域 */
+.action-section {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 24rpx 30rpx;
+  background: #FFFFFF;
+  border-top: 1rpx solid #F0F0F0;
+  box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
+  z-index: 100;
+  display: flex;
+  gap: 20rpx;
+}
+
+.stop-btn {
+  flex: 1;
+  padding: 20rpx 0;
+  background: linear-gradient(90deg, #F5222D 0%, #FF4D4F 100%);
+  color: #FFFFFF;
+  border: none;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  box-shadow: 0 6rpx 20rpx rgba(245, 34, 45, 0.35);
+}
+
+.start-btn {
+  flex: 1;
+  padding: 20rpx 0;
+  background: linear-gradient(90deg, #52C41A 0%, #73D13D 100%);
+  color: #FFFFFF;
+  border: none;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.35);
+}
+
+.refresh-btn {
+  flex: 1;
+  padding: 20rpx 0;
+  background: linear-gradient(90deg, #667EEA 0%, #764BA2 100%);
+  color: #FFFFFF;
+  border: none;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  box-shadow: 0 6rpx 20rpx rgba(102, 126, 234, 0.35);
+}
+
+.stop-btn:active,
+.start-btn:active,
+.refresh-btn:active {
+  transform: translateY(2rpx);
+  opacity: 0.9;
+}
+
+/* 加载状态 */
+.loading-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(2px);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+
+.loading-spinner {
+  font-size: 64rpx;
+  animation: spin 1s linear infinite;
+  margin-bottom: 32rpx;
+  color: #fff;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #fff;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* 空状态 */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999999;
+}
+
+.empty-icon {
+  font-size: 120rpx;
+  margin-bottom: 32rpx;
+  opacity: 0.5;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  margin-bottom: 40rpx;
+}
+</style>

+ 391 - 61
admin-app/src/pages/device/list.vue

@@ -27,18 +27,51 @@
     <view class="device-list">
       <view 
         v-for="device in deviceList" 
-        :key="device.id" 
+        :key="device.id || device.deviceId || device.id_ || device.id"
         class="device-item"
-        @click="viewDeviceDetail(device.id)"
+        @click="viewDeviceDetail(getDeviceId(device))"
       >
         <view class="device-header">
           <view class="device-info">
             <text class="device-name">{{ device.name }}</text>
-            <text class="device-id">设备编号: {{ device.shortId }}</text>
-          </view>
-          <view class="device-status" :class="getDeviceStatusClass(getDeviceStatusValue(device))">
-            <text class="status-dot"></text>
-            <text>{{ getDeviceStatusText(getDeviceStatusValue(device)) }}</text>
+            <view class="device-id-and-status">
+                  <text class="device-id">ID: {{ device.shortId }}</text>
+                  <view class="device-status" :class="getDeviceStatusClass(getDeviceStatusValue(device))">
+                    <text class="status-dot"></text>
+                    <text>{{ getDeviceStatusText(getDeviceStatusValue(device)) }}</text>
+                  </view>
+                  <!-- 设备配置下拉选择 -->
+                  <view class="config-container">
+                    <button 
+                      class="config-btn"
+                      @click.stop="toggleConfigDropdown(getDeviceId(device))"
+                    >
+                      设备配置
+                    </button>
+                    
+                    <!-- 配置下拉框 -->
+                    <view v-if="showConfigDropdown === getDeviceId(device)" class="config-dropdown">
+                      <view class="dropdown-header">
+                        <text class="dropdown-title">选择配置</text>
+                        <text class="dropdown-close" @click.stop="closeConfigDropdown">✕</text>
+                      </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>
         </view>
         <view class="device-content">
@@ -46,20 +79,18 @@
             <text class="station-label">所属站点:</text>
             <text class="station-name">{{ device.stationName || '未分配站点' }}</text>
           </view>
+          <view class="device-config-display">
+            <text class="config-label">价格配置:</text>
+            <text class="config-name-val">{{ device.deviceConfigName || '未绑定配置' }}</text>
+          </view>
         </view>
         <view class="device-footer">
-          <view class="device-metrics">
-            <view class="metric-item">
-              <text class="metric-label">今日订单:</text>
-              <text class="metric-value">{{ device.todayOrders || 0 }}</text>
-            </view>
-            <view class="metric-item">
-              <text class="metric-label">累计订单:</text>
-              <text class="metric-value">{{ device.totalOrders || 0 }}</text>
-            </view>
+          <!-- 设备闲忙状态 -->
+          <view class="device-busy-status" :class="getDeviceBusyStatusClass(device)">
+            <text class="busy-status-text">{{ getDeviceBusyStatus(device) }}</text>
           </view>
           <button 
-            v-if="device.status === 'ONLINE'" 
+            v-if="getDeviceStatusValue(device) === 1 || getDeviceStatusValue(device) === '1' || getDeviceStatusValue(device) === 'ONLINE'" 
             class="stop-btn"
             @click.stop="handleStopDevice(device.shortId)"
           >
@@ -88,7 +119,7 @@
 
 <script setup>
 import { ref, onMounted } from 'vue'
-import { getDeviceList, stopDevice } from '../../api/device.js'
+import { getDeviceList, stopDevice, getDeviceConfigList, batchModifyDeviceConfig } from '../../api/device.js'
 import { formatTime, showToast, storage } from '../../utils/index.js'
 
 const deviceList = ref([])
@@ -100,6 +131,11 @@ const loadMoreStatus = ref('more')
 const activeFilter = ref(0)
 const searchKeyword = ref('')
 
+// 设备配置相关
+const configList = ref([])
+const showConfigDropdown = ref(null)
+const loadingConfig = ref(false)
+
 const filterOptions = ['全部', '在线', '离线', '故障']
 const loadMoreText = {
   contentdown: '上拉加载更多',
@@ -193,7 +229,10 @@ const handleFilterChange = (index) => {
 }
 
 const viewDeviceDetail = (deviceId) => {
-  showToast('设备详情功能开发中')
+  // 跳转到设备详情页面
+  uni.navigateTo({
+    url: `/pages/device/detail?id=${deviceId}`
+  })
 }
 
 const handleStopDevice = async (shortId) => {
@@ -208,6 +247,100 @@ const handleStopDevice = async (shortId) => {
   }
 }
 
+// 加载设备配置列表
+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
+      // 适配分页结构 list, records 或直接数组
+      configList.value = data.list || data.records || (Array.isArray(data) ? data : [])
+      console.log('加载真实配置列表成功:', configList.value)
+    } else {
+      showToast(res.message || '加载配置失败')
+    }
+  } catch (error) {
+    console.error('加载设备配置列表失败:', 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) {
+    console.error('绑定设备配置失败:', error)
+    showToast('网络连接失败')
+  }
+}
+
+// 设备配置(保留旧方法,可能其他地方还在使用)
+const handleDeviceConfig = (deviceId) => {
+  uni.navigateTo({
+    url: `/pages/setting/device-config?id=${deviceId}&type=device_binding`
+  })
+}
+
+// 从设备对象中提取ID,处理不同的字段名
+const getDeviceId = (device) => {
+  if (!device) return null
+  
+  // 尝试不同的ID字段名
+  const possibleFields = ['id', 'deviceId', 'device_id', 'id_']
+  
+  for (const field of possibleFields) {
+    if (device[field] !== undefined && device[field] !== null) {
+      console.log('使用设备ID字段:', field, '值:', device[field])
+      return device[field]
+    }
+  }
+  
+  // 如果没有找到ID字段,尝试使用shortId
+  if (device.shortId) {
+    console.log('使用设备shortId作为ID:', device.shortId)
+    return device.shortId
+  }
+  
+  console.warn('设备对象没有ID字段:', device)
+  return null
+}
+
 // 从设备对象中提取状态值,处理不同的字段名
 const getDeviceStatusValue = (device) => {
   if (!device) return null
@@ -237,10 +370,16 @@ const getDeviceStatusText = (status) => {
   
   try {
     const dicts = storage.get('dicts')
-    if (!dicts || !dicts['Device.status']) {
-      console.warn('字典数据未加载或不存在 Device.status')
+    if (!dicts || !dicts['WashDevice.status']) {
+      console.warn('字典数据未加载或不存在 WashDevice.status')
       // 降级处理:使用默认映射
       const statusMap = {
+        'init': '初始化',
+        'idle': '设备空闲',
+        'busy': '设备忙碌',
+        'sleep': '不在营业时间',
+        'maintenance': '维护模式',
+        'fault': '设备故障',
         0: '离线', 1: '在线', 2: '故障', 3: '维护中',
         '0': '离线', '1': '在线', '2': '故障', '3': '维护中',
         'ONLINE': '在线', 'OFFLINE': '离线', 'FAULT': '故障', 'MAINTENANCE': '维护中'
@@ -248,7 +387,7 @@ const getDeviceStatusText = (status) => {
       return statusMap[status] || statusMap[String(status)] || String(status)
     }
     
-    const deviceStatusDict = dicts['Device.status']
+    const deviceStatusDict = dicts['WashDevice.status']
     const dictItem = deviceStatusDict.find(item => item.value == status)
     
     return dictItem ? dictItem.name : String(status)
@@ -258,12 +397,53 @@ const getDeviceStatusText = (status) => {
   }
 }
 
+// 获取设备闲忙状态
+const getDeviceBusyStatus = (device) => {
+  const status = getDeviceStatusValue(device)
+  
+  // 如果设备不在线,不显示闲忙状态
+  if (status !== 1 && status !== '1' && status !== 'ONLINE') {
+    return null
+  }
+  
+  // 检查是否有当前订单或工作状态
+  // 假设device对象有isBusy或currentOrder等字段表示闲忙状态
+  // 如果没有这些字段,我们可以根据今日订单数来推测
+  const hasCurrentOrder = device.isBusy || device.currentOrder || device.workingStatus === 'BUSY'
+  
+  // 如果没有明确的闲忙状态字段,我们可以假设如果今日有订单则可能忙碌
+  // 这是一个降级处理方案
+  const isLikelyBusy = hasCurrentOrder || (device.todayOrders > 0)
+  
+  return isLikelyBusy ? '忙碌' : '空闲'
+}
+
+// 获取设备闲忙状态样式类
+const getDeviceBusyStatusClass = (device) => {
+  const busyStatus = getDeviceBusyStatus(device)
+  if (!busyStatus) return ''
+  
+  return busyStatus === '忙碌' ? 'busy-status' : 'idle-status'
+}
+
 const getDeviceStatusClass = (status) => {
   const statusClassMap = {
+    // 新状态值映射
+    'init': 'status-online',      // 初始化 - 在线样式
+    'idle': 'status-online',      // 设备空闲 - 在线样式
+    'busy': 'status-online',      // 设备忙碌 - 在线样式
+    'sleep': 'status-offline',    // 不在营业时间 - 离线样式
+    'maintenance': 'status-maintenance',  // 维护模式 - 维护样式
+    'fault': 'status-fault',      // 设备故障 - 故障样式
+    // 旧状态值映射(兼容)
     0: 'status-offline',
     1: 'status-online',
     2: 'status-fault',
     3: 'status-maintenance',
+    '0': 'status-offline',
+    '1': 'status-online',
+    '2': 'status-fault',
+    '3': 'status-maintenance',
     'ONLINE': 'status-online',
     'OFFLINE': 'status-offline',
     'FAULT': 'status-fault',
@@ -286,11 +466,11 @@ const getStatusValue = (filterIndex) => {
 
 <style scoped>
 .device-list-container {
-  padding: 30rpx;
+  padding: 20rpx;
   background-color: #F8F9FA;
   min-height: 100vh;
   box-sizing: border-box;
-  padding-bottom: 120rpx;
+  padding-bottom: 100rpx;
 }
 
 .search-bar {
@@ -406,15 +586,19 @@ const getStatusValue = (filterIndex) => {
 .device-list {
   display: flex;
   flex-direction: column;
-  gap: 20rpx;
+  gap: 16rpx;
 }
 
 .device-item {
   background-color: #FFFFFF;
-  border-radius: 24rpx;
-  padding: 30rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+  border-radius: 20rpx;
+  padding: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
   transition: all 0.3s;
+  height: 260rpx; /* 增加高度以容纳新字段 */
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
 }
 
 .device-item:active {
@@ -425,10 +609,10 @@ const getStatusValue = (filterIndex) => {
 .device-header {
   display: flex;
   justify-content: space-between;
-  align-items: flex-start;
-  margin-bottom: 24rpx;
-  padding-bottom: 24rpx;
-  border-bottom: 2rpx solid #F0F0F0;
+  align-items: center;
+  margin-bottom: 16rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #F5F5F5;
 }
 
 .device-info {
@@ -436,28 +620,39 @@ const getStatusValue = (filterIndex) => {
 }
 
 .device-name {
-  font-size: 32rpx;
+  font-size: 28rpx;
   font-weight: 600;
   color: #1A1A1A;
-  margin-bottom: 12rpx;
+  margin-bottom: 8rpx;
   display: block;
 }
 
-.device-id {
-  font-size: 28rpx;
-  color: #667EEA;
-  font-weight: 600;
-  display: block;
-  margin-top: 8rpx;
+.device-id-and-status {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12rpx;
 }
 
 .device-status {
   display: flex;
   align-items: center;
-  font-size: 24rpx;
+  font-size: 22rpx;
   font-weight: 600;
-  padding: 8rpx 24rpx;
-  border-radius: 30rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 24rpx;
+  white-space: nowrap;
+}
+
+.device-id {
+  font-size: 26rpx;
+  color: #667EEA;
+  font-weight: 700;
+  white-space: nowrap;
+  flex: 1;
+  min-width: 180rpx;
+  text-transform: uppercase;
+  letter-spacing: 1rpx;
 }
 
 .status-dot {
@@ -490,39 +685,61 @@ const getStatusValue = (filterIndex) => {
 }
 
 .device-content {
-  margin-bottom: 24rpx;
-}
-
-.device-location {
+  margin-bottom: 8rpx;
+  flex: 1;
   display: flex;
-  align-items: center;
-  font-size: 26rpx;
-  color: #666666;
-  margin-bottom: 16rpx;
+  flex-direction: column;
+  justify-content: center;
 }
 
-.device-station {
+.device-station, .device-config-display {
   display: flex;
   align-items: center;
-  font-size: 26rpx;
+  font-size: 24rpx;
+  margin-bottom: 6rpx;
 }
 
-.station-label {
+.station-label, .config-label {
   color: #999999;
-  margin-right: 12rpx;
+  margin-right: 8rpx;
+  width: 110rpx;
 }
 
-.station-name {
+.station-name, .config-name-val {
   color: #1A1A1A;
   font-weight: 500;
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 .device-footer {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  padding-top: 24rpx;
-  border-top: 2rpx solid #F0F0F0;
+  padding-top: 12rpx;
+  border-top: 1rpx solid #F5F5F5;
+}
+
+.device-busy-status {
+  display: inline-flex;
+  align-items: center;
+  font-size: 22rpx;
+  font-weight: 600;
+  padding: 6rpx 16rpx;
+  border-radius: 24rpx;
+  margin-right: 12rpx;
+}
+
+.busy-status {
+  color: #F5222D;
+  background-color: #FFF1F0;
+}
+
+.idle-status {
+  color: #52C41A;
+  background-color: #F6FFED;
 }
 
 .device-metrics {
@@ -548,14 +765,15 @@ const getStatusValue = (filterIndex) => {
 }
 
 .stop-btn {
-  padding: 12rpx 32rpx;
+  padding: 6rpx 20rpx;
   background: linear-gradient(135deg, #F5222D 0%, #FF4D4F 100%);
   color: #FFFFFF;
   border: none;
-  border-radius: 16rpx;
-  font-size: 24rpx;
+  border-radius: 12rpx;
+  font-size: 22rpx;
   font-weight: 500;
   transition: all 0.3s;
+  margin-left: 12rpx;
 }
 
 .stop-btn:active {
@@ -563,6 +781,118 @@ const getStatusValue = (filterIndex) => {
   opacity: 0.9;
 }
 
+/* 设备配置按钮 */
+.config-btn {
+  padding: 6rpx 20rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  color: #FFFFFF;
+  border: none;
+  border-radius: 12rpx;
+  font-size: 22rpx;
+  font-weight: 500;
+  transition: all 0.3s;
+  white-space: nowrap;
+}
+
+/* 停止设备按钮 */
+.stop-btn {
+  margin-left: 12rpx;
+}
+
+.config-btn:active {
+  transform: scale(0.95);
+  opacity: 0.9;
+}
+
+/* 配置容器 */
+.config-container {
+  position: relative;
+}
+
+/* 配置下拉框 */
+.config-dropdown {
+  position: absolute;
+  top: 100%;
+  right: 0;
+  margin-top: 8rpx;
+  width: 300rpx;
+  background-color: #FFFFFF;
+  border-radius: 12rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  overflow: hidden;
+}
+
+/* 下拉框头部 */
+.dropdown-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx 20rpx;
+  background-color: #F8F9FA;
+  border-bottom: 1rpx solid #E0E0E0;
+}
+
+.dropdown-title {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+}
+
+.dropdown-close {
+  font-size: 24rpx;
+  color: #999999;
+  cursor: pointer;
+  padding: 4rpx;
+}
+
+.dropdown-close:active {
+  color: #666666;
+}
+
+/* 下拉框内容 */
+.dropdown-content {
+  max-height: 300rpx;
+  overflow-y: auto;
+}
+
+/* 配置选项 */
+.config-option {
+  padding: 16rpx 20rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  transition: background-color 0.2s;
+}
+
+.config-option:last-child {
+  border-bottom: none;
+}
+
+.config-option:active {
+  background-color: #F5F5F5;
+}
+
+.config-option-name {
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #1A1A1A;
+  display: block;
+  margin-bottom: 4rpx;
+}
+
+.config-option-desc {
+  font-size: 20rpx;
+  color: #999999;
+  display: block;
+}
+
+/* 无配置提示 */
+.no-config {
+  padding: 32rpx 20rpx;
+  text-align: center;
+  color: #999999;
+  font-size: 22rpx;
+}
+
 .empty-state {
   display: flex;
   flex-direction: column;

+ 6 - 0
admin-app/src/pages/index/index.vue

@@ -119,6 +119,11 @@
           <view class="action-icon">💸</view>
           <text class="action-label">提现审核</text>
         </view>
+        
+        <view class="action-item orange" @click="navigateTo('/pages/setting/device-config')">
+          <view class="action-icon">⚙️</view>
+          <text class="action-label">设备配置</text>
+        </view>
       </view>
     </view>
     
@@ -641,6 +646,7 @@ const handleLogout = async () => {
 .action-item.pink::before { background: linear-gradient(90deg, #F093FB, #F5576C); }
 .action-item.blue::before { background: linear-gradient(90deg, #4FACFE, #00F2FE); }
 .action-item.green::before { background: linear-gradient(90deg, #43E97B, #38F9D7); }
+.action-item.orange::before { background: linear-gradient(90deg, #FF9A9E, #FAD0C4); }
 
 .action-icon {
   width: 96rpx;

+ 376 - 0
admin-app/src/pages/setting/device-binding.vue

@@ -0,0 +1,376 @@
+<template>
+  <view class="device-binding-container">
+    <view class="header-nav">
+      <text class="back-icon" @click="goBack">←</text>
+      <text class="nav-title">设备绑定管理</text>
+    </view>
+
+    <view class="tabs">
+      <view class="tab" :class="{ active: activeTab === 0 }" @click="activeTab = 0">设备参数绑定</view>
+      <view class="tab" :class="{ active: activeTab === 1 }" @click="activeTab = 1">站点费率绑定</view>
+    </view>
+
+    <scroll-view class="content-scroll" scroll-y>
+      <!-- 设备参数绑定 -->
+      <view v-if="activeTab === 0" class="binding-section">
+        <view class="card">
+          <view class="card-header">1. 选择设备 (可多选)</view>
+          <view class="station-filter">
+            <picker @change="onStationChange" :value="stationIndex" :range="stationList" range-key="stationName">
+              <view class="picker-item">
+                {{ stationIndex === -1 ? '全部站点' : stationList[stationIndex].stationName }}
+              </view>
+            </picker>
+          </view>
+          <view class="device-list">
+            <view v-for="device in filteredDevices" :key="device.id" class="device-item" @click="toggleDeviceSelection(device.id)">
+              <checkbox :checked="selectedDeviceIds.includes(device.id)" />
+              <text class="device-name">{{ device.deviceName }} ({{ device.id }})</text>
+            </view>
+            <view v-if="filteredDevices.length === 0" class="empty-tip">暂无可选设备</view>
+          </view>
+        </view>
+
+        <view class="card">
+          <view class="card-header">2. 选择参数模板</view>
+          <picker @change="onConfigTemplateChange" :value="configIndex" :range="configTemplates" range-key="name">
+            <view class="picker-item">
+              {{ configIndex === -1 ? '点击选择模板' : configTemplates[configIndex].name }}
+            </view>
+          </picker>
+        </view>
+
+        <button class="action-btn" @click="handleBatchBindConfig" :disabled="selectedDeviceIds.length === 0 || configIndex === -1">
+          执行批量绑定
+        </button>
+      </view>
+
+      <!-- 站点费率绑定 -->
+      <view v-if="activeTab === 1" class="binding-section">
+        <view class="card">
+          <view class="card-header">1. 选择站点</view>
+          <picker @change="onStationSelectForFee" :value="stationIndexForFee" :range="stationList" range-key="stationName">
+            <view class="picker-item">
+              {{ stationIndexForFee === -1 ? '点击选择站点' : stationList[stationIndexForFee].stationName }}
+            </view>
+          </picker>
+        </view>
+
+        <view class="card">
+          <view class="card-header">2. 选择费率模板</view>
+          <picker @change="onFeeTemplateChange" :value="feeIndex" :range="feeTemplates" range-key="name">
+            <view class="picker-item">
+              {{ feeIndex === -1 ? '点击选择费率模板' : feeTemplates[feeIndex].name }}
+            </view>
+          </picker>
+        </view>
+
+        <button class="action-btn" @click="handleBindStationFee" :disabled="stationIndexForFee === -1 || feeIndex === -1">
+          保存绑定关系
+        </button>
+      </view>
+    </scroll-view>
+
+    <view class="loading-overlay" v-if="loading">
+      <text class="loading-spinner">🔄</text>
+      <text class="loading-text">处理中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { getDeviceList, getDeviceConfigList, batchModifyDeviceConfig } from '../../api/device.js'
+import { getPlatformFeeRateList, bindStationFeeRate } from '../../api/rate.js'
+import { getStationList } from '../../api/station.js'
+import { showToast } from '../../utils/index.js'
+
+const loading = ref(false)
+const activeTab = ref(0)
+
+const stationList = ref([])
+const stationIndex = ref(-1)
+const deviceList = ref([])
+const selectedDeviceIds = ref([])
+
+const configTemplates = ref([])
+const configIndex = ref(-1)
+
+const stationIndexForFee = ref(-1)
+const feeTemplates = ref([])
+const feeIndex = ref(-1)
+
+onMounted(() => {
+  fetchInitialData()
+})
+
+const fetchInitialData = async () => {
+  loading.value = true
+  try {
+    const [stationsRes, configsRes, feesRes, devicesRes] = await Promise.all([
+      getStationList(),
+      getDeviceConfigList({ page: 1, pageSize: 1000 }),
+      getPlatformFeeRateList({ page: 1, pageSize: 1000 }),
+      getDeviceList({ page: 1, pageSize: 1000 })
+    ])
+
+    const adapt = (res) => {
+      if (!res || res.code !== 200) return []
+      const data = res.data
+      return data.list || data.records || (Array.isArray(data) ? data : [])
+    }
+
+    stationList.value = adapt(stationsRes)
+    configTemplates.value = adapt(configsRes)
+    feeTemplates.value = adapt(feesRes)
+    deviceList.value = adapt(devicesRes)
+  } catch (error) {
+    console.error('加载数据失败:', error)
+    showToast('加载数据失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const filteredDevices = computed(() => {
+  if (stationIndex.value === -1) return deviceList.value
+  const targetStationId = stationList.value[stationIndex.value].stationId
+  return deviceList.value.filter(d => d.stationId == targetStationId)
+})
+
+const onStationChange = (e) => {
+  stationIndex.value = e.detail.value
+  selectedDeviceIds.value = []
+}
+
+const toggleDeviceSelection = (id) => {
+  const index = selectedDeviceIds.value.indexOf(id)
+  if (index > -1) {
+    selectedDeviceIds.value.splice(index, 1)
+  } else {
+    selectedDeviceIds.value.push(id)
+  }
+}
+
+const onConfigTemplateChange = (e) => {
+  configIndex.value = e.detail.value
+}
+
+const handleBatchBindConfig = async () => {
+  const template = configTemplates.value[configIndex.value]
+  loading.value = true
+  try {
+    const res = await batchModifyDeviceConfig({
+      deviceIds: selectedDeviceIds.value,
+      deviceConfigId: template.id
+    })
+    if (res.code === 200) {
+      showToast('绑定成功', 'success')
+      selectedDeviceIds.value = []
+    } else {
+      showToast(res.message || '绑定失败')
+    }
+  } catch (error) {
+    showToast('操作失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const onStationSelectForFee = (e) => {
+  stationIndexForFee.value = e.detail.value
+}
+
+const onFeeTemplateChange = (e) => {
+  feeIndex.value = e.detail.value
+}
+
+const handleBindStationFee = async () => {
+  const station = stationList.value[stationIndexForFee.value]
+  const template = feeTemplates.value[feeIndex.value]
+  loading.value = true
+  try {
+    const res = await bindStationFeeRate({
+      stationId: station.stationId,
+      feeRateId: template.id
+    })
+    if (res.code === 200) {
+      showToast('费率绑定成功', 'success')
+    } else {
+      showToast(res.message || '操作失败')
+    }
+  } catch (error) {
+    showToast('操作失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const goBack = () => {
+  uni.navigateBack()
+}
+</script>
+
+<style scoped>
+.device-binding-container {
+  min-height: 100vh;
+  background-color: #F8F9FA;
+  display: flex;
+  flex-direction: column;
+}
+
+.header-nav {
+  display: flex;
+  align-items: center;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  color: #fff;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  margin-right: 20rpx;
+}
+
+.nav-title {
+  font-size: 34rpx;
+  font-weight: 600;
+  flex: 1;
+  text-align: center;
+}
+
+.tabs {
+  display: flex;
+  background-color: #fff;
+  border-bottom: 1rpx solid #eee;
+}
+
+.tab {
+  flex: 1;
+  text-align: center;
+  padding: 24rpx 0;
+  font-size: 28rpx;
+  color: #666;
+  position: relative;
+}
+
+.tab.active {
+  color: #667EEA;
+  font-weight: 600;
+}
+
+.tab.active::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 25%;
+  width: 50%;
+  height: 4rpx;
+  background-color: #667EEA;
+}
+
+.content-scroll {
+  flex: 1;
+  padding: 20rpx;
+}
+
+.card {
+  background-color: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.card-header {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+  padding-bottom: 12rpx;
+}
+
+.station-filter {
+  margin-bottom: 20rpx;
+}
+
+.picker-item {
+  background-color: #f9f9f9;
+  padding: 20rpx;
+  border-radius: 8rpx;
+  font-size: 26rpx;
+  color: #666;
+  border: 1rpx solid #eee;
+}
+
+.device-list {
+  max-height: 400rpx;
+  overflow-y: auto;
+}
+
+.device-item {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f9f9f9;
+}
+
+.device-name {
+  font-size: 26rpx;
+  color: #333;
+  margin-left: 12rpx;
+}
+
+.empty-tip {
+  text-align: center;
+  padding: 40rpx;
+  color: #999;
+  font-size: 24rpx;
+}
+
+.action-btn {
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  color: #fff;
+  margin-top: 40rpx;
+  border-radius: 12rpx;
+}
+
+.action-btn:disabled {
+  opacity: 0.5;
+}
+
+.loading-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.loading-spinner {
+  font-size: 64rpx;
+  animation: spin 1s linear infinite;
+  margin-bottom: 32rpx;
+  color: #fff;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #fff;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+</style>

+ 834 - 0
admin-app/src/pages/setting/device-config-detail.vue

@@ -0,0 +1,834 @@
+<template>
+  <view class="device-config-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left">
+        <text class="back-icon" @click="goBack">←</text>
+      </view>
+      <text class="nav-title">{{ isEditMode ? '编辑配置' : '添加配置' }}</text>
+      <view class="nav-right">
+        <text class="save-btn" @click="saveConfig">保存</text>
+      </view>
+    </view>
+    
+    <!-- 配置表单 -->
+    <scroll-view class="config-form" scroll-y>
+      <!-- 基本参数 -->
+      <view class="form-section">
+        <text class="section-title">基本参数</text>
+        <view class="form-item">
+          <text class="label">配置名称</text>
+          <input 
+            class="input" 
+            v-model="configForm.name" 
+            placeholder="请输入配置名称"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">配置备注</text>
+          <textarea 
+            class="textarea" 
+            v-model="configForm.remark" 
+            placeholder="请输入配置备注"
+            :rows="3"
+          />
+        </view>
+      </view>
+      
+      <!-- 选项参数 -->
+      <view class="form-section">
+        <text class="section-title">选项参数</text>
+        <view class="form-item">
+          <text class="label">灯光工作模式</text>
+          <input 
+            class="input" 
+            v-model="configForm.lightMode" 
+            placeholder="0:全天暂停 1:全天营业 2:按时间段"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">维护模式</text>
+          <input 
+            class="input" 
+            v-model="configForm.maintenanceMode" 
+            placeholder="0:未设置 1:已设置"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">工作模式</text>
+          <input 
+            class="input" 
+            v-model="configForm.workMode" 
+            placeholder="0:全天暂停 1:全天营业 2:按时间段"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">屏幕类型</text>
+          <input 
+            class="input" 
+            v-model="configForm.screenType" 
+            placeholder="0:不支持视频 1:支持视频"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">屏幕左下方文本</text>
+          <input 
+            class="input" 
+            v-model="configForm.userMessage1" 
+            placeholder="请输入屏幕左下方文本"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">屏幕右下方文本</text>
+          <input 
+            class="input" 
+            v-model="configForm.userMessage2" 
+            placeholder="请输入屏幕右下方文本"
+          />
+        </view>
+      </view>
+      
+      <!-- 时间参数 -->
+      <view class="form-section">
+        <text class="section-title">时间参数</text>
+        <view class="form-item">
+          <text class="label">自动启动</text>
+          <view class="switch-container">
+            <switch 
+              :checked="configForm.motorFlowOn" 
+              @change="configForm.motorFlowOn = $event.detail.value"
+              class="switch"
+            />
+            <text class="switch-desc">有流量时自动启动水泵</text>
+          </view>
+        </view>
+        <view class="form-item">
+          <text class="label">自动关闭</text>
+          <view class="switch-container">
+            <switch 
+              :checked="configForm.motorFlowOff" 
+              @change="configForm.motorFlowOff = $event.detail.value"
+              class="switch"
+            />
+            <text class="switch-desc">无流量时自动关闭水泵</text>
+          </view>
+        </view>
+        <view class="form-item">
+          <text class="label">启动延时(毫秒)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.motorOnDelay" 
+            placeholder="推荐5000毫秒"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">关闭延时(毫秒)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.motorOffDelay" 
+            placeholder="推荐1000毫秒"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">关闭灵敏度(毫秒)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.motorOnInterval" 
+            placeholder="推荐10000毫秒"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">操作超时关机(秒)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.operationTimeout" 
+            placeholder="推荐3600秒"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">空闲超时关机(秒)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.idleTimeout" 
+            placeholder="推荐1200秒"
+          />
+        </view>
+      </view>
+      
+      <!-- 价格参数 -->
+      <view class="form-section">
+        <text class="section-title">价格参数</text>
+        <view class="form-item">
+          <text class="label">清水单价(分/分钟)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.priceWater" 
+            placeholder="请输入清水单价"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">泡沫单价(分/分钟)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.priceFoam" 
+            placeholder="请输入泡沫单价"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">镀膜单价(分/分钟)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.priceCoat" 
+            placeholder="请输入镀膜单价"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">吹气单价(分/分钟)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.priceBlow" 
+            placeholder="请输入吹气单价"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">吸尘单价(分/分钟)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.priceCleaner" 
+            placeholder="请输入吸尘单价"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">洗手单价(分/分钟)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.priceTap" 
+            placeholder="请输入洗手单价"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">场地费单价(分/分钟)</text>
+          <input 
+            class="input" 
+            type="number" 
+            v-model="configForm.priceSpace" 
+            placeholder="请输入场地费单价"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">扩展项目单价(分/分钟)</text>
+          <input 
+            class="input" 
+            type="number" 
+            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可关闭此功能"
+          />
+        </view>
+      </view>
+      
+      <!-- 时间周期设置 -->
+      <view class="form-section">
+        <text class="section-title">时间周期设置</text>
+        <view class="form-item">
+          <text class="label">照明时间段1</text>
+          <view class="time-picker-container">
+            <view class="time-range">
+              <picker mode="time" :value="lightStart1" @change="onTimeChange($event, 'lightStart1')" class="time-picker-item">
+                <view class="time-input">
+                  <text v-if="lightStart1">{{ lightStart1 }}</text>
+                  <text v-else class="placeholder">开始时间</text>
+                </view>
+              </picker>
+              <text class="time-separator">-</text>
+              <picker mode="time" :value="lightEnd1" @change="onTimeChange($event, 'lightEnd1')" class="time-picker-item">
+                <view class="time-input">
+                  <text v-if="lightEnd1">{{ lightEnd1 }}</text>
+                  <text v-else class="placeholder">结束时间</text>
+                </view>
+              </picker>
+            </view>
+          </view>
+        </view>
+        <view class="form-item">
+          <text class="label">照明时间段2</text>
+          <view class="time-picker-container">
+            <view class="time-range">
+              <picker mode="time" :value="lightStart2" @change="onTimeChange($event, 'lightStart2')" class="time-picker-item">
+                <view class="time-input">
+                  <text v-if="lightStart2">{{ lightStart2 }}</text>
+                  <text v-else class="placeholder">开始时间</text>
+                </view>
+              </picker>
+              <text class="time-separator">-</text>
+              <picker mode="time" :value="lightEnd2" @change="onTimeChange($event, 'lightEnd2')" class="time-picker-item">
+                <view class="time-input">
+                  <text v-if="lightEnd2">{{ lightEnd2 }}</text>
+                  <text v-else class="placeholder">结束时间</text>
+                </view>
+              </picker>
+            </view>
+          </view>
+        </view>
+        <view class="form-item">
+          <text class="label">营业时间段1</text>
+          <view class="time-picker-container">
+            <view class="time-range">
+              <picker mode="time" :value="workStart1" @change="onTimeChange($event, 'workStart1')" class="time-picker-item">
+                <view class="time-input">
+                  <text v-if="workStart1">{{ workStart1 }}</text>
+                  <text v-else class="placeholder">开始时间</text>
+                </view>
+              </picker>
+              <text class="time-separator">-</text>
+              <picker mode="time" :value="workEnd1" @change="onTimeChange($event, 'workEnd1')" class="time-picker-item">
+                <view class="time-input">
+                  <text v-if="workEnd1">{{ workEnd1 }}</text>
+                  <text v-else class="placeholder">结束时间</text>
+                </view>
+              </picker>
+            </view>
+          </view>
+        </view>
+        <view class="form-item">
+          <text class="label">营业时间段2</text>
+          <view class="time-picker-container">
+            <view class="time-range">
+              <picker mode="time" :value="workStart2" @change="onTimeChange($event, 'workStart2')" class="time-picker-item">
+                <view class="time-input">
+                  <text v-if="workStart2">{{ workStart2 }}</text>
+                  <text v-else class="placeholder">开始时间</text>
+                </view>
+              </picker>
+              <text class="time-separator">-</text>
+              <picker mode="time" :value="workEnd2" @change="onTimeChange($event, 'workEnd2')" class="time-picker-item">
+                <view class="time-input">
+                  <text v-if="workEnd2">{{ workEnd2 }}</text>
+                  <text v-else class="placeholder">结束时间</text>
+                </view>
+              </picker>
+            </view>
+          </view>
+        </view>
+      </view>
+    </scroll-view>
+
+    <!-- 底部操作栏 -->
+    <view class="bottom-action">
+      <button class="submit-btn" @click="saveConfig" :loading="loading">
+        {{ isEditMode ? '更新配置信息' : '立即添加配置' }}
+      </button>
+    </view>
+    
+    <!-- 加载状态 -->
+    <view class="loading-overlay" v-if="loading">
+      <text class="loading-spinner">🔄</text>
+      <text class="loading-text">保存中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+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'
+
+const configId = ref(null)
+const loading = ref(false)
+
+onLoad((options) => {
+  if (options.id) {
+    configId.value = options.id
+    loadConfigData()
+  }
+})
+
+// 时间变量
+const lightStart1 = ref('')
+const lightEnd1 = ref('')
+const lightStart2 = ref('')
+const lightEnd2 = ref('')
+const workStart1 = ref('')
+const workEnd1 = ref('')
+const workStart2 = ref('')
+const workEnd2 = ref('')
+
+const configForm = ref({
+  id: null,
+  name: '',
+  remark: '',
+  lightMode: 1,
+  maintenanceMode: 0,
+  workMode: 1,
+  screenType: 0,
+  userMessage1: '',
+  userMessage2: '',
+  motorFlowOn: false,
+  motorFlowOff: true,
+  motorOnDelay: 5000,
+  motorOffDelay: 1000,
+  motorOnInterval: 10000,
+  operationTimeout: 3600,
+  idleTimeout: 1200,
+  priceWater: 0,
+  priceFoam: 0,
+  priceCoat: 0,
+  priceBlow: 0,
+  priceCleaner: 0,
+  priceTap: 0,
+  priceSpace: 0,
+  priceUserExt: 0,
+  quickOpenMoney: 0,
+  lightTimePeriod1: '',
+  lightTimePeriod2: '',
+  workTimePeriod1: '',
+  workTimePeriod2: '',
+  soundVolume: 50,
+  videoSource: 0,
+  videoPlayDelay: 30,
+  workLightDelay: 30,
+  billDelay: 10,
+  tapOnDelay: 30,
+  noticeThresholdIdle: 60,
+  noticeThresholdOperation: 300
+})
+
+// 时间选择处理
+const onTimeChange = (e, type) => {
+  updateTimeValue(type, e.detail.value)
+}
+
+// 更新时间值
+const updateTimeValue = (type, time) => {
+  switch (type) {
+    case 'lightStart1':
+      lightStart1.value = time
+      break
+    case 'lightEnd1':
+      lightEnd1.value = time
+      break
+    case 'lightStart2':
+      lightStart2.value = time
+      break
+    case 'lightEnd2':
+      lightEnd2.value = time
+      break
+    case 'workStart1':
+      workStart1.value = time
+      break
+    case 'workEnd1':
+      workEnd1.value = time
+      break
+    case 'workStart2':
+      workStart2.value = time
+      break
+    case 'workEnd2':
+      workEnd2.value = time
+      break
+  }
+}
+
+// 同步时间到表单
+const syncTimeToForm = () => {
+  configForm.value.lightTimePeriod1 = lightStart1.value && lightEnd1.value 
+    ? `${lightStart1.value} - ${lightEnd1.value}` 
+    : ''
+  configForm.value.lightTimePeriod2 = lightStart2.value && lightEnd2.value 
+    ? `${lightStart2.value} - ${lightEnd2.value}` 
+    : ''
+  configForm.value.workTimePeriod1 = workStart1.value && workEnd1.value 
+    ? `${workStart1.value} - ${workEnd1.value}` 
+    : ''
+  configForm.value.workTimePeriod2 = workStart2.value && workEnd2.value 
+    ? `${workStart2.value} - ${workEnd2.value}` 
+    : ''
+}
+
+// 解析时间从表单
+const parseTimeFromForm = () => {
+  parseTimeRange(configForm.value.lightTimePeriod1, 'light')
+  parseTimeRange(configForm.value.lightTimePeriod2, 'light', 2)
+  parseTimeRange(configForm.value.workTimePeriod1, 'work')
+  parseTimeRange(configForm.value.workTimePeriod2, 'work', 2)
+}
+
+// 解析时间范围
+const parseTimeRange = (timeRange, type, index = 1) => {
+  if (!timeRange) return
+  
+  const parts = timeRange.split(' - ')
+  if (parts.length === 2) {
+    if (type === 'light' && index === 1) {
+      lightStart1.value = parts[0]
+      lightEnd1.value = parts[1]
+    } else if (type === 'light' && index === 2) {
+      lightStart2.value = parts[0]
+      lightEnd2.value = parts[1]
+    } else if (type === 'work' && index === 1) {
+      workStart1.value = parts[0]
+      workEnd1.value = parts[1]
+    } else if (type === 'work' && index === 2) {
+      workStart2.value = parts[0]
+      workEnd2.value = parts[1]
+    }
+  }
+}
+
+const isEditMode = computed(() => !!configId.value)
+
+// 加载配置数据
+const loadConfigData = async () => {
+  if (!configId.value) return
+  
+  loading.value = true
+  try {
+    const res = await getDeviceConfigDetail(configId.value)
+    if (res && res.code === 200) {
+      // 覆盖表单数据
+      Object.assign(configForm.value, res.data)
+      // 解析时间数据
+      parseTimeFromForm()
+    } else {
+      showToast('加载配置失败')
+    }
+  } catch (error) {
+    console.error('加载配置失败:', error)
+    showToast('加载配置失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 保存配置
+const saveConfig = async () => {
+  // 验证表单
+  if (!configForm.value.name) {
+    showToast('请输入配置名称')
+    return
+  }
+  
+  // 同步时间到表单
+  syncTimeToForm()
+  
+  loading.value = true
+  try {
+    const res = isEditMode.value 
+      ? await modifyDeviceConfig(configForm.value)
+      : await addDeviceConfig(configForm.value)
+    
+    if (res && res.code === 200) {
+      showToast('保存成功')
+      setTimeout(() => {
+        goBack()
+      }, 1500)
+    } else {
+      showToast(res.message || '保存失败')
+    }
+  } catch (error) {
+    console.error('保存配置失败:', error)
+    showToast('保存失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 返回上一页
+const goBack = () => {
+  uni.navigateBack()
+}
+
+// 页面加载时加载配置数据
+onMounted(() => {
+  // onLoad 已经处理了加载
+})
+</script>
+
+<style scoped>
+.device-config-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #F8F9FA;
+}
+
+/* 顶部导航栏 */
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+}
+
+.nav-left {
+  width: 180rpx;
+  display: flex;
+  align-items: center;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+}
+
+.nav-title {
+  font-size: 34rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+  flex: 1;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.nav-right {
+  width: 180rpx;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+}
+
+.save-btn {
+  font-size: 30rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+  text-align: right;
+}
+
+/* 配置表单 */
+.config-form {
+  flex: 1;
+  height: 0; /* 必须设置,否则 flex:1 在某些机型不生效 */
+  padding-bottom: 20rpx;
+}
+
+/* 底部操作栏 */
+.bottom-action {
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + var(--window-bottom));
+  background-color: #FFFFFF;
+  box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
+}
+
+.submit-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  color: #FFFFFF;
+  border-radius: 44rpx;
+  font-size: 30rpx;
+  font-weight: 600;
+  border: none;
+  box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.3);
+}
+
+.submit-btn:active {
+  transform: scale(0.98);
+  opacity: 0.9;
+}
+
+/* 表单部分 */
+.form-section {
+  margin: 20rpx;
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+  padding: 24rpx;
+}
+
+.section-title {
+  font-size: 28rpx;
+  color: #1A1A1A;
+  font-weight: 600;
+  margin-bottom: 20rpx;
+  display: block;
+  padding-bottom: 12rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+
+/* 表单项目 */
+.form-item {
+  margin-bottom: 24rpx;
+}
+
+.form-item:last-child {
+  margin-bottom: 0;
+}
+
+.label {
+  font-size: 24rpx;
+  color: #666666;
+  margin-bottom: 12rpx;
+  display: block;
+  font-weight: 500;
+}
+
+.input {
+  width: 100%;
+  padding: 16rpx 20rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 12rpx;
+  font-size: 24rpx;
+  color: #1A1A1A;
+  background-color: #F9F9F9;
+  box-sizing: border-box;
+  height: 72rpx;
+  line-height: 40rpx;
+  display: flex;
+  align-items: center;
+}
+
+.textarea {
+  width: 100%;
+  padding: 16rpx 20rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 12rpx;
+  font-size: 24rpx;
+  color: #1A1A1A;
+  background-color: #F9F9F9;
+  box-sizing: border-box;
+  min-height: 120rpx;
+  line-height: 36rpx;
+  display: flex;
+  align-items: flex-start;
+  padding-top: 20rpx;
+}
+
+/* 开关容器 */
+.switch-container {
+  display: flex;
+  align-items: center;
+}
+
+.switch {
+  transform: scale(0.8);
+  margin-right: 16rpx;
+}
+
+.switch-desc {
+  font-size: 22rpx;
+  color: #999999;
+  flex: 1;
+}
+
+/* 加载状态 */
+.loading-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(2px);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+
+.loading-spinner {
+  font-size: 64rpx;
+  animation: spin 1s linear infinite;
+  margin-bottom: 32rpx;
+  color: #fff;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #fff;
+}
+
+/* 时间选择器样式 */
+.time-picker-container {
+  width: 100%;
+}
+
+.time-range {
+  display: flex;
+  align-items: center;
+  width: 100%;
+}
+
+.time-picker-item {
+  flex: 1;
+}
+
+.time-input {
+  width: 100%;
+  height: 72rpx;
+  padding: 0 20rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 12rpx;
+  font-size: 24rpx;
+  color: #1A1A1A;
+  background-color: #F9F9F9;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  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;
+  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;
+}
+
+.time-input .placeholder {
+  color: #999999;
+}
+
+.time-input:active {
+  background-color: #F0F0F0;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+</style>

+ 323 - 0
admin-app/src/pages/setting/device-config.vue

@@ -0,0 +1,323 @@
+<template>
+  <view class="device-config-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left">
+        <text class="back-icon" @click="goBack">←</text>
+      </view>
+      <text class="nav-title" style="text-align: center;">设备参数模板</text>
+      <view class="nav-right"></view>
+    </view>
+    
+    <!-- 配置列表 -->
+    <view class="config-list">
+      <view 
+        v-for="config in configList" 
+        :key="config.id" 
+        class="config-item"
+        @click="navigateToEditConfig(config.id)"
+      >
+        <view class="config-id-section">
+          <text class="id-badge">ID: {{ config.id }}</text>
+        </view>
+        <view class="config-main-info">
+          <text class="config-name">{{ config.name }}</text>
+          <text class="config-desc">{{ config.remark || '无描述' }}</text>
+        </view>
+        <view class="config-action">
+          <text class="arrow">→</text>
+        </view>
+      </view>
+    </view>
+    
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="configList.length === 0 && !loading">
+      <text class="empty-icon">⚙️</text>
+      <text class="empty-text">暂无设备配置</text>
+      <text class="empty-desc">点击右上角添加配置</text>
+    </view>
+    
+    <!-- 悬浮添加按钮 -->
+    <view class="fab-add" @click="navigateToAddConfig">
+      <text class="plus-icon">+</text>
+    </view>
+    
+    <!-- 加载状态 -->
+    <view class="loading-overlay" v-if="loading">
+      <text class="loading-spinner">🔄</text>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { getDeviceConfigList } from '../../api/device.js'
+import { showToast } from '../../utils/index.js'
+
+const configList = ref([])
+const loading = ref(false)
+const stationId = ref('') // 可选过滤
+
+// 加载设备配置列表
+const loadDeviceConfigs = async () => {
+  if (loading.value) return
+  loading.value = true
+  try {
+    const res = await getDeviceConfigList({
+      page: 1,
+      pageSize: 100,
+      stationId: stationId.value
+    })
+    
+    if (res && res.code === 200) {
+      // 适配分页结构 list, records 或直接数组
+      const data = res.data
+      configList.value = data.list || data.records || (Array.isArray(data) ? data : [])
+      console.log('设备配置列表加载成功:', configList.value)
+    } else {
+      showToast(res.message || '获取配置列表失败')
+    }
+  } catch (error) {
+    console.error('加载设备配置失败:', error)
+    showToast('网络请求失败,请检查网络')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 页面每次显示时(包括从编辑页返回)都刷新数据
+onShow(() => {
+  loadDeviceConfigs()
+})
+
+// 导航到添加配置页面
+const navigateToAddConfig = () => {
+  uni.navigateTo({
+    url: '/pages/setting/device-config-detail'
+  })
+}
+
+// 导航到编辑配置页面
+const navigateToEditConfig = (configId) => {
+  uni.navigateTo({
+    url: `/pages/setting/device-config-detail?id=${configId}`
+  })
+}
+
+// 返回上一页
+const goBack = () => {
+  uni.navigateBack()
+}
+</script>
+
+<style scoped>
+.device-config-container {
+  min-height: 100vh;
+  background-color: #F8F9FA;
+  padding-bottom: 180rpx; /* 关键:间距改在这里,确保滚动到底部不被按钮遮挡 */
+  box-sizing: border-box;
+}
+
+/* 顶部导航栏 */
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+}
+
+.nav-left {
+  width: 180rpx; /* 增加宽度以平衡右侧胶囊按钮空间 */
+  display: flex;
+  align-items: center;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+}
+
+.nav-title {
+  font-size: 34rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+  flex: 1;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.nav-right {
+  width: 180rpx;
+}
+
+/* 悬浮添加按钮 */
+.fab-add {
+  position: fixed;
+  left: 50%;
+  transform: translateX(-50%);
+  bottom: 60rpx;
+  width: 110rpx;
+  height: 110rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 10rpx 30rpx rgba(102, 126, 234, 0.4);
+  z-index: 99;
+  transition: all 0.3s;
+}
+
+.fab-add:active {
+  transform: translateX(-50%) scale(0.9);
+  box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.3);
+}
+
+.plus-icon {
+  font-size: 70rpx;
+  color: #FFFFFF;
+  font-weight: 300;
+  margin-top: -6rpx;
+}
+
+/* 配置列表 */
+.config-list {
+  margin: 20rpx;
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+  /* 移除了内部 padding-bottom */
+}
+
+.config-item {
+  display: flex;
+  align-items: center;
+  height: 140rpx; /* 固定高度确保等高 */
+  padding: 0 30rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  box-sizing: border-box;
+}
+
+.config-item:last-child {
+  border-bottom: none;
+}
+
+.config-id-section {
+  margin-right: 24rpx;
+  flex-shrink: 0;
+}
+
+.id-badge {
+  display: inline-block;
+  font-size: 22rpx;
+  color: #667EEA;
+  background-color: rgba(102, 126, 234, 0.1);
+  padding: 6rpx 16rpx;
+  border-radius: 8rpx;
+  font-weight: 600;
+  min-width: 80rpx;
+  text-align: center;
+}
+
+.config-main-info {
+  flex: 1;
+  overflow: hidden; /* 必须,用于文本溢出省略 */
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.config-name {
+  font-size: 30rpx;
+  color: #1A1A1A;
+  font-weight: 600;
+  margin-bottom: 6rpx;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.config-desc {
+  font-size: 24rpx;
+  color: #999999;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.config-action {
+  margin-left: 20rpx;
+  flex-shrink: 0;
+}
+
+.arrow {
+  font-size: 30rpx;
+  color: #CCCCCC;
+}
+
+/* 空状态 */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999999;
+}
+
+.empty-icon {
+  font-size: 120rpx;
+  margin-bottom: 32rpx;
+  opacity: 0.5;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  margin-bottom: 16rpx;
+}
+
+.empty-desc {
+  font-size: 24rpx;
+}
+
+/* 加载状态 */
+.loading-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(2px);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+
+.loading-spinner {
+  font-size: 64rpx;
+  animation: spin 1s linear infinite;
+  margin-bottom: 32rpx;
+  color: #fff;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #fff;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+</style>

+ 142 - 0
admin-app/src/pages/setting/index.vue

@@ -0,0 +1,142 @@
+<template>
+  <view class="setting-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left"></view>
+      <text class="nav-title">平台配置</text>
+      <view class="nav-right"></view>
+    </view>
+    
+    <!-- 配置菜单列表 -->
+    <view class="setting-menu">
+      <view class="menu-item" @click="navigateToRateConfig">
+        <view class="menu-info">
+          <text class="menu-title">平台费率配置</text>
+          <text class="menu-desc">设置平台相关费率及提现手续费</text>
+        </view>
+        <text class="menu-arrow">→</text>
+      </view>
+      
+      <view class="menu-item" @click="navigateToDeviceConfig">
+        <view class="menu-info">
+          <text class="menu-title">设备参数模板</text>
+          <text class="menu-desc">管理设备参数模板(价格、时间、模式等)</text>
+        </view>
+        <text class="menu-arrow">→</text>
+      </view>
+
+      <view class="menu-item" @click="navigateToDeviceBinding">
+        <view class="menu-info">
+          <text class="menu-title">设备绑定管理</text>
+          <text class="menu-desc">设备与参数模板、费率模板的绑定</text>
+        </view>
+        <text class="menu-arrow">→</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+
+// 导航到平台费率配置
+const navigateToRateConfig = () => {
+  uni.navigateTo({
+    url: '/pages/setting/rate-config'
+  })
+}
+
+// 导航到设备配置
+const navigateToDeviceConfig = () => {
+  uni.navigateTo({
+    url: '/pages/setting/device-config'
+  })
+}
+
+// 导航到设备绑定
+const navigateToDeviceBinding = () => {
+  uni.navigateTo({
+    url: '/pages/setting/device-binding'
+  })
+}
+</script>
+
+<style scoped>
+.setting-container {
+  min-height: 100vh;
+  background-color: #F8F9FA;
+}
+
+/* 顶部导航栏 */
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+}
+
+.nav-left {
+  width: 180rpx; /* 增加宽度以平衡右侧胶囊按钮空间 */
+}
+
+.nav-title {
+  font-size: 34rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+  flex: 1;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.nav-right {
+  width: 180rpx;
+}
+
+/* 配置菜单列表 */
+.setting-menu {
+  margin: 20rpx;
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.menu-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+
+.menu-item:last-child {
+  border-bottom: none;
+}
+
+.menu-info {
+  flex: 1;
+}
+
+.menu-title {
+  font-size: 30rpx;
+  color: #1A1A1A;
+  font-weight: 600;
+  margin-bottom: 8rpx;
+  display: block;
+}
+
+.menu-desc {
+  font-size: 24rpx;
+  color: #999999;
+  display: block;
+}
+
+.menu-arrow {
+  font-size: 30rpx;
+  color: #CCCCCC;
+}
+</style>

+ 317 - 0
admin-app/src/pages/setting/rate-config-detail.vue

@@ -0,0 +1,317 @@
+<template>
+  <view class="rate-config-detail-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left">
+        <text class="back-icon" @click="goBack">←</text>
+      </view>
+      <text class="nav-title">{{ isEditMode ? '编辑模板' : '添加模板' }}</text>
+      <view class="nav-right">
+        <text class="save-btn" @click="saveConfig">保存</text>
+      </view>
+    </view>
+    
+    <!-- 表单内容 -->
+    <scroll-view class="form-content" scroll-y>
+      <view class="form-section">
+        <view class="form-item">
+          <text class="label">模板名称</text>
+          <input 
+            class="input" 
+            v-model="rateForm.name" 
+            placeholder="请输入模板名称"
+          />
+        </view>
+      </view>
+
+      <view class="form-section">
+        <view class="form-item">
+          <text class="label">平台服务费比例 (%)</text>
+          <input 
+            class="input" 
+            type="digit"
+            v-model="rateForm.feeRatePercent" 
+            placeholder="例如: 10 (表示10%)"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">充值冻结金额比例 (%)</text>
+          <input 
+            class="input" 
+            type="digit"
+            v-model="rateForm.frozenRatioPercent" 
+            placeholder="例如: 30 (表示30%)"
+          />
+        </view>
+        <view class="form-item">
+          <text class="label">提现手续费率 (%)</text>
+          <input 
+            class="input" 
+            type="digit"
+            v-model="rateForm.withdrawalFeeRatePercent" 
+            placeholder="例如: 0.6 (表示0.6%)"
+          />
+        </view>
+      </view>
+    </scroll-view>
+
+    <!-- 底部操作栏 -->
+    <view class="bottom-action">
+      <button class="submit-btn" @click="saveConfig" :loading="loading">
+        {{ isEditMode ? '更新价格模板' : '立即添加模板' }}
+      </button>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-overlay" v-if="loading">
+      <text class="loading-spinner">🔄</text>
+      <text class="loading-text">处理中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { showToast } from '../../utils/index.js'
+import { addPlatformFeeRate, modifyPlatformFeeRate, getPlatformFeeRateList } from '../../api/rate.js'
+
+const id = ref(null)
+const loading = ref(false)
+
+const rateForm = ref({
+  id: null,
+  name: '',
+  feeRatePercent: '',
+  frozenRatioPercent: '',
+  withdrawalFeeRatePercent: ''
+})
+
+const isEditMode = computed(() => !!id.value)
+
+onLoad((options) => {
+  if (options.id) {
+    id.value = options.id
+    loadData()
+  }
+})
+
+const loadData = async () => {
+  loading.value = true
+  try {
+    // 列表接口通常返回所有,我们从中找
+    const res = await getPlatformFeeRateList({ page: 1, pageSize: 1000 })
+    if (res && res.code === 200) {
+      const records = res.data.records || res.data || []
+      const item = records.find(r => r.id == id.value)
+      if (item) {
+        rateForm.value = {
+          id: item.id,
+          name: item.name,
+          feeRatePercent: (item.feeRate * 100).toString(),
+          frozenRatioPercent: (item.frozenRatio * 100).toString(),
+          withdrawalFeeRatePercent: (item.withdrawalFeeRate * 100).toString()
+        }
+      }
+    }
+  } catch (error) {
+    console.error('加载详情失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const saveConfig = async () => {
+  if (!rateForm.value.name) {
+    showToast('请输入名称')
+    return
+  }
+
+  const data = {
+    id: rateForm.value.id,
+    name: rateForm.value.name,
+    feeRate: parseFloat(rateForm.value.feeRatePercent) / 100,
+    frozenRatio: parseFloat(rateForm.value.frozenRatioPercent) / 100,
+    withdrawalFeeRate: parseFloat(rateForm.value.withdrawalFeeRatePercent) / 100
+  }
+
+  loading.value = true
+  try {
+    const res = isEditMode.value 
+      ? await modifyPlatformFeeRate(data)
+      : await addPlatformFeeRate(data)
+    
+    if (res && res.code === 200) {
+      showToast('保存成功')
+      setTimeout(() => {
+        goBack()
+      }, 1500)
+    } else {
+      showToast(res.message || '保存失败')
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    showToast('保存失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const goBack = () => {
+  uni.navigateBack()
+}
+</script>
+
+<style scoped>
+.rate-config-detail-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background-color: #F8F9FA;
+}
+
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+}
+
+.nav-left {
+  width: 180rpx;
+  display: flex;
+  align-items: center;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+}
+
+.nav-title {
+  font-size: 34rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+  flex: 1;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.nav-right {
+  width: 180rpx;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+}
+
+.save-btn {
+  font-size: 30rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+  text-align: right;
+}
+
+.form-content {
+  flex: 1;
+  height: 0;
+  padding: 20rpx;
+  box-sizing: border-box;
+}
+
+/* 底部操作栏 */
+.bottom-action {
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + var(--window-bottom));
+  background-color: #FFFFFF;
+  box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
+}
+
+.submit-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  color: #FFFFFF;
+  border-radius: 44rpx;
+  font-size: 30rpx;
+  font-weight: 600;
+  border: none;
+  box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.3);
+}
+
+.submit-btn:active {
+  transform: scale(0.98);
+  opacity: 0.9;
+}
+
+.form-section {
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.form-item {
+  margin-bottom: 24rpx;
+}
+
+.form-item:last-child {
+  margin-bottom: 0;
+}
+
+.label {
+  font-size: 26rpx;
+  color: #666666;
+  margin-bottom: 12rpx;
+  display: block;
+}
+
+.input {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 12rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+  background-color: #F9F9F9;
+}
+
+.loading-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+
+.loading-spinner {
+  font-size: 64rpx;
+  animation: spin 1s linear infinite;
+  margin-bottom: 32rpx;
+  color: #fff;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #fff;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+</style>

+ 302 - 0
admin-app/src/pages/setting/rate-config.vue

@@ -0,0 +1,302 @@
+<template>
+  <view class="rate-config-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left">
+        <text class="back-icon" @click="goBack">←</text>
+      </view>
+      <text class="nav-title">价格模板管理</text>
+      <view class="nav-right"></view>
+    </view>
+    
+    <!-- 模板列表 -->
+    <view class="config-list">
+      <view 
+        v-for="item in rateList" 
+        :key="item.id" 
+        class="config-item"
+        @click="navigateToEditConfig(item.id)"
+      >
+        <view class="config-info">
+          <text class="config-name">{{ item.name }}</text>
+          <text class="config-desc">平台费率: {{ (item.feeRate * 100).toFixed(2) }}% | 提现手续费: {{ (item.withdrawalFeeRate * 100).toFixed(2) }}%</text>
+        </view>
+        <view class="config-status">
+          <text class="status-text active">ID: {{ item.id }}</text>
+          <text class="arrow">→</text>
+        </view>
+      </view>
+    </view>
+    
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="rateList.length === 0 && !loading">
+      <text class="empty-icon">💰</text>
+      <text class="empty-text">暂无价格模板</text>
+      <text class="empty-desc">点击右上角添加模板</text>
+    </view>
+    
+    <!-- 悬浮添加按钮 -->
+    <view class="fab-add" @click="navigateToAddConfig">
+      <text class="plus-icon">+</text>
+    </view>
+    
+    <!-- 加载状态 -->
+    <view class="loading-overlay" v-if="loading">
+      <text class="loading-spinner">🔄</text>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getPlatformFeeRateList } from '../../api/rate.js'
+import { showToast } from '../../utils/index.js'
+
+const rateList = ref([])
+const loading = ref(false)
+
+// 加载费率模板列表
+const loadRateConfigs = async () => {
+  loading.value = true
+  try {
+    const res = await getPlatformFeeRateList({
+      page: 1,
+      pageSize: 100
+    })
+    
+    if (res && res.code === 200) {
+      const data = res.data
+      rateList.value = data.list || data.records || (Array.isArray(data) ? data : [])
+    } else {
+      showToast('获取费率模板失败')
+    }
+  } catch (error) {
+    console.error('加载费率模板失败:', error)
+    showToast('加载费率模板失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 导航到添加页面
+const navigateToAddConfig = () => {
+  uni.navigateTo({
+    url: '/pages/setting/rate-config-detail'
+  })
+}
+
+// 导航到编辑页面
+const navigateToEditConfig = (id) => {
+  uni.navigateTo({
+    url: `/pages/setting/rate-config-detail?id=${id}`
+  })
+}
+
+// 返回上一页
+const goBack = () => {
+  uni.navigateBack()
+}
+
+onMounted(() => {
+  loadRateConfigs()
+})
+</script>
+
+<style scoped>
+.rate-config-container {
+  min-height: 100vh;
+  background-color: #F8F9FA;
+  padding-bottom: 180rpx;
+  box-sizing: border-box;
+}
+
+/* 顶部导航栏 */
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+}
+
+.nav-left {
+  width: 180rpx;
+  display: flex;
+  align-items: center;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+}
+
+.nav-title {
+  font-size: 34rpx;
+  color: #FFFFFF;
+  font-weight: 600;
+  flex: 1;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.nav-right {
+  width: 180rpx;
+}
+
+/* 悬浮添加按钮 */
+.fab-add {
+  position: fixed;
+  left: 50%;
+  transform: translateX(-50%);
+  bottom: 60rpx;
+  width: 110rpx;
+  height: 110rpx;
+  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 10rpx 30rpx rgba(102, 126, 234, 0.4);
+  z-index: 99;
+  transition: all 0.3s;
+}
+
+.fab-add:active {
+  transform: translateX(-50%) scale(0.9);
+  box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.3);
+}
+
+.plus-icon {
+  font-size: 70rpx;
+  color: #FFFFFF;
+  font-weight: 300;
+  margin-top: -6rpx;
+}
+
+/* 列表 */
+.config-list {
+  margin: 20rpx;
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+  /* 移除内部 padding */
+}
+
+.config-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+
+.config-item:last-child {
+  border-bottom: none;
+}
+
+.config-info {
+  flex: 1;
+}
+
+.config-name {
+  font-size: 30rpx;
+  color: #1A1A1A;
+  font-weight: 600;
+  margin-bottom: 8rpx;
+  display: block;
+}
+
+.config-desc {
+  font-size: 24rpx;
+  color: #999999;
+  display: block;
+}
+
+.config-status {
+  display: flex;
+  align-items: center;
+}
+
+.status-text {
+  font-size: 22rpx;
+  color: #667EEA;
+  background-color: rgba(102, 126, 234, 0.1);
+  padding: 4rpx 12rpx;
+  border-radius: 12rpx;
+  margin-right: 12rpx;
+}
+
+.status-text.active {
+  color: #52C41A;
+  background-color: rgba(82, 196, 26, 0.1);
+}
+
+.arrow {
+  font-size: 30rpx;
+  color: #CCCCCC;
+}
+
+/* 空状态 */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999999;
+}
+
+.empty-icon {
+  font-size: 120rpx;
+  margin-bottom: 32rpx;
+  opacity: 0.5;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  margin-bottom: 16rpx;
+}
+
+.empty-desc {
+  font-size: 24rpx;
+}
+
+/* 加载状态 */
+.loading-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(2px);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+
+.loading-spinner {
+  font-size: 64rpx;
+  animation: spin 1s linear infinite;
+  margin-bottom: 32rpx;
+  color: #fff;
+}
+
+.loading-text {
+  font-size: 28rpx;
+  color: #fff;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+</style>