gaoluyang
2025-09-16 793391c23ba45b3dab55657ecd2448d87e17f854
设备巡检、智能派单
已添加6个文件
已修改3个文件
2433 ■■■■■ 文件已修改
src/api/equipmentManagement/inspection.js 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/inspection/detail.vue 805 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/inspection/index.vue 526 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/smartDispatch/index.vue 801 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/app-logo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/shebeixunjian@2x.png 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/inspection.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,220 @@
import request from "@/utils/request";
/**
 * @desc èŽ·å–å·¡æ£€æ¸…å•åˆ—è¡¨
 * @param {Object} params - æŸ¥è¯¢å‚æ•°
 * @param {string} params.date - å·¡æ£€æ—¥æœŸ
 * @param {string} params.inspector - å·¡æ£€å‘˜
 * @param {number} params.status - å·¡æ£€çŠ¶æ€ 0:待巡检 1:巡检中 2:已完成
 * @returns {Promise}
 */
export const getInspectionList = (params) => {
  return request({
    url: "/device/inspection/list",
    method: "get",
    params,
  });
};
/**
 * @desc èŽ·å–å·¡æ£€è¯¦æƒ…
 * @param {string|number} id - å·¡æ£€ID
 * @returns {Promise}
 */
export const getInspectionDetail = (id) => {
  return request({
    url: `/device/inspection/${id}`,
    method: "get",
  });
};
/**
 * @desc å¼€å§‹å·¡æ£€
 * @param {Object} data - å·¡æ£€æ•°æ®
 * @param {string|number} data.inspectionId - å·¡æ£€ID
 * @param {string} data.startTime - å¼€å§‹æ—¶é—´
 * @returns {Promise}
 */
export const startInspection = (data) => {
  return request({
    url: "/device/inspection/start",
    method: "post",
    data,
  });
};
/**
 * @desc æäº¤å·¡æ£€è®°å½•
 * @param {Object} data - å·¡æ£€è®°å½•数据
 * @param {string|number} data.deviceId - è®¾å¤‡ID
 * @param {string} data.deviceCode - è®¾å¤‡ç¼–码
 * @param {string} data.inspectionDate - å·¡æ£€æ—¥æœŸ
 * @param {string} data.inspector - å·¡æ£€å‘˜
 * @param {string} data.scanTime - æ‰«ç æ—¶é—´
 * @param {Array} data.items - å·¡æ£€é¡¹ç›®åˆ—表
 * @param {string} data.completedAt - å®Œæˆæ—¶é—´
 * @returns {Promise}
 */
export const submitInspectionRecord = (data) => {
  return request({
    url: "/device/inspection/submit",
    method: "post",
    data,
  });
};
/**
 * @desc æ›´æ–°å·¡æ£€é¡¹ç›®
 * @param {Object} data - å·¡æ£€é¡¹ç›®æ•°æ®
 * @param {string|number} data.inspectionId - å·¡æ£€ID
 * @param {string|number} data.itemId - é¡¹ç›®ID
 * @param {string} data.result - å·¡æ£€ç»“æžœ normal:正常 abnormal:异常
 * @param {string} data.abnormalDesc - å¼‚常描述
 * @param {Array} data.images - å›¾ç‰‡åˆ—表
 * @param {Array} data.videos - è§†é¢‘列表
 * @param {string} data.remark - å¤‡æ³¨
 * @returns {Promise}
 */
export const updateInspectionItem = (data) => {
  return request({
    url: "/device/inspection/item/update",
    method: "put",
    data,
  });
};
/**
 * @desc æ‰«ç æ‰“卡
 * @param {Object} data - æ‰“卡数据
 * @param {string|number} data.inspectionId - å·¡æ£€ID
 * @param {string} data.deviceCode - è®¾å¤‡ç¼–码
 * @param {string} data.qrCode - äºŒç»´ç å†…容
 * @param {string} data.checkInTime - æ‰“卡时间
 * @param {string} data.location - æ‰“卡位置
 * @returns {Promise}
 */
export const checkInByQRCode = (data) => {
  return request({
    url: "/device/inspection/checkin",
    method: "post",
    data,
  });
};
/**
 * @desc ä¸Šä¼ å·¡æ£€æ–‡ä»¶ï¼ˆå›¾ç‰‡/视频)
 * @param {FormData} formData - æ–‡ä»¶æ•°æ®
 * @returns {Promise}
 */
export const uploadInspectionFile = (formData) => {
  return request({
    url: "/device/inspection/upload",
    method: "post",
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  });
};
/**
 * @desc èŽ·å–å·¡æ£€ç»Ÿè®¡æ•°æ®
 * @param {Object} params - æŸ¥è¯¢å‚æ•°
 * @param {string} params.startDate - å¼€å§‹æ—¥æœŸ
 * @param {string} params.endDate - ç»“束日期
 * @param {string} params.inspector - å·¡æ£€å‘˜
 * @returns {Promise}
 */
export const getInspectionStats = (params) => {
  return request({
    url: "/device/inspection/stats",
    method: "get",
    params,
  });
};
/**
 * @desc èŽ·å–å·¡æ£€åŽ†å²è®°å½•
 * @param {Object} params - æŸ¥è¯¢å‚æ•°
 * @param {string|number} params.deviceId - è®¾å¤‡ID
 * @param {number} params.current - å½“前页
 * @param {number} params.size - é¡µé¢å¤§å°
 * @returns {Promise}
 */
export const getInspectionHistory = (params) => {
  return request({
    url: "/device/inspection/history",
    method: "get",
    params,
  });
};
/**
 * @desc å¯¼å‡ºå·¡æ£€è®°å½•
 * @param {Object} params - å¯¼å‡ºå‚æ•°
 * @param {string} params.startDate - å¼€å§‹æ—¥æœŸ
 * @param {string} params.endDate - ç»“束日期
 * @param {string} params.inspector - å·¡æ£€å‘˜
 * @param {Array} params.deviceIds - è®¾å¤‡ID列表
 * @returns {Promise}
 */
export const exportInspectionRecords = (params) => {
  return request({
    url: "/device/inspection/export",
    method: "get",
    params,
    responseType: 'blob'
  });
};
/**
 * @desc åˆ é™¤å·¡æ£€è®°å½•
 * @param {string|number} id - å·¡æ£€è®°å½•ID
 * @returns {Promise}
 */
export const deleteInspectionRecord = (id) => {
  return request({
    url: `/device/inspection/${id}`,
    method: "delete",
  });
};
/**
 * @desc æ‰¹é‡åˆ é™¤å·¡æ£€è®°å½•
 * @param {Array} ids - å·¡æ£€è®°å½•ID列表
 * @returns {Promise}
 */
export const batchDeleteInspectionRecords = (ids) => {
  return request({
    url: "/device/inspection/batch/delete",
    method: "delete",
    data: { ids },
  });
};
/**
 * @desc èŽ·å–è®¾å¤‡äºŒç»´ç 
 * @param {string|number} deviceId - è®¾å¤‡ID
 * @returns {Promise}
 */
export const getDeviceQRCode = (deviceId) => {
  return request({
    url: `/device/qrcode/${deviceId}`,
    method: "get",
  });
};
/**
 * @desc éªŒè¯è®¾å¤‡äºŒç»´ç 
 * @param {Object} data - éªŒè¯æ•°æ®
 * @param {string} data.qrCode - äºŒç»´ç å†…容
 * @param {string|number} data.deviceId - è®¾å¤‡ID
 * @returns {Promise}
 */
