Prechádzať zdrojové kódy

feat(locales, utils, views): add time formatting utility and enhance localization

- Added "停止推流" (Stop Stream) translation to English and Chinese locale files.
- Introduced a new utility file for time formatting using the dayjs library, with functions to format time and date in the Japan timezone.
- Updated various components (camera, camera-vendor, live-stream, lss, machine) to utilize the new time formatting utility, improving consistency in date-time display across the application.
yb 3 dní pred
rodič
commit
3082f7d0fe

+ 1 - 0
src/locales/en.json

@@ -61,6 +61,7 @@
   "修改成功": "Updated successfully",
   "停止": "Stop",
   "停止失败": "Stop failed",
+  "停止推流": "Stop Stream",
   "停止推流失败": "Stop stream failed",
   "停止时间未超过24小时,暂时无法删除": "Cannot delete: stopped less than 24 hours ago",
   "全屏": "Fullscreen",

+ 1 - 0
src/locales/zh-cn.json

@@ -61,6 +61,7 @@
   "修改成功": "修改成功",
   "停止": "停止",
   "停止失败": "停止失败",
+  "停止推流": "停止推流",
   "停止推流失败": "停止推流失败",
   "停止时间未超过24小时,暂时无法删除": "停止时间未超过24小时,暂时无法删除",
   "全屏": "全屏",

+ 42 - 0
src/utils/dayjs.ts

@@ -0,0 +1,42 @@
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+import timezone from 'dayjs/plugin/timezone'
+
+// 扩展 dayjs 插件
+dayjs.extend(utc)
+dayjs.extend(timezone)
+
+// 设置默认时区为日本
+const DEFAULT_TIMEZONE = 'Asia/Tokyo'
+
+/**
+ * 格式化时间(使用日本时区)
+ * @param time 时间字符串
+ * @param format 格式化模板,默认 'YYYY-MM-DD HH:mm:ss'
+ * @returns 格式化后的时间字符串
+ */
+export function formatTime(time: string | undefined | null, format = 'YYYY-MM-DD HH:mm:ss'): string {
+  if (!time) return '-'
+  return dayjs(time).tz(DEFAULT_TIMEZONE).format(format)
+}
+
+/**
+ * 格式化日期(使用日本时区)
+ * @param time 时间字符串
+ * @returns 格式化后的日期字符串 (YYYY-MM-DD)
+ */
+export function formatDate(time: string | undefined | null): string {
+  return formatTime(time, 'YYYY-MM-DD')
+}
+
+/**
+ * 获取配置了时区的 dayjs 实例
+ * @param time 时间字符串
+ * @returns dayjs 实例
+ */
+export function dayjsTZ(time?: string | Date) {
+  return dayjs(time).tz(DEFAULT_TIMEZONE)
+}
+
+export { dayjs }
+export default dayjs

+ 3 - 4
src/views/camera-vendor/index.vue

@@ -240,7 +240,6 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus'
 import { Plus, Edit, Delete, Search, RefreshRight, Setting } from '@element-plus/icons-vue'
-import dayjs from 'dayjs'
 import { Icon } from '@iconify/vue'
 import { useI18n } from 'vue-i18n'
 import {
@@ -250,14 +249,14 @@ import {
   deleteCameraVendor,
   initCameraVendors
 } from '@/api/camera-vendor'
+import { formatTime } from '@/utils/dayjs'
 import type { CameraVendorDTO, CameraVendorAddRequest, CameraVendorUpdateRequest } from '@/types'
 
 const { t } = useI18n({ useScope: 'global' })
 
-// 格式化时间
+// 格式化时间(不含秒)
 function formatDateTime(dateStr: string | undefined): string {
-  if (!dateStr) return '-'
-  return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
+  return formatTime(dateStr, 'YYYY-MM-DD HH:mm')
 }
 
 const loading = ref(false)

+ 3 - 4
src/views/camera/index.vue

@@ -311,18 +311,17 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
 import { Plus, Edit, Delete, Search, RefreshRight, View, Connection } from '@element-plus/icons-vue'
-import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
 import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminCheckCamera } from '@/api/camera'
 import { listAllMachines } from '@/api/machine'
