|
|
@@ -305,7 +305,7 @@
|
|
|
<div :class="['timeline-track', { 'is-playing': isTimelinePlaying }]" ref="timelineTrackRef">
|
|
|
<!-- 时间轴进度指示器 -->
|
|
|
<div class="timeline-progress" :style="{ width: `${timelineProgress}%` }"></div>
|
|
|
- <!-- 关键点 -->
|
|
|
+ <!-- 关键点 - 竖线风格 -->
|
|
|
<div
|
|
|
v-for="point in timelinePoints"
|
|
|
:key="point.id"
|
|
|
@@ -314,15 +314,22 @@
|
|
|
{
|
|
|
active: point.active,
|
|
|
selected: !isTimelinePlaying && selectedPoint?.id === point.id,
|
|
|
- dragging: draggingPoint?.id === point.id
|
|
|
+ dragging: draggingPoint?.id === point.id,
|
|
|
+ passed: isTimelinePlaying && point.passed
|
|
|
}
|
|
|
]"
|
|
|
:style="{ left: `${(point.time / timelineDuration) * 100}%` }"
|
|
|
@click.stop="!isTimelinePlaying && selectPoint(point)"
|
|
|
@mousedown.stop="!isTimelinePlaying && startDragPoint($event, point)"
|
|
|
- @contextmenu.prevent="!isTimelinePlaying && handlePointContextMenu($event, point)"
|
|
|
+ @contextmenu.prevent="!isTimelinePlaying && showPointContextMenu($event, point)"
|
|
|
>
|
|
|
- <span class="point-label">{{ point.id }}</span>
|
|
|
+ <!-- 顶部数字标签 -->
|
|
|
+ <div class="point-number">{{ point.id }}</div>
|
|
|
+ <!-- 拖拽时显示时间 -->
|
|
|
+ <div v-if="draggingPoint?.id === point.id" class="point-drag-time">
|
|
|
+ {{ formatTimelineTime(point.time) }}
|
|
|
+ </div>
|
|
|
+ <!-- 悬停 tooltip -->
|
|
|
<div class="point-tooltip">
|
|
|
<div>{{ point.presetName || `Point ${point.id}` }}</div>
|
|
|
<div class="point-time">{{ formatTimelineTime(point.time) }}</div>
|
|
|
@@ -337,46 +344,25 @@
|
|
|
</span>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 选中点的操作面板(巡航中隐藏) -->
|
|
|
- <div v-if="selectedPoint && !isTimelinePlaying" class="timeline-point-panel">
|
|
|
- <span class="panel-label">
|
|
|
- {{ t('当前选中') }}: {{ selectedPoint.presetName || `Point ${selectedPoint.id}` }}
|
|
|
- </span>
|
|
|
- <!-- 关联已有预置位 -->
|
|
|
- <el-select
|
|
|
- v-model="linkPresetId"
|
|
|
- size="small"
|
|
|
- :placeholder="t('关联预置位')"
|
|
|
- style="width: 140px"
|
|
|
- clearable
|
|
|
- @change="handleLinkPreset"
|
|
|
- >
|
|
|
- <el-option
|
|
|
- v-for="preset in ptzPresetList"
|
|
|
- :key="preset.id"
|
|
|
- :value="preset.id"
|
|
|
- :label="preset.name || `Preset ${preset.id}`"
|
|
|
- />
|
|
|
- </el-select>
|
|
|
- <el-button size="small" type="primary" :loading="savingPreset" @click="saveCurrentPoint">
|
|
|
- {{ selectedPoint.active ? t('更新位置') : t('保存位置') }}
|
|
|
- </el-button>
|
|
|
- <el-button size="small" type="danger" @click="deleteSelectedPoint">{{ t('删除') }}</el-button>
|
|
|
- </div>
|
|
|
- <!-- 自动映射按钮(巡航中隐藏) -->
|
|
|
- <div v-if="timelinePoints.length > 0 && !isTimelinePlaying" class="timeline-auto-link">
|
|
|
- <el-button
|
|
|
- size="small"
|
|
|
- type="success"
|
|
|
- @click="autoLinkPresets"
|
|
|
- :disabled="ptzPresetList.length === 0"
|
|
|
- >
|
|
|
+ <!-- 右键菜单 -->
|
|
|
+ <div
|
|
|
+ v-if="contextMenu.visible"
|
|
|
+ class="timeline-context-menu"
|
|
|
+ :style="{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }"
|
|
|
+ @click.stop
|
|
|
+ >
|
|
|
+ <div class="context-menu-item" @click="handleContextMenuUpdate">
|
|
|
<el-icon>
|
|
|
- <Link />
|
|
|
+ <Position />
|
|
|
</el-icon>
|
|
|
- {{ t('自动映射') }}
|
|
|
- </el-button>
|
|
|
- <span class="auto-link-hint">{{ t('将点1-N映射到Preset 1-N') }}</span>
|
|
|
+ {{ t('更新位置') }}
|
|
|
+ </div>
|
|
|
+ <div class="context-menu-item danger" @click="handleContextMenuDelete">
|
|
|
+ <el-icon>
|
|
|
+ <Delete />
|
|
|
+ </el-icon>
|
|
|
+ {{ t('删除') }}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -669,7 +655,7 @@ import {
|
|
|
ZoomOut,
|
|
|
Close,
|
|
|
Position,
|
|
|
- Link
|
|
|
+ Delete
|
|
|
} from '@element-plus/icons-vue'
|
|
|
import { Icon } from '@iconify/vue'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
@@ -761,6 +747,7 @@ interface TimelinePoint {
|
|
|
presetId?: number // 关联的预置位ID (保存后才有)
|
|
|
presetName?: string // 预置位名称
|
|
|
active: boolean // 是否已激活(已保存预置位)
|
|
|
+ passed?: boolean // 巡航模式下是否已走过
|
|
|
}
|
|
|
|
|
|
// localStorage 存储 key
|
|
|
@@ -774,10 +761,17 @@ const selectedPoint = ref<TimelinePoint | null>(null)
|
|
|
const isTimelinePlaying = ref(false)
|
|
|
const timelineProgress = ref(0)
|
|
|
const savingPreset = ref(false)
|
|
|
-const linkPresetId = ref<string | null>(null) // 关联预置位选择
|
|
|
const isLoopEnabled = ref(false) // 是否循环巡航
|
|
|
let timelinePlayAbort: AbortController | null = null
|
|
|
|
|
|
+// 右键菜单状态
|
|
|
+const contextMenu = ref({
|
|
|
+ visible: false,
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ point: null as TimelinePoint | null
|
|
|
+})
|
|
|
+
|
|
|
// 拖拽状态
|
|
|
const draggingPoint = ref<TimelinePoint | null>(null)
|
|
|
|
|
|
@@ -1524,10 +1518,38 @@ async function handleGotoPTZPreset(preset: PresetInfo) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 编辑预置位 (设置按钮)
|
|
|
-function handleEditPreset(preset: PTZPresetInfo) {
|
|
|
- ElMessage.info(`${t('设置预置位')}: ${preset.name || preset.id}`)
|
|
|
- // TODO: 打开预置位设置对话框
|
|
|
+// 编辑预置位 (设置按钮) - 将当前摄像头位置保存到该预置位
|
|
|
+async function handleEditPreset(preset: PTZPresetInfo) {
|
|
|
+ const cameraId = currentMediaStream.value?.cameraId
|
|
|
+ if (!cameraId) {
|
|
|
+ ElMessage.warning(t('请先配置摄像头连接'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `${t('将当前摄像头位置保存到预置位')} "${preset.name || `Preset ${preset.id}`}"?`,
|
|
|
+ t('设置预置位'),
|
|
|
+ { type: 'info' }
|
|
|
+ )
|
|
|
+
|
|
|
+ const res = await presetSet({
|
|
|
+ cameraId,
|
|
|
+ presetId: parseInt(preset.id),
|
|
|
+ presetName: preset.name || `Preset ${preset.id}`
|
|
|
+ })
|
|
|
+
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage.success(`${t('预置位设置成功')}: ${preset.name || `Preset ${preset.id}`}`)
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.errMsg || t('设置失败'))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 用户取消确认
|
|
|
+ if ((error as Error).toString().includes('cancel')) return
|
|
|
+ console.error('设置预置位失败', error)
|
|
|
+ ElMessage.error(t('设置失败'))
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 删除预置位
|
|
|
@@ -1637,24 +1659,60 @@ function handleDragEnd() {
|
|
|
document.removeEventListener('mouseup', handleDragEnd)
|
|
|
}
|
|
|
|
|
|
-// 添加关键点
|
|
|
-function addTimelinePoint(time?: number) {
|
|
|
+// 添加关键点并自动保存预置位
|
|
|
+async function addTimelinePoint(time?: number) {
|
|
|
+ const cameraId = currentMediaStream.value?.cameraId
|
|
|
+ if (!cameraId) {
|
|
|
+ ElMessage.warning(t('请先配置摄像头连接'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
const newId = timelinePoints.value.length > 0 ? Math.max(...timelinePoints.value.map((p) => p.id)) + 1 : 1
|
|
|
const newTime = time ?? Math.round(timelineDuration.value / 2)
|
|
|
|
|
|
// 确保时间在有效范围内
|
|
|
const clampedTime = Math.max(0, Math.min(newTime, timelineDuration.value))
|
|
|
|
|
|
- const newPoint: TimelinePoint = {
|
|
|
- id: newId,
|
|
|
- time: clampedTime,
|
|
|
- active: false
|
|
|
- }
|
|
|
+ // 使用时间轴专用的预置位ID范围 (100+)
|
|
|
+ const presetIdNum = 100 + newId
|
|
|
+ const presetName = `Timeline_${newId}`
|
|
|
|
|
|
- timelinePoints.value.push(newPoint)
|
|
|
- sortAndRenumberPoints()
|
|
|
- saveTimelineConfig()
|
|
|
- selectPoint(newPoint)
|
|
|
+ // 先保存当前摄像头位置到预置位
|
|
|
+ try {
|
|
|
+ const res = await presetSet({
|
|
|
+ cameraId,
|
|
|
+ presetId: presetIdNum,
|
|
|
+ presetName,
|
|
|
+ presetTime: 5, // 默认停留5秒
|
|
|
+ presetTotalTime: timelineDuration.value
|
|
|
+ })
|
|
|
+
|
|
|
+ if (res.success) {
|
|
|
+ // 保存成功后添加打点
|
|
|
+ const newPoint: TimelinePoint = {
|
|
|
+ id: newId,
|
|
|
+ time: clampedTime,
|
|
|
+ presetId: presetIdNum,
|
|
|
+ presetName: presetName,
|
|
|
+ active: true // 已保存预置位,标记为激活
|
|
|
+ }
|
|
|
+
|
|
|
+ timelinePoints.value.push(newPoint)
|
|
|
+ sortAndRenumberPoints()
|
|
|
+ saveTimelineConfig()
|
|
|
+ selectPoint(newPoint)
|
|
|
+
|
|
|
+ // 刷新预置位列表
|
|
|
+ loadPTZPresets()
|
|
|
+
|
|
|
+ ElMessage.success(`${t('已添加打点')} ${newId}`)
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.errMsg || t('添加失败'))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('添加打点失败', error)
|
|
|
+ ElMessage.error(t('添加失败'))
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 按时间排序并重新编号
|
|
|
@@ -1697,12 +1755,20 @@ async function saveCurrentPoint() {
|
|
|
|
|
|
savingPreset.value = true
|
|
|
try {
|
|
|
- const res = await presetSet({ cameraId, presetId: presetIdNum, presetName })
|
|
|
+ const res = await presetSet({
|
|
|
+ cameraId,
|
|
|
+ presetId: presetIdNum,
|
|
|
+ presetName,
|
|
|
+ presetTime: 5,
|
|
|
+ presetTotalTime: timelineDuration.value
|
|
|
+ })
|
|
|
if (res.success) {
|
|
|
point.presetId = presetIdNum
|
|
|
point.presetName = presetName
|
|
|
point.active = true
|
|
|
saveTimelineConfig()
|
|
|
+ // 刷新预置位列表
|
|
|
+ loadPTZPresets()
|
|
|
ElMessage.success(`${t('已保存')} ${presetName}`)
|
|
|
} else {
|
|
|
ElMessage.error(res.errMsg || t('保存失败'))
|
|
|
@@ -1715,87 +1781,78 @@ async function saveCurrentPoint() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 删除选中的点
|
|
|
-function deleteSelectedPoint() {
|
|
|
- if (!selectedPoint.value) return
|
|
|
+// 显示右键菜单
|
|
|
+function showPointContextMenu(e: MouseEvent, point: TimelinePoint) {
|
|
|
+ e.preventDefault()
|
|
|
+ selectedPoint.value = point
|
|
|
+ contextMenu.value = {
|
|
|
+ visible: true,
|
|
|
+ x: e.clientX,
|
|
|
+ y: e.clientY,
|
|
|
+ point
|
|
|
+ }
|
|
|
|
|
|
- const index = timelinePoints.value.findIndex((p) => p.id === selectedPoint.value!.id)
|
|
|
- if (index !== -1) {
|
|
|
- timelinePoints.value.splice(index, 1)
|
|
|
- sortAndRenumberPoints()
|
|
|
- saveTimelineConfig()
|
|
|
- selectedPoint.value = null
|
|
|
- ElMessage.success(t('已删除'))
|
|
|
+ // 点击其他地方关闭菜单
|
|
|
+ const closeMenu = () => {
|
|
|
+ contextMenu.value.visible = false
|
|
|
+ document.removeEventListener('click', closeMenu)
|
|
|
}
|
|
|
+ setTimeout(() => {
|
|
|
+ document.addEventListener('click', closeMenu)
|
|
|
+ }, 0)
|
|
|
}
|
|
|
|
|
|
-// 关联已有预置位
|
|
|
-function handleLinkPreset(presetId: string | null) {
|
|
|
- if (!selectedPoint.value || !presetId) {
|
|
|
- linkPresetId.value = null
|
|
|
- return
|
|
|
- }
|
|
|
+// 右键菜单 - 更新位置
|
|
|
+async function handleContextMenuUpdate() {
|
|
|
+ const point = contextMenu.value.point
|
|
|
+ if (!point) return
|
|
|
|
|
|
- const preset = ptzPresetList.value.find((p) => p.id === presetId)
|
|
|
- if (preset) {
|
|
|
- selectedPoint.value.presetId = parseInt(preset.id)
|
|
|
- selectedPoint.value.presetName = preset.name || `Preset ${preset.id}`
|
|
|
- selectedPoint.value.active = true
|
|
|
- saveTimelineConfig()
|
|
|
- ElMessage.success(`${t('已关联')}: ${selectedPoint.value.presetName}`)
|
|
|
- linkPresetId.value = null
|
|
|
- }
|
|
|
+ contextMenu.value.visible = false
|
|
|
+ selectedPoint.value = point
|
|
|
+
|
|
|
+ // 调用 saveCurrentPoint 保存当前摄像头位置
|
|
|
+ await saveCurrentPoint()
|
|
|
}
|
|
|
|
|
|
-// 自动映射:点1-N 对应 Preset 1-N
|
|
|
-function autoLinkPresets() {
|
|
|
- if (ptzPresetList.value.length === 0) {
|
|
|
- ElMessage.warning(t('暂无可用预置位'))
|
|
|
- return
|
|
|
- }
|
|
|
+// 右键菜单 - 删除
|
|
|
+async function handleContextMenuDelete() {
|
|
|
+ const point = contextMenu.value.point
|
|
|
+ if (!point) return
|
|
|
|
|
|
- // 按ID排序预置位列表(取前几个数字小的)
|
|
|
- const sortedPresets = [...ptzPresetList.value]
|
|
|
- .filter((p) => !isNaN(parseInt(p.id)))
|
|
|
- .sort((a, b) => parseInt(a.id) - parseInt(b.id))
|
|
|
+ contextMenu.value.visible = false
|
|
|
|
|
|
- let linkedCount = 0
|
|
|
- timelinePoints.value.forEach((point, index) => {
|
|
|
- if (index < sortedPresets.length) {
|
|
|
- const preset = sortedPresets[index]
|
|
|
- point.presetId = parseInt(preset.id)
|
|
|
- point.presetName = preset.name || `Preset ${preset.id}`
|
|
|
- point.active = true
|
|
|
- linkedCount++
|
|
|
- }
|
|
|
- })
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`${t('确定删除')} "${point.presetName || `Point ${point.id}`}"?`, t('删除确认'), {
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
|
|
|
- if (linkedCount > 0) {
|
|
|
- saveTimelineConfig()
|
|
|
- ElMessage.success(`${t('已映射')} ${linkedCount} ${t('个点位')}`)
|
|
|
- } else {
|
|
|
- ElMessage.warning(t('没有可映射的点位'))
|
|
|
- }
|
|
|
-}
|
|
|
+ const cameraId = currentMediaStream.value?.cameraId
|
|
|
|
|
|
-// 右键菜单删除点
|
|
|
-function handlePointContextMenu(e: MouseEvent, point: TimelinePoint) {
|
|
|
- ElMessageBox.confirm(`${t('确定删除')} "${point.presetName || `Point ${point.id}`}"?`, t('删除确认'), {
|
|
|
- type: 'warning'
|
|
|
- })
|
|
|
- .then(() => {
|
|
|
- const index = timelinePoints.value.findIndex((p) => p.id === point.id)
|
|
|
- if (index !== -1) {
|
|
|
- timelinePoints.value.splice(index, 1)
|
|
|
- sortAndRenumberPoints()
|
|
|
- saveTimelineConfig()
|
|
|
- if (selectedPoint.value?.id === point.id) {
|
|
|
- selectedPoint.value = null
|
|
|
- }
|
|
|
- ElMessage.success(t('已删除'))
|
|
|
+ // 如果有预置位,同时删除预置位
|
|
|
+ if (point.presetId && cameraId) {
|
|
|
+ await presetRemove({ cameraId, presetId: point.presetId })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除打点
|
|
|
+ const index = timelinePoints.value.findIndex((p) => p.id === point.id)
|
|
|
+ if (index !== -1) {
|
|
|
+ timelinePoints.value.splice(index, 1)
|
|
|
+ sortAndRenumberPoints()
|
|
|
+ saveTimelineConfig()
|
|
|
+ if (selectedPoint.value?.id === point.id) {
|
|
|
+ selectedPoint.value = null
|
|
|
}
|
|
|
- })
|
|
|
- .catch(() => {})
|
|
|
+ // 刷新预置位列表
|
|
|
+ if (cameraId) {
|
|
|
+ loadPTZPresets()
|
|
|
+ }
|
|
|
+ ElMessage.success(t('已删除'))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 用户取消确认
|
|
|
+ if ((error as Error).toString().includes('cancel')) return
|
|
|
+ console.error('删除打点失败', error)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 时长改变时重新分配点的时间
|
|
|
@@ -1830,6 +1887,8 @@ async function playTimeline() {
|
|
|
isTimelinePlaying.value = true
|
|
|
timelineProgress.value = 0
|
|
|
selectedPoint.value = null // 清除选中状态
|
|
|
+ // 重置所有点的 passed 状态
|
|
|
+ timelinePoints.value.forEach((p) => (p.passed = false))
|
|
|
timelinePlayAbort = new AbortController()
|
|
|
|
|
|
const totalDuration = timelineDuration.value * 1000 // 转为毫秒
|
|
|
@@ -1848,6 +1907,8 @@ async function playTimeline() {
|
|
|
do {
|
|
|
loopStartTime = Date.now()
|
|
|
timelineProgress.value = 0
|
|
|
+ // 循环开始时重置 passed 状态
|
|
|
+ timelinePoints.value.forEach((p) => (p.passed = false))
|
|
|
|
|
|
// 启动/重启进度条动画
|
|
|
if (progressAnimationId) {
|
|
|
@@ -1873,6 +1934,8 @@ async function playTimeline() {
|
|
|
// 跳转到该预置位
|
|
|
if (point.presetId) {
|
|
|
await presetGoto({ cameraId, presetId: point.presetId })
|
|
|
+ // 标记该点已走过
|
|
|
+ point.passed = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1903,6 +1966,8 @@ async function playTimeline() {
|
|
|
isTimelinePlaying.value = false
|
|
|
timelineProgress.value = 0
|
|
|
timelinePlayAbort = null
|
|
|
+ // 清除 passed 状态
|
|
|
+ timelinePoints.value.forEach((p) => (p.passed = false))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1919,6 +1984,8 @@ function stopTimeline() {
|
|
|
}
|
|
|
isTimelinePlaying.value = false
|
|
|
timelineProgress.value = 0
|
|
|
+ // 清除 passed 状态
|
|
|
+ timelinePoints.value.forEach((p) => (p.passed = false))
|
|
|
}
|
|
|
|
|
|
// 带取消功能的 sleep
|
|
|
@@ -2319,10 +2386,11 @@ onMounted(async () => {
|
|
|
|
|
|
.timeline-track {
|
|
|
position: relative;
|
|
|
- height: 32px;
|
|
|
- background: linear-gradient(to right, #374151, #374151);
|
|
|
- border-radius: 4px;
|
|
|
+ height: 24px;
|
|
|
+ background: #374151;
|
|
|
+ border-radius: 2px;
|
|
|
margin-bottom: 8px;
|
|
|
+ cursor: pointer;
|
|
|
|
|
|
&.is-playing {
|
|
|
cursor: not-allowed;
|
|
|
@@ -2338,33 +2406,31 @@ onMounted(async () => {
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
height: 100%;
|
|
|
- background: linear-gradient(to right, #dc2626, #f87171);
|
|
|
- border-radius: 4px 0 0 4px;
|
|
|
+ background: linear-gradient(to right, #dc2626, #ef4444);
|
|
|
+ border-radius: 2px 0 0 2px;
|
|
|
pointer-events: none;
|
|
|
transition: width 0.3s ease;
|
|
|
}
|
|
|
|
|
|
+ // 圆形打点
|
|
|
.timeline-point {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
width: 24px;
|
|
|
height: 24px;
|
|
|
- border-radius: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
background: #6b7280; // 未激活 - 灰色
|
|
|
- border: 2px solid #fff;
|
|
|
+ border-radius: 50%;
|
|
|
cursor: grab;
|
|
|
+ z-index: 5;
|
|
|
+ transition: all 0.15s ease;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- font-size: 11px;
|
|
|
- color: #fff;
|
|
|
- font-weight: bold;
|
|
|
- z-index: 5;
|
|
|
- transition: all 0.2s;
|
|
|
|
|
|
&:hover {
|
|
|
transform: translate(-50%, -50%) scale(1.15);
|
|
|
+ background: #9ca3af;
|
|
|
|
|
|
.point-tooltip {
|
|
|
opacity: 1;
|
|
|
@@ -2373,12 +2439,18 @@ onMounted(async () => {
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
- background: #22c55e; // 激活 - 绿色
|
|
|
+ background: #ffffff; // 激活 - 白色
|
|
|
+ box-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
|
|
|
+
|
|
|
+ .point-number {
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
&.selected {
|
|
|
- box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5);
|
|
|
- transform: translate(-50%, -50%) scale(1.2);
|
|
|
+ transform: translate(-50%, -50%) scale(1.15);
|
|
|
+ background: #3b82f6; // 选中 - 蓝色
|
|
|
+ box-shadow: 0 0 12px rgba(59, 130, 246, 0.7);
|
|
|
z-index: 10;
|
|
|
|
|
|
.point-tooltip {
|
|
|
@@ -2387,29 +2459,77 @@ onMounted(async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ &.passed {
|
|
|
+ background: #22c55e; // 巡航走过 - 绿色
|
|
|
+ box-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
|
|
|
+ }
|
|
|
+
|
|
|
&.dragging {
|
|
|
cursor: grabbing;
|
|
|
- transform: translate(-50%, -50%) scale(1.3);
|
|
|
- box-shadow: 0 0 0 4px rgba(251, 191, 36, 0.6);
|
|
|
+ transform: translate(-50%, -50%) scale(1.2);
|
|
|
+ background: #fbbf24; // 拖拽中 - 黄色
|
|
|
+ box-shadow: 0 0 14px rgba(251, 191, 36, 0.8);
|
|
|
z-index: 20;
|
|
|
- transition: none; // 拖拽时禁用过渡动画
|
|
|
+ transition: none;
|
|
|
|
|
|
- .point-tooltip {
|
|
|
+ .point-drag-time {
|
|
|
opacity: 1;
|
|
|
visibility: visible;
|
|
|
}
|
|
|
+
|
|
|
+ .point-tooltip {
|
|
|
+ opacity: 0;
|
|
|
+ visibility: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .point-number {
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- .point-label {
|
|
|
+ // 圆内数字
|
|
|
+ .point-number {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ pointer-events: none;
|
|
|
line-height: 1;
|
|
|
}
|
|
|
|
|
|
+ // 拖拽时显示时间
|
|
|
+ .point-drag-time {
|
|
|
+ position: absolute;
|
|
|
+ bottom: calc(100% + 6px);
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ background: #fbbf24;
|
|
|
+ color: #000;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ white-space: nowrap;
|
|
|
+ opacity: 0;
|
|
|
+ visibility: hidden;
|
|
|
+ pointer-events: none;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 100%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ border: 5px solid transparent;
|
|
|
+ border-top-color: #fbbf24;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.point-tooltip {
|
|
|
position: absolute;
|
|
|
- bottom: calc(100% + 8px);
|
|
|
+ bottom: calc(100% + 6px);
|
|
|
left: 50%;
|
|
|
transform: translateX(-50%);
|
|
|
- background: rgba(0, 0, 0, 0.85);
|
|
|
+ background: rgba(0, 0, 0, 0.9);
|
|
|
color: #fff;
|
|
|
padding: 6px 10px;
|
|
|
border-radius: 4px;
|
|
|
@@ -2417,7 +2537,7 @@ onMounted(async () => {
|
|
|
white-space: nowrap;
|
|
|
opacity: 0;
|
|
|
visibility: hidden;
|
|
|
- transition: all 0.2s;
|
|
|
+ transition: all 0.15s ease;
|
|
|
pointer-events: none;
|
|
|
|
|
|
.point-time {
|
|
|
@@ -2433,7 +2553,7 @@ onMounted(async () => {
|
|
|
left: 50%;
|
|
|
transform: translateX(-50%);
|
|
|
border: 5px solid transparent;
|
|
|
- border-top-color: rgba(0, 0, 0, 0.85);
|
|
|
+ border-top-color: rgba(0, 0, 0, 0.9);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -2450,36 +2570,42 @@ onMounted(async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .timeline-point-panel {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- margin-top: 12px;
|
|
|
- padding: 10px 12px;
|
|
|
+ // 右键菜单
|
|
|
+ .timeline-context-menu {
|
|
|
+ position: fixed;
|
|
|
background: #2a2a2a;
|
|
|
- border-radius: 4px;
|
|
|
- flex-wrap: wrap;
|
|
|
+ border: 1px solid #404040;
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 4px 0;
|
|
|
+ min-width: 140px;
|
|
|
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
|
+ z-index: 1000;
|
|
|
+
|
|
|
+ .context-menu-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ color: #e5e7eb;
|
|
|
+ font-size: 13px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background 0.15s ease;
|
|
|
|
|
|
- .panel-label {
|
|
|
- color: #d1d5db;
|
|
|
- font-size: 12px;
|
|
|
- flex: 1;
|
|
|
- min-width: 100%;
|
|
|
- }
|
|
|
- }
|
|
|
+ .el-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
|
|
|
- .timeline-auto-link {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- margin-top: 8px;
|
|
|
- padding: 8px 12px;
|
|
|
- background: #1f1f1f;
|
|
|
- border-radius: 4px;
|
|
|
+ &:hover {
|
|
|
+ background: #374151;
|
|
|
+ }
|
|
|
|
|
|
- .auto-link-hint {
|
|
|
- color: #6b7280;
|
|
|
- font-size: 11px;
|
|
|
+ &.danger {
|
|
|
+ color: #f87171;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(239, 68, 68, 0.15);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|