index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. <template>
  2. <div class="osd-settings">
  3. <!-- 左侧:视频预览 + OSD拖拽 -->
  4. <div class="osd-settings__left">
  5. <div class="osd-settings__video" ref="videoWrapRef">
  6. <MyVideo ref="myVideoRef" :drag-flag="false"/>
  7. <!-- OSD叠加层:用于拖拽定位 -->
  8. <div class="osd-overlay" ref="overlayRef">
  9. <!-- 时间OSD -->
  10. <div
  11. v-if="form.EnTime"
  12. class="osd-tag osd-tag--time"
  13. :style="osdTimeStyle"
  14. @mousedown.prevent="startDragOsd($event, 'time')"
  15. >
  16. {{ currentTime }}
  17. </div>
  18. <!-- 名称OSD -->
  19. <div
  20. v-if="form.EnName"
  21. class="osd-tag osd-tag--name"
  22. :style="osdNameStyle"
  23. @mousedown.prevent="startDragOsd($event, 'name')"
  24. >
  25. {{ form.OsdName }}
  26. </div>
  27. </div>
  28. </div>
  29. <div class="video-hint">
  30. <span class="video-hint__text">Press and hold the red box to drag the OSD position. Changes take effect after saving.</span>
  31. </div>
  32. </div>
  33. <!-- 右侧设置面板 -->
  34. <div class="osd-settings__panel">
  35. <!-- 显示设置 -->
  36. <div class="section">
  37. <div class="section__title">Display Settings</div>
  38. <div class="section__body">
  39. <!-- 显示名称 -->
  40. <div class="form-item">
  41. <el-checkbox v-model="form.EnName">Display Name</el-checkbox>
  42. </div>
  43. <div class="form-item form-item--indent" v-if="form.EnName">
  44. <span class="form-item__label">Name</span>
  45. <div class="form-item__control name-control">
  46. <span class="name-text">{{ form.OsdName }}</span>
  47. <el-button link type="primary" @click="showEditName = true">
  48. <el-icon>
  49. <Edit/>
  50. </el-icon>
  51. Modify
  52. </el-button>
  53. </div>
  54. </div>
  55. <!-- 显示时间 -->
  56. <div class="form-item">
  57. <el-checkbox v-model="form.EnTime">Display Time:</el-checkbox>
  58. <span class="form-item__value" v-if="form.EnTime">{{ currentTime2 }}</span>
  59. </div>
  60. <!-- 显示码率 -->
  61. <!-- <div class="form-item">
  62. <el-checkbox v-model="form.showBitrate">显示码率:</el-checkbox>
  63. <span class="form-item__value" v-if="form.showBitrate">BR={{ form.bitrateText }}</span>
  64. </div> -->
  65. <!-- 自定义 -->
  66. <!-- <div class="form-item">
  67. <el-checkbox v-model="form.showCustom">自定义</el-checkbox>
  68. <el-input
  69. v-if="form.showCustom"
  70. v-model="form.customText"
  71. size="small"
  72. style="width: 160px; margin-left: 8px"
  73. placeholder="custom"
  74. />
  75. </div> -->
  76. </div>
  77. </div>
  78. <!-- 字体设置 -->
  79. <!-- <div class="section">
  80. <div class="section__title">字体设置</div>
  81. <div class="section__body">
  82. <div class="form-item">
  83. <span class="form-item__label">字体大小:</span>
  84. <div class="form-item__control">
  85. <el-select v-model="form.fontSize">
  86. <el-option label="小" :value="0" />
  87. <el-option label="中" :value="1" />
  88. <el-option label="大" :value="2" />
  89. </el-select>
  90. </div>
  91. </div>
  92. <div class="form-item">
  93. <span class="form-item__label">字体颜色:</span>
  94. <div class="form-item__control">
  95. <el-select v-model="form.fontColor">
  96. <el-option label="自动" :value="0" />
  97. <el-option label="白色" :value="1" />
  98. <el-option label="黑色" :value="2" />
  99. </el-select>
  100. </div>
  101. </div>
  102. </div>
  103. </div> -->
  104. <!-- 保存按钮 -->
  105. <div class="save-btn-wrapper">
  106. <el-button type="primary" round @click="saveOsd">Save</el-button>
  107. </div>
  108. </div>
  109. <!-- 修改名称弹窗 -->
  110. <el-dialog v-model="showEditName" title="Modify Name" width="360px" :close-on-click-modal="false">
  111. <el-input v-model="editNameValue" maxlength="32" placeholder="Please enter a name"/>
  112. <template #footer>
  113. <el-button @click="showEditName = false">Cancel</el-button>
  114. <el-button type="primary" @click="confirmEditName">Confirm</el-button>
  115. </template>
  116. </el-dialog>
  117. </div>
  118. </template>
  119. <script setup lang="ts">
  120. import {ref, reactive, computed, onMounted, onUnmounted} from 'vue'
  121. import {ElMessage} from 'element-plus'
  122. import {Edit} from '@element-plus/icons-vue'
  123. import MyVideo from '@/components/myVideo.vue'
  124. import {getOsdPara, putOsdPara, GetTimePara} from '@/api/setting'
  125. const myVideoRef = ref<InstanceType<typeof MyVideo> | null>(null)
  126. const timeFormat = ref(0)
  127. // 表单数据
  128. const form = reactive({
  129. EnName: true,
  130. OsdName: 'ANSJER',
  131. EnTime: true,
  132. // showBitrate: false,
  133. // bitrateText: '79K',
  134. // showCustom: false,
  135. // customText: 'custom',
  136. // fontSize: 0, // 0=小 1=中 2=大
  137. // fontColor: 0, // 0=自动 1=白色 2=黑色
  138. // OSD归一化坐标 (0~1)
  139. OsdTimeX: 0.01,
  140. OsdTimeY: 0.02,
  141. OsdNameX: 0.90,
  142. OsdNameY: 0.92
  143. })
  144. // 修改名称弹窗
  145. const showEditName = ref(false)
  146. const editNameValue = ref('')
  147. function confirmEditName() {
  148. if (!editNameValue.value.trim()) {
  149. ElMessage.warning('Name cannot be empty')
  150. return
  151. }
  152. form.OsdName = editNameValue.value.trim()
  153. showEditName.value = false
  154. }
  155. // 实时时间显示
  156. const currentTime = ref('')
  157. const currentTime2 = ref('')
  158. let timeTimer: ReturnType<typeof setInterval> | null = null
  159. function updateTime(timeFormat: number) {
  160. const now = new Date()
  161. const y = now.getFullYear()
  162. const M = String(now.getMonth() + 1).padStart(2, '0')
  163. const d = String(now.getDate()).padStart(2, '0')
  164. const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
  165. const w = days[now.getDay()]
  166. const h = String(now.getHours()).padStart(2, '0')
  167. const m = String(now.getMinutes()).padStart(2, '0')
  168. const s = String(now.getSeconds()).padStart(2, '0')
  169. if (timeFormat === 0) {
  170. currentTime.value = `${y}-${M}-${d}\u00A0\u00A0\u00A0${h}:${m}:${s}\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${w}`
  171. currentTime2.value = `${y}-${M}-${d} ${h}:${m}:${s} ${w}`
  172. } else if (timeFormat === 1) {
  173. currentTime.value = `${M}-${d}-${y}\u00A0\u00A0\u00A0${h}:${m}:${s}\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${w}`
  174. currentTime2.value = `${M}-${d}-${y} ${h}:${m}:${s} ${w}`
  175. } else {
  176. currentTime.value = `${d}-${M}-${y}\u00A0\u00A0\u00A0${h}:${m}:${s}\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${w}`
  177. currentTime2.value = `${d}-${M}-${y} ${h}:${m}:${s} ${w}`
  178. }
  179. }
  180. // OSD拖拽相关
  181. const overlayRef = ref<HTMLElement | null>(null)
  182. let draggingTarget: 'time' | 'name' | null = null
  183. let dragStartMouseX = 0
  184. let dragStartMouseY = 0
  185. let dragStartPosX = 0
  186. let dragStartPosY = 0
  187. const osdTimeStyle = computed(() => ({
  188. left: `${form.OsdTimeX * 100}%`,
  189. top: `${form.OsdTimeY * 100}%`
  190. }))
  191. const osdNameStyle = computed(() => ({
  192. left: `${form.OsdNameX * 100}%`,
  193. top: `${form.OsdNameY * 100}%`
  194. }))
  195. function startDragOsd(e: MouseEvent, target: 'time' | 'name') {
  196. draggingTarget = target
  197. dragStartMouseX = e.clientX
  198. dragStartMouseY = e.clientY
  199. dragStartPosX = target === 'time' ? form.OsdTimeX : form.OsdNameX
  200. dragStartPosY = target === 'time' ? form.OsdTimeY : form.OsdNameY
  201. document.addEventListener('mousemove', onDragOsd)
  202. document.addEventListener('mouseup', endDragOsd)
  203. }
  204. function onDragOsd(e: MouseEvent) {
  205. if (!draggingTarget || !overlayRef.value) return
  206. const rect = overlayRef.value.getBoundingClientRect()
  207. const dx = (e.clientX - dragStartMouseX) / rect.width
  208. const dy = (e.clientY - dragStartMouseY) / rect.height
  209. let nx = Math.max(0, Math.min(0.95, dragStartPosX + dx)) // 避免文字溢出到画面外
  210. let ny = Math.max(0, Math.min(0.95, dragStartPosY + dy)) // 避免文字溢出到画面外
  211. if (draggingTarget === 'time') {
  212. form.OsdTimeX = nx
  213. form.OsdTimeY = ny
  214. } else {
  215. form.OsdNameX = nx
  216. form.OsdNameY = ny
  217. }
  218. }
  219. function endDragOsd() {
  220. draggingTarget = null
  221. document.removeEventListener('mousemove', onDragOsd)
  222. document.removeEventListener('mouseup', endDragOsd)
  223. }
  224. // 获取OSD参数
  225. async function fetchOsd() {
  226. try {
  227. const res = await getOsdPara()
  228. if (res.data) {
  229. const d = res.data
  230. if (d.EnName !== undefined) form.EnName = !!d.EnName
  231. if (d.OsdName !== undefined) form.OsdName = d.OsdName
  232. if (d.EnTime !== undefined) form.EnTime = !!d.EnTime
  233. // if (d.showBitrate !== undefined) form.showBitrate = !!d.showBitrate
  234. // if (d.showCustom !== undefined) form.showCustom = !!d.showCustom
  235. // if (d.customText !== undefined) form.customText = d.customText
  236. // if (d.fontSize !== undefined) form.fontSize = Number(d.fontSize)
  237. // if (d.fontColor !== undefined) form.fontColor = Number(d.fontColor)
  238. if (d.OsdTimeX !== undefined) form.OsdTimeX = Number(d.OsdTimeX)
  239. if (d.OsdTimeY !== undefined) form.OsdTimeY = Number(d.OsdTimeY)
  240. if (d.OsdNameX !== undefined) form.OsdNameX = Number(d.OsdNameX)
  241. if (d.OsdNameY !== undefined) form.OsdNameY = Number(d.OsdNameY)
  242. }
  243. } catch {
  244. console.error('Failed to get OSD parameters')
  245. }
  246. }
  247. // 保存OSD参数
  248. async function saveOsd() {
  249. try {
  250. const data = {
  251. EnName: form.EnName ? 1 : 0,
  252. OsdName: form.OsdName,
  253. EnTime: form.EnTime ? 1 : 0,
  254. // showBitrate: form.showBitrate ? 1 : 0,
  255. // showCustom: form.showCustom ? 1 : 0,
  256. // customText: form.customText,
  257. // fontSize: form.fontSize,
  258. // fontColor: form.fontColor,
  259. OsdTimeX: form.OsdTimeX,
  260. OsdTimeY: form.OsdTimeY,
  261. OsdNameX: form.OsdNameX,
  262. OsdNameY: form.OsdNameY
  263. }
  264. const res = await putOsdPara(data)
  265. if (res.data === 'ok\n') {
  266. myVideoRef.value?.refreshWebSocket()
  267. ElMessage.success('Saved successfully')
  268. }
  269. } catch {
  270. ElMessage.warning('Failed to save')
  271. }
  272. }
  273. async function GetTime() {
  274. const res = await GetTimePara()
  275. if (res.data) {
  276. timeFormat.value = res.data.timeFormat
  277. }
  278. updateTime(timeFormat.value)
  279. }
  280. onMounted( () => {
  281. GetTime()
  282. // timeTimer = setInterval(updateTime, 1000)
  283. fetchOsd()
  284. })
  285. defineExpose({ myVideoRef, fetchOsd, GetTime })
  286. onUnmounted(() => {
  287. if (timeTimer) clearInterval(timeTimer)
  288. endDragOsd()
  289. })
  290. </script>
  291. <style scoped lang="scss">
  292. .osd-settings {
  293. display: flex;
  294. align-items: flex-start;
  295. gap: 30px;
  296. width: 100%;
  297. &__left {
  298. width: 48%;
  299. flex-shrink: 0;
  300. }
  301. &__video {
  302. width: 100%;
  303. height: 0;
  304. padding-bottom: calc(100% * 9 / 16);
  305. background: #000;
  306. border-radius: 4px;
  307. overflow: hidden;
  308. position: relative;
  309. :deep(.preview-container) {
  310. position: absolute;
  311. top: 0;
  312. left: 0;
  313. width: 100%;
  314. height: 100%;
  315. }
  316. :deep(.video_container) {
  317. position: absolute;
  318. top: 0;
  319. left: 0;
  320. width: 100%;
  321. height: 100%;
  322. }
  323. :deep(#player) {
  324. width: 100%;
  325. height: 100%;
  326. object-fit: contain;
  327. }
  328. :deep(.video-control) {
  329. display: none;
  330. }
  331. .osd-overlay {
  332. position: absolute;
  333. top: 0;
  334. left: 0;
  335. width: 100%;
  336. height: 100%;
  337. z-index: 10;
  338. pointer-events: none;
  339. }
  340. }
  341. &__panel {
  342. flex: 1;
  343. overflow-y: auto;
  344. max-height: calc(100vh - 180px);
  345. padding-right: 8px;
  346. }
  347. }
  348. .osd-tag {
  349. position: absolute;
  350. pointer-events: auto;
  351. cursor: move;
  352. padding: 2px 6px;
  353. font-size: 18px;
  354. font-weight: 500;
  355. color: #ed0c0c;
  356. white-space: nowrap;
  357. user-select: none;
  358. border: 1px solid transparent;
  359. transition: border-color 0.2s;
  360. &:hover,
  361. &:active {
  362. border-color: #e74c3c;
  363. }
  364. }
  365. .video-hint {
  366. display: flex;
  367. align-items: center;
  368. gap: 12px;
  369. margin-top: 3px;
  370. padding: 8px 12px;
  371. background: #f0f2f5;
  372. border-radius: 4px;
  373. border: 1px solid #e4e7ed;
  374. &__text {
  375. font-size: 12px;
  376. color: #7a7d83;
  377. line-height: 1.2;
  378. }
  379. }
  380. .section {
  381. margin-bottom: 20px;
  382. &__title {
  383. font-size: 15px;
  384. font-weight: 600;
  385. color: #4A90E2;;
  386. // color: #c0392b;
  387. margin-bottom: 12px;
  388. padding-bottom: 6px;
  389. border-bottom: 1px solid #e3e0e0;
  390. }
  391. &__body {
  392. display: flex;
  393. flex-direction: column;
  394. gap: 14px;
  395. }
  396. }
  397. .form-item {
  398. display: flex;
  399. align-items: center;
  400. &--indent {
  401. padding-left: 24px;
  402. }
  403. &__label {
  404. flex: 0 0 auto;
  405. font-size: 13px;
  406. color: #606266;
  407. padding-right: 12px;
  408. }
  409. &__value {
  410. font-size: 13px;
  411. color: #606266;
  412. margin-left: 8px;
  413. }
  414. &__control {
  415. flex: 1;
  416. max-width: 200px;
  417. .el-select {
  418. width: 100%;
  419. }
  420. }
  421. }
  422. .name-control {
  423. display: flex;
  424. align-items: center;
  425. gap: 8px;
  426. max-width: none;
  427. .name-text {
  428. font-size: 13px;
  429. color: #303133;
  430. }
  431. }
  432. .save-btn-wrapper {
  433. margin-top: 40px;
  434. .el-button {
  435. min-width: 80px;
  436. }
  437. }
  438. </style>