|
@@ -1,59 +1,110 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <ScreenLayout :show-back-btn="false">
|
|
|
|
|
- <div class="page-status">
|
|
|
|
|
- <!-- 导航状态 -->
|
|
|
|
|
- <div class="nav-container">
|
|
|
|
|
- <div class="nav-header">
|
|
|
|
|
- <h1>导航中</h1>
|
|
|
|
|
- <p>目的地:{{ targetName }}</p>
|
|
|
|
|
|
|
+ <div class="status-layout">
|
|
|
|
|
+ <!-- 顶部状态栏 -->
|
|
|
|
|
+ <StatusBar :title="robotName" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 主内容区 -->
|
|
|
|
|
+ <div class="layout-main">
|
|
|
|
|
+ <div class="status-page">
|
|
|
|
|
+ <!-- 动态背景层 -->
|
|
|
|
|
+ <div class="bg-layer">
|
|
|
|
|
+ <div class="bg-orb orb-1"></div>
|
|
|
|
|
+ <div class="bg-orb orb-2"></div>
|
|
|
|
|
+ <div class="bg-orb orb-3"></div>
|
|
|
|
|
+ <div class="bg-grid-overlay"></div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 机器人动画 -->
|
|
|
|
|
- <div class="robot-animation">
|
|
|
|
|
- <div class="robot-icon" :class="{ moving: isMoving }">
|
|
|
|
|
- <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
- <circle cx="40" cy="28" r="16" fill="currentColor" />
|
|
|
|
|
- <rect x="20" y="48" width="40" height="24" rx="8" fill="currentColor" />
|
|
|
|
|
- <circle cx="32" cy="24" r="3" fill="white" class="eye-left" />
|
|
|
|
|
- <circle cx="48" cy="24" r="3" fill="white" class="eye-right" />
|
|
|
|
|
- <rect x="60" y="56" width="16" height="4" rx="2" fill="currentColor" class="wheel" />
|
|
|
|
|
- <rect x="4" y="56" width="16" height="4" rx="2" fill="currentColor" class="wheel" />
|
|
|
|
|
- </svg>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="path-line">
|
|
|
|
|
- <div class="path-progress" :style="{ width: progress + '%' }"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <!-- 背景模糊遮罩 -->
|
|
|
|
|
+ <div class="content-shade"></div>
|
|
|
|
|
|
|
|
- <div class="target-icon">
|
|
|
|
|
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
- <circle cx="12" cy="12" r="10" />
|
|
|
|
|
- <circle cx="12" cy="12" r="6" />
|
|
|
|
|
- <circle cx="12" cy="12" r="2" />
|
|
|
|
|
- </svg>
|
|
|
|
|
|
|
+ <!-- 内容区 -->
|
|
|
|
|
+ <div class="status-content">
|
|
|
|
|
+ <!-- 标题区 -->
|
|
|
|
|
+ <div class="status-hero">
|
|
|
|
|
+ <h1 class="page-title">导航中</h1>
|
|
|
|
|
+ <p class="page-subtitle">正在前往:{{ targetName }}</p>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 状态消息 -->
|
|
|
|
|
- <div class="status-message" :class="statusClass">
|
|
|
|
|
- <span class="message-text">{{ statusMessage }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- <!-- 进度 -->
|
|
|
|
|
- <div class="progress-info">
|
|
|
|
|
- <div class="progress-bar">
|
|
|
|
|
- <div class="progress-fill" :style="{ width: progress + '%' }"></div>
|
|
|
|
|
|
|
+ <!-- 导航状态大卡片 -->
|
|
|
|
|
+ <div class="nav-container">
|
|
|
|
|
+ <!-- 顶部状态提示 -->
|
|
|
|
|
+ <div class="status-banner" :class="statusClass">
|
|
|
|
|
+ <span class="banner-text">{{ statusMessage }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 路线状态:机器人 + 路径线 + 终点 -->
|
|
|
|
|
+ <div class="route-track">
|
|
|
|
|
+ <!-- 机器人 -->
|
|
|
|
|
+ <div class="route-robot" :class="{ moving: isMoving }">
|
|
|
|
|
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
+ <circle cx="40" cy="28" r="16" fill="currentColor" />
|
|
|
|
|
+ <rect x="20" y="48" width="40" height="24" rx="8" fill="currentColor" />
|
|
|
|
|
+ <circle cx="32" cy="24" r="3" fill="white" class="eye-left" />
|
|
|
|
|
+ <circle cx="48" cy="24" r="3" fill="white" class="eye-right" />
|
|
|
|
|
+ <rect x="60" y="56" width="16" height="4" rx="2" fill="currentColor" class="wheel" />
|
|
|
|
|
+ <rect x="4" y="56" width="16" height="4" rx="2" fill="currentColor" class="wheel" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 路径条 -->
|
|
|
|
|
+ <div class="route-line">
|
|
|
|
|
+ <div class="route-progress" :style="{ width: displayProgress + '%' }"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 终点图标 -->
|
|
|
|
|
+ <div class="route-end">
|
|
|
|
|
+ <svg viewBox="0 0 24 24" fill="currentColor">
|
|
|
|
|
+ <path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 目的地信息三列卡片 -->
|
|
|
|
|
+ <div class="info-panel">
|
|
|
|
|
+ <div class="info-item">
|
|
|
|
|
+ <div class="info-label">目的地</div>
|
|
|
|
|
+ <div class="info-value">{{ targetName }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-item">
|
|
|
|
|
+ <div class="info-label">位置</div>
|
|
|
|
|
+ <div class="info-value">{{ targetFloor }} · {{ targetDescription }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-item">
|
|
|
|
|
+ <div class="info-label">预计用时</div>
|
|
|
|
|
+ <div class="info-value">{{ estimatedTime }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 进度区 -->
|
|
|
|
|
+ <div class="progress-section">
|
|
|
|
|
+ <div class="progress-header">
|
|
|
|
|
+ <span class="progress-label">导航进度</span>
|
|
|
|
|
+ <span class="progress-value">{{ displayProgress }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="progress-track">
|
|
|
|
|
+ <div class="progress-bar">
|
|
|
|
|
+ <div class="progress-fill" :style="{ width: displayProgress + '%' }"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 取消按钮 -->
|
|
|
|
|
+ <button class="btn-cancel" @click="handleCancel">
|
|
|
|
|
+ 取消导航
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
- <span class="progress-text">{{ progress }}%</span>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <!-- 取消按钮 -->
|
|
|
|
|
- <button class="btn-cancel" @click="handleCancel">
|
|
|
|
|
- 取消导航
|
|
|
|
|
- </button>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </ScreenLayout>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 左下角返回按钮 -->
|
|
|
|
|
+ <button class="btn-back" @click="handleCancel">
|
|
|
|
|
+ <svg class="back-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
|
|
|
+ <path d="M19 12H5M12 5l-7 7 7 7" />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ 返回路线引导
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
@@ -61,7 +112,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
import { useRouter } from 'vue-router'
|
|
|
import { useNavigationStore } from '@/stores/navigation'
|
|
import { useNavigationStore } from '@/stores/navigation'
|
|
|
import { useScreenStore } from '@/stores/screen'
|
|
import { useScreenStore } from '@/stores/screen'
|
|
|
-import ScreenLayout from '@/layouts/ScreenLayout.vue'
|
|
|
|
|
|
|
+import StatusBar from '@/components/StatusBar.vue'
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
const router = useRouter()
|
|
|
const navigationStore = useNavigationStore()
|
|
const navigationStore = useNavigationStore()
|
|
@@ -70,44 +121,110 @@ const screenStore = useScreenStore()
|
|
|
const progress = ref(0)
|
|
const progress = ref(0)
|
|
|
const isMoving = ref(true)
|
|
const isMoving = ref(true)
|
|
|
|
|
|
|
|
-const targetName = computed(() => navigationStore.navigationStatus.targetName || '未知地点')
|
|
|
|
|
|
|
+const robotName = computed(() => screenStore.screenTheme?.robotName || screenStore.robotName || '迎宾巡逻机器人')
|
|
|
|
|
+
|
|
|
|
|
+const displayProgress = computed(() => Math.round(progress.value))
|
|
|
|
|
+
|
|
|
|
|
+const targetName = computed(() => {
|
|
|
|
|
+ return navigationStore.navigationStatus?.targetName
|
|
|
|
|
+ || navigationStore.currentTask?.targetName
|
|
|
|
|
+ || navigationStore.currentTask?.destinationName
|
|
|
|
|
+ || navigationStore.currentTask?.name
|
|
|
|
|
+ || '前台'
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const targetFloor = computed(() => {
|
|
|
|
|
+ return navigationStore.currentTask?.floor
|
|
|
|
|
+ || navigationStore.navigationStatus?.floor
|
|
|
|
|
+ || '1楼'
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const targetDescription = computed(() => {
|
|
|
|
|
+ return navigationStore.currentTask?.description
|
|
|
|
|
+ || navigationStore.navigationStatus?.description
|
|
|
|
|
+ || '综合服务前台'
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const estimatedTime = computed(() => {
|
|
|
|
|
+ const t = navigationStore.currentTask?.estimatedTime
|
|
|
|
|
+ || navigationStore.navigationStatus?.estimatedTime
|
|
|
|
|
+ || '约2分钟'
|
|
|
|
|
+ return t
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
const statusMessage = computed(() => {
|
|
const statusMessage = computed(() => {
|
|
|
- const p = progress.value
|
|
|
|
|
- if (p === 0) return '正在规划路线...'
|
|
|
|
|
- if (p < 30) return '开始带路,请跟我来'
|
|
|
|
|
- if (p < 60) return '前方直行'
|
|
|
|
|
- if (p < 90) return '即将到达'
|
|
|
|
|
- if (p === 100) return '已到达目的地'
|
|
|
|
|
- return '导航中'
|
|
|
|
|
|
|
+ const p = displayProgress.value
|
|
|
|
|
+ if (p === 0) return '正在规划路线'
|
|
|
|
|
+ if (p < 30) return '请跟随机器人前进'
|
|
|
|
|
+ if (p < 60) return '正在前往目的地'
|
|
|
|
|
+ if (p < 90) return '即将到达,请继续跟随'
|
|
|
|
|
+ if (p < 100) return '即将到达目的地'
|
|
|
|
|
+ return '已到达目的地'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const statusClass = computed(() => {
|
|
const statusClass = computed(() => {
|
|
|
- if (progress.value === 100) return 'status-arrived'
|
|
|
|
|
- if (progress.value >= 90) return 'status-near'
|
|
|
|
|
- return 'status-moving'
|
|
|
|
|
|
|
+ if (displayProgress.value === 100) return 'banner-arrived'
|
|
|
|
|
+ if (displayProgress.value >= 90) return 'banner-near'
|
|
|
|
|
+ return 'banner-moving'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-let timer = null
|
|
|
|
|
|
|
+const isMock = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+let progressTimer = null
|
|
|
|
|
+let alertTimer = null
|
|
|
|
|
+let finishTimer = null
|
|
|
|
|
+
|
|
|
|
|
+const createMockTask = () => {
|
|
|
|
|
+ const mockTask = {
|
|
|
|
|
+ taskId: `mock_nav_${Date.now()}`,
|
|
|
|
|
+ targetName: '前台',
|
|
|
|
|
+ destinationName: '前台',
|
|
|
|
|
+ name: '前台',
|
|
|
|
|
+ floor: '1楼',
|
|
|
|
|
+ description: '综合服务前台',
|
|
|
|
|
+ estimatedTime: '约2分钟',
|
|
|
|
|
+ status: 'navigating'
|
|
|
|
|
+ }
|
|
|
|
|
+ navigationStore.currentTask = mockTask
|
|
|
|
|
+ navigationStore.navigationStatus = {
|
|
|
|
|
+ taskId: mockTask.taskId,
|
|
|
|
|
+ status: 'navigating',
|
|
|
|
|
+ targetName: mockTask.name,
|
|
|
|
|
+ progress: 0,
|
|
|
|
|
+ estimatedTime: mockTask.estimatedTime
|
|
|
|
|
+ }
|
|
|
|
|
+ isMock.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const clearAllTimers = () => {
|
|
|
|
|
+ if (progressTimer) { clearInterval(progressTimer); progressTimer = null }
|
|
|
|
|
+ if (alertTimer) { clearTimeout(alertTimer); alertTimer = null }
|
|
|
|
|
+ if (finishTimer) { clearTimeout(finishTimer); finishTimer = null }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
const startNavigation = () => {
|
|
const startNavigation = () => {
|
|
|
- timer = setInterval(() => {
|
|
|
|
|
|
|
+ progress.value = 0
|
|
|
|
|
+ isMoving.value = true
|
|
|
|
|
+
|
|
|
|
|
+ progressTimer = setInterval(() => {
|
|
|
if (progress.value < 100) {
|
|
if (progress.value < 100) {
|
|
|
progress.value += Math.random() * 5 + 2
|
|
progress.value += Math.random() * 5 + 2
|
|
|
if (progress.value >= 100) {
|
|
if (progress.value >= 100) {
|
|
|
progress.value = 100
|
|
progress.value = 100
|
|
|
isMoving.value = false
|
|
isMoving.value = false
|
|
|
- clearInterval(timer)
|
|
|
|
|
- // 到达后延迟返回
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
|
|
+ clearInterval(progressTimer)
|
|
|
|
|
+ progressTimer = null
|
|
|
|
|
+
|
|
|
|
|
+ alertTimer = setTimeout(() => {
|
|
|
screenStore.showAlert({
|
|
screenStore.showAlert({
|
|
|
type: 'success',
|
|
type: 'success',
|
|
|
message: `已到达${targetName.value}`,
|
|
message: `已到达${targetName.value}`,
|
|
|
- duration: 3000
|
|
|
|
|
|
|
+ duration: import.meta.env.DEV ? 10000 : 3000
|
|
|
})
|
|
})
|
|
|
- setTimeout(() => {
|
|
|
|
|
- handleFinish()
|
|
|
|
|
- }, 3000)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (!import.meta.env.DEV) {
|
|
|
|
|
+ finishTimer = setTimeout(handleFinish, 3000)
|
|
|
|
|
+ }
|
|
|
}, 1000)
|
|
}, 1000)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -115,98 +232,253 @@ const startNavigation = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleCancel = async () => {
|
|
const handleCancel = async () => {
|
|
|
- if (timer) clearInterval(timer)
|
|
|
|
|
|
|
+ clearAllTimers()
|
|
|
try {
|
|
try {
|
|
|
await navigationStore.cancelNavigation()
|
|
await navigationStore.cancelNavigation()
|
|
|
- router.push('/navigation')
|
|
|
|
|
} catch {
|
|
} catch {
|
|
|
- router.push('/navigation')
|
|
|
|
|
|
|
+ // ignore
|
|
|
}
|
|
}
|
|
|
|
|
+ router.push('/navigation')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleFinish = () => {
|
|
const handleFinish = () => {
|
|
|
|
|
+ clearAllTimers()
|
|
|
navigationStore.clearNavigation()
|
|
navigationStore.clearNavigation()
|
|
|
router.push('/idle')
|
|
router.push('/idle')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
if (!navigationStore.currentTask) {
|
|
if (!navigationStore.currentTask) {
|
|
|
- router.push('/navigation')
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ if (import.meta.env.DEV) {
|
|
|
|
|
+ createMockTask()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ router.push('/navigation')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
startNavigation()
|
|
startNavigation()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
- if (timer) clearInterval(timer)
|
|
|
|
|
|
|
+ clearAllTimers()
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
-.page-status {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
|
|
+/* ===== 布局结构 ===== */
|
|
|
|
|
+.status-layout {
|
|
|
|
|
+ width: 100vw;
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: linear-gradient(155deg, #e8f4fd 0%, #dbeafe 40%, #eff6ff 100%);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.layout-main {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-page {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== 动态背景层 ===== */
|
|
|
|
|
+.bg-layer {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ z-index: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bg-orb {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ filter: blur(80px);
|
|
|
|
|
+ opacity: 0.45;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.orb-1 {
|
|
|
|
|
+ width: 480px;
|
|
|
|
|
+ height: 480px;
|
|
|
|
|
+ background: rgba(47, 142, 229, 0.28);
|
|
|
|
|
+ top: -180px;
|
|
|
|
|
+ left: -140px;
|
|
|
|
|
+ animation: float1 12s ease-in-out infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.orb-2 {
|
|
|
|
|
+ width: 360px;
|
|
|
|
|
+ height: 360px;
|
|
|
|
|
+ background: rgba(32, 183, 199, 0.22);
|
|
|
|
|
+ bottom: -100px;
|
|
|
|
|
+ right: -80px;
|
|
|
|
|
+ animation: float2 15s ease-in-out infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.orb-3 {
|
|
|
|
|
+ width: 280px;
|
|
|
|
|
+ height: 280px;
|
|
|
|
|
+ background: rgba(139, 92, 246, 0.18);
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ right: 15%;
|
|
|
|
|
+ animation: float3 18s ease-in-out infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes float1 {
|
|
|
|
|
+ 0%, 100% { transform: translate(-50%, -50%) scale(1); }
|
|
|
|
|
+ 50% { transform: translate(-40%, -60%) scale(1.08); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes float2 {
|
|
|
|
|
+ 0%, 100% { transform: translate(50%, 50%) scale(1); }
|
|
|
|
|
+ 50% { transform: translate(60%, 40%) scale(1.06); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes float3 {
|
|
|
|
|
+ 0%, 100% { transform: translate(0, 0) scale(1); }
|
|
|
|
|
+ 50% { transform: translate(-5%, 8%) scale(1.1); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bg-grid-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ background-image: radial-gradient(circle, rgba(47, 142, 229, 0.08) 1px, transparent 1px);
|
|
|
|
|
+ background-size: 32px 32px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content-shade {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.18);
|
|
|
|
|
+ z-index: 0;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== 内容区 ===== */
|
|
|
|
|
+.status-content {
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
|
|
- background: linear-gradient(135deg, #e9f8ff 0%, #dff8f5 50%, #f7fbff 100%);
|
|
|
|
|
|
|
+ padding: 0 36px 24px;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* ===== 标题区 ===== */
|
|
|
|
|
+.status-hero {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ margin: 28px 0 16px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.page-title {
|
|
|
|
|
+ font-size: 46px;
|
|
|
|
|
+ font-weight: 900;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ letter-spacing: 2px;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.page-subtitle {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin: 8px 0 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== 导航状态大卡片 ===== */
|
|
|
.nav-container {
|
|
.nav-container {
|
|
|
- width: 100%;
|
|
|
|
|
- max-width: 600px;
|
|
|
|
|
- padding: 40px;
|
|
|
|
|
|
|
+ flex: 1;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- gap: 32px;
|
|
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ width: min(920px, calc(100vw - 120px));
|
|
|
|
|
+ max-width: 920px;
|
|
|
|
|
+ padding: 32px 48px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.88);
|
|
|
|
|
+ backdrop-filter: blur(12px);
|
|
|
|
|
+ -webkit-backdrop-filter: blur(12px);
|
|
|
|
|
+ border-radius: 36px;
|
|
|
|
|
+ box-shadow: 0 12px 40px rgba(47, 142, 229, 0.12), 0 4px 12px rgba(0, 0, 0, 0.06);
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.nav-header {
|
|
|
|
|
|
|
+/* ===== 顶部状态提示 ===== */
|
|
|
|
|
+.status-banner {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 16px 32px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.nav-header h1 {
|
|
|
|
|
- font-size: 36px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
- margin: 0 0 8px;
|
|
|
|
|
|
|
+.banner-moving {
|
|
|
|
|
+ background: rgba(47, 142, 229, 0.08);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.nav-header p {
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- margin: 0;
|
|
|
|
|
|
|
+.banner-near {
|
|
|
|
|
+ background: rgba(245, 158, 11, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.banner-arrived {
|
|
|
|
|
+ background: rgba(16, 185, 129, 0.1);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.robot-animation {
|
|
|
|
|
|
|
+.banner-text {
|
|
|
|
|
+ font-size: 30px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.banner-near .banner-text {
|
|
|
|
|
+ color: #d97706;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.banner-arrived .banner-text {
|
|
|
|
|
+ color: #10b981;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== 路线状态:机器人 + 路径 + 终点 ===== */
|
|
|
|
|
+.route-track {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 120px;
|
|
height: 120px;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.robot-icon {
|
|
|
|
|
- width: 80px;
|
|
|
|
|
- height: 80px;
|
|
|
|
|
- color: var(--primary);
|
|
|
|
|
|
|
+.route-robot {
|
|
|
|
|
+ width: 100px;
|
|
|
|
|
+ height: 100px;
|
|
|
|
|
+ color: #2f8ee5;
|
|
|
z-index: 2;
|
|
z-index: 2;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
transition: transform 0.5s ease;
|
|
transition: transform 0.5s ease;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.robot-icon.moving {
|
|
|
|
|
|
|
+.route-robot.moving {
|
|
|
animation: walkBounce 0.5s ease-in-out infinite;
|
|
animation: walkBounce 0.5s ease-in-out infinite;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.robot-icon svg {
|
|
|
|
|
|
|
+.route-robot svg {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.robot-icon .eye-left,
|
|
|
|
|
-.robot-icon .eye-right {
|
|
|
|
|
|
|
+.route-robot .eye-left,
|
|
|
|
|
+.route-robot .eye-right {
|
|
|
animation: blink 3s ease-in-out infinite;
|
|
animation: blink 3s ease-in-out infinite;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -221,106 +493,271 @@ onUnmounted(() => {
|
|
|
95% { transform: scaleY(0.1); }
|
|
95% { transform: scaleY(0.1); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.path-line {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- left: 10%;
|
|
|
|
|
- right: 10%;
|
|
|
|
|
- height: 8px;
|
|
|
|
|
- background: var(--border-light);
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
|
|
+.route-line {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 14px;
|
|
|
|
|
+ background: rgba(47, 142, 229, 0.1);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
+ margin: 0 12px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.path-progress {
|
|
|
|
|
|
|
+.route-progress {
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
- background: linear-gradient(90deg, var(--primary), var(--secondary));
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- transition: width 0.3s ease;
|
|
|
|
|
|
|
+ background: linear-gradient(90deg, #2f8ee5, #20b7c7);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ transition: width 0.4s ease;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.target-icon {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- right: 8%;
|
|
|
|
|
- width: 32px;
|
|
|
|
|
- height: 32px;
|
|
|
|
|
- color: var(--success);
|
|
|
|
|
|
|
+.route-end {
|
|
|
|
|
+ width: 52px;
|
|
|
|
|
+ height: 52px;
|
|
|
|
|
+ color: #10b981;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.target-icon svg {
|
|
|
|
|
|
|
+.route-end svg {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
- animation: pulse 1.5s ease-in-out infinite;
|
|
|
|
|
|
|
+ animation: endPulse 1.5s ease-in-out infinite;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-@keyframes pulse {
|
|
|
|
|
|
|
+@keyframes endPulse {
|
|
|
0%, 100% { transform: scale(1); opacity: 1; }
|
|
0%, 100% { transform: scale(1); opacity: 1; }
|
|
|
- 50% { transform: scale(1.2); opacity: 0.7; }
|
|
|
|
|
|
|
+ 50% { transform: scale(1.12); opacity: 0.75; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.status-message {
|
|
|
|
|
- padding: 16px 32px;
|
|
|
|
|
- background: var(--bg-card);
|
|
|
|
|
- border-radius: var(--radius-full);
|
|
|
|
|
- box-shadow: var(--shadow-md);
|
|
|
|
|
|
|
+/* ===== 目的地信息三列卡片 ===== */
|
|
|
|
|
+.info-panel {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
+ gap: 14px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.message-text {
|
|
|
|
|
- font-size: 20px;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- color: var(--text-primary);
|
|
|
|
|
|
|
+.info-item {
|
|
|
|
|
+ background: rgba(47, 142, 229, 0.06);
|
|
|
|
|
+ border-radius: 18px;
|
|
|
|
|
+ padding: 16px 18px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.status-arrived .message-text {
|
|
|
|
|
- color: var(--success);
|
|
|
|
|
|
|
+.info-label {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.status-near .message-text {
|
|
|
|
|
- color: var(--primary);
|
|
|
|
|
|
|
+.info-value {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.progress-info {
|
|
|
|
|
|
|
+/* ===== 进度区 ===== */
|
|
|
|
|
+.progress-section {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.progress-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- gap: 16px;
|
|
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.progress-label {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.progress-value {
|
|
|
|
|
+ font-size: 26px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #2f8ee5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.progress-track {
|
|
|
|
|
+ width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.progress-bar {
|
|
.progress-bar {
|
|
|
- flex: 1;
|
|
|
|
|
- height: 12px;
|
|
|
|
|
- background: var(--border-light);
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ background: rgba(47, 142, 229, 0.1);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.progress-fill {
|
|
.progress-fill {
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
- background: linear-gradient(90deg, var(--primary), var(--secondary));
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- transition: width 0.3s ease;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.progress-text {
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: var(--primary);
|
|
|
|
|
- min-width: 50px;
|
|
|
|
|
- text-align: right;
|
|
|
|
|
|
|
+ background: linear-gradient(90deg, #2f8ee5, #20b7c7);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ transition: width 0.4s ease;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* ===== 取消导航按钮 ===== */
|
|
|
.btn-cancel {
|
|
.btn-cancel {
|
|
|
- padding: 16px 48px;
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- color: var(--text-secondary);
|
|
|
|
|
- background: var(--bg-card);
|
|
|
|
|
- border: 2px solid var(--border-light);
|
|
|
|
|
- border-radius: var(--radius-full);
|
|
|
|
|
|
|
+ height: 72px;
|
|
|
|
|
+ padding: 0 56px;
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
+ border: 2px solid rgba(148, 163, 184, 0.3);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
- transition: all var(--transition-fast);
|
|
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.btn-cancel:hover {
|
|
.btn-cancel:hover {
|
|
|
- border-color: var(--danger);
|
|
|
|
|
- color: var(--danger);
|
|
|
|
|
|
|
+ border-color: #ef4444;
|
|
|
|
|
+ color: #ef4444;
|
|
|
|
|
+ box-shadow: 0 6px 20px rgba(239, 68, 68, 0.15);
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-cancel:active {
|
|
|
|
|
+ transform: scale(0.97);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== 左下角返回按钮 ===== */
|
|
|
|
|
+.btn-back {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ left: 24px;
|
|
|
|
|
+ bottom: 28px;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ height: 66px;
|
|
|
|
|
+ padding: 0 28px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
|
|
+ -webkit-backdrop-filter: blur(10px);
|
|
|
|
|
+ border: 2px solid rgba(148, 163, 184, 0.22);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ letter-spacing: 1px;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-back:hover,
|
|
|
|
|
+.btn-back:focus-visible {
|
|
|
|
|
+ color: #2f8ee5;
|
|
|
|
|
+ border-color: rgba(47, 142, 229, 0.36);
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.98);
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 10px 30px rgba(47, 142, 229, 0.18);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-back:active {
|
|
|
|
|
+ transform: scale(0.97);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.back-icon {
|
|
|
|
|
+ width: 22px;
|
|
|
|
|
+ height: 22px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ===== 响应式适配 ===== */
|
|
|
|
|
+@media (max-height: 700px) {
|
|
|
|
|
+ .status-hero {
|
|
|
|
|
+ margin: 18px 0 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .page-title {
|
|
|
|
|
+ font-size: 38px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .page-subtitle {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .nav-container {
|
|
|
|
|
+ gap: 14px;
|
|
|
|
|
+ padding: 24px 36px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .status-banner {
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .banner-text {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .route-track {
|
|
|
|
|
+ height: 100px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .route-robot {
|
|
|
|
|
+ width: 80px;
|
|
|
|
|
+ height: 80px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .route-end {
|
|
|
|
|
+ width: 44px;
|
|
|
|
|
+ height: 44px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .route-line {
|
|
|
|
|
+ height: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-panel {
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-item {
|
|
|
|
|
+ padding: 12px 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-label {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-value {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .progress-value {
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .progress-bar {
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .btn-cancel {
|
|
|
|
|
+ height: 62px;
|
|
|
|
|
+ padding: 0 44px;
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .btn-back {
|
|
|
|
|
+ height: 58px;
|
|
|
|
|
+ padding: 0 22px;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ left: 20px;
|
|
|
|
|
+ bottom: 22px;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|