RobotStatusCard.vue 7.6 KB
<script lang="ts" setup>
/**
 * 机器人状态卡片组件
 * @author zzy
 */
import { computed, ref } from 'vue'
import { useToast } from 'vuestic-ui'
import type { RobotStatus } from '../../../../services/robotStatusWs'
import robotsApi from '../../../../services/robots'

const props = defineProps<{
  robot: RobotStatus
}>()

const { init: notify } = useToast()
const isLoading = ref(false)

// 状态文本
const statusText = computed(() => {
  switch (props.robot.status) {
    case 1:
      return '空闲'
    case 2:
      return '忙碌'
    case 3:
      return '异常'
    default:
      return '未知'
  }
})

// 状态颜色
const statusColor = computed(() => {
  switch (props.robot.status) {
    case 1:
      return 'success'
    case 2:
      return 'warning'
    case 3:
      return 'danger'
    default:
      return 'secondary'
  }
})

// 在线状态
const onlineText = computed(() => {
  switch (props.robot.online) {
    case 1:
      return '在线'
    case 2:
      return '离线'
    case 3:
      return '连接中断'
    default:
      return '未知'
  }
})

// 电量颜色
const batteryColor = computed(() => {
  const level = props.robot.batteryLevel ?? 0
  if (level <= 20) return 'danger'
  if (level <= 50) return 'warning'
  return 'success'
})

// 格式化坐标
const formatCoord = (val: number | null) => (val != null ? val.toFixed(2) : '-')

// 解析错误信息
const errorList = computed(() => {
  return props.robot.errors ?? []
})

// 根据错误级别获取颜色
const getErrorColor = (err: any) => {
  const level = (err.errorType || err.ErrorType || '').toUpperCase()
  return level === 'ERROR' ? 'danger' : 'warning'
}

/**
 * 复位机器人
 * @author zzy
 */
const handleReset = async () => {
  if (isLoading.value) return
  isLoading.value = true
  try {
    console.log(props.robot)
    const res = await robotsApi.reset(props.robot.robotId)
    if (res?.Success || res?.success) {
      notify({ message: '复位成功', color: 'success' })
    } else {
      notify({ message: res?.Message || res?.message || '复位失败', color: 'danger' })
    }
  } catch (err: any) {
    notify({ message: err?.message || '复位失败', color: 'danger' })
  } finally {
    isLoading.value = false
  }
}

/**
 * 取消任务
 * @author zzy
 */
const handleCancelTask = async () => {
  if (isLoading.value) return
  isLoading.value = true
  try {
    const res = await robotsApi.cancelTask(props.robot.robotId)
    if (res?.Success || res?.success) {
      notify({ message: '取消任务成功', color: 'success' })
    } else {
      notify({ message: res?.Message || res?.message || '取消任务失败', color: 'danger' })
    }
  } catch (err: any) {
    notify({ message: err?.message || '取消任务失败', color: 'danger' })
  } finally {
    isLoading.value = false
  }
}
</script>

<template>
  <VaCard class="robot-card" :class="{ offline: robot.online !== 1 }">
    <VaCardContent class="robot-card-content">
      <!-- 头部:名称和状态 -->
      <div class="robot-header">
        <span class="robot-name">{{ robot.robotName }}-{{ robot.robotCode }}</span>
        <VaBadge :color="statusColor" :text="statusText" />
      </div>

      <!-- 在线状态 -->
      <div class="robot-row">
        <VaIcon :name="robot.online === 1 ? 'wifi' : 'wifi_off'" :color="robot.online === 1 ? 'success' : 'danger'" size="small" />
        <span class="robot-label">{{ onlineText }}</span>
      </div>

      <!-- 电量 -->
      <div class="robot-row">
        <template v-if="robot.charging">
          <div class="battery-icon-stack">
            <VaIcon name="battery_std" color="success" size="small" />
            <VaIcon name="bolt" color="warning" size="small" class="bolt-overlay" />
          </div>
          <div class="battery-charging">
            <div class="battery-fill"></div>
          </div>
        </template>
        <template v-else>
          <VaIcon name="battery_std" color="success" size="small" />
          <VaProgressBar :model-value="robot.batteryLevel ?? 0" :color="batteryColor" size="small" class="battery-bar" />
        </template>
        <span class="battery-text">{{ robot.batteryLevel ?? 0 }}%</span>
      </div>

      <!-- 坐标 -->
      <div class="robot-row coords">
        <span>X: {{ formatCoord(robot.x) }}</span>
        <span>Y: {{ formatCoord(robot.y) }}</span>
      </div>

      <!-- 行驶状态 -->
      <div class="robot-row" v-if="robot.driving">
        <VaIcon name="directions_car" color="primary" size="small" />
        <span class="robot-label">行驶中</span>
      </div>

      <!-- 错误信息 -->
      <div class="robot-errors" v-if="errorList.length > 0">
        <div v-for="(err, idx) in errorList.slice(0, 2)" :key="idx" class="error-item" :class="getErrorColor(err)">
          <VaIcon name="error" :color="getErrorColor(err)" size="small" />
          <span>{{ err.errorDescription || '未知错误' }}</span>
        </div>
      </div>

      <!-- 操作按钮 -->
      <div class="robot-actions">
        <VaButton size="small" color="warning" :loading="isLoading" @click="handleReset" title="复位" class="action-btn">
          <VaIcon name="restart_alt" size="small" />
          复位
        </VaButton>
        <VaButton size="small" color="secondary" :loading="isLoading" @click="handleCancelTask" title="取消任务" class="action-btn">
          <VaIcon name="cancel" size="small" />
          取消任务
        </VaButton>
      </div>
    </VaCardContent>
  </VaCard>
</template>

<style scoped>
.robot-card {
  margin-bottom: 10px;
  transition: all 0.2s;
  border: 1px solid var(--va-background-border);
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}

.robot-card:hover {
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
}

.robot-card.offline {
  opacity: 0.6;
  background: var(--va-background-secondary);
}

.robot-card-content {
  padding: 12px !important;
}

.robot-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.robot-name {
  font-weight: 600;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 120px;
}

.robot-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
  font-size: 12px;
}

.robot-row.coords {
  justify-content: space-between;
  color: var(--va-text-secondary);
}

.robot-label {
  color: var(--va-text-secondary);
}

.battery-bar {
  flex: 1;
  max-width: 80px;
}

.battery-text {
  min-width: 32px;
  text-align: right;
  font-size: 11px;
}

.robot-errors {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--va-background-border);
}

.error-item {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  font-size: 11px;
  margin-bottom: 2px;
}

.error-item.danger {
  color: var(--va-danger);
}

.error-item.warning {
  color: var(--va-warning);
}

.error-item span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.robot-actions {
  display: flex;
  gap: 6px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--va-background-border);
}

.action-btn {
  flex: 1;
}

.battery-icon-stack {
  position: relative;
  display: inline-flex;
}

.bolt-overlay {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(0.7);
}

.battery-charging {
  flex: 1;
  max-width: 80px;
  height: 4px;
  background: #e0e0e0;
  border-radius: 2px;
  overflow: hidden;
  position: relative;
}

.battery-fill {
  height: 100%;
  background: linear-gradient(90deg, #4caf50, #8bc34a);
  border-radius: 2px;
  animation: battery-charge 1.5s ease-in-out infinite;
}

@keyframes battery-charge {
  0% { width: 0%; }
  100% { width: 100%; }
}
</style>