| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- <template>
- <div class="osd-settings">
- <!-- 左侧:视频预览 + OSD拖拽 -->
- <div class="osd-settings__left">
- <div class="osd-settings__video" ref="videoWrapRef">
- <MyVideo ref="myVideoRef" :drag-flag="false"/>
- <!-- OSD叠加层:用于拖拽定位 -->
- <div class="osd-overlay" ref="overlayRef">
- <!-- 时间OSD -->
- <div
- v-if="form.EnTime"
- class="osd-tag osd-tag--time"
- :style="osdTimeStyle"
- @mousedown.prevent="startDragOsd($event, 'time')"
- >
- {{ currentTime }}
- </div>
- <!-- 名称OSD -->
- <div
- v-if="form.EnName"
- class="osd-tag osd-tag--name"
- :style="osdNameStyle"
- @mousedown.prevent="startDragOsd($event, 'name')"
- >
- {{ form.OsdName }}
- </div>
- </div>
- </div>
- <div class="video-hint">
- <span class="video-hint__text">Press and hold the red box to drag the OSD position. Changes take effect after saving.</span>
- </div>
- </div>
- <!-- 右侧设置面板 -->
- <div class="osd-settings__panel">
- <!-- 显示设置 -->
- <div class="section">
- <div class="section__title">Display Settings</div>
- <div class="section__body">
- <!-- 显示名称 -->
- <div class="form-item">
- <el-checkbox v-model="form.EnName">Display Name</el-checkbox>
- </div>
- <div class="form-item form-item--indent" v-if="form.EnName">
- <span class="form-item__label">Name</span>
- <div class="form-item__control name-control">
- <span class="name-text">{{ form.OsdName }}</span>
- <el-button link type="primary" @click="showEditName = true">
- <el-icon>
- <Edit/>
- </el-icon>
- Modify
- </el-button>
- </div>
- </div>
- <!-- 显示时间 -->
- <div class="form-item">
- <el-checkbox v-model="form.EnTime">Display Time:</el-checkbox>
- <span class="form-item__value" v-if="form.EnTime">{{ currentTime2 }}</span>
- </div>
- <!-- 显示码率 -->
- <!-- <div class="form-item">
- <el-checkbox v-model="form.showBitrate">显示码率:</el-checkbox>
- <span class="form-item__value" v-if="form.showBitrate">BR={{ form.bitrateText }}</span>
- </div> -->
- <!-- 自定义 -->
- <!-- <div class="form-item">
- <el-checkbox v-model="form.showCustom">自定义</el-checkbox>
- <el-input
- v-if="form.showCustom"
- v-model="form.customText"
- size="small"
- style="width: 160px; margin-left: 8px"
- placeholder="custom"
- />
- </div> -->
- </div>
- </div>
- <!-- 字体设置 -->
- <!-- <div class="section">
- <div class="section__title">字体设置</div>
- <div class="section__body">
- <div class="form-item">
- <span class="form-item__label">字体大小:</span>
- <div class="form-item__control">
- <el-select v-model="form.fontSize">
- <el-option label="小" :value="0" />
- <el-option label="中" :value="1" />
- <el-option label="大" :value="2" />
- </el-select>
- </div>
- </div>
- <div class="form-item">
- <span class="form-item__label">字体颜色:</span>
- <div class="form-item__control">
- <el-select v-model="form.fontColor">
- <el-option label="自动" :value="0" />
- <el-option label="白色" :value="1" />
- <el-option label="黑色" :value="2" />
- </el-select>
- </div>
- </div>
- </div>
- </div> -->
- <!-- 保存按钮 -->
- <div class="save-btn-wrapper">
- <el-button type="primary" round @click="saveOsd">Save</el-button>
- </div>
- </div>
- <!-- 修改名称弹窗 -->
- <el-dialog v-model="showEditName" title="Modify Name" width="360px" :close-on-click-modal="false">
- <el-input v-model="editNameValue" maxlength="32" placeholder="Please enter a name"/>
- <template #footer>
- <el-button @click="showEditName = false">Cancel</el-button>
- <el-button type="primary" @click="confirmEditName">Confirm</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import {ref, reactive, computed, onMounted, onUnmounted} from 'vue'
- import {ElMessage} from 'element-plus'
- import {Edit} from '@element-plus/icons-vue'
- import MyVideo from '@/components/myVideo.vue'
- import {getOsdPara, putOsdPara, GetTimePara} from '@/api/setting'
- const myVideoRef = ref<InstanceType<typeof MyVideo> | null>(null)
- const timeFormat = ref(0)
- // 表单数据
- const form = reactive({
- EnName: true,
- OsdName: 'ANSJER',
- EnTime: true,
- // showBitrate: false,
- // bitrateText: '79K',
- // showCustom: false,
- // customText: 'custom',
- // fontSize: 0, // 0=小 1=中 2=大
- // fontColor: 0, // 0=自动 1=白色 2=黑色
- // OSD归一化坐标 (0~1)
- OsdTimeX: 0.01,
- OsdTimeY: 0.02,
- OsdNameX: 0.90,
- OsdNameY: 0.92
- })
- // 修改名称弹窗
- const showEditName = ref(false)
- const editNameValue = ref('')
- function confirmEditName() {
- if (!editNameValue.value.trim()) {
- ElMessage.warning('Name cannot be empty')
- return
- }
- form.OsdName = editNameValue.value.trim()
- showEditName.value = false
- }
- // 实时时间显示
- const currentTime = ref('')
- const currentTime2 = ref('')
- let timeTimer: ReturnType<typeof setInterval> | null = null
- function updateTime(timeFormat: number) {
- const now = new Date()
- const y = now.getFullYear()
- const M = String(now.getMonth() + 1).padStart(2, '0')
- const d = String(now.getDate()).padStart(2, '0')
- const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
- const w = days[now.getDay()]
- const h = String(now.getHours()).padStart(2, '0')
- const m = String(now.getMinutes()).padStart(2, '0')
- const s = String(now.getSeconds()).padStart(2, '0')
- if (timeFormat === 0) {
- currentTime.value = `${y}-${M}-${d}\u00A0\u00A0\u00A0${h}:${m}:${s}\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${w}`
- currentTime2.value = `${y}-${M}-${d} ${h}:${m}:${s} ${w}`
- } else if (timeFormat === 1) {
- currentTime.value = `${M}-${d}-${y}\u00A0\u00A0\u00A0${h}:${m}:${s}\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${w}`
- currentTime2.value = `${M}-${d}-${y} ${h}:${m}:${s} ${w}`
- } else {
- currentTime.value = `${d}-${M}-${y}\u00A0\u00A0\u00A0${h}:${m}:${s}\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${w}`
- currentTime2.value = `${d}-${M}-${y} ${h}:${m}:${s} ${w}`
- }
- }
- // OSD拖拽相关
- const overlayRef = ref<HTMLElement | null>(null)
- let draggingTarget: 'time' | 'name' | null = null
- let dragStartMouseX = 0
- let dragStartMouseY = 0
- let dragStartPosX = 0
- let dragStartPosY = 0
- const osdTimeStyle = computed(() => ({
- left: `${form.OsdTimeX * 100}%`,
- top: `${form.OsdTimeY * 100}%`
- }))
- const osdNameStyle = computed(() => ({
- left: `${form.OsdNameX * 100}%`,
- top: `${form.OsdNameY * 100}%`
- }))
- function startDragOsd(e: MouseEvent, target: 'time' | 'name') {
- draggingTarget = target
- dragStartMouseX = e.clientX
- dragStartMouseY = e.clientY
- dragStartPosX = target === 'time' ? form.OsdTimeX : form.OsdNameX
- dragStartPosY = target === 'time' ? form.OsdTimeY : form.OsdNameY
- document.addEventListener('mousemove', onDragOsd)
- document.addEventListener('mouseup', endDragOsd)
- }
- function onDragOsd(e: MouseEvent) {
- if (!draggingTarget || !overlayRef.value) return
- const rect = overlayRef.value.getBoundingClientRect()
- const dx = (e.clientX - dragStartMouseX) / rect.width
- const dy = (e.clientY - dragStartMouseY) / rect.height
- let nx = Math.max(0, Math.min(0.95, dragStartPosX + dx)) // 避免文字溢出到画面外
- let ny = Math.max(0, Math.min(0.95, dragStartPosY + dy)) // 避免文字溢出到画面外
- if (draggingTarget === 'time') {
- form.OsdTimeX = nx
- form.OsdTimeY = ny
- } else {
- form.OsdNameX = nx
- form.OsdNameY = ny
- }
- }
- function endDragOsd() {
- draggingTarget = null
- document.removeEventListener('mousemove', onDragOsd)
- document.removeEventListener('mouseup', endDragOsd)
- }
- // 获取OSD参数
- async function fetchOsd() {
- try {
- const res = await getOsdPara()
- if (res.data) {
- const d = res.data
- if (d.EnName !== undefined) form.EnName = !!d.EnName
- if (d.OsdName !== undefined) form.OsdName = d.OsdName
- if (d.EnTime !== undefined) form.EnTime = !!d.EnTime
- // if (d.showBitrate !== undefined) form.showBitrate = !!d.showBitrate
- // if (d.showCustom !== undefined) form.showCustom = !!d.showCustom
- // if (d.customText !== undefined) form.customText = d.customText
- // if (d.fontSize !== undefined) form.fontSize = Number(d.fontSize)
- // if (d.fontColor !== undefined) form.fontColor = Number(d.fontColor)
- if (d.OsdTimeX !== undefined) form.OsdTimeX = Number(d.OsdTimeX)
- if (d.OsdTimeY !== undefined) form.OsdTimeY = Number(d.OsdTimeY)
- if (d.OsdNameX !== undefined) form.OsdNameX = Number(d.OsdNameX)
- if (d.OsdNameY !== undefined) form.OsdNameY = Number(d.OsdNameY)
- }
- } catch {
- console.error('Failed to get OSD parameters')
- }
- }
- // 保存OSD参数
- async function saveOsd() {
- try {
- const data = {
- EnName: form.EnName ? 1 : 0,
- OsdName: form.OsdName,
- EnTime: form.EnTime ? 1 : 0,
- // showBitrate: form.showBitrate ? 1 : 0,
- // showCustom: form.showCustom ? 1 : 0,
- // customText: form.customText,
- // fontSize: form.fontSize,
- // fontColor: form.fontColor,
- OsdTimeX: form.OsdTimeX,
- OsdTimeY: form.OsdTimeY,
- OsdNameX: form.OsdNameX,
- OsdNameY: form.OsdNameY
- }
- const res = await putOsdPara(data)
- if (res.data === 'ok\n') {
- myVideoRef.value?.refreshWebSocket()
- ElMessage.success('Saved successfully')
- }
- } catch {
- ElMessage.warning('Failed to save')
- }
- }
- async function GetTime() {
- const res = await GetTimePara()
- if (res.data) {
- timeFormat.value = res.data.timeFormat
- }
- updateTime(timeFormat.value)
- }
- onMounted( () => {
- GetTime()
- // timeTimer = setInterval(updateTime, 1000)
- fetchOsd()
- })
- defineExpose({ myVideoRef, fetchOsd, GetTime })
- onUnmounted(() => {
- if (timeTimer) clearInterval(timeTimer)
- endDragOsd()
- })
- </script>
- <style scoped lang="scss">
- .osd-settings {
- display: flex;
- align-items: flex-start;
- gap: 30px;
- width: 100%;
- &__left {
- width: 48%;
- flex-shrink: 0;
- }
- &__video {
- width: 100%;
- height: 0;
- padding-bottom: calc(100% * 9 / 16);
- background: #000;
- border-radius: 4px;
- overflow: hidden;
- position: relative;
- :deep(.preview-container) {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- }
- :deep(.video_container) {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- }
- :deep(#player) {
- width: 100%;
- height: 100%;
- object-fit: contain;
- }
- :deep(.video-control) {
- display: none;
- }
- .osd-overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 10;
- pointer-events: none;
- }
- }
- &__panel {
- flex: 1;
- overflow-y: auto;
- max-height: calc(100vh - 180px);
- padding-right: 8px;
- }
- }
- .osd-tag {
- position: absolute;
- pointer-events: auto;
- cursor: move;
- padding: 2px 6px;
- font-size: 18px;
- font-weight: 500;
- color: #ed0c0c;
- white-space: nowrap;
- user-select: none;
- border: 1px solid transparent;
- transition: border-color 0.2s;
- &:hover,
- &:active {
- border-color: #e74c3c;
- }
- }
- .video-hint {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-top: 3px;
- padding: 8px 12px;
- background: #f0f2f5;
- border-radius: 4px;
- border: 1px solid #e4e7ed;
- &__text {
- font-size: 12px;
- color: #7a7d83;
- line-height: 1.2;
- }
- }
- .section {
- margin-bottom: 20px;
- &__title {
- font-size: 15px;
- font-weight: 600;
- color: #4A90E2;;
- // color: #c0392b;
- margin-bottom: 12px;
- padding-bottom: 6px;
- border-bottom: 1px solid #e3e0e0;
- }
- &__body {
- display: flex;
- flex-direction: column;
- gap: 14px;
- }
- }
- .form-item {
- display: flex;
- align-items: center;
- &--indent {
- padding-left: 24px;
- }
- &__label {
- flex: 0 0 auto;
- font-size: 13px;
- color: #606266;
- padding-right: 12px;
- }
- &__value {
- font-size: 13px;
- color: #606266;
- margin-left: 8px;
- }
- &__control {
- flex: 1;
- max-width: 200px;
- .el-select {
- width: 100%;
- }
- }
- }
- .name-control {
- display: flex;
- align-items: center;
- gap: 8px;
- max-width: none;
- .name-text {
- font-size: 13px;
- color: #303133;
- }
- }
- .save-btn-wrapper {
- margin-top: 40px;
- .el-button {
- min-width: 80px;
- }
- }
- </style>
|