Просмотр исходного кода

feat(ui): enhance live stream and LSS management interfaces with new features and optimizations

- Added ElTooltip component to the Vue types for improved UI interactions.
- Introduced configParams and runParams fields in CameraInfoDTO and CameraUpdateRequest for enhanced camera configuration flexibility.
- Streamlined the live stream search form and table layout for better user experience.
- Implemented new functionality for adding cameras and viewing parameters in the LSS management interface.
- Enhanced the LSS editing drawer with tabbed navigation for detailed views and improved data handling.
- Refactored various components for consistency and readability across the application.
yb 1 неделя назад
Родитель
Сommit
44095f3701
4 измененных файлов с 500 добавлено и 147 удалено
  1. 1 0
      src/components.d.ts
  2. 4 0
      src/types/index.ts
  3. 26 17
      src/views/live-stream/index.vue
  4. 469 130
      src/views/lss/index.vue

+ 1 - 0
src/components.d.ts

@@ -52,6 +52,7 @@ declare module 'vue' {
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
+    ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     HelloWorld: typeof import('./components/HelloWorld.vue')['default']
     IEpArrowDown: typeof import('~icons/ep/arrow-down')['default']

+ 4 - 0
src/types/index.ts

@@ -160,6 +160,8 @@ export interface CameraInfoDTO {
   machineName?: string
   enabled: boolean
   channels?: ChannelInfoDTO[]
+  configParams?: string
+  runParams?: string
   createdAt: string
   updatedAt: string
 }
@@ -208,6 +210,8 @@ export interface CameraUpdateRequest {
   channelNo?: string
   remark?: string
   enabled?: boolean
+  configParams?: string
+  runParams?: string
   channels?: ChannelUpdateRequest[]
 }
 

+ 26 - 17
src/views/live-stream/index.vue

@@ -4,12 +4,7 @@
     <div class="search-form">
       <el-form :model="searchForm" inline>
         <el-form-item :label="t('Stream SN')">
-          <el-input
-            v-model.trim="searchForm.aoAgent"
-            placeholder="请输入AO代理"
-            clearable
-            @keyup.enter="handleSearch"
-          />
+          <el-input v-model.trim="searchForm.aoAgent" placeholder="请输入" clearable @keyup.enter="handleSearch" />
         </el-form-item>
         <el-form-item :label="t('功能')">
           <el-input v-model.trim="searchForm.feature" placeholder="请输入功能" clearable @keyup.enter="handleSearch" />
@@ -17,6 +12,7 @@
         <el-form-item>
           <el-button type="primary" :icon="Search" @click="handleSearch">{{ t('查询') }}</el-button>
           <el-button :icon="RefreshRight" @click="handleReset">{{ t('重置') }}</el-button>
+          <el-button type="primary" :icon="Plus" @click="handleAdd">{{ t('新增') }}</el-button>
         </el-form-item>
       </el-form>
     </div>
@@ -32,28 +28,28 @@
         height="100%"
         @sort-change="handleSortChange"
       >
-        <el-table-column prop="streamSn" :label="t('stream sn')" min-width="140" show-overflow-tooltip />
-        <el-table-column prop="name" :label="t('name')" min-width="120" show-overflow-tooltip>
+        <el-table-column prop="streamSn" :label="t('stream sn')" show-overflow-tooltip />
+        <el-table-column prop="name" :label="t('name')" show-overflow-tooltip>
           <template #default="{ row }">
             <el-link type="primary" @click="handleEdit(row)">{{ row.name }}</el-link>
           </template>
         </el-table-column>
-        <el-table-column prop="lssName" :label="t('LSS')" min-width="120" show-overflow-tooltip>
+        <el-table-column prop="lssName" :label="t('LSS')" show-overflow-tooltip>
           <template #default="{ row }">
             <span>{{ row.lssName ? `${row.lssId}/${row.lssName}` : '-' }}</span>
           </template>
         </el-table-column>
-        <el-table-column prop="cameraName" :label="t('摄像头编号')" min-width="150" show-overflow-tooltip>
+        <el-table-column prop="cameraName" :label="t('摄像头编号')" show-overflow-tooltip>
           <template #default="{ row }">
             <span>{{ row.cameraName || '-' }}</span>
           </template>
         </el-table-column>
-        <el-table-column prop="streamMethod" :label="t('推流方式')" width="100" align="center">
+        <el-table-column prop="streamMethod" :label="t('推流方式')" align="center">
           <template #default="{ row }">
             <el-tag size="small">{{ row.streamMethod }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="commandTemplate" :label="t('命令模板')" width="90" align="center">
+        <el-table-column prop="commandTemplate" :label="t('命令模板')" align="center">
           <template #default="{ row }">
             <el-link v-if="row.commandTemplate" type="primary" @click="showCommandTemplate(row)">
               {{ t('查看') }}
@@ -61,7 +57,7 @@
             <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column :label="t('操作')" width="100" align="center">
+        <el-table-column :label="t('操作')" align="center">
           <template #default="{ row }">
             <el-button
               v-if="row.status !== 'running'"
@@ -77,17 +73,17 @@
             </el-button>
           </template>
         </el-table-column>
-        <el-table-column prop="startedAt" :label="t('启动时间')" width="160" align="center">
+        <el-table-column prop="startedAt" :label="t('启动时间')" align="center">
           <template #default="{ row }">
             {{ formatDateTime(row.startedAt) }}
           </template>
         </el-table-column>
-        <el-table-column prop="stoppedAt" :label="t('关闭时间')" width="160" align="center">
+        <el-table-column prop="stoppedAt" :label="t('关闭时间')" align="center">
           <template #default="{ row }">
             {{ formatDateTime(row.stoppedAt) }}
           </template>
         </el-table-column>
-        <el-table-column :label="t('观看')" width="80" align="center">
+        <el-table-column :label="t('观看')" align="center">
           <template #default="{ row }">
             <el-button
               type="primary"
@@ -169,7 +165,7 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
-import { Search, RefreshRight, View } from '@element-plus/icons-vue'
+import { Search, RefreshRight, View, Plus } from '@element-plus/icons-vue'
 import {
   listLiveStreams,
   addLiveStream,
@@ -562,6 +558,19 @@ function handleSortChange({ prop, order }: { prop: string; order: 'ascending' |
   getList()
 }
 
+function handleAdd() {
+  Object.assign(form, {
+    id: undefined,
+    streamSn: '',
+    name: '',
+    lssId: undefined,
+    cameraId: undefined,
+    streamMethod: 'ffmpeg',
+    commandTemplate: ''
+  })
+  dialogVisible.value = true
+}
+
 function handleEdit(row: LiveStreamDTO) {
   Object.assign(form, {
     id: row.id,

+ 469 - 130
src/views/lss/index.vue

@@ -51,6 +51,7 @@
       >
         <el-table-column prop="lssId" label="LSS ID" min-width="120" sortable="custom" show-overflow-tooltip />
         <el-table-column prop="lssName" :label="t('名称')" min-width="140" sortable="custom" show-overflow-tooltip />
+        <el-table-column prop="ip" :label="t('IP')" min-width="180" sortable="custom" show-overflow-tooltip />
         <el-table-column prop="address" :label="t('地址')" min-width="180" sortable="custom" show-overflow-tooltip />
         <el-table-column prop="status" :label="t('状态')" min-width="100" sortable="custom">
           <template #default="{ row }">
@@ -59,9 +60,9 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="currentTasks" :label="t('当前任务')" min-width="100" sortable="custom" align="center">
+        <!-- <el-table-column prop="currentTasks" :label="t('当前任务')" min-width="100" sortable="custom" align="center">
           <template #default="{ row }">{{ row.currentTasks }} / {{ row.maxTasks }}</template>
-        </el-table-column>
+        </el-table-column> -->
         <el-table-column prop="enabled" :label="t('启用')" min-width="80" align="center">
           <template #default="{ row }">
             <el-switch
@@ -72,7 +73,7 @@
           </template>
         </el-table-column>
         <el-table-column prop="ffmpegVersion" label="FFmpeg" show-overflow-tooltip />
-        <el-table-column :label="t('摄像头')" min-width="80" align="center">
+        <el-table-column :label="t('设备列表')" align="center">
           <template #default="{ row }">
             <el-button type="primary" link :icon="VideoCamera" @click="handleCameraList(row)" />
           </template>
@@ -82,6 +83,16 @@
             <el-button type="primary" link :icon="View" @click="handleViewDetail(row)" />
           </template>
         </el-table-column>
+        <el-table-column label="心跳时间" align="center">
+          <template #default="{ row }">
+            {{ formatTime(row.lastHeartbeatAt) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="status" align="center">
+          <template #default="{ row }">
+            {{ row.status === 'ONLINE' ? '在线' : '离线' }}
+          </template>
+        </el-table-column>
         <el-table-column :label="t('操作')" align="center" fixed="right">
           <template #default="{ row }">
             <el-button type="primary" link :icon="Edit" @click="handleEdit(row)" />
@@ -122,43 +133,148 @@
     <el-drawer
       v-model="lssEditDrawerVisible"
       direction="rtl"
-      size="500px"
+      :size="editDrawerSize"
       :with-header="false"
       destroy-on-close
       class="lss-edit-drawer"
     >
       <div class="drawer-content">
-        <div class="drawer-header">LSS详情</div>
+        <!-- 顶部 Tabs -->
+        <el-tabs v-model="editActiveTab" class="drawer-tabs">
+          <el-tab-pane label="LSS详情" name="detail" />
+          <el-tab-pane label="摄像头列表" name="camera" />
+          <el-tab-pane label="推币机列表" name="pusher" />
+        </el-tabs>
+
         <div class="drawer-body">
-          <div class="lss-detail-form">
-            <div class="form-item">
-              <label class="form-label">LSS ID:</label>
-              <span class="form-value">{{ currentLss?.lssId }}</span>
-            </div>
-            <div class="form-item">
-              <label class="form-label">名称:</label>
-              <el-input v-model="lssEditForm.lssName" placeholder="请输入名称" />
-            </div>
-            <div class="form-item">
-              <label class="form-label">地址:</label>
-              <el-input v-model="lssEditForm.address" placeholder="请输入地址" />
-            </div>
-            <div class="form-item">
-              <label class="form-label">IP:</label>
-              <span class="form-value">{{ currentLss?.publicIp || '-' }}</span>
-            </div>
-            <div class="form-item">
-              <label class="form-label">心跳:</label>
-              <span class="form-value" :class="getHeartbeatClass(currentLss?.heartbeat)">
-                {{ formatHeartbeat(currentLss) }}
-              </span>
-            </div>
-            <div class="form-item">
-              <label class="form-label">ably信息:</label>
-              <el-input v-model="lssEditForm.ablyInfo" placeholder="请输入ably信息" />
+          <!-- LSS 详情 Tab -->
+          <div v-show="editActiveTab === 'detail'" class="lss-detail-form">
+            <el-form ref="lssEditFormRef" :model="lssEditForm" label-width="80px" label-position="left">
+              <el-form-item label="LSS ID:">
+                <span class="form-value">{{ currentLss?.lssId }}</span>
+              </el-form-item>
+              <el-form-item label="名称:" prop="lssName">
+                <el-input v-model="lssEditForm.lssName" placeholder="请输入名称" style="width: 180px" />
+              </el-form-item>
+              <el-form-item label="地址:" prop="address">
+                <el-input v-model="lssEditForm.address" placeholder="请输入地址" />
+              </el-form-item>
+              <el-form-item label="IP:">
+                <span class="form-value">{{ lssEditForm?.ip }}</span>
+              </el-form-item>
+              <el-form-item label="心跳:">
+                <span class="heartbeat-status" :class="getHeartbeatClass(currentLss?.heartbeat)">
+                  {{ formatHeartbeat(currentLss) }}
+                  <span class="heartbeat-dot" :class="getHeartbeatClass(currentLss?.heartbeat)"></span>
+                </span>
+                <el-tooltip placement="right" effect="light">
+                  <template #content>
+                    <div class="heartbeat-tooltip">
+                      <div class="tooltip-title">心跳状态:</div>
+                      <div>active - 持续返回中,并且频繁</div>
+                      <div>hold - 五分钟内有返回</div>
+                      <div>dead - 五分钟内没有返回</div>
+                      <div class="tooltip-format">表现形式为:</div>
+                      <div class="tooltip-example">Status [yy-mm-dd 00:00:00]</div>
+                    </div>
+                  </template>
+                  <el-icon class="heartbeat-info-icon">
+                    <QuestionFilled />
+                  </el-icon>
+                </el-tooltip>
+              </el-form-item>
+              <el-form-item label="ably信息:" prop="ablyInfo">
+                <div class="textarea-wrapper">
+                  <el-input
+                    type="textarea"
+                    :rows="8"
+                    v-model="lssEditForm.ablyInfo"
+                    placeholder="请输入ably信息"
+                    maxlength="1000"
+                    show-word-limit
+                  />
+                </div>
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <!-- 摄像头列表 Tab -->
+          <div v-show="editActiveTab === 'camera'" class="tab-content" v-loading="cameraLoading">
+            <div class="camera-toolbar">
+              <el-form :model="cameraSearchForm" inline>
+                <el-form-item>
+                  <el-input
+                    v-model.trim="cameraSearchForm.keyword"
+                    placeholder="IP / 设备ID / 名称"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleCameraSearch"
+                  />
+                </el-form-item>
+                <el-form-item>
+                  <el-select v-model="cameraSearchForm.status" placeholder="状态" clearable style="width: 120px">
+                    <el-option label="全部" value="" />
+                    <el-option label="在线" value="ONLINE" />
+                    <el-option label="离线" value="OFFLINE" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" :icon="Search" @click="handleCameraSearch">查询</el-button>
+                  <el-button :icon="RefreshRight" @click="handleCameraReset">重置</el-button>
+                </el-form-item>
+              </el-form>
+              <el-button type="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
             </div>
+            <el-empty v-if="!cameraLoading && cameraList.length === 0" description="暂无关联设备" />
+            <el-table v-else :data="cameraList" stripe size="small" border>
+              <el-table-column prop="ip" label="本地IP" min-width="110" />
+              <el-table-column prop="cameraId" label="设备ID" min-width="100" show-overflow-tooltip />
+              <el-table-column prop="name" label="名称" min-width="100" show-overflow-tooltip />
+              <el-table-column label="状态(心跳)" min-width="140">
+                <template #default="{ row }">
+                  <span :class="['status-text', row.status === 'ONLINE' ? 'status-active' : 'status-dead']">
+                    {{ formatCameraStatus(row) }}
+                  </span>
+                </template>
+              </el-table-column>
+              <el-table-column label="参数配置" min-width="80" align="center">
+                <template #default="{ row }">
+                  <el-button type="primary" link @click="handleViewConfig(row)">查看</el-button>
+                </template>
+              </el-table-column>
+              <el-table-column label="运行参数" min-width="80" align="center">
+                <template #default="{ row }">
+                  <el-button type="primary" link @click="handleViewRunParams(row)">查看</el-button>
+                </template>
+              </el-table-column>
+              <el-table-column prop="brand" label="厂商" min-width="90">
+                <template #default="{ row }">
+                  {{ formatBrand(row.brand) }}
+                </template>
+              </el-table-column>
+              <el-table-column prop="model" label="型号" min-width="130" show-overflow-tooltip />
+              <el-table-column label="添加时间" min-width="140">
+                <template #default="{ row }">
+                  {{ formatTime(row.createdAt) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="设备控制" min-width="100" align="center" fixed="right">
+                <template #default="{ row }">
+                  <el-button type="primary" link :icon="Edit" @click="handleEditCamera(row)" />
+                  <el-button type="danger" link :icon="Delete" @click="handleDeleteCamera(row)" />
+                  <el-button type="primary" link :icon="View" @click="handleViewCamera(row)" />
+                </template>
+              </el-table-column>
+            </el-table>
+            <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个设备</div>
+          </div>
+
+          <!-- 推币机列表 Tab -->
+          <div v-show="editActiveTab === 'pusher'" class="tab-content">
+            <el-empty description="暂无推币机数据" />
           </div>
         </div>
+
         <div class="drawer-footer">
           <el-button @click="lssEditDrawerVisible = false">{{ t('取消') }}</el-button>
           <el-button type="primary" :loading="lssUpdating" @click="handleUpdateLss">{{ t('更新') }}</el-button>
@@ -173,75 +289,129 @@
       direction="rtl"
       size="80%"
       destroy-on-close
+      class="device-drawer"
     >
-      <div v-loading="cameraLoading">
-        <div class="camera-toolbar">
-          <el-form :model="cameraSearchForm" inline>
-            <el-form-item>
-              <el-input
-                v-model.trim="cameraSearchForm.keyword"
-                placeholder="IP / 设备ID / 名称"
-                clearable
-                style="width: 200px"
-                @keyup.enter="handleCameraSearch"
-              />
-            </el-form-item>
-            <el-form-item>
-              <el-select v-model="cameraSearchForm.status" placeholder="状态" clearable style="width: 120px">
-                <el-option label="全部" value="" />
-                <el-option label="在线" value="ONLINE" />
-                <el-option label="离线" value="OFFLINE" />
-              </el-select>
-            </el-form-item>
-            <el-form-item>
-              <el-button type="primary" :icon="Search" @click="handleCameraSearch">查询</el-button>
-              <el-button :icon="RefreshRight" @click="handleCameraReset">重置</el-button>
-            </el-form-item>
-          </el-form>
-          <el-button type="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
-        </div>
-        <el-empty v-if="!cameraLoading && cameraList.length === 0" description="暂无关联设备" />
-        <el-table v-else :data="cameraList" stripe size="small" border>
-          <el-table-column prop="ip" label="本地IP" min-width="110" />
-          <el-table-column prop="cameraId" label="设备ID" min-width="100" show-overflow-tooltip />
-          <el-table-column prop="name" label="名称" min-width="100" show-overflow-tooltip />
-          <el-table-column label="状态(心跳)" min-width="140">
-            <template #default="{ row }">
-              <span :class="['status-text', row.status === 'ONLINE' ? 'status-active' : 'status-dead']">
-                {{ formatCameraStatus(row) }}
-              </span>
-            </template>
-          </el-table-column>
-          <el-table-column label="参数配置" min-width="80" align="center">
-            <template #default="{ row }">
-              <el-button type="primary" link @click="handleViewConfig(row)">查看</el-button>
-            </template>
-          </el-table-column>
-          <el-table-column label="运行参数" min-width="80" align="center">
-            <template #default="{ row }">
-              <el-button type="primary" link @click="handleViewRunParams(row)">查看</el-button>
-            </template>
-          </el-table-column>
-          <el-table-column prop="brand" label="厂商" min-width="90">
-            <template #default="{ row }">
-              {{ formatBrand(row.brand) }}
-            </template>
-          </el-table-column>
-          <el-table-column prop="model" label="型号" min-width="130" show-overflow-tooltip />
-          <el-table-column label="添加时间" min-width="140">
-            <template #default="{ row }">
-              {{ formatTime(row.createdAt) }}
-            </template>
-          </el-table-column>
-          <el-table-column label="设备控制" min-width="100" align="center" fixed="right">
-            <template #default="{ row }">
-              <el-button type="primary" link :icon="Edit" @click="handleEditCamera(row)" />
-              <el-button type="danger" link :icon="Delete" @click="handleDeleteCamera(row)" />
-            </template>
-          </el-table-column>
-        </el-table>
-        <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个设备</div>
-      </div>
+      <el-tabs v-model="deviceActiveTab" class="device-tabs">
+        <el-tab-pane label="摄像头列表" name="camera">
+          <div v-loading="cameraLoading" class="tab-content-wrapper">
+            <div class="camera-toolbar">
+              <el-form :model="cameraSearchForm" inline>
+                <el-form-item>
+                  <el-input
+                    v-model.trim="cameraSearchForm.keyword"
+                    placeholder="IP / 设备ID / 名称"
+                    clearable
+                    style="width: 200px"
+                    @keyup.enter="handleCameraSearch"
+                  />
+                </el-form-item>
+                <el-form-item>
+                  <el-select v-model="cameraSearchForm.status" placeholder="状态" clearable style="width: 120px">
+                    <el-option label="全部" value="" />
+                    <el-option label="在线" value="ONLINE" />
+                    <el-option label="离线" value="OFFLINE" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" :icon="Search" @click="handleCameraSearch">查询</el-button>
+                  <el-button :icon="RefreshRight" @click="handleCameraReset">重置</el-button>
+                </el-form-item>
+              </el-form>
+              <el-button type="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
+            </div>
+            <el-empty v-if="!cameraLoading && cameraList.length === 0" description="暂无关联设备" />
+            <el-table v-else :data="cameraList" stripe size="small" border>
+              <el-table-column prop="ip" label="本地IP" min-width="110" />
+              <el-table-column prop="cameraId" label="设备ID" min-width="100" show-overflow-tooltip />
+              <el-table-column prop="name" label="名称" min-width="100" show-overflow-tooltip />
+              <el-table-column label="状态(心跳)" min-width="140">
+                <template #default="{ row }">
+                  <span :class="['status-text', row.status === 'ONLINE' ? 'status-active' : 'status-dead']">
+                    {{ formatCameraStatus(row) }}
+                  </span>
+                </template>
+              </el-table-column>
+              <el-table-column label="参数配置" min-width="80" align="center">
+                <template #default="{ row }">
+                  <el-button type="primary" link @click="handleViewConfig(row)">查看</el-button>
+                </template>
+              </el-table-column>
+              <el-table-column label="运行参数" min-width="80" align="center">
+                <template #default="{ row }">
+                  <el-button type="primary" link @click="handleViewRunParams(row)">查看</el-button>
+                </template>
+              </el-table-column>
+              <el-table-column prop="brand" label="厂商" min-width="90">
+                <template #default="{ row }">
+                  {{ formatBrand(row.brand) }}
+                </template>
+              </el-table-column>
+              <el-table-column prop="model" label="型号" min-width="130" show-overflow-tooltip />
+              <el-table-column label="添加时间" min-width="140">
+                <template #default="{ row }">
+                  {{ formatTime(row.createdAt) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="设备控制" min-width="100" align="center" fixed="right">
+                <template #default="{ row }">
+                  <el-button type="primary" link :icon="Edit" @click="handleEditCamera(row)" />
+                  <el-button type="danger" link :icon="Delete" @click="handleDeleteCamera(row)" />
+                  <el-button type="primary" link :icon="View" @click="handleViewCamera(row)" />
+                </template>
+              </el-table-column>
+            </el-table>
+            <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个设备</div>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="推币机列表" name="pusher">
+          <div class="tab-content-wrapper">
+            <div class="camera-toolbar">
+              <el-form inline>
+                <el-form-item>
+                  <el-input placeholder="设备ID / 名称" clearable style="width: 200px" />
+                </el-form-item>
+                <el-form-item>
+                  <el-select placeholder="状态" clearable style="width: 120px">
+                    <el-option label="全部" value="" />
+                    <el-option label="在线" value="ONLINE" />
+                    <el-option label="离线" value="OFFLINE" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" :icon="Search">查询</el-button>
+                  <el-button :icon="RefreshRight">重置</el-button>
+                </el-form-item>
+              </el-form>
+              <el-button type="primary" :icon="Plus">{{ t('新增') }}</el-button>
+            </div>
+            <el-empty description="暂无推币机数据" />
+          </div>
+        </el-tab-pane>
+        <el-tab-pane label="其他设备" name="other">
+          <div class="tab-content-wrapper">
+            <div class="camera-toolbar">
+              <el-form inline>
+                <el-form-item>
+                  <el-input placeholder="设备ID / 名称" clearable style="width: 200px" />
+                </el-form-item>
+                <el-form-item>
+                  <el-select placeholder="状态" clearable style="width: 120px">
+                    <el-option label="全部" value="" />
+                    <el-option label="在线" value="ONLINE" />
+                    <el-option label="离线" value="OFFLINE" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" :icon="Search">查询</el-button>
+                  <el-button :icon="RefreshRight">重置</el-button>
+                </el-form-item>
+              </el-form>
+              <el-button type="primary" :icon="Plus">{{ t('新增') }}</el-button>
+            </div>
+            <el-empty description="暂无其他设备数据" />
+          </div>
+        </el-tab-pane>
+      </el-tabs>
     </el-drawer>
 
     <!-- 摄像头编辑弹窗 -->
@@ -321,6 +491,20 @@
       </template>
     </el-dialog>
 
+    <!-- 参数配置/运行参数弹窗 -->
+    <el-dialog v-model="paramsDialogVisible" :title="paramsDialogTitle" width="600px" :close-on-click-modal="false">
+      <el-input
+        v-model="paramsContent"
+        type="textarea"
+        :rows="15"
+        :placeholder="paramsDialogType === 'config' ? '请输入参数配置(JSON 格式)' : '请输入运行参数(JSON 格式)'"
+      />
+      <template #footer>
+        <el-button @click="paramsDialogVisible = false">{{ t('取消') }}</el-button>
+        <el-button type="primary" :loading="paramsSubmitting" @click="handleSaveParams">{{ t('更新') }}</el-button>
+      </template>
+    </el-dialog>
+
     <!-- 分页 -->
     <div class="pagination-container">
       <el-pagination
@@ -338,8 +522,8 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, computed } from 'vue'
-import { Search, RefreshRight, Delete, View, Edit, VideoCamera, Plus } from '@element-plus/icons-vue'
+import { ref, reactive, onMounted, computed, watch } from 'vue'
+import { Search, RefreshRight, Delete, View, Edit, VideoCamera, Plus, QuestionFilled } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { listLssNodes, deleteLssNode, setLssNodeEnabled, updateLssNode } from '@/api/lss'
 import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera } from '@/api/camera'
@@ -407,6 +591,10 @@ function formatCameraStatus(row: CameraInfoDTO): string {
   }
 }
 
+function handleViewCamera(row: CameraInfoDTO) {
+  console.log(row)
+}
+
 // 格式化品牌
 function formatBrand(brand: string | undefined): string {
   const brandMap: Record<string, string> = {
@@ -447,14 +635,56 @@ function getHeartbeatClass(status: LssHeartbeatStatus | undefined): string {
 
 // 查看参数配置
 function handleViewConfig(row: CameraInfoDTO) {
-  ElMessage.info(`查看 ${row.name} 的参数配置`)
-  // TODO: 打开参数配置弹窗
+  paramsCamera.value = row
+  paramsDialogType.value = 'config'
+  paramsDialogTitle.value = `参数配置 - ${row.name}`
+  paramsContent.value = row.configParams || ''
+  paramsDialogVisible.value = true
 }
 
 // 查看运行参数
 function handleViewRunParams(row: CameraInfoDTO) {
-  ElMessage.info(`查看 ${row.name} 的运行参数`)
-  // TODO: 打开运行参数弹窗
+  paramsCamera.value = row
+  paramsDialogType.value = 'run'
+  paramsDialogTitle.value = `运行参数 - ${row.name}`
+  paramsContent.value = row.runParams || ''
+  paramsDialogVisible.value = true
+}
+
+// 保存参数配置/运行参数
+async function handleSaveParams() {
+  if (!paramsCamera.value) return
+
+  paramsSubmitting.value = true
+  try {
+    const data: CameraUpdateRequest = {
+      id: paramsCamera.value.id
+    }
+    if (paramsDialogType.value === 'config') {
+      data.configParams = paramsContent.value
+    } else {
+      data.runParams = paramsContent.value
+    }
+
+    const res = await adminUpdateCamera(data)
+    if (res.success) {
+      ElMessage.success('保存成功')
+      paramsDialogVisible.value = false
+      // 更新本地数据
+      if (paramsDialogType.value === 'config') {
+        paramsCamera.value.configParams = paramsContent.value
+      } else {
+        paramsCamera.value.runParams = paramsContent.value
+      }
+    } else {
+      ElMessage.error(res.errMessage || '保存失败')
+    }
+  } catch (error) {
+    console.error('保存参数失败', error)
+    ElMessage.error('保存失败')
+  } finally {
+    paramsSubmitting.value = false
+  }
 }
 
 const loading = ref(false)
@@ -468,15 +698,24 @@ const currentLss = ref<LssNodeDTO | null>(null)
 // LSS 编辑抽屉状态
 const lssEditDrawerVisible = ref(false)
 const lssUpdating = ref(false)
+const editActiveTab = ref('detail')
+const lssEditFormRef = ref<FormInstance>()
 const lssEditForm = reactive({
   lssName: '',
   address: '',
+  ip: '',
   ablyInfo: ''
 })
 
+// 根据当前 tab 计算抽屉宽度
+const editDrawerSize = computed(() => {
+  return editActiveTab.value === 'detail' ? '500px' : '80%'
+})
+
 // 设备列表抽屉状态
 const cameraDrawerVisible = ref(false)
 const cameraLoading = ref(false)
+const deviceActiveTab = ref('camera')
 const cameraList = ref<CameraInfoDTO[]>([])
 
 // 摄像头搜索表单
@@ -493,6 +732,14 @@ const cameraSubmitting = ref(false)
 const currentCamera = ref<CameraInfoDTO | null>(null)
 const availableVendors = ref<CameraVendorDTO[]>([])
 
+// 参数配置/运行参数弹窗状态
+const paramsDialogVisible = ref(false)
+const paramsDialogTitle = ref('')
+const paramsDialogType = ref<'config' | 'run'>('config')
+const paramsContent = ref('')
+const paramsSubmitting = ref(false)
+const paramsCamera = ref<CameraInfoDTO | null>(null)
+
 // 摄像头表单
 const cameraForm = reactive({
   selectedVendorId: null as number | null,
@@ -621,7 +868,9 @@ function handleEdit(row: LssNodeDTO) {
   currentLss.value = row
   lssEditForm.lssName = row.lssName || ''
   lssEditForm.address = row.address || ''
+  lssEditForm.ip = row.ip || ''
   lssEditForm.ablyInfo = row.ablyInfo || ''
+  editActiveTab.value = 'detail'
   lssEditDrawerVisible.value = true
 }
 
@@ -629,6 +878,7 @@ async function handleCameraList(row: LssNodeDTO) {
   currentLss.value = row
   cameraSearchForm.keyword = ''
   cameraSearchForm.status = ''
+  deviceActiveTab.value = 'camera'
   cameraDrawerVisible.value = true
   await loadCameraList()
 }
@@ -901,6 +1151,15 @@ async function handleDelete(row: LssNodeDTO) {
   }
 }
 
+// 监听 tab 切换,加载对应数据
+watch(editActiveTab, (newTab) => {
+  if (newTab === 'camera' && currentLss.value) {
+    cameraSearchForm.keyword = ''
+    cameraSearchForm.status = ''
+    loadCameraList()
+  }
+})
+
 onMounted(() => {
   getList()
 })
@@ -1043,13 +1302,38 @@ onMounted(() => {
   height: 100%;
 }
 
-.drawer-header {
+.drawer-tabs {
   flex-shrink: 0;
-  padding: 16px 20px;
-  font-size: 16px;
-  font-weight: 500;
-  color: #303133;
   border-bottom: 1px solid #e5e7eb;
+
+  :deep(.el-tabs__header) {
+    margin: 0;
+    padding: 0 20px;
+  }
+
+  :deep(.el-tabs__nav-wrap::after) {
+    display: none;
+  }
+
+  :deep(.el-tabs__item) {
+    height: 48px;
+    line-height: 48px;
+    font-size: 14px;
+    color: #606266;
+
+    &.is-active {
+      color: #409eff;
+      font-weight: 500;
+    }
+
+    &:hover {
+      color: #409eff;
+    }
+  }
+
+  :deep(.el-tabs__active-bar) {
+    background-color: #409eff;
+  }
 }
 
 .drawer-body {
@@ -1059,31 +1343,86 @@ onMounted(() => {
 }
 
 .lss-detail-form {
-  .form-item {
-    display: flex;
-    align-items: flex-start;
-    margin-bottom: 16px;
-
-    .form-label {
-      flex-shrink: 0;
-      width: 80px;
-      line-height: 32px;
-      color: #606266;
-      font-size: 14px;
-    }
+  :deep(.el-form-item) {
+    margin-bottom: 18px;
+  }
 
-    .form-value {
-      line-height: 32px;
-      color: #303133;
-      font-size: 14px;
-    }
+  :deep(.el-form-item__label) {
+    color: #606266;
+    font-size: 14px;
+  }
 
-    .el-input {
-      flex: 1;
-    }
+  .form-value {
+    line-height: 32px;
+    color: #303133;
+    font-size: 14px;
+  }
+
+  .textarea-wrapper {
+    width: 100%;
   }
 }
 
+.heartbeat-status {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  line-height: 32px;
+  font-size: 14px;
+}
+
+.heartbeat-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+
+  &.status-active {
+    background-color: #67c23a;
+  }
+
+  &.status-hold {
+    background-color: #e6a23c;
+  }
+
+  &.status-dead {
+    background-color: #f56c6c;
+  }
+}
+
+.heartbeat-info-icon {
+  margin-left: 8px;
+  color: #909399;
+  cursor: pointer;
+
+  &:hover {
+    color: #409eff;
+  }
+}
+
+.heartbeat-tooltip {
+  font-size: 12px;
+  line-height: 1.6;
+
+  .tooltip-title {
+    font-weight: 500;
+    margin-bottom: 4px;
+  }
+
+  .tooltip-format {
+    margin-top: 8px;
+    font-weight: 500;
+  }
+
+  .tooltip-example {
+    color: #409eff;
+  }
+}
+
+.tab-content {
+  min-height: 200px;
+}
+
 .drawer-footer {
   flex-shrink: 0;
   display: flex;