index.vue 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364
  1. <template>
  2. <div class="page-container">
  3. <!-- 搜索表单 -->
  4. <div class="search-form">
  5. <el-form :model="searchForm" inline data-id="search-form">
  6. <el-form-item>
  7. <el-input
  8. v-model.trim="searchForm.lssId"
  9. :placeholder="t('LSS ID')"
  10. clearable
  11. data-id="search-keyword"
  12. @keyup.enter="handleSearch"
  13. />
  14. </el-form-item>
  15. <el-form-item>
  16. <el-input
  17. v-model.trim="searchForm.lssName"
  18. :placeholder="t('名称')"
  19. clearable
  20. data-id="search-keyword"
  21. @keyup.enter="handleSearch"
  22. />
  23. </el-form-item>
  24. <el-form-item>
  25. <el-select v-model="searchForm.status" :placeholder="t('心跳')" clearable data-id="search-enabled">
  26. <el-option :label="t('全部')" value="" />
  27. <el-option :label="t('active')" value="active" />
  28. <el-option :label="t('hold')" value="hold" />
  29. <el-option :label="t('dead')" value="dead" />
  30. </el-select>
  31. </el-form-item>
  32. <el-form-item>
  33. <el-button type="primary" data-id="btn-search" @click="handleSearch">
  34. <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
  35. {{ t('查询') }}
  36. </el-button>
  37. <el-button type="info" data-id="btn-reset" @click="handleReset">
  38. <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
  39. {{ t('重置') }}
  40. </el-button>
  41. </el-form-item>
  42. </el-form>
  43. </div>
  44. <!-- 数据表格 -->
  45. <div class="table-wrapper">
  46. <el-table
  47. ref="tableRef"
  48. v-loading="loading"
  49. :data="lssList"
  50. stripe
  51. size="default"
  52. data-id="lss-table"
  53. height="100%"
  54. @sort-change="handleSortChange"
  55. >
  56. <el-table-column prop="lssId" :label="t('LSS ID')" min-width="120" sortable="custom" show-overflow-tooltip />
  57. <el-table-column prop="lssName" :label="t('名称')" min-width="140" sortable="custom" show-overflow-tooltip />
  58. <el-table-column prop="address" :label="t('地址')" min-width="180" sortable="custom" show-overflow-tooltip />
  59. <el-table-column prop="ip" :label="t('IP')" min-width="180" sortable="custom" show-overflow-tooltip />
  60. <el-table-column :label="t('心跳')" width="220" align="center">
  61. <template #default="{ row }">
  62. <span :class="getHeartbeatClass(row.status)">
  63. {{ t(row.status) || '-' }}
  64. </span>
  65. | {{ formatTime(row.lastHeartbeatAt) }}
  66. </template>
  67. </el-table-column>
  68. <el-table-column :label="t('设备列表')" align="center">
  69. <template #default="{ row }">
  70. <el-button type="primary" link @click="handleEdit(row, 'camera')">
  71. <Icon icon="mdi:cctv" width="20" height="20" />
  72. </el-button>
  73. </template>
  74. </el-table-column>
  75. <el-table-column :label="t('ably')" align="center" fixed="right">
  76. <template #default="{ row }">
  77. {{ row.ablyClientId }}
  78. </template>
  79. </el-table-column>
  80. <el-table-column :label="t('操作')" width="130" align="center" fixed="right">
  81. <template #default="{ row }">
  82. <el-button data-id="btn-edit" type="primary" link @click="handleEdit(row, 'detail')">
  83. <Icon icon="mdi:note-edit-outline" width="20" height="20" />
  84. </el-button>
  85. <el-button
  86. :class="{ 'scan-btn': true, scanned: row.scanned }"
  87. data-id="btn-scan-devices"
  88. type="primary"
  89. link
  90. @click="handleScanDevices(row)"
  91. >
  92. <Icon icon="mdi:radar" width="20" height="20" />
  93. </el-button>
  94. <el-button data-id="btn-delete" type="danger" link @click="handleDelete(row)">
  95. <Icon icon="mdi:delete" width="20" height="20" />
  96. </el-button>
  97. </template>
  98. </el-table-column>
  99. </el-table>
  100. </div>
  101. <!-- LSS 详情抽屉 -->
  102. <el-drawer v-model="detailDrawerVisible" :title="t('LSS 节点详情')" direction="rtl" size="500px" destroy-on-close>
  103. <el-descriptions :column="1" border>
  104. <el-descriptions-item :label="t('LSS ID')">{{ currentLss?.lssId }}</el-descriptions-item>
  105. <el-descriptions-item :label="t('名称')">{{ currentLss?.lssName }}</el-descriptions-item>
  106. <el-descriptions-item :label="t('地址')">{{ currentLss?.address }}</el-descriptions-item>
  107. <el-descriptions-item :label="t('机器 ID')">{{ currentLss?.machineId || '-' }}</el-descriptions-item>
  108. <el-descriptions-item :label="t('状态')">
  109. <el-tag :type="getStatusTagType(currentLss?.status)" size="small">
  110. {{ formatStatus(currentLss?.status) }}
  111. </el-tag>
  112. </el-descriptions-item>
  113. <el-descriptions-item :label="t('任务数')">
  114. {{ currentLss?.currentTasks }} / {{ currentLss?.maxTasks }}
  115. </el-descriptions-item>
  116. <el-descriptions-item :label="t('FFmpeg 版本')">{{ currentLss?.ffmpegVersion || '-' }}</el-descriptions-item>
  117. <el-descriptions-item :label="t('系统信息')">{{ currentLss?.systemInfo || '-' }}</el-descriptions-item>
  118. <el-descriptions-item :label="t('启用状态')">
  119. <el-tag :type="currentLss?.enabled ? 'success' : 'info'" size="small">
  120. {{ currentLss?.enabled ? '已启用' : '已禁用' }}
  121. </el-tag>
  122. </el-descriptions-item>
  123. <el-descriptions-item :label="t('创建时间')">{{ formatTime(currentLss?.createdAt) }}</el-descriptions-item>
  124. <el-descriptions-item :label="t('更新时间')">{{ formatTime(currentLss?.updatedAt) }}</el-descriptions-item>
  125. </el-descriptions>
  126. </el-drawer>
  127. <!-- LSS 编辑抽屉 -->
  128. <el-drawer
  129. v-model="lssEditDrawerVisible"
  130. direction="rtl"
  131. :size="editDrawerSize"
  132. :with-header="false"
  133. destroy-on-close
  134. class="lss-edit-drawer"
  135. >
  136. <div class="drawer-content">
  137. <!-- 顶部 Tabs -->
  138. <el-tabs v-model="editActiveTab" class="drawer-tabs">
  139. <el-tab-pane :label="t('LSS详情')" name="detail" />
  140. <el-tab-pane :label="t('摄像头列表')" name="camera" />
  141. <el-tab-pane :label="t('推币机列表')" name="pusher" />
  142. </el-tabs>
  143. <div class="drawer-body">
  144. <!-- LSS 详情 Tab -->
  145. <div v-show="editActiveTab === 'detail'" class="lss-detail-form">
  146. <el-form ref="lssEditFormRef" :model="lssEditForm" label-width="auto">
  147. <el-form-item :label="t('LSS ID') + ':'">
  148. <span class="form-value">{{ currentLss?.lssId }}</span>
  149. </el-form-item>
  150. <el-form-item :label="t('名称') + ':'" prop="lssName">
  151. <el-input v-model="lssEditForm.lssName" :placeholder="t('请输入名称')" style="width: 180px" />
  152. </el-form-item>
  153. <el-form-item :label="t('地址') + ':'" prop="address">
  154. <el-input type="textarea" :rows="5" v-model="lssEditForm.address" :placeholder="t('请输入地址')" />
  155. </el-form-item>
  156. <el-form-item :label="t('IP') + ':'">
  157. <span class="form-value">{{ currentLss?.ip }}</span>
  158. </el-form-item>
  159. <el-form-item :label="t('心跳') + ':'">
  160. <span class="heartbeat-status" :class="getHeartbeatClass(currentLss?.status)">
  161. {{ formatHeartbeat(currentLss) }}
  162. <span class="heartbeat-dot" :class="getHeartbeatClass(currentLss?.status)"></span>
  163. </span>
  164. <el-tooltip placement="right" effect="light">
  165. <template #content>
  166. <div class="heartbeat-tooltip">
  167. <div class="tooltip-title">{{ t('心跳状态') }}:</div>
  168. <div>{{ t('活跃') }} - {{ t('持续返回中,并且频繁') }}</div>
  169. <div>{{ t('待机') }} - {{ t('五分钟内有返回') }}</div>
  170. <div>{{ t('离线') }} - {{ t('五分钟内没有返回') }}</div>
  171. <div class="tooltip-format">{{ t('表现形式为') }}:</div>
  172. <div class="tooltip-example">{{ t('Status') }} [yy-mm-dd 00:00:00]</div>
  173. </div>
  174. </template>
  175. <Icon icon="mdi:help-circle" class="heartbeat-info-icon" width="16" height="16" />
  176. </el-tooltip>
  177. </el-form-item>
  178. <el-form-item :label="t('ably') + ':'" prop="ably">
  179. <div class="textarea-wrapper">
  180. <el-input
  181. disabled
  182. type="textarea"
  183. :rows="8"
  184. v-model="lssEditForm.ably"
  185. :placeholder="t('请输入ably信息')"
  186. maxlength="1000"
  187. show-word-limit
  188. />
  189. </div>
  190. </el-form-item>
  191. </el-form>
  192. </div>
  193. <!-- 摄像头列表 Tab -->
  194. <div v-show="editActiveTab === 'camera'" v-loading="cameraLoading" class="tab-content-wrapper">
  195. <div class="camera-toolbar">
  196. <el-form :model="cameraSearchForm" inline>
  197. <el-form-item>
  198. <el-input
  199. v-model.trim="cameraSearchForm.cameraId"
  200. :placeholder="t('设备ID')"
  201. clearable
  202. style="width: 200px"
  203. @keyup.enter="handleCameraSearch"
  204. />
  205. </el-form-item>
  206. <el-form-item>
  207. <el-input
  208. v-model.trim="cameraSearchForm.cameraName"
  209. :placeholder="t('名称')"
  210. clearable
  211. style="width: 200px"
  212. @keyup.enter="handleCameraSearch"
  213. />
  214. </el-form-item>
  215. <el-form-item>
  216. <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
  217. <el-option :label="t('全部')" value="" />
  218. <el-option label="active" value="active" />
  219. <el-option label="hold" value="hold" />
  220. <el-option label="dead" value="dead" />
  221. </el-select>
  222. </el-form-item>
  223. <el-form-item>
  224. <el-button type="primary" @click="handleCameraSearch">
  225. <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
  226. {{ t('查询') }}
  227. </el-button>
  228. <el-button type="info" @click="handleCameraReset">
  229. <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
  230. {{ t('重置') }}
  231. </el-button>
  232. <el-button type="primary" @click="handleAddCamera">
  233. <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
  234. {{ t('新增') }}
  235. </el-button>
  236. </el-form-item>
  237. </el-form>
  238. </div>
  239. <el-table :data="cameraList" stripe :height="cameraTableHeight">
  240. <template #empty>
  241. <el-empty :description="t('暂无关联设备')" />
  242. </template>
  243. <el-table-column prop="cameraId" :label="t('设备ID')" min-width="100" show-overflow-tooltip />
  244. <el-table-column prop="cameraName" :label="t('名称')" min-width="100" show-overflow-tooltip />
  245. <el-table-column :label="t('状态(心跳)')" min-width="140">
  246. <template #default="{ row }">
  247. <span :class="['status-text', row.status === 'active' ? 'status-active' : 'status-dead']">
  248. {{ formatCameraStatus(row) }}
  249. </span>
  250. </template>
  251. </el-table-column>
  252. <el-table-column :label="t('参数配置')" min-width="80" align="center">
  253. <template #default="{ row }">
  254. <el-button type="primary" link @click="handleViewConfig(row)">{{ t('查看') }}</el-button>
  255. </template>
  256. </el-table-column>
  257. <el-table-column :label="t('运行参数')" min-width="80" align="center">
  258. <template #default="{ row }">
  259. <el-button type="primary" link @click="handleViewRunParams(row)">{{ t('查看') }}</el-button>
  260. </template>
  261. </el-table-column>
  262. <el-table-column prop="brand" :label="t('厂商')" min-width="90">
  263. <template #default="{ row }">
  264. {{ formatBrand(row.vendorName) }}
  265. </template>
  266. </el-table-column>
  267. <el-table-column prop="model" :label="t('型号')" min-width="130" show-overflow-tooltip />
  268. <el-table-column :label="t('添加时间')" min-width="140">
  269. <template #default="{ row }">
  270. {{ formatTime(row.createdAt) }}
  271. </template>
  272. </el-table-column>
  273. <el-table-column :label="t('设备控制')" width="130" align="center">
  274. <template #default="{ row }">
  275. <el-button type="primary" link @click="handleEditCamera(row)">
  276. <Icon icon="mdi:note-edit-outline" width="20" height="20" />
  277. </el-button>
  278. <!-- :tooltip="t('查看Cloudflare Stream')" -->
  279. <el-button type="primary" link @click="handleViewCamera(row)">
  280. <Icon
  281. icon="mdi:controller-right"
  282. :class="['crosshairs-btn', { active: row.streamSn }]"
  283. width="20"
  284. height="20"
  285. />
  286. </el-button>
  287. <el-button type="danger" link @click="handleDeleteCamera(row)">
  288. <Icon icon="mdi:delete" width="20" height="20" />
  289. </el-button>
  290. </template>
  291. </el-table-column>
  292. </el-table>
  293. <div class="camera-pagination">
  294. <el-pagination
  295. v-model:current-page="cameraCurrentPage"
  296. v-model:page-size="cameraPageSize"
  297. :page-sizes="[10, 15, 20, 50, 100]"
  298. :total="cameraTotal"
  299. layout="total, sizes, prev, pager, next, jumper"
  300. background
  301. @size-change="handleCameraSizeChange"
  302. @current-change="handleCameraPageChange"
  303. />
  304. </div>
  305. </div>
  306. <!-- 推币机列表 Tab -->
  307. <div v-show="editActiveTab === 'pusher'" class="tab-content">
  308. <el-empty :description="t('暂无推币机数据')" />
  309. </div>
  310. </div>
  311. <div v-show="editActiveTab === 'detail'" class="drawer-footer">
  312. <el-button @click="lssEditDrawerVisible = false">{{ t('取消') }}</el-button>
  313. <el-button type="primary" :loading="lssUpdating" @click="handleUpdateLss">{{ t('更新') }}</el-button>
  314. </div>
  315. </div>
  316. </el-drawer>
  317. <!-- 设备列表抽屉 -->
  318. <el-drawer
  319. v-model="cameraDrawerVisible"
  320. :title="`${t('设备列表')} - ${currentLss?.lssName || ''}`"
  321. direction="rtl"
  322. size="80%"
  323. destroy-on-close
  324. class="device-drawer"
  325. >
  326. <el-tabs v-model="deviceActiveTab" class="device-tabs">
  327. <el-tab-pane :label="t('摄像头列表')" name="camera">
  328. <div v-loading="cameraLoading" class="tab-content-wrapper">
  329. <div class="camera-toolbar">
  330. <el-form :model="cameraSearchForm" inline>
  331. <el-form-item>
  332. <el-input
  333. v-model.trim="cameraSearchForm.cameraId"
  334. :placeholder="t('设备ID')"
  335. clearable
  336. style="width: 150px"
  337. @keyup.enter="handleCameraSearch"
  338. />
  339. </el-form-item>
  340. <el-form-item>
  341. <el-input
  342. v-model.trim="cameraSearchForm.cameraName"
  343. :placeholder="t('设备名称')"
  344. clearable
  345. style="width: 150px"
  346. @keyup.enter="handleCameraSearch"
  347. />
  348. </el-form-item>
  349. <el-form-item>
  350. <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
  351. <el-option :label="t('全部')" value="" />
  352. <el-option label="active" value="active" />
  353. <el-option label="hold" value="hold" />
  354. <el-option label="dead" value="dead" />
  355. </el-select>
  356. </el-form-item>
  357. <el-form-item>
  358. <el-button type="primary" @click="handleCameraSearch">
  359. <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
  360. {{ t('查询') }}
  361. </el-button>
  362. <el-button type="info" @click="handleCameraReset">
  363. <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
  364. {{ t('重置') }}
  365. </el-button>
  366. <el-button type="primary" @click="handleAddCamera">
  367. <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
  368. {{ t('新增') }}
  369. </el-button>
  370. </el-form-item>
  371. </el-form>
  372. </div>
  373. <el-table :data="cameraList" stripe>
  374. <template #empty>
  375. <el-empty :description="t('暂无关联设备')" />
  376. </template>
  377. <!-- <el-table-column prop="ip" label="本地IP" min-width="110" /> -->
  378. <el-table-column prop="cameraId" :label="t('设备ID')" min-width="100" show-overflow-tooltip />
  379. <el-table-column prop="cameraName" :label="t('名称')" min-width="100" show-overflow-tooltip />
  380. <el-table-column :label="t('状态(心跳)')" min-width="140">
  381. <template #default="{ row }">
  382. <span :class="['status-text', row.status === 'active' ? 'status-active' : 'status-dead']">
  383. {{ formatCameraStatus(row) }}
  384. </span>
  385. </template>
  386. </el-table-column>
  387. <el-table-column :label="t('参数配置')" min-width="80" align="center">
  388. <template #default="{ row }">
  389. <el-button type="primary" link @click="handleViewConfig(row)">查看</el-button>
  390. </template>
  391. </el-table-column>
  392. <el-table-column :label="t('运行参数')" min-width="80" align="center">
  393. <template #default="{ row }">
  394. <el-button type="primary" link @click="handleViewRunParams(row)">查看</el-button>
  395. </template>
  396. </el-table-column>
  397. <el-table-column prop="vendorName" :label="t('厂商')" min-width="90">
  398. <template #default="{ row }">
  399. {{ formatBrand(row.vendorName) }}
  400. </template>
  401. </el-table-column>
  402. <el-table-column prop="model" :label="t('型号')" min-width="130" show-overflow-tooltip />
  403. <el-table-column :label="t('添加时间')" min-width="140">
  404. <template #default="{ row }">
  405. {{ formatTime(row.createdAt) }}
  406. </template>
  407. </el-table-column>
  408. <el-table-column :label="t('设备控制')" min-width="100" align="center" fixed="right">
  409. <template #default="{ row }">
  410. <el-button type="primary" link @click="handleEditCamera(row)">
  411. <Icon icon="mdi:note-edit-outline" width="20" height="20" />
  412. </el-button>
  413. <el-button type="danger" link @click="handleDeleteCamera(row)">
  414. <Icon icon="mdi:delete" width="20" height="20" />
  415. </el-button>
  416. <el-button link :class="['crosshairs-btn', { active: !row.streamSn }]" @click="handleViewCamera(row)">
  417. <Icon icon="mdi:crosshairs" />
  418. </el-button>
  419. </template>
  420. </el-table-column>
  421. </el-table>
  422. <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个设备</div>
  423. </div>
  424. </el-tab-pane>
  425. <el-tab-pane :label="t('推币机列表')" name="pusher">
  426. <div class="tab-content-wrapper">
  427. <div class="camera-toolbar">
  428. <el-form inline>
  429. <el-form-item>
  430. <el-input :placeholder="t('设备ID / 名称')" clearable style="width: 200px" />
  431. </el-form-item>
  432. <el-form-item>
  433. <el-select :placeholder="t('状态')" clearable style="width: 120px">
  434. <el-option :label="t('全部')" value="" />
  435. <el-option :label="t('在线')" value="ONLINE" />
  436. <el-option :label="t('离线')" value="OFFLINE" />
  437. </el-select>
  438. </el-form-item>
  439. <el-form-item>
  440. <el-button type="primary">
  441. <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
  442. {{ t('查询') }}
  443. </el-button>
  444. <el-button type="info">
  445. <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
  446. {{ t('重置') }}
  447. </el-button>
  448. </el-form-item>
  449. </el-form>
  450. <el-button type="primary">
  451. <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
  452. {{ t('新增') }}
  453. </el-button>
  454. </div>
  455. <el-empty :description="t('暂无推币机数据')" />
  456. </div>
  457. </el-tab-pane>
  458. <el-tab-pane :label="t('其他设备')" name="other">
  459. <div class="tab-content-wrapper">
  460. <div class="camera-toolbar">
  461. <el-form inline>
  462. <el-form-item>
  463. <el-input :placeholder="t('设备ID / 名称')" clearable style="width: 200px" />
  464. </el-form-item>
  465. <el-form-item>
  466. <el-select :placeholder="t('状态')" clearable style="width: 120px">
  467. <el-option :label="t('全部')" value="" />
  468. <el-option :label="t('在线')" value="ONLINE" />
  469. <el-option :label="t('离线')" value="OFFLINE" />
  470. </el-select>
  471. </el-form-item>
  472. <el-form-item>
  473. <el-button type="primary">
  474. <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
  475. {{ t('查询') }}
  476. </el-button>
  477. <el-button type="info">
  478. <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
  479. {{ t('重置') }}
  480. </el-button>
  481. </el-form-item>
  482. </el-form>
  483. <el-button type="primary">
  484. <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
  485. {{ t('新增') }}
  486. </el-button>
  487. </div>
  488. <el-empty :description="t('暂无其他设备数据')" />
  489. </div>
  490. </el-tab-pane>
  491. </el-tabs>
  492. </el-drawer>
  493. <!-- 摄像头编辑抽屉 -->
  494. <el-drawer
  495. v-model="cameraDialogVisible"
  496. :title="isEditCamera ? t('摄像头详情') : t('添加摄像头')"
  497. direction="rtl"
  498. size="600px"
  499. :close-on-click-modal="false"
  500. :show-close="false"
  501. destroy-on-close
  502. class="camera-edit-drawer"
  503. >
  504. <el-form ref="cameraFormRef" :model="cameraForm" :rules="cameraRules" label-width="auto">
  505. <div class="camera-form-container">
  506. <!-- <el-form-item label="IP 地址" prop="ip">
  507. <el-input v-model="cameraForm.ip" :disabled="isEditCamera" placeholder="请输入 IP 地址" />
  508. </el-form-item> -->
  509. <el-form-item :label="t('设备ID') + ':'" prop="cameraId">
  510. <el-input
  511. v-model="cameraForm.cameraId"
  512. :disabled="isEditCamera"
  513. :placeholder="t('请输入设备ID')"
  514. style="max-width: 300px"
  515. >
  516. <template v-if="!isEditCamera" #suffix>
  517. <el-icon class="generate-id-btn" @click="generateCameraId">
  518. <Icon icon="mdi:refresh" />
  519. </el-icon>
  520. </template>
  521. </el-input>
  522. </el-form-item>
  523. <el-form-item :label="t('设备名称') + ':'" prop="cameraName">
  524. <el-input v-model="cameraForm.cameraName" :placeholder="t('请输入设备名称')" style="max-width: 300px" />
  525. </el-form-item>
  526. <el-form-item :label="t('厂商') + ':'" prop="vendorName">
  527. <el-select
  528. v-model="cameraForm.vendorName"
  529. :placeholder="t('请选择摄像头')"
  530. style="width: 100%; max-width: 300px"
  531. filterable
  532. @change="handleVendorSelect"
  533. >
  534. <el-option
  535. v-for="vendor in [
  536. { id: 'hikvision', name: '海康威视' },
  537. { id: 'dahua', name: '大华' },
  538. { id: 'uniview', name: '宇视' },
  539. { id: 'other', name: '其他' }
  540. ]"
  541. :key="vendor.id"
  542. :label="vendor.name"
  543. :value="vendor.id"
  544. />
  545. </el-select>
  546. </el-form-item>
  547. <el-form-item :label="t('型号') + ':'" prop="model">
  548. <el-input v-model="cameraForm.model" :placeholder="t('请输入型号')" style="max-width: 300px" />
  549. </el-form-item>
  550. <!-- <el-form-item label="摄像头型号" prop="cameraId">
  551. <el-select v-model="cameraForm.selectedVendorId" placeholder="请选择摄像头" style="width: 100%" filterable
  552. @change="handleVendorSelect">
  553. <el-option v-for="vendor in cameraVendorList" :key="vendor.id" :label="vendor.name" :value="vendor.id" />
  554. </el-select>
  555. </el-form-item> -->
  556. <!-- <el-form-item label="名称" prop="name">
  557. <el-input v-model="cameraForm.name" placeholder="请输入名称" />
  558. </el-form-item> -->
  559. <!-- <el-form-item label="端口" prop="port">
  560. <el-input-number v-model="cameraForm.port" :min="1" :max="65535" style="width: 100%" />
  561. </el-form-item> -->
  562. <!-- <el-form-item label="用户名" prop="username">
  563. <el-input v-model="cameraForm.username" placeholder="请输入用户名" />
  564. </el-form-item>
  565. <el-form-item label="密码" prop="password">
  566. <el-input v-model="cameraForm.password" type="password" show-password placeholder="请输入密码" />
  567. </el-form-item> -->
  568. <el-form-item :label="t('参数配置') + ':'">
  569. <CodeEditor
  570. v-model="cameraForm.paramConfig"
  571. language="json"
  572. height="200px"
  573. :placeholder="t('请输入参数配置 (JSON)')"
  574. />
  575. </el-form-item>
  576. <br />
  577. <el-form-item :label="t('运行参数') + ':'">
  578. <CodeEditor
  579. v-model="cameraForm.runtimeParams"
  580. language="json"
  581. height="200px"
  582. :placeholder="t('设备运行参数 (JSON)')"
  583. />
  584. </el-form-item>
  585. <el-form-item v-if="isEditCamera" :label="t('添加时间') + ':'">
  586. {{ formatTime(cameraForm.createdAt) }}
  587. </el-form-item>
  588. </div>
  589. </el-form>
  590. <template #footer>
  591. <div class="drawer-footer">
  592. <el-button @click="cameraDialogVisible = false">{{ t('取消') }}</el-button>
  593. <el-button type="primary" :loading="cameraSubmitting" @click="handleSubmitCamera">
  594. {{ isEditCamera ? t('更新') : t('添加') }}
  595. </el-button>
  596. </div>
  597. </template>
  598. </el-drawer>
  599. <!-- 参数配置/运行参数抽屉 -->
  600. <el-drawer
  601. v-model="paramsDialogVisible"
  602. :title="paramsDialogTitle"
  603. direction="rtl"
  604. size="650px"
  605. :close-on-click-modal="false"
  606. :show-close="false"
  607. destroy-on-close
  608. class="params-drawer"
  609. >
  610. <div class="params-content-container">
  611. <CodeEditor
  612. v-model="paramsContent"
  613. language="json"
  614. height="500px"
  615. :placeholder="
  616. paramsDialogType === 'config' ? t('请输入参数配置(JSON 格式)') : t('请输入运行参数(JSON 格式)')
  617. "
  618. />
  619. </div>
  620. <template #footer>
  621. <div class="drawer-footer">
  622. <el-button @click="paramsDialogVisible = false">{{ t('取消') }}</el-button>
  623. <el-button type="primary" :loading="paramsSubmitting" @click="handleSaveParams">{{ t('更新') }}</el-button>
  624. </div>
  625. </template>
  626. </el-drawer>
  627. <!-- 扫描设备抽屉 -->
  628. <el-drawer
  629. v-model="scanDrawerVisible"
  630. :title="t('扫描')"
  631. direction="rtl"
  632. size="50%"
  633. destroy-on-close
  634. class="scan-drawer"
  635. >
  636. <div class="scan-drawer-content">
  637. <div class="scan-toolbar">
  638. <div class="scan-toolbar-left">
  639. <el-button v-if="scanMatched" @click="handleRematch">
  640. <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
  641. {{ t('再次匹配') }}
  642. </el-button>
  643. </div>
  644. <div class="scan-toolbar-right">
  645. <el-button @click="credentialDrawerVisible = true">
  646. <Icon icon="mdi:key-variant" width="16" height="16" style="margin-right: 4px" />
  647. {{ t('账号配置') }}
  648. </el-button>
  649. </div>
  650. </div>
  651. <el-table v-loading="scanLoading" :data="discoveredDevices" stripe>
  652. <template #empty>
  653. <el-empty :description="t('暂无发现设备')" />
  654. </template>
  655. <el-table-column type="index" :label="t('序号')" width="60" align="center" />
  656. <el-table-column prop="ip" label="IP" min-width="120" show-overflow-tooltip />
  657. <el-table-column prop="port" :label="t('端口')" width="80" align="center" />
  658. <el-table-column prop="deviceName" :label="t('设备名称')" min-width="140" show-overflow-tooltip />
  659. <el-table-column :label="t('匹配状态')" width="100" align="center">
  660. <template #default="{ row }">
  661. <Icon
  662. v-if="row.matchStatus === 'MATCHED'"
  663. icon="mdi:check-circle"
  664. width="20"
  665. height="20"
  666. style="color: #67c23a"
  667. />
  668. <Icon
  669. v-else-if="row.matchStatus === 'UNMATCHED'"
  670. icon="mdi:close-circle"
  671. width="20"
  672. height="20"
  673. style="color: #f56c6c"
  674. />
  675. <Icon
  676. v-else-if="row.matchStatus === 'MATCHING'"
  677. icon="mdi:progress-clock"
  678. width="20"
  679. height="20"
  680. style="color: #e6a23c"
  681. />
  682. <span v-else style="color: #909399">{{ t('待匹配') }}</span>
  683. </template>
  684. </el-table-column>
  685. <el-table-column :label="t('操作')" width="80" align="center">
  686. <template #default="{ row }">
  687. <el-button
  688. v-if="row.matchStatus === 'MATCHED' && !row.bound"
  689. type="primary"
  690. link
  691. @click="handleBindDevice(row)"
  692. >
  693. {{ t('添加') }}
  694. </el-button>
  695. </template>
  696. </el-table-column>
  697. </el-table>
  698. </div>
  699. <template #footer>
  700. <div class="drawer-footer">
  701. <el-button @click="scanDrawerVisible = false">{{ t('取消') }}</el-button>
  702. <el-button v-if="!scanMatched" type="primary" :loading="matchLoading" @click="handleTriggerMatch">
  703. {{ t('匹配') }}
  704. </el-button>
  705. <el-button v-else type="primary" @click="scanDrawerVisible = false">
  706. {{ t('完成') }}
  707. </el-button>
  708. </div>
  709. </template>
  710. </el-drawer>
  711. <!-- 账号配置抽屉(第二层) -->
  712. <el-drawer
  713. v-model="credentialDrawerVisible"
  714. :title="t('账号配置')"
  715. direction="rtl"
  716. size="50%"
  717. destroy-on-close
  718. :append-to-body="true"
  719. class="credential-drawer"
  720. >
  721. <div class="credential-content">
  722. <div class="credential-toolbar">
  723. <el-form :model="credentialSearchForm" inline>
  724. <el-form-item>
  725. <el-input
  726. v-model.trim="credentialSearchForm.username"
  727. :placeholder="t('用户名')"
  728. clearable
  729. style="width: 150px"
  730. />
  731. </el-form-item>
  732. <el-form-item>
  733. <el-input
  734. v-model.trim="credentialSearchForm.password"
  735. :placeholder="t('密码')"
  736. clearable
  737. style="width: 150px"
  738. />
  739. </el-form-item>
  740. <el-form-item>
  741. <el-button type="primary" @click="loadCredentials">
  742. <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
  743. {{ t('查询') }}
  744. </el-button>
  745. <el-button type="info" @click="handleCredentialReset">
  746. <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
  747. {{ t('重置') }}
  748. </el-button>
  749. <el-button type="primary" @click="handleAddCredential">
  750. <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
  751. {{ t('新增') }}
  752. </el-button>
  753. </el-form-item>
  754. </el-form>
  755. </div>
  756. <el-table v-loading="credentialLoading" :data="filteredCredentials" stripe>
  757. <template #empty>
  758. <el-empty :description="t('暂无发现设备')" />
  759. </template>
  760. <el-table-column prop="username" :label="t('用户名')" min-width="120" show-overflow-tooltip />
  761. <el-table-column prop="password" :label="t('密码')" min-width="120" show-overflow-tooltip />
  762. <el-table-column :label="t('设备控制')" width="100" align="center">
  763. <template #default="{ row }">
  764. <el-button type="primary" link @click="handleEditCredential(row)">
  765. <Icon icon="mdi:note-edit-outline" width="20" height="20" />
  766. </el-button>
  767. <el-button type="danger" link @click="handleDeleteCredential(row)">
  768. <Icon icon="mdi:delete" width="20" height="20" />
  769. </el-button>
  770. </template>
  771. </el-table-column>
  772. </el-table>
  773. </div>
  774. </el-drawer>
  775. <!-- 凭证编辑对话框 -->
  776. <el-dialog
  777. v-model="credentialDialogVisible"
  778. :title="isEditCredential ? t('编辑凭证') : t('新增凭证')"
  779. width="500px"
  780. :close-on-click-modal="false"
  781. :append-to-body="true"
  782. destroy-on-close
  783. >
  784. <el-form ref="credentialFormRef" :model="credentialForm" :rules="credentialRules" label-width="100px">
  785. <el-form-item :label="t('凭证名称')" prop="name">
  786. <el-input v-model="credentialForm.name" :placeholder="t('请输入凭证名称')" />
  787. </el-form-item>
  788. <el-form-item :label="t('用户名')" prop="username">
  789. <el-input v-model="credentialForm.username" :placeholder="t('请输入用户名')" />
  790. </el-form-item>
  791. <el-form-item :label="t('密码')" prop="password">
  792. <el-input v-model="credentialForm.password" :placeholder="t('请输入密码')" />
  793. </el-form-item>
  794. <el-form-item :label="t('厂商')">
  795. <el-input v-model="credentialForm.vendor" :placeholder="t('请选择')" />
  796. </el-form-item>
  797. <el-form-item :label="t('优先级')">
  798. <el-input-number v-model="credentialForm.priority" :min="0" :max="999" />
  799. </el-form-item>
  800. <el-form-item :label="t('启用')">
  801. <el-switch v-model="credentialForm.enabled" />
  802. </el-form-item>
  803. <el-form-item :label="t('备注')">
  804. <el-input v-model="credentialForm.remark" type="textarea" :rows="3" :placeholder="t('请输入备注')" />
  805. </el-form-item>
  806. </el-form>
  807. <template #footer>
  808. <el-button @click="credentialDialogVisible = false">{{ t('取消') }}</el-button>
  809. <el-button type="primary" :loading="credentialSubmitting" @click="handleSubmitCredential">
  810. {{ isEditCredential ? t('更新') : t('添加') }}
  811. </el-button>
  812. </template>
  813. </el-dialog>
  814. <!-- 分页 -->
  815. <div class="pagination-container">
  816. <el-pagination
  817. v-model:current-page="currentPage"
  818. v-model:page-size="pageSize"
  819. :page-sizes="[10, 15, 20, 50, 100]"
  820. :total="total"
  821. layout="total, sizes, prev, pager, next, jumper"
  822. background
  823. @size-change="handleSizeChange"
  824. @current-change="handleCurrentChange"
  825. />
  826. </div>
  827. </div>
  828. </template>
  829. <script setup lang="ts">
  830. import { onMounted, watch } from 'vue'
  831. import { Icon } from '@iconify/vue'
  832. import { useI18n } from 'vue-i18n'
  833. import { formatTime } from '@/utils/dayjs'
  834. import CodeEditor from '@/components/CodeEditor.vue'
  835. import {
  836. formatStatus,
  837. getStatusTagType,
  838. formatCameraStatus,
  839. formatHeartbeat,
  840. getHeartbeatClass,
  841. formatBrand
  842. } from './composables/useFormatters'
  843. import { useLssList } from './composables/useLssList'
  844. import { useCameraList } from './composables/useCameraList'
  845. import { useScanDevices } from './composables/useScanDevices'
  846. import { useCredentials } from './composables/useCredentials'
  847. const { t } = useI18n({ useScope: 'global' })
  848. // ==================== LSS 列表 ====================
  849. const {
  850. loading,
  851. lssList,
  852. tableRef,
  853. detailDrawerVisible,
  854. currentLss,
  855. lssEditDrawerVisible,
  856. lssUpdating,
  857. editActiveTab,
  858. lssEditFormRef,
  859. lssEditForm,
  860. editDrawerSize,
  861. searchForm,
  862. currentPage,
  863. pageSize,
  864. total,
  865. getList,
  866. handleSearch,
  867. handleReset,
  868. handleSortChange,
  869. handleSizeChange,
  870. handleCurrentChange,
  871. handleUpdateLss,
  872. handleDelete
  873. } = useLssList()
  874. // ==================== 摄像头列表 ====================
  875. const {
  876. cameraDrawerVisible,
  877. cameraLoading,
  878. deviceActiveTab,
  879. cameraList,
  880. cameraCurrentPage,
  881. cameraPageSize,
  882. cameraTotal,
  883. cameraTableHeight,
  884. cameraSearchForm,
  885. cameraDialogVisible,
  886. cameraFormRef,
  887. isEditCamera,
  888. cameraSubmitting,
  889. cameraForm,
  890. cameraRules,
  891. paramsDialogVisible,
  892. paramsDialogTitle,
  893. paramsDialogType,
  894. paramsContent,
  895. paramsSubmitting,
  896. loadCameraList,
  897. handleCameraSearch,
  898. handleCameraReset,
  899. handleCameraSizeChange,
  900. handleCameraPageChange,
  901. handleVendorSelect,
  902. generateCameraId,
  903. handleAddCamera,
  904. handleEditCamera,
  905. handleSubmitCamera,
  906. handleDeleteCamera,
  907. handleViewCamera,
  908. handleViewConfig,
  909. handleViewRunParams,
  910. handleSaveParams,
  911. resetCameraSearch
  912. } = useCameraList(currentLss)
  913. // ==================== 扫描设备 ====================
  914. const {
  915. scanDrawerVisible,
  916. scanLoading,
  917. matchLoading,
  918. scanMatched,
  919. discoveredDevices,
  920. handleScanDevices,
  921. handleTriggerMatch,
  922. handleRematch,
  923. handleBindDevice
  924. } = useScanDevices()
  925. // ==================== 凭证管理 ====================
  926. const {
  927. credentialDrawerVisible,
  928. credentialLoading,
  929. credentialSearchForm,
  930. filteredCredentials,
  931. credentialDialogVisible,
  932. isEditCredential,
  933. credentialSubmitting,
  934. credentialFormRef,
  935. credentialForm,
  936. credentialRules,
  937. loadCredentials,
  938. handleCredentialReset,
  939. handleAddCredential,
  940. handleEditCredential,
  941. handleSubmitCredential,
  942. handleDeleteCredential
  943. } = useCredentials()
  944. // ==================== 页面级编排 ====================
  945. function handleEdit(row: any, tab: 'detail' | 'camera' | 'pusher') {
  946. currentLss.value = row
  947. lssEditForm.lssName = row.lssName || ''
  948. lssEditForm.address = row.address || ''
  949. lssEditForm.ably = JSON.stringify(row.ably)
  950. editActiveTab.value = tab
  951. lssEditDrawerVisible.value = true
  952. resetCameraSearch()
  953. loadCameraList()
  954. }
  955. // 监听 tab 切换,加载对应数据
  956. watch(editActiveTab, (newTab) => {
  957. if (newTab === 'camera' && currentLss.value) {
  958. resetCameraSearch()
  959. cameraCurrentPage.value = 1
  960. loadCameraList()
  961. }
  962. })
  963. onMounted(() => {
  964. getList()
  965. })
  966. </script>
  967. <style lang="scss" scoped>
  968. .page-container {
  969. display: flex;
  970. flex-direction: column;
  971. box-sizing: border-box;
  972. }
  973. .camera-form-container {
  974. padding: 0 20px;
  975. }
  976. .params-content-container {
  977. padding: 0 20px;
  978. }
  979. .search-form {
  980. flex-shrink: 0;
  981. margin-bottom: 16px;
  982. padding: 16px 16px 4px 16px;
  983. background: #f5f7fa;
  984. :deep(.el-form-item) {
  985. margin-bottom: 12px;
  986. margin-right: 16px;
  987. }
  988. :deep(.el-input),
  989. :deep(.el-select) {
  990. width: 160px;
  991. }
  992. }
  993. .table-wrapper {
  994. flex: 1;
  995. min-height: 0;
  996. overflow: hidden;
  997. }
  998. .pagination-container {
  999. flex-shrink: 0;
  1000. display: flex;
  1001. justify-content: flex-end;
  1002. padding-top: 16px;
  1003. }
  1004. .camera-toolbar {
  1005. display: flex;
  1006. justify-content: space-between;
  1007. align-items: flex-start;
  1008. margin-bottom: 16px;
  1009. :deep(.el-form) {
  1010. .el-form-item {
  1011. margin-bottom: 0;
  1012. margin-right: 12px;
  1013. }
  1014. }
  1015. }
  1016. .camera-pagination {
  1017. margin-top: 16px;
  1018. display: flex;
  1019. justify-content: flex-end;
  1020. }
  1021. .generate-id-btn {
  1022. cursor: pointer;
  1023. color: #909399;
  1024. transition: color 0.2s;
  1025. &:hover {
  1026. color: var(--el-color-primary);
  1027. }
  1028. }
  1029. .status-text {
  1030. font-size: 12px;
  1031. }
  1032. // 十字瞄准按钮样式
  1033. .crosshairs-btn {
  1034. color: #bbbbbb;
  1035. &.active {
  1036. color: var(--color-primary);
  1037. }
  1038. }
  1039. .status-active {
  1040. color: #67c23a;
  1041. }
  1042. .status-hold {
  1043. color: #e6a23c;
  1044. }
  1045. .status-dead {
  1046. color: #f56c6c;
  1047. }
  1048. // LSS 编辑抽屉样式
  1049. .lss-edit-drawer {
  1050. :deep(.el-drawer__body) {
  1051. padding: 0;
  1052. display: flex;
  1053. flex-direction: column;
  1054. height: 100%;
  1055. }
  1056. }
  1057. .drawer-content {
  1058. display: flex;
  1059. flex-direction: column;
  1060. height: 100%;
  1061. }
  1062. .drawer-tabs {
  1063. flex-shrink: 0;
  1064. border-bottom: 1px solid #e5e7eb;
  1065. :deep(.el-tabs__header) {
  1066. margin: 0;
  1067. padding: 0 20px;
  1068. }
  1069. :deep(.el-tabs__nav-wrap::after) {
  1070. display: none;
  1071. }
  1072. :deep(.el-tabs__item) {
  1073. height: 48px;
  1074. line-height: 48px;
  1075. font-size: 14px;
  1076. color: #606266;
  1077. &.is-active {
  1078. color: #409eff;
  1079. font-weight: 500;
  1080. }
  1081. &:hover {
  1082. color: #409eff;
  1083. }
  1084. }
  1085. :deep(.el-tabs__active-bar) {
  1086. background-color: #409eff;
  1087. }
  1088. }
  1089. .drawer-body {
  1090. flex: 1;
  1091. overflow-y: auto;
  1092. padding: 20px;
  1093. }
  1094. .lss-detail-form {
  1095. :deep(.el-form-item) {
  1096. margin-bottom: 18px;
  1097. }
  1098. :deep(.el-form-item__label) {
  1099. color: #606266;
  1100. font-size: 14px;
  1101. }
  1102. .form-value {
  1103. line-height: 32px;
  1104. color: #303133;
  1105. font-size: 14px;
  1106. }
  1107. .textarea-wrapper {
  1108. width: 100%;
  1109. }
  1110. }
  1111. .heartbeat-status {
  1112. display: inline-flex;
  1113. align-items: center;
  1114. gap: 6px;
  1115. line-height: 32px;
  1116. font-size: 14px;
  1117. }
  1118. .heartbeat-dot {
  1119. display: inline-block;
  1120. width: 8px;
  1121. height: 8px;
  1122. border-radius: 50%;
  1123. &.status-active {
  1124. background-color: #67c23a;
  1125. }
  1126. &.status-hold {
  1127. background-color: #e6a23c;
  1128. }
  1129. &.status-dead {
  1130. background-color: #f56c6c;
  1131. }
  1132. }
  1133. .heartbeat-info-icon {
  1134. margin-left: 8px;
  1135. color: #909399;
  1136. cursor: pointer;
  1137. &:hover {
  1138. color: #409eff;
  1139. }
  1140. }
  1141. .heartbeat-tooltip {
  1142. font-size: 12px;
  1143. line-height: 1.6;
  1144. .tooltip-title {
  1145. font-weight: 500;
  1146. margin-bottom: 4px;
  1147. }
  1148. .tooltip-format {
  1149. margin-top: 8px;
  1150. font-weight: 500;
  1151. }
  1152. .tooltip-example {
  1153. color: #409eff;
  1154. }
  1155. }
  1156. .tab-content {
  1157. min-height: 200px;
  1158. }
  1159. // 设备列表抽屉样式
  1160. .device-drawer {
  1161. :deep(.el-drawer__body) {
  1162. padding: 0;
  1163. }
  1164. }
  1165. .device-tabs {
  1166. height: 100%;
  1167. display: flex;
  1168. flex-direction: column;
  1169. :deep(.el-tabs__header) {
  1170. margin: 0;
  1171. padding: 0 20px;
  1172. flex-shrink: 0;
  1173. border-bottom: 1px solid #e5e7eb;
  1174. }
  1175. :deep(.el-tabs__nav-wrap::after) {
  1176. display: none;
  1177. }
  1178. :deep(.el-tabs__item) {
  1179. height: 48px;
  1180. line-height: 48px;
  1181. font-size: 14px;
  1182. color: #606266;
  1183. &.is-active {
  1184. color: #409eff;
  1185. font-weight: 500;
  1186. }
  1187. &:hover {
  1188. color: #409eff;
  1189. }
  1190. }
  1191. :deep(.el-tabs__active-bar) {
  1192. background-color: #409eff;
  1193. }
  1194. :deep(.el-tabs__content) {
  1195. flex: 1;
  1196. overflow: hidden;
  1197. padding: 16px;
  1198. }
  1199. :deep(.el-tab-pane) {
  1200. height: 100%;
  1201. overflow-y: auto;
  1202. }
  1203. }
  1204. .tab-content-wrapper {
  1205. height: 100%;
  1206. }
  1207. .drawer-footer {
  1208. flex-shrink: 0;
  1209. display: flex;
  1210. justify-content: flex-end;
  1211. padding: 12px 20px;
  1212. border-top: 1px solid #e5e7eb;
  1213. gap: 12px;
  1214. }
  1215. // 扫描抽屉样式
  1216. .scan-drawer {
  1217. :deep(.el-drawer__body) {
  1218. padding: 0;
  1219. display: flex;
  1220. flex-direction: column;
  1221. }
  1222. }
  1223. .scan-btn {
  1224. color: grey;
  1225. &.scanned {
  1226. color: var(--el-color-primary);
  1227. }
  1228. }
  1229. .scan-drawer-content {
  1230. flex: 1;
  1231. overflow-y: auto;
  1232. padding: 16px;
  1233. }
  1234. .scan-toolbar {
  1235. display: flex;
  1236. justify-content: space-between;
  1237. align-items: center;
  1238. margin-bottom: 16px;
  1239. }
  1240. // 凭证抽屉样式
  1241. .credential-drawer {
  1242. :deep(.el-drawer__body) {
  1243. padding: 0;
  1244. }
  1245. }
  1246. .credential-content {
  1247. padding: 16px;
  1248. }
  1249. .credential-toolbar {
  1250. margin-bottom: 16px;
  1251. :deep(.el-form) {
  1252. .el-form-item {
  1253. margin-bottom: 0;
  1254. margin-right: 12px;
  1255. }
  1256. }
  1257. }
  1258. // 表格样式
  1259. :deep(.el-table) {
  1260. --el-table-row-hover-bg-color: #f0f0ff;
  1261. .el-table__row--striped td.el-table__cell {
  1262. background-color: #f8f9fc;
  1263. }
  1264. .el-table__header th {
  1265. background-color: #f5f7fa;
  1266. color: #333;
  1267. font-weight: 600;
  1268. }
  1269. }
  1270. </style>