export const verifyDeviceQRCode = (data) => {
  return request({
    url: "/device/qrcode/verify",
    method: "post",
    data,
  });
};
src/config.js
@@ -12,7 +12,7 @@
     // åº”用版本
     version: "1.1.0",
     // åº”用logo
     logo: "/static/logo.png",
     logo: "/static/app-logo.png",
     // å®˜æ–¹ç½‘ç«™
     site_url: "http://ruoyi.vip",
     // æ”¿ç­–协议
src/pages.json
@@ -350,6 +350,27 @@
        "navigationBarTitleText": "维修保养",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/inspection/index",
      "style": {
        "navigationBarTitleText": "设备巡检",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/inspection/detail",
      "style": {
        "navigationBarTitleText": "巡检详情",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/smartDispatch/index",
      "style": {
        "navigationBarTitleText": "智能派单",
        "navigationStyle": "custom"
      }
    }
  ],
  "subPackages": [
src/pages/equipmentManagement/inspection/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,805 @@
<template>
  <view class="inspection-detail">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="设备巡检详情" @back="goBack" />
    <!-- è®¾å¤‡ä¿¡æ¯å¡ç‰‡ -->
    <view class="device-card">
      <view class="device-header">
        <view class="device-icon">
          <up-icon name="settings" size="24" color="#1890ff"></up-icon>
        </view>
        <view class="device-info">
          <text class="device-name">{{ deviceInfo.deviceName }}</text>
          <text class="device-code">{{ deviceInfo.deviceCode }}</text>
        </view>
        <view class="qr-scan" @click="scanDeviceQR">
          <up-icon name="scan" size="20" color="#1890ff"></up-icon>
          <text class="scan-text">扫码</text>
        </view>
      </view>
      <view class="device-details">
        <view class="detail-item">
          <text class="label">位置:</text>
          <text class="value">{{ deviceInfo.location }}</text>
        </view>
        <view class="detail-item">
          <text class="label">巡检时间:</text>
          <text class="value">{{ deviceInfo.inspectionTime }}</text>
        </view>
        <view class="detail-item">
          <text class="label">负责人:</text>
          <text class="value">{{ deviceInfo.inspector }}</text>
        </view>
      </view>
    </view>
    <!-- å·¡æ£€é¡¹ç›®æ¸…单 -->
    <view class="inspection-items">
      <view class="section-title">
        <up-icon name="list" size="18" color="#333"></up-icon>
        <text class="title-text">巡检项目清单</text>
        <text class="progress-text">({{ completedItems }}/{{ totalItems }})</text>
      </view>
      <view class="items-list">
        <view
          v-for="(item, index) in inspectionItems"
          :key="index"
          class="inspection-item"
          :class="{ 'completed': item.completed, 'abnormal': item.isAbnormal }"
        >
          <view class="item-header" @click="toggleItem(index)">
            <view class="item-left">
              <view class="checkbox" :class="{ 'checked': item.completed }">
                <up-icon v-if="item.completed" name="checkmark" size="14" color="#ffffff"></up-icon>
              </view>
              <text class="item-name">{{ item.name }}</text>
            </view>
            <view class="item-status">
              <u-tag v-if="item.isAbnormal" type="error" size="mini">异常</u-tag>
              <u-tag v-else-if="item.completed" type="success" size="mini">正常</u-tag>
              <u-tag v-else type="info" size="mini">待检</u-tag>
            </view>
          </view>
          <!-- å±•开的详情内容 -->
          <view v-if="item.expanded" class="item-content">
            <view class="item-description">
              <text class="desc-text">{{ item.description }}</text>
            </view>
            <!-- å·¡æ£€ç»“果选择 -->
            <view class="result-section">
              <text class="section-label">巡检结果:</text>
              <view class="result-options">
                <u-radio-group v-model="item.result" @change="onResultChange(index, $event)">
                  <u-radio
                    v-for="option in resultOptions"
                    :key="option.value"
                    :label="option.value"
                    :name="option.label"
                    size="small"
                  >
                    {{ option.label }}
                  </u-radio>
                </u-radio-group>
              </view>
            </view>
            <!-- å¼‚常情况描述 -->
            <view v-if="item.result === 'abnormal'" class="abnormal-section">
              <text class="section-label">异常描述:</text>
              <up-textarea
                v-model="item.abnormalDesc"
                placeholder="请详细描述异常情况"
                :maxlength="200"
                count
                height="80"
              ></up-textarea>
            </view>
            <!-- å›¾ç‰‡ä¸Šä¼  -->
            <view class="upload-section">
              <text class="section-label">现场照片:</text>
              <up-upload
                :fileList="item.images"
                @afterRead="(event) => afterRead(event, index, 'images')"
                @delete="(event) => deleteFile(event, index, 'images')"
                name="images"
                multiple
                :maxCount="5"
                :previewImage="true"
              >
                <view class="upload-btn">
                  <up-icon name="camera" size="20" color="#999"></up-icon>
                  <text class="upload-text">添加照片</text>
                </view>
              </up-upload>
            </view>
            <!-- è§†é¢‘上传 -->
            <view class="upload-section">
              <text class="section-label">现场视频:</text>
              <up-upload
                :fileList="item.videos"
                @afterRead="(event) => afterRead(event, index, 'videos')"
                @delete="(event) => deleteFile(event, index, 'videos')"
                name="videos"
                :maxCount="2"
                accept="video"
              >
                <view class="upload-btn">
                  <up-icon name="play-circle" size="20" color="#999"></up-icon>
                  <text class="upload-text">添加视频</text>
                </view>
              </up-upload>
            </view>
            <!-- å¤‡æ³¨ -->
            <view class="remark-section">
              <text class="section-label">备注:</text>
              <up-textarea
                v-model="item.remark"
                placeholder="请输入备注信息(可选)"
                :maxlength="100"
                count
                height="60"
              ></up-textarea>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- åº•部操作按钮 -->
    <view class="bottom-actions">
      <u-button
        type="primary"
        size="large"
        :disabled="!canSubmit"
        @click="submitInspection"
        :loading="submitting"
      >
        {{ allCompleted ? '提交巡检记录' : `继续巡检 (${completedItems}/${totalItems})` }}
      </u-button>
    </view>
  </view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { submitInspectionRecord } from '@/api/equipmentManagement/inspection'
import dayjs from 'dayjs'
// è®¾å¤‡ä¿¡æ¯
const deviceInfo = ref({})
// å·¡æ£€é¡¹ç›®åˆ—表
const inspectionItems = ref([])
// æäº¤çŠ¶æ€
const submitting = ref(false)
// å·¡æ£€ç»“果选项
const resultOptions = [
  { label: '正常', value: 'normal' },
  { label: '异常', value: 'abnormal' }
]
// æ˜¾ç¤ºæç¤ºä¿¡æ¯
const showToast = (message) => {
  uni.showToast({
    title: message,
    icon: 'none'
  })
}
// è®¡ç®—属性
const totalItems = computed(() => inspectionItems.value.length)
const completedItems = computed(() => inspectionItems.value.filter(item => item.completed).length)
const allCompleted = computed(() => completedItems.value === totalItems.value && totalItems.value > 0)
const canSubmit = computed(() => completedItems.value > 0)
// è¿”回上一页
const goBack = () => {
  if (completedItems.value > 0) {
    uni.showModal({
      title: '提示',
      content: '当前有未保存的巡检记录,确定要离开吗?',
      success: (res) => {
        if (res.confirm) {
          uni.navigateBack()
        }
      }
    })
  } else {
    uni.navigateBack()
  }
}
// æ‰«æè®¾å¤‡äºŒç»´ç 
const scanDeviceQR = () => {
  uni.scanCode({
    success: (res) => {
      console.log('扫码结果:', res)
      if (res.result.includes(deviceInfo.value.deviceCode)) {
        showToast('设备确认成功')
        // è®°å½•扫码时间
        deviceInfo.value.scanTime = new Date().toISOString()
      } else {
        showToast('设备二维码不匹配')
      }
    },
    fail: (err) => {
      console.log('扫码失败:', err)
      showToast('扫码失败')
    }
  })
}
// åˆ‡æ¢å·¡æ£€é¡¹ç›®
const toggleItem = (index) => {
  inspectionItems.value[index].expanded = !inspectionItems.value[index].expanded
}
// å·¡æ£€ç»“果改变
const onResultChange = (index, value) => {
  const item = inspectionItems.value[index]
  item.result = value
  item.completed = true
  item.isAbnormal = value === 'abnormal'
  // å¦‚果选择正常,清空异常描述
  if (value === 'normal') {
    item.abnormalDesc = ''
  }
}
// æ–‡ä»¶ä¸Šä¼ åŽå¤„理
const afterRead = async (event, index, type) => {
  const { file } = event
  const item = inspectionItems.value[index]
  // æ¨¡æ‹Ÿä¸Šä¼ è¿‡ç¨‹
  uni.showLoading({ title: '上传中...' })
  try {
    // è¿™é‡Œåº”该调用实际的上传API
    await new Promise(resolve => setTimeout(resolve, 1000))
    // æ·»åŠ åˆ°å¯¹åº”çš„æ–‡ä»¶åˆ—è¡¨
    if (type === 'images') {
      item.images = item.images || []
      item.images.push({
        url: file.url,
        name: file.name,
        size: file.size
      })
    } else if (type === 'videos') {
      item.videos = item.videos || []
      item.videos.push({
        url: file.url,
        name: file.name,
        size: file.size
      })
    }
    uni.hideLoading()
    showToast('上传成功')
  } catch (error) {
    uni.hideLoading()
    showToast('上传失败')
  }
}
// åˆ é™¤æ–‡ä»¶
const deleteFile = (event, index, type) => {
  const item = inspectionItems.value[index]
  if (type === 'images') {
    item.images.splice(event.index, 1)
  } else if (type === 'videos') {
    item.videos.splice(event.index, 1)
  }
}
// æäº¤å·¡æ£€è®°å½•
const submitInspection = async () => {
  if (!canSubmit.value) {
    showToast('请至少完成一项巡检')
    return
  }
  // æ£€æŸ¥å¼‚常项目是否填写了描述
  const abnormalItems = inspectionItems.value.filter(item => item.isAbnormal)
  for (const item of abnormalItems) {
    if (!item.abnormalDesc || item.abnormalDesc.trim() === '') {
      showToast(`请填写"${item.name}"的异常描述`)
      return
    }
  }
  submitting.value = true
  try {
    const recordData = {
      deviceId: deviceInfo.value.id,
      deviceCode: deviceInfo.value.deviceCode,
      inspectionDate: dayjs().format('YYYY-MM-DD'),
      inspector: deviceInfo.value.inspector,
      scanTime: deviceInfo.value.scanTime,
      items: inspectionItems.value.map(item => ({
        name: item.name,
        result: item.result,
        completed: item.completed,
        isAbnormal: item.isAbnormal,
        abnormalDesc: item.abnormalDesc,
        images: item.images || [],
        videos: item.videos || [],
        remark: item.remark
      })),
      completedAt: new Date().toISOString()
    }
    // æ¨¡æ‹ŸAPI调用
    await new Promise(resolve => setTimeout(resolve, 2000))
    // å®žé™…API调用
    // await submitInspectionRecord(recordData)
    showToast('巡检记录提交成功')
    // è¿”回列表页面
    setTimeout(() => {
      uni.navigateBack()
    }, 1500)
  } catch (error) {
    showToast('提交失败,请重试')
  } finally {
    submitting.value = false
  }
}
// åˆå§‹åŒ–数据
const initData = () => {
  // ä»Žå­˜å‚¨ä¸­èŽ·å–å½“å‰å·¡æ£€ä¿¡æ¯
  const currentInspection = uni.getStorageSync('currentInspection')
  if (currentInspection) {
    deviceInfo.value = currentInspection
  }
  // æ¨¡æ‹Ÿå·¡æ£€é¡¹ç›®æ•°æ®
  inspectionItems.value = [
    {
      name: '设备外观检查',
      description: '检查设备外观是否有损坏、锈蚀、变形等异常情况',
      completed: false,
      expanded: false,
      result: '',
      isAbnormal: false,
      abnormalDesc: '',
      images: [],
      videos: [],
      remark: ''
    },
    {
      name: '运行状态检查',
      description: '检查设备运行是否正常,有无异常声音、振动等',
      completed: false,
      expanded: false,
      result: '',
      isAbnormal: false,
      abnormalDesc: '',
      images: [],
      videos: [],
      remark: ''
    },
    {
      name: '安全装置检查',
      description: '检查各类安全装置是否完好,安全标识是否清晰',
      completed: false,
      expanded: false,
      result: '',
      isAbnormal: false,
      abnormalDesc: '',
      images: [],
      videos: [],
      remark: ''
    },
    {
      name: '环境条件检查',
      description: '检查设备周围环境是否符合要求,通风、照明等是否正常',
      completed: false,
      expanded: false,
      result: '',
      isAbnormal: false,
      abnormalDesc: '',
      images: [],
      videos: [],
      remark: ''
    },
    {
      name: '仪表读数记录',
      description: '记录相关仪表的读数,检查是否在正常范围内',
      completed: false,
      expanded: false,
      result: '',
      isAbnormal: false,
      abnormalDesc: '',
      images: [],
      videos: [],
      remark: ''
    }
  ]
}
onMounted(() => {
  initData()
})
onShow(() => {
  // é¡µé¢æ˜¾ç¤ºæ—¶åˆ·æ–°æ•°æ®
})
</script>
<style scoped lang="scss">
.inspection-detail {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding-bottom: 80px;
  position: relative;
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 200px;
    background: linear-gradient(135deg, rgba(102, 126, 234, 0.8) 0%, rgba(118, 75, 162, 0.8) 100%);
    z-index: 0;
  }
}
.device-card {
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(15px);
  margin: 10px 20px;
  border-radius: 20px;
  padding: 24px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
  position: relative;
  z-index: 1;
  transition: all 0.3s ease;
  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
  }
}
.device-header {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 20px;
}
.device-icon {
  width: 56px;
  height: 56px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  border-radius: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
  transition: all 0.3s ease;
  &:hover {
    transform: scale(1.05);
  }
}
.device-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.device-name {
  font-size: 20px;
  font-weight: 600;
  color: #1a1a1a;
  line-height: 1.3;
}
.device-code {
  font-size: 13px;
  color: #8c8c8c;
  font-weight: 500;
  padding: 4px 12px;
  background: rgba(140, 140, 140, 0.1);
  border-radius: 12px;
  display: inline-block;
  width: fit-content;
}
.qr-scan {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 12px 16px;
  background: linear-gradient(135deg, #52c41a, #389e0d);
  border-radius: 12px;
  box-shadow: 0 4px 15px rgba(82, 196, 26, 0.3);
  transition: all 0.3s ease;
  &:hover {
    transform: scale(1.05);
    box-shadow: 0 6px 20px rgba(82, 196, 26, 0.4);
  }
  &:active {
    transform: scale(0.98);
  }
}
.scan-text {
  font-size: 13px;
  color: #ffffff;
  font-weight: 600;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.device-details {
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: rgba(248, 250, 252, 0.8);
  border-radius: 16px;
  padding: 16px;
  backdrop-filter: blur(10px);
}
.detail-item {
  display: flex;
  align-items: center;
  font-size: 14px;
  padding: 8px 0;
  transition: all 0.2s ease;
  &:hover {
    background: rgba(255, 255, 255, 0.5);
    margin: 0 -8px;
    padding-left: 8px;
    padding-right: 8px;
    border-radius: 8px;
  }
}
.label {
  color: #595959;
  min-width: 80px;
  font-weight: 500;
}
.value {
  color: #262626;
  flex: 1;
  font-weight: 500;
}
.inspection-items {
  margin: 10px 20px;
  position: relative;
  z-index: 1;
}
.section-title {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 20px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(15px);
  border-radius: 16px;
  padding: 20px;
  margin-bottom: 16px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.title-text {
  font-size: 18px;
  font-weight: 600;
  color: #1a1a1a;
  flex: 1;
}
.progress-text {
  font-size: 15px;
  font-weight: 600;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.items-list {
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(15px);
  border-radius: 20px;
  overflow: hidden;
  margin-top: 0;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
}
.inspection-item {
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  transition: all 0.3s ease;
  &:last-child {
    border-bottom: none;
  }
  &.completed {
    background: rgba(82, 196, 26, 0.05);
    border-left: 4px solid #52c41a;
  }
  &.abnormal {
    background: rgba(255, 77, 79, 0.05);
    border-left: 4px solid #ff4d4f;
  }
  &:hover {
    background: rgba(102, 126, 234, 0.05);
  }
}
.item-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px;
  cursor: pointer;
}
.item-left {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
}
.checkbox {
  width: 20px;
  height: 20px;
  border: 2px solid #d9d9d9;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
  &.checked {
    background: #52c41a;
    border-color: #52c41a;
  }
}
.item-name {
  font-size: 15px;
  color: #333;
  font-weight: 500;
}
.item-status {
  flex-shrink: 0;
}
.item-content {
  padding: 0 15px 20px;
  border-top: 1px solid #f5f5f5;
}
.item-description {
  padding: 15px 0;
}
.desc-text {
  font-size: 14px;
  color: #666;
  line-height: 1.5;
}
.result-section,
.abnormal-section,
.upload-section,
.remark-section {
  margin-top: 15px;
}
.section-label {
  display: block;
  font-size: 14px;
  color: #333;
  margin-bottom: 8px;
  font-weight: 500;
}
.result-options {
  display: flex;
  gap: 20px;
}
.upload-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 88px;
  height: 88px;
  border: 2px dashed rgba(102, 126, 234, 0.3);
  border-radius: 16px;
  background: rgba(102, 126, 234, 0.05);
  gap: 8px;
  transition: all 0.3s ease;
  &:hover {
    border-color: rgba(102, 126, 234, 0.5);
    background: rgba(102, 126, 234, 0.1);
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
  }
  &:active {
    transform: translateY(0);
  }
}
.upload-text {
  font-size: 13px;
  color: #667eea;
  font-weight: 500;
}
.bottom-actions {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  padding: 20px;
  border-top: 1px solid rgba(255, 255, 255, 0.2);
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.1);
  z-index: 10;
  button {
    height: 48px;
    border-radius: 16px;
    font-weight: 600;
    font-size: 16px;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    &:hover {
      transform: translateY(-2px);
      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
    }
    &:active {
      transform: translateY(0);
    }
  }
}
</style>
src/pages/equipmentManagement/inspection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,526 @@
<template>
  <view class="inspection-page">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="设备巡检" @back="goBack" />
    <!-- ç»Ÿè®¡ä¿¡æ¯å¡ç‰‡ -->
    <view class="stats-cards">
      <view class="stat-card">
        <text class="stat-number">{{ totalCount }}</text>
        <text class="stat-label">总任务</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ completedCount }}</text>
        <text class="stat-label">已完成</text>
      </view>
      <view class="stat-card">
        <text class="stat-number">{{ pendingCount }}</text>
        <text class="stat-label">待巡检</text>
      </view>
    </view>
    <!-- å·¡æ£€æ¸…单 -->
    <view class="inspection-list" v-if="inspectionList.length > 0">
      <view v-for="(item, index) in inspectionList" :key="index">
        <view class="inspection-item" @click="startInspection(item)">
          <view class="item-header">
            <view class="item-left">
              <view class="device-icon" :class="getStatusClass(item.status)">
                <up-icon :name="getStatusIcon(item.status)" size="16" color="#ffffff"></up-icon>
              </view>
              <view class="device-info">
                <text class="device-name">{{ item.deviceName }}</text>
                <text class="device-location">{{ item.location }}</text>
              </view>
            </view>
            <view class="status-tag">
              <u-tag :type="getTagType(item.status)" size="mini">
                {{ getStatusText(item.status) }}
              </u-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">设备编号</text>
              <text class="detail-value">{{ item.deviceCode || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">巡检时间</text>
              <text class="detail-value">{{ item.inspectionTime || '-' }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">负责人</text>
              <text class="detail-value">{{ item.inspector || '-' }}</text>
            </view>
            <view class="detail-row" v-if="item.status === 2">
              <text class="detail-label">完成时间</text>
              <text class="detail-value">{{ formatDateTime(item.completedTime) || '-' }}</text>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <u-button
              type="primary"
              size="small"
              class="action-btn"
              :disabled="item.status === 2"
              @click.stop="startInspection(item)"
            >
              {{ item.status === 0 ? '开始巡检' : item.status === 1 ? '继续巡检' : '查看详情' }}
            </u-button>
            <u-button
              type="success"
              size="small"
              class="action-btn"
              :disabled="item.status !== 1"
              @click.stop="scanQRCode(item)"
            >
              æ‰«ç æ‰“卡
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无巡检任务"></up-empty>
    </view>
  </view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { getInspectionList } from '@/api/equipmentManagement/inspection'
import dayjs from 'dayjs'
// é€‰ä¸­çš„æ—¥æœŸ
const selectedDate = ref(Date.now())
// å·¡æ£€æ¸…单数据
const inspectionList = ref([])
// æ˜¾ç¤ºæç¤ºä¿¡æ¯
const showToast = (message) => {
  uni.showToast({
    title: message,
    icon: 'none'
  })
}
// è®¡ç®—统计数据
const totalCount = computed(() => inspectionList.value.length)
const completedCount = computed(() => inspectionList.value.filter(item => item.status === 2).length)
const pendingCount = computed(() => inspectionList.value.filter(item => item.status === 0).length)
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
// æ—¥æœŸæ ¼å¼åŒ–器
const dateFormatter = (type, value) => {
  if (type === 'year') {
    return `${value}å¹´`
  }
  if (type === 'month') {
    return `${value}月`
  }
  if (type === 'day') {
    return `${value}日`
  }
  return value
}
// æ ¼å¼åŒ–日期
const formatDate = (timestamp) => {
  return dayjs(timestamp).format('YYYYå¹´MM月DD日')
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateStr) => {
  if (!dateStr) return ''
  return dayjs(dateStr).format('MM-DD HH:mm')
}
// æ—¥æœŸæ”¹å˜äº‹ä»¶
const onDateChange = (value) => {
  selectedDate.value = value.value
  getList()
}
// èŽ·å–çŠ¶æ€æ ·å¼ç±»
const getStatusClass = (status) => {
  switch (status) {
    case 0: return 'status-pending'
    case 1: return 'status-progress'
    case 2: return 'status-completed'
    default: return 'status-pending'
  }
}
// èŽ·å–çŠ¶æ€å›¾æ ‡
const getStatusIcon = (status) => {
  switch (status) {
    case 0: return 'clock'
    case 1: return 'play-circle'
    case 2: return 'checkmark-circle'
    default: return 'clock'
  }
}
// èŽ·å–æ ‡ç­¾ç±»åž‹
const getTagType = (status) => {
  switch (status) {
    case 0: return 'warning'
    case 1: return 'primary'
    case 2: return 'success'
    default: return 'info'
  }
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  switch (status) {
    case 0: return '待巡检'
    case 1: return '巡检中'
    case 2: return '已完成'
    default: return '未知'
  }
}
// å¼€å§‹å·¡æ£€
const startInspection = (item) => {
  // å­˜å‚¨å½“前巡检项目信息
  uni.setStorageSync('currentInspection', item)
  uni.navigateTo({
    url: '/pages/equipmentManagement/inspection/detail'
  })
}
// æ‰«ç æ‰“卡
const scanQRCode = (item) => {
  uni.scanCode({
    success: (res) => {
      console.log('扫码结果:', res)
      // éªŒè¯äºŒç»´ç å†…容
      if (res.result.includes(item.deviceCode)) {
        showToast('打卡成功')
        // æ›´æ–°æ‰“卡状态
        updateCheckInStatus(item.id)
      } else {
        showToast('二维码不匹配,请扫描正确的设备二维码')
      }
    },
    fail: (err) => {
      console.log('扫码失败:', err)
      showToast('扫码失败')
    }
  })
}
// æ›´æ–°æ‰“卡状态
const updateCheckInStatus = (id) => {
  // è¿™é‡Œåº”该调用API更新打卡状态
  // æš‚时模拟更新本地数据
  const item = inspectionList.value.find(item => item.id === id)
  if (item) {
    item.checkInTime = new Date().toISOString()
  }
}
// æŸ¥è¯¢å·¡æ£€æ¸…单
const getList = () => {
  uni.showLoading({
    title: '加载中...',
    mask: true
  })
  const params = {
    date: dayjs(selectedDate.value).format('YYYY-MM-DD')
  }
  // æ¨¡æ‹Ÿæ•°æ®ï¼Œå®žé™…应该调用API
  setTimeout(() => {
    inspectionList.value = [
      {
        id: 1,
        deviceName: '空压机A01',
        deviceCode: 'KYJ-A01',
        location: '生产车间A区',
        inspectionTime: '08:00-09:00',
        inspector: '张三',
        status: 0, // 0:待巡检 1:巡检中 2:已完成
        completedTime: null
      },
      {
        id: 2,
        deviceName: '冷却塔B02',
        deviceCode: 'LQT-B02',
        location: '生产车间B区',
        inspectionTime: '09:00-10:00',
        inspector: '李四',
        status: 1,
        completedTime: null
      },
      {
        id: 3,
        deviceName: '变压器C03',
        deviceCode: 'BYQ-C03',
        location: '配电房',
        inspectionTime: '10:00-11:00',
        inspector: '王五',
        status: 2,
        completedTime: '2024-01-15T10:30:00'
      }
    ]
    uni.hideLoading()
  }, 1000)
  // å®žé™…API调用
  // getInspectionList(params)
  //   .then((res) => {
  //     inspectionList.value = res.records || res.data?.records || []
  //     uni.hideLoading()
  //   })
  //   .catch(() => {
  //     uni.hideLoading()
  //     showToast('获取数据失败')
  //   })
}
onMounted(() => {
  getList()
})
onShow(() => {
  getList()
})
</script>
<style scoped lang="scss">
@import '@/styles/sales-common.scss';
.inspection-page {
  background: #ffffff;
  min-height: 100vh;
  padding-bottom: 20px;
}
.stats-cards {
  display: flex;
  gap: 10px;
  padding: 0 15px;
  margin: 15px 0;
}
.stat-card {
  flex: 1;
  background: #ffffff;
  border-radius: 12px;
  padding: 15px 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  border: 1px solid #f0f0f0;
  transition: all 0.3s ease;
  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }
  .stat-number {
    font-size: 24px;
    font-weight: 700;
    color: #1890ff;
  }
  .stat-label {
    font-size: 13px;
    color: #666;
    font-weight: 500;
  }
}
.inspection-list {
  padding: 0 15px;
}
.inspection-item {
  background: #ffffff;
  border-radius: 12px;
  padding: 15px;
  margin-bottom: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  border: 1px solid #f0f0f0;
  transition: all 0.3s ease;
  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 12px;
}
.item-left {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex: 1;
}
.device-icon {
  width: 48px;
  height: 48px;
  border-radius: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
  &.status-pending {
    background: linear-gradient(135deg, #faad14, #fa8c16);
    box-shadow: 0 4px 16px rgba(250, 173, 20, 0.3);
  }
  &.status-progress {
    background: linear-gradient(135deg, #1890ff, #096dd9);
    box-shadow: 0 4px 16px rgba(24, 144, 255, 0.3);
  }
  &.status-completed {
    background: linear-gradient(135deg, #52c41a, #389e0d);
    box-shadow: 0 4px 16px rgba(82, 196, 26, 0.3);
  }
  &:hover {
    transform: scale(1.05);
  }
}
.device-info {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.device-name {
  font-size: 18px;
  font-weight: 600;
  color: #1a1a1a;
  line-height: 1.3;
}
.device-location {
  font-size: 13px;
  color: #8c8c8c;
  font-weight: 500;
  padding: 2px 8px;
  background: rgba(140, 140, 140, 0.1);
  border-radius: 12px;
  display: inline-block;
  width: fit-content;
}
.status-tag {
  flex-shrink: 0;
  transform: scale(1.1);
}
.item-details {
  margin: 15px 0;
  background: rgba(248, 250, 252, 0.8);
  border-radius: 12px;
  padding: 12px;
  backdrop-filter: blur(10px);
}
.detail-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  transition: all 0.2s ease;
  &:last-child {
    border-bottom: none;
    padding-bottom: 0;
  }
  &:hover {
    background: rgba(255, 255, 255, 0.5);
    margin: 0 -8px;
    padding-left: 8px;
    padding-right: 8px;
    border-radius: 8px;
  }
}
.detail-label {
  font-size: 14px;
  color: #595959;
  min-width: 80px;
  font-weight: 500;
}
.detail-value {
  font-size: 14px;
  color: #262626;
  text-align: right;
  flex: 1;
  font-weight: 500;
}
.action-buttons {
  display: flex;
  gap: 10px;
  margin-top: 15px;
  padding-top: 15px;
  border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.action-btn {
  flex: 1;
  height: 44px;
  border-radius: 12px;
  font-weight: 600;
  font-size: 14px;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
  }
  &:active {
    transform: translateY(0);
  }
}
.no-data {
  padding: 80px 20px;
  text-align: center;
}
</style>
src/pages/equipmentManagement/smartDispatch/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,801 @@
<template>
  <view class="sales-account">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="智能派单" @back="goBack" />
    <!-- ç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input
            class="search-text"
            placeholder="请输入设备名称搜索"
            v-model="searchKeyword"
            @change="filterTasks"
            clearable
          />
        </view>
        <view class="filter-button" @click="filterTasks">
          <up-icon name="search" size="24" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- ç»Ÿè®¡ä¿¡æ¯åŒºåŸŸ -->
    <view class="summary-info">
      <view class="summary-item">
        <text class="summary-label">待派单任务</text>
        <text class="summary-value highlight">{{ pendingTasks.length }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">维修组数量</text>
        <text class="summary-value">{{ repairTeams.length }}</text>
      </view>
      <view class="summary-item">
        <text class="summary-label">今日已派单</text>
        <text class="summary-value">{{ todayDispatchedCount }}</text>
      </view>
    </view>
    <!-- å¾…派单任务列表 -->
    <view class="ledger-list" v-if="filteredTasks.length > 0">
      <view v-for="(task, index) in filteredTasks" :key="task.id">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ task.deviceName }}</text>
            </view>
            <view class="item-right">
              <view class="item-tag" :class="getDeviceTypeClass(task.deviceType)">
                <text class="tag-text">{{ task.deviceType }}</text>
              </view>
              <view class="priority-tag" :class="getPriorityClass(task.priority)">
                <text class="tag-text">{{ getPriorityText(task.priority) }}</text>
              </view>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">设备编号</text>
              <text class="detail-value">{{ task.deviceCode }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">故障描述</text>
              <text class="detail-value">{{ task.faultDescription }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">报修人</text>
              <text class="detail-value">{{ task.reporterName }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">报修时间</text>
              <text class="detail-value">{{ formatDate(task.reportTime) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">预估工时</text>
              <text class="detail-value highlight">{{ task.estimatedHours }}小时</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">所需技能</text>
              <text class="detail-value">{{ task.requiredSkills.join('、') }}</text>
            </view>
            <!-- æŽ¨èç»´ä¿®ç»„ -->
            <view class="recommended-team" v-if="task.recommendedTeam">
              <view class="team-header">
                <text class="team-title">推荐维修组</text>
                <view class="match-score">
                  <text class="score-text">匹配度: {{ task.recommendedTeam.matchScore }}%</text>
                </view>
              </view>
              <view class="team-info">
                <view class="detail-row">
                  <text class="detail-label">组名</text>
                  <text class="detail-value">{{ task.recommendedTeam.teamName }}</text>
                </view>
                <view class="detail-row">
                  <text class="detail-label">负责人</text>
                  <text class="detail-value">{{ task.recommendedTeam.leaderName }}</text>
                </view>
                <view class="detail-row">
                  <text class="detail-label">当前负载</text>
                  <text class="detail-value" :class="getWorkloadClass(task.recommendedTeam.currentWorkload)">
                    {{ task.recommendedTeam.currentWorkload }}%
                  </text>
                </view>
                <view class="detail-row">
                  <text class="detail-label">技能匹配</text>
                  <text class="detail-value">{{ task.recommendedTeam.matchedSkills.join('、') }}</text>
                </view>
              </view>
            </view>
          </view>
          <!-- æ“ä½œæŒ‰é’® -->
          <view class="action-buttons">
            <u-button
              type="success"
              size="small"
              class="action-btn"
              @click="confirmDispatch(task)"
              :disabled="!task.recommendedTeam"
            >
              ç¡®è®¤æ´¾å•
            </u-button>
            <u-button
              type="primary"
              size="small"
              plain
              class="action-btn"
              @click="manualDispatch(task)"
            >
              æ‰‹åŠ¨æ´¾å•
            </u-button>
            <u-button
              type="warning"
              size="small"
              plain
              class="action-btn"
              @click="reAnalyze(task)"
            >
              é‡æ–°åˆ†æž
            </u-button>
          </view>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无待派单任务</text>
    </view>
    <!-- æ‰‹åŠ¨æ´¾å•å¼¹çª— -->
    <up-popup v-model:show="showManualDispatchPopup" mode="center" border-radius="20">
      <view class="manual-dispatch-popup">
        <view class="popup-header">
          <text class="popup-title">手动派单</text>
        </view>
        <view class="team-list">
          <view
            v-for="team in availableTeams"
            :key="team.id"
            class="team-option"
            :class="{ active: selectedTeamId === team.id }"
            @click="selectedTeamId = team.id"
          >
            <view class="team-info">
              <text class="team-name">{{ team.teamName }}</text>
              <text class="team-leader">负责人: {{ team.leaderName }}</text>
              <text class="team-workload" :class="getWorkloadClass(team.currentWorkload)">
                è´Ÿè½½: {{ team.currentWorkload }}%
              </text>
            </view>
            <view class="team-skills">
              <text class="skills-label">技能:</text>
              <text class="skills-text">{{ team.skills.join('、') }}</text>
            </view>
          </view>
        </view>
        <view class="popup-actions">
          <u-button type="info" plain @click="showManualDispatchPopup = false">取消</u-button>
          <u-button type="primary" @click="confirmManualDispatch" :disabled="!selectedTeamId">确认派单</u-button>
        </view>
      </view>
    </up-popup>
    <!-- æµ®åŠ¨æ“ä½œæŒ‰é’® -->
    <view class="fab-button" @click="autoDispatchAll">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
const showToast = (message) => {
  uni.showToast({
    title: message,
    icon: 'none'
  })
}
// æœç´¢å…³é”®è¯
const searchKeyword = ref('')
// æ‰‹åŠ¨æ´¾å•ç›¸å…³
const showManualDispatchPopup = ref(false)
const selectedTask = ref(null)
const selectedTeamId = ref(null)
// ä»Šæ—¥å·²æ´¾å•数量
const todayDispatchedCount = ref(12)
// å¾…派单任务数据
const pendingTasks = ref([
  {
    id: 1,
    deviceName: '数控车床CK6140',
    deviceCode: 'CNC-001',
    deviceType: '机械设备',
    faultDescription: '主轴异响,切削精度下降',
    reporterName: '张师傅',
    reportTime: '2024-01-15 09:30:00',
    priority: 1,
    estimatedHours: 4,
    requiredSkills: ['机械维修', '数控技术'],
    recommendedTeam: {
      id: 1,
      teamName: '机械维修一组',
      leaderName: '李工程师',
      currentWorkload: 65,
      matchScore: 95,
      matchedSkills: ['机械维修', '数控技术']
    }
  },
  {
    id: 2,
    deviceName: '变频器ABB-ACS800',
    deviceCode: 'INV-002',
    deviceType: '电气设备',
    faultDescription: '频繁报警,输出电压不稳定',
    reporterName: '王技术员',
    reportTime: '2024-01-15 10:15:00',
    priority: 2,
    estimatedHours: 3,
    requiredSkills: ['电气维修', '变频器技术'],
    recommendedTeam: {
      id: 2,
      teamName: '电气维修组',
      leaderName: '赵工程师',
      currentWorkload: 45,
      matchScore: 88,
      matchedSkills: ['电气维修', '变频器技术']
    }
  },
  {
    id: 3,
    deviceName: '压力传感器PT100',
    deviceCode: 'SEN-003',
    deviceType: '仪表设备',
    faultDescription: '读数异常,零点漂移严重',
    reporterName: '陈操作员',
    reportTime: '2024-01-15 11:00:00',
    priority: 3,
    estimatedHours: 2,
    requiredSkills: ['仪表维修', '传感器技术'],
    recommendedTeam: {
      id: 3,
      teamName: '仪表维修组',
      leaderName: '孙工程师',
      currentWorkload: 30,
      matchScore: 92,
      matchedSkills: ['仪表维修', '传感器技术']
    }
  },
  {
    id: 4,
    deviceName: '工控机IPC-610',
    deviceCode: 'PC-004',
    deviceType: '计算机设备',
    faultDescription: '系统频繁死机,硬盘故障',
    reporterName: '刘程序员',
    reportTime: '2024-01-15 13:45:00',
    priority: 1,
    estimatedHours: 5,
    requiredSkills: ['计算机维修', '系统维护'],
    recommendedTeam: {
      id: 4,
      teamName: 'IT维修组',
      leaderName: '周工程师',
      currentWorkload: 80,
      matchScore: 90,
      matchedSkills: ['计算机维修', '系统维护']
    }
  },
  {
    id: 5,
    deviceName: '叉车TCM-FD30',
    deviceCode: 'VEH-005',
    deviceType: '车辆设备',
    faultDescription: '液压系统漏油,升降无力',
    reporterName: '马司机',
    reportTime: '2024-01-15 14:20:00',
    priority: 2,
    estimatedHours: 6,
    requiredSkills: ['车辆维修', '液压技术'],
    recommendedTeam: {
      id: 5,
      teamName: '车辆维修组',
      leaderName: '吴师傅',
      currentWorkload: 55,
      matchScore: 85,
      matchedSkills: ['车辆维修', '液压技术']
    }
  }
])
// ç»´ä¿®ç»„数据
const repairTeams = ref([
  {
    id: 1,
    teamName: '机械维修一组',
    leaderName: '李工程师',
    currentWorkload: 65,
    skills: ['机械维修', '数控技术', '焊接技术']
  },
  {
    id: 2,
    teamName: '电气维修组',
    leaderName: '赵工程师',
    currentWorkload: 45,
    skills: ['电气维修', '变频器技术', 'PLC编程']
  },
  {
    id: 3,
    teamName: '仪表维修组',
    leaderName: '孙工程师',
    currentWorkload: 30,
    skills: ['仪表维修', '传感器技术', '自动化控制']
  },
  {
    id: 4,
    teamName: 'IT维修组',
    leaderName: '周工程师',
    currentWorkload: 80,
    skills: ['计算机维修', '系统维护', '网络技术']
  },
  {
    id: 5,
    teamName: '车辆维修组',
    leaderName: '吴师傅',
    currentWorkload: 55,
    skills: ['车辆维修', '液压技术', '发动机维修']
  },
  {
    id: 6,
    teamName: '机械维修二组',
    leaderName: '钱工程师',
    currentWorkload: 40,
    skills: ['机械维修', '精密加工', '设备调试']
  }
])
// è®¡ç®—可用维修组(排除当前推荐的组)
const availableTeams = computed(() => {
  if (!selectedTask.value) return repairTeams.value
  return repairTeams.value.filter(team =>
    !selectedTask.value.recommendedTeam || team.id !== selectedTask.value.recommendedTeam.id
  )
})
// ç­›é€‰åŽçš„任务列表
const filteredTasks = computed(() => {
  let tasks = pendingTasks.value
  // æœç´¢å…³é”®è¯ç­›é€‰
  if (searchKeyword.value) {
    tasks = tasks.filter(task =>
      task.deviceName.includes(searchKeyword.value) ||
      task.deviceCode.includes(searchKeyword.value) ||
      task.faultDescription.includes(searchKeyword.value)
    )
  }
  return tasks
})
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
// æ ¼å¼åŒ–日期
const formatDate = (dateStr) => {
  if (!dateStr) return ''
  const date = new Date(dateStr)
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  const hours = String(date.getHours()).padStart(2, '0')
  const minutes = String(date.getMinutes()).padStart(2, '0')
  return `${month}-${day} ${hours}:${minutes}`
}
// èŽ·å–è®¾å¤‡ç±»åž‹æ ·å¼ç±»
const getDeviceTypeClass = (deviceType) => {
  const typeMap = {
    '机械设备': 'tag-mechanical',
    '电气设备': 'tag-electric',
    '仪表设备': 'tag-instrument',
    '计算机设备': 'tag-computer',
    '车辆设备': 'tag-vehicle',
    '其他设备': 'tag-other'
  }
  return typeMap[deviceType] || 'tag-unknown'
}
// èŽ·å–ä¼˜å…ˆçº§æ ·å¼ç±»
const getPriorityClass = (priority) => {
  const priorityMap = {
    1: 'priority-urgent',
    2: 'priority-high',
    3: 'priority-medium',
    4: 'priority-low'
  }
  return priorityMap[priority] || 'priority-medium'
}
// èŽ·å–ä¼˜å…ˆçº§æ–‡æœ¬
const getPriorityText = (priority) => {
  const priorityMap = {
    1: '紧急',
    2: '高',
    3: '中',
    4: '低'
  }
  return priorityMap[priority] || '中'
}
// èŽ·å–å·¥ä½œè´Ÿè½½æ ·å¼ç±»
const getWorkloadClass = (workload) => {
  if (workload >= 80) return 'danger'
  if (workload >= 60) return 'highlight'
  return ''
}
// ç­›é€‰ä»»åŠ¡
const filterTasks = () => {
  // æœç´¢é€»è¾‘已在计算属性filteredTasks中处理
  // å½“searchKeyword变化时,计算属性会自动重新计算
  // è¿™é‡Œå¯ä»¥æ·»åŠ é¢å¤–çš„ç­›é€‰é€»è¾‘æˆ–æç¤º
  if (searchKeyword.value && filteredTasks.value.length === 0) {
    // å¦‚果有搜索关键词但没有结果,可以给用户提示
    console.log('未找到匹配的设备任务')
  }
}
// ç¡®è®¤æ´¾å•
const confirmDispatch = (task) => {
  uni.showModal({
    title: '确认派单',
    content: `确认将任务"${task.deviceName}"派给"${task.recommendedTeam.teamName}"?`,
    confirmText: '确认',
    cancelText: '取消',
    success: (res) => {
      if (res.confirm) {
        // æ¨¡æ‹Ÿæ´¾å•成功
        const index = pendingTasks.value.findIndex(t => t.id === task.id)
        if (index > -1) {
          pendingTasks.value.splice(index, 1)
          todayDispatchedCount.value++
          showToast('派单成功')
        }
      }
    }
  })
}
// æ‰‹åŠ¨æ´¾å•
const manualDispatch = (task) => {
  selectedTask.value = task
  selectedTeamId.value = null
  showManualDispatchPopup.value = true
}
// ç¡®è®¤æ‰‹åŠ¨æ´¾å•
const confirmManualDispatch = () => {
  if (!selectedTeamId.value) {
    showToast('请选择维修组')
    return
  }
  const selectedTeam = repairTeams.value.find(team => team.id === selectedTeamId.value)
  if (!selectedTeam) {
    showToast('维修组不存在')
    return
  }
  // å…ˆå…³é—­æ‰‹åŠ¨æ´¾å•å¼¹æ¡†
  showManualDispatchPopup.value = false
  uni.showModal({
    title: '确认派单',
    content: `确认将任务"${selectedTask.value.deviceName}"派给"${selectedTeam.teamName}"?`,
    confirmText: '确认',
    cancelText: '取消',
    success: (res) => {
      if (res.confirm) {
        // æ¨¡æ‹Ÿæ´¾å•成功
        const index = pendingTasks.value.findIndex(t => t.id === selectedTask.value.id)
        if (index > -1) {
          pendingTasks.value.splice(index, 1)
          todayDispatchedCount.value++
          showToast('派单成功')
        }
      }
    }
  })
}
// é‡æ–°åˆ†æž
const reAnalyze = (task) => {
  showToast('正在重新分析...')
  // æ¨¡æ‹Ÿé‡æ–°åˆ†æžè¿‡ç¨‹
  setTimeout(() => {
    // éšæœºè°ƒæ•´åŒ¹é…åº¦å’ŒæŽ¨èç»„
    const teams = repairTeams.value.filter(team =>
      team.skills.some(skill => task.requiredSkills.includes(skill))
    )
    if (teams.length > 0) {
      const randomTeam = teams[Math.floor(Math.random() * teams.length)]
      const matchedSkills = randomTeam.skills.filter(skill => task.requiredSkills.includes(skill))
      const matchScore = Math.floor(Math.random() * 20) + 80 // 80-99的随机匹配度
      task.recommendedTeam = {
        id: randomTeam.id,
        teamName: randomTeam.teamName,
        leaderName: randomTeam.leaderName,
        currentWorkload: randomTeam.currentWorkload,
        matchScore: matchScore,
        matchedSkills: matchedSkills
      }
      showToast('重新分析完成')
    } else {
      showToast('未找到合适的维修组')
    }
  }, 1500)
}
// ä¸€é”®è‡ªåŠ¨æ´¾å•
const autoDispatchAll = () => {
  const tasksWithRecommendation = filteredTasks.value.filter(task => task.recommendedTeam)
  if (tasksWithRecommendation.length === 0) {
    showToast('没有可自动派单的任务')
    return
  }
  uni.showModal({
    title: '一键派单',
    content: `确认自动派发${tasksWithRecommendation.length}个任务?`,
    confirmText: '确认',
    cancelText: '取消',
    success: (res) => {
      if (res.confirm) {
        // æ¨¡æ‹Ÿæ‰¹é‡æ´¾å•
        tasksWithRecommendation.forEach(task => {
          const index = pendingTasks.value.findIndex(t => t.id === task.id)
          if (index > -1) {
            pendingTasks.value.splice(index, 1)
            todayDispatchedCount.value++
          }
        })
        showToast(`成功派发${tasksWithRecommendation.length}个任务`)
      }
    }
  })
}
onMounted(() => {
  // é¡µé¢åŠ è½½æ—¶çš„åˆå§‹åŒ–é€»è¾‘
})
onShow(() => {
  // é¡µé¢æ˜¾ç¤ºæ—¶çš„逻辑
})
</script>
<style scoped lang="scss">
@import '@/styles/sales-common.scss';
// æ™ºèƒ½æ´¾å•特有样式
.sales-account {
  padding-bottom: 80px;
}
// è®¾å¤‡ç±»åž‹æ ‡ç­¾æ ·å¼
.tag-mechanical {
  background: #4caf50;
}
.tag-electric {
  background: #2196f3;
}
.tag-instrument {
  background: #ff9800;
}
.tag-computer {
  background: #9c27b0;
}
.tag-vehicle {
  background: #795548;
}
.tag-other {
  background: #607d8b;
}
// ä¼˜å…ˆçº§æ ‡ç­¾æ ·å¼
.priority-urgent {
  background: #f44336;
}
.priority-high {
  background: #ff9800;
}
.priority-medium {
  background: #2196f3;
}
.priority-low {
  background: #4caf50;
}
// æŽ¨èç»´ä¿®ç»„样式
.recommended-team {
  margin-top: 16px;
  padding: 12px;
  background: #f8f9fa;
  border-radius: 8px;
  border-left: 4px solid #2979ff;
}
.team-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}
.team-title {
  font-size: 14px;
  font-weight: 500;
  color: #333;
}
.match-score {
  background: #2979ff;
  padding: 2px 8px;
  border-radius: 12px;
}
.score-text {
  font-size: 12px;
  color: #ffffff;
  font-weight: 500;
}
.team-info {
  .detail-row {
    margin-bottom: 4px;
  }
}
// å¼¹çª—样式
.manual-dispatch-popup {
  padding: 20px;
  background: #ffffff;
  border-radius: 20px;
  max-height: 80vh;
  width: 90vw;
  max-width: 400px;
  overflow-y: auto;
}
.popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 12px;
  border-bottom: 1px solid #f0f0f0;
}
.popup-title {
  font-size: 16px;
  font-weight: 500;
  color: #333;
}
.popup-actions {
  display: flex;
  gap: 12px;
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid #f0f0f0;
}
.popup-actions .u-button {
  flex: 1;
}
// ç»´ä¿®ç»„选项样式
.team-list {
  max-height: 300px;
  overflow-y: auto;
}
.team-option {
  padding: 12px;
  margin-bottom: 8px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s ease;
  &.active {
    border-color: #2979ff;
    background: #f3f7ff;
  }
  &:hover {
    border-color: #2979ff;
  }
}
.team-name {
  font-size: 14px;
  font-weight: 500;
  color: #333;
  display: block;
  margin-bottom: 4px;
}
.team-leader,
.team-workload {
  font-size: 12px;
  color: #666;
  display: block;
  margin-bottom: 2px;
}
.team-skills {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid #f0f0f0;
}
.skills-label {
  font-size: 12px;
  color: #666;
  margin-right: 4px;
}
.skills-text {
  font-size: 12px;
  color: #333;
}
</style>
src/pages/index.vue
@@ -266,6 +266,15 @@
    {
        icon: '/static/images/icon/shbeibaoyang@2x.png',
        label: '设备保养',
    },
    {
        icon: '/static/images/icon/shebeixunjian@2x.png',
        label: '设备巡检',
    },
    {
        icon: 'flash',
        label: '智能派单',
        bgColor: '#ff6b35'
    }
]);
@@ -358,6 +367,16 @@
                url: '/pages/equipmentManagement/upkeep/index'
            });
            break;
        case '设备巡检':
            uni.navigateTo({
                url: '/pages/equipmentManagement/inspection/index'
            });
            break;
        case '智能派单':
            uni.navigateTo({
                url: '/pages/equipmentManagement/smartDispatch/index'
            });
            break;
        default:
            uni.showToast({
                title: `点击了${item.label}`,
src/static/app-logo.png
src/static/images/icon/shebeixunjian@2x.png
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
  <!-- èƒŒæ™¯åœ†å½¢ -->
  <circle cx="24" cy="24" r="20" fill="#1890ff" opacity="0.1"/>
  <!-- è®¾å¤‡ä¸»ä½“ -->
  <rect x="14" y="12" width="20" height="16" rx="2" fill="#1890ff" stroke="#1890ff" stroke-width="1.5"/>
  <!-- è®¾å¤‡å±å¹• -->
  <rect x="16" y="14" width="16" height="8" rx="1" fill="#ffffff"/>
  <!-- å±å¹•内容线条 -->
  <line x1="18" y1="16" x2="30" y2="16" stroke="#1890ff" stroke-width="1"/>
  <line x1="18" y1="18" x2="26" y2="18" stroke="#1890ff" stroke-width="1"/>
  <line x1="18" y1="20" x2="28" y2="20" stroke="#1890ff" stroke-width="1"/>
  <!-- è®¾å¤‡æŒ‰é’® -->
  <circle cx="18" cy="25" r="1.5" fill="#ffffff"/>
  <circle cx="22" cy="25" r="1.5" fill="#ffffff"/>
  <circle cx="26" cy="25" r="1.5" fill="#ffffff"/>
  <circle cx="30" cy="25" r="1.5" fill="#ffffff"/>
  <!-- å·¡æ£€è·¯å¾„ -->
  <path d="M10 32 Q16 28 24 32 Q32 36 38 32" stroke="#52c41a" stroke-width="2" fill="none" stroke-dasharray="2,2"/>
  <!-- å·¡æ£€ç‚¹ -->
  <circle cx="10" cy="32" r="2" fill="#52c41a"/>
  <circle cx="24" cy="32" r="2" fill="#52c41a"/>
  <circle cx="38" cy="32" r="2" fill="#52c41a"/>
  <!-- æ£€æŸ¥æ ‡è®° -->
  <path d="M20 36 L22 38 L26 34" stroke="#52c41a" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
  <!-- æ‰«æçº¿ -->
  <line x1="8" y1="8" x2="12" y2="8" stroke="#faad14" stroke-width="2" stroke-linecap="round"/>
  <line x1="8" y1="8" x2="8" y2="12" stroke="#faad14" stroke-width="2" stroke-linecap="round"/>
  <line x1="36" y1="8" x2="40" y2="8" stroke="#faad14" stroke-width="2" stroke-linecap="round"/>
  <line x1="40" y1="8" x2="40" y2="12" stroke="#faad14" stroke-width="2" stroke-linecap="round"/>
</svg>