+import { formatTime } from '@/utils/dayjs'
 import type { CameraInfoDTO, ChannelInfoDTO, CameraAddRequest, CameraUpdateRequest, MachineDTO } from '@/types'
 
 const { t } = useI18n({ useScope: 'global' })
 
-// 格式化时间
+// 格式化时间(不含秒)
 function formatDateTime(dateStr: string | undefined): string {
-  if (!dateStr) return '-'
-  return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
+  return formatTime(dateStr, 'YYYY-MM-DD HH:mm')
 }
 
 // 获取品牌标签

+ 60 - 21
src/views/live-stream/index.vue

@@ -80,12 +80,12 @@
         </el-table-column>
         <el-table-column prop="startedAt" :label="t('启动时间')" width="160" align="center">
           <template #default="{ row }">
-            {{ formatDateTime(row.startedAt) }}
+            {{ formatTime(row.startedAt) }}
           </template>
         </el-table-column>
         <el-table-column prop="stoppedAt" :label="t('关闭时间')" width="160" align="center">
           <template #default="{ row }">
-            {{ formatDateTime(row.stoppedAt) }}
+            {{ formatTime(row.stoppedAt) }}
           </template>
         </el-table-column>
         <el-table-column :label="t('操作')" align="center" fixed="right">
@@ -171,7 +171,7 @@
                   <CodeEditor
                     v-model="form.commandTemplate"
                     language="bash"
-                    height="200px"
+                    height="400px"
                     placeholder="#!/bin/bash&#10;# FFmpeg 命令模板,可使用 {RTSP_URL} 等变量"
                   />
                 </div>
@@ -197,9 +197,18 @@
                   <el-tag v-if="playbackInfo.isLive" type="success" size="small">{{ t('直播中') }}</el-tag>
                   <el-tag v-else type="info" size="small">{{ t('离线') }}</el-tag>
                 </div>
-                <el-button type="danger" size="small" @click="drawerVisible = false">
+                <!-- <el-button type="danger" size="small" @click="drawerVisible = false">
                   <Icon icon="mdi:close" width="16" height="16" />
                   {{ t('关闭') }}
+                </el-button> -->
+                <el-button
+                  v-if="currentMediaStream && currentMediaStream.status === '1'"
+                  type="danger"
+                  size="small"
+                  :loading="streamStopping"
+                  @click="handleStopStreamFromPlayer"
+                >
+                  {{ t('停止推流') }}
                 </el-button>
               </div>
               <div class="player-container">
@@ -240,6 +249,8 @@
                   :inactive-text="t('有声')"
                   style="margin-left: 16px"
                 />
+                <el-divider direction="vertical" />
+                <!-- 停止推流按钮 -->
               </div>
             </div>
 
@@ -442,8 +453,8 @@ import {
   Close
 } from '@element-plus/icons-vue'
 import { Icon } from '@iconify/vue'
-import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
+import { formatTime } from '@/utils/dayjs'
 import { listLiveStreams, addLiveStream, updateLiveStream, deleteLiveStream } from '@/api/live-stream'
 import { listAllLssNodes } from '@/api/lss'
 import { adminListCameras } from '@/api/camera'
@@ -457,12 +468,6 @@ const { t } = useI18n({ useScope: 'global' })
 const route = useRoute()
 const router = useRouter()
 
-// 格式化时间
-function formatDateTime(dateStr: string | undefined): string {
-  if (!dateStr) return '-'
-  return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss')
-}
-
 const loading = ref(false)
 const submitLoading = ref(false)
 const streamList = ref<LiveStreamDTO[]>([])
@@ -479,6 +484,7 @@ const commandUpdateLoading = ref(false)
 const activeDrawerTab = ref<'edit' | 'play'>('edit')
 const currentMediaStream = ref<LiveStreamDTO | null>(null)
 const streamStarting = ref(false)
+const streamStopping = ref(false)
 const playerRef = ref<InstanceType<typeof VideoPlayer>>()
 const playbackInfo = ref<{
   videoId: string
@@ -912,6 +918,41 @@ async function handleStartStreamFromPlayer() {
   }
 }
 
+// 从播放器窗口停止推流
+async function handleStopStreamFromPlayer() {
+  if (!currentMediaStream.value) return
+
+  try {
+    await ElMessageBox.confirm(t('确定要停止该推流任务吗?'), t('提示'), {
+      type: 'warning',
+      confirmButtonText: t('确定'),
+      cancelButtonText: t('取消')
+    })
+
+    streamStopping.value = true
+    const res = await stopStreamTask({
+      taskId: currentMediaStream.value.taskStreamSn,
+      lssId: currentMediaStream.value.lssId
+    })
+    if (res.success) {
+      ElMessage.success(t('推流任务已停止'))
+      // 更新当前流的状态
+      currentMediaStream.value.status = '0'
+      playbackInfo.value.isLive = false
+      getList()
+    } else {
+      ElMessage.error(res.errMessage || t('停止失败'))
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('停止推流失败', error)
+      ElMessage.error(t('停止推流失败'))
+    }
+  } finally {
+    streamStopping.value = false
+  }
+}
+
 // 停止推流
 async function handleStopStream(row: LiveStreamDTO) {
   try {
@@ -1332,7 +1373,7 @@ onMounted(async () => {
 }
 
 .play-content {
-  background-color: #f5f7fa;
+  //background-color: #f5f7fa;
 }
 
 .drawer-header {
@@ -1406,8 +1447,8 @@ onMounted(async () => {
 .media-drawer-content {
   display: flex;
   height: 100%;
-  padding: 16px;
-  gap: 16px;
+  padding: 16px 0;
+  gap: 8px;
   overflow: hidden;
 }
 
@@ -1417,18 +1458,17 @@ onMounted(async () => {
   display: flex;
   flex-direction: column;
   min-width: 0;
-  background-color: #fff;
-  border-radius: 8px;
+  background-color: #e1e1e1;
+  // border-radius: 0px;
   overflow: hidden;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  // box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
 
   .video-header {
     display: flex;
     align-items: center;
     justify-content: space-between;
     padding: 12px 16px;
-    background-color: #fff;
-    border-bottom: 1px solid #e5e7eb;
+    background-color: #f5f7fa;
 
     .header-left {
       display: flex;
@@ -1485,8 +1525,7 @@ onMounted(async () => {
     align-items: center;
     gap: 8px;
     padding: 12px 16px;
-    background-color: #fff;
-    border-top: 1px solid #e5e7eb;
+    background: #e5e7eb;
   }
 }
 

+ 1 - 7
src/views/lss/index.vue

@@ -570,8 +570,8 @@ import { Search, RefreshRight, Delete, View, Edit, VideoCamera, Plus, QuestionFi
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Icon } from '@iconify/vue'
 import type { FormInstance, FormRules } from 'element-plus'
-import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
+import { formatTime } from '@/utils/dayjs'
 import { useRouter } from 'vue-router'
 import { listLssNodes, deleteLssNode, setLssNodeEnabled, updateLssNode } from '@/api/lss'
 import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminGetCamera } from '@/api/camera'
@@ -622,12 +622,6 @@ function getStatusTagType(status: LssNodeStatus | undefined): 'success' | 'dange
   }
 }
 
-// 格式化时间
-function formatTime(time: string | undefined): string {
-  if (!time) return '-'
-  return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
-}
-
 // 格式化摄像头状态
 function formatCameraStatus(row: CameraInfoDTO): string {
   if (row.status === 'active') {

+ 3 - 4
src/views/machine/index.vue

@@ -188,17 +188,16 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus'
 import { Plus, Edit, Delete, Search, RefreshRight } from '@element-plus/icons-vue'
-import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
 import { listMachines, addMachine, updateMachine, deleteMachine } from '@/api/machine'
+import { formatTime } from '@/utils/dayjs'
 import type { MachineDTO, MachineAddRequest, MachineUpdateRequest } from '@/types'
 
 const { t } = useI18n({ useScope: 'global' })
 
-// 格式化时间
+// 格式化时间(不含秒)
 function formatDateTime(dateStr: string | undefined): string {
-  if (!dateStr) return '-'
-  return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
+  return formatTime(dateStr, 'YYYY-MM-DD HH:mm')
 }
 
 const loading = ref(false)