| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364 |
- <template>
- <div class="page-container">
- <!-- 搜索表单 -->
- <div class="search-form">
- <el-form :model="searchForm" inline data-id="search-form">
- <el-form-item>
- <el-input
- v-model.trim="searchForm.lssId"
- :placeholder="t('LSS ID')"
- clearable
- data-id="search-keyword"
- @keyup.enter="handleSearch"
- />
- </el-form-item>
- <el-form-item>
- <el-input
- v-model.trim="searchForm.lssName"
- :placeholder="t('名称')"
- clearable
- data-id="search-keyword"
- @keyup.enter="handleSearch"
- />
- </el-form-item>
- <el-form-item>
- <el-select v-model="searchForm.status" :placeholder="t('心跳')" clearable data-id="search-enabled">
- <el-option :label="t('全部')" value="" />
- <el-option :label="t('active')" value="active" />
- <el-option :label="t('hold')" value="hold" />
- <el-option :label="t('dead')" value="dead" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" data-id="btn-search" @click="handleSearch">
- <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
- {{ t('查询') }}
- </el-button>
- <el-button type="info" data-id="btn-reset" @click="handleReset">
- <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
- {{ t('重置') }}
- </el-button>
- </el-form-item>
- </el-form>
- </div>
- <!-- 数据表格 -->
- <div class="table-wrapper">
- <el-table
- ref="tableRef"
- v-loading="loading"
- :data="lssList"
- stripe
- size="default"
- data-id="lss-table"
- height="100%"
- @sort-change="handleSortChange"
- >
- <el-table-column prop="lssId" :label="t('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="address" :label="t('地址')" min-width="180" sortable="custom" show-overflow-tooltip />
- <el-table-column prop="ip" :label="t('IP')" min-width="180" sortable="custom" show-overflow-tooltip />
- <el-table-column :label="t('心跳')" width="220" align="center">
- <template #default="{ row }">
- <span :class="getHeartbeatClass(row.status)">
- {{ t(row.status) || '-' }}
- </span>
- | {{ formatTime(row.lastHeartbeatAt) }}
- </template>
- </el-table-column>
- <el-table-column :label="t('设备列表')" align="center">
- <template #default="{ row }">
- <el-button type="primary" link @click="handleEdit(row, 'camera')">
- <Icon icon="mdi:cctv" width="20" height="20" />
- </el-button>
- </template>
- </el-table-column>
- <el-table-column :label="t('ably')" align="center" fixed="right">
- <template #default="{ row }">
- {{ row.ablyClientId }}
- </template>
- </el-table-column>
- <el-table-column :label="t('操作')" width="130" align="center" fixed="right">
- <template #default="{ row }">
- <el-button data-id="btn-edit" type="primary" link @click="handleEdit(row, 'detail')">
- <Icon icon="mdi:note-edit-outline" width="20" height="20" />
- </el-button>
- <el-button
- :class="{ 'scan-btn': true, scanned: row.scanned }"
- data-id="btn-scan-devices"
- type="primary"
- link
- @click="handleScanDevices(row)"
- >
- <Icon icon="mdi:radar" width="20" height="20" />
- </el-button>
- <el-button data-id="btn-delete" type="danger" link @click="handleDelete(row)">
- <Icon icon="mdi:delete" width="20" height="20" />
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <!-- LSS 详情抽屉 -->
- <el-drawer v-model="detailDrawerVisible" :title="t('LSS 节点详情')" direction="rtl" size="500px" destroy-on-close>
- <el-descriptions :column="1" border>
- <el-descriptions-item :label="t('LSS ID')">{{ currentLss?.lssId }}</el-descriptions-item>
- <el-descriptions-item :label="t('名称')">{{ currentLss?.lssName }}</el-descriptions-item>
- <el-descriptions-item :label="t('地址')">{{ currentLss?.address }}</el-descriptions-item>
- <el-descriptions-item :label="t('机器 ID')">{{ currentLss?.machineId || '-' }}</el-descriptions-item>
- <el-descriptions-item :label="t('状态')">
- <el-tag :type="getStatusTagType(currentLss?.status)" size="small">
- {{ formatStatus(currentLss?.status) }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item :label="t('任务数')">
- {{ currentLss?.currentTasks }} / {{ currentLss?.maxTasks }}
- </el-descriptions-item>
- <el-descriptions-item :label="t('FFmpeg 版本')">{{ currentLss?.ffmpegVersion || '-' }}</el-descriptions-item>
- <el-descriptions-item :label="t('系统信息')">{{ currentLss?.systemInfo || '-' }}</el-descriptions-item>
- <el-descriptions-item :label="t('启用状态')">
- <el-tag :type="currentLss?.enabled ? 'success' : 'info'" size="small">
- {{ currentLss?.enabled ? '已启用' : '已禁用' }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item :label="t('创建时间')">{{ formatTime(currentLss?.createdAt) }}</el-descriptions-item>
- <el-descriptions-item :label="t('更新时间')">{{ formatTime(currentLss?.updatedAt) }}</el-descriptions-item>
- </el-descriptions>
- </el-drawer>
- <!-- LSS 编辑抽屉 -->
- <el-drawer
- v-model="lssEditDrawerVisible"
- direction="rtl"
- :size="editDrawerSize"
- :with-header="false"
- destroy-on-close
- class="lss-edit-drawer"
- >
- <div class="drawer-content">
- <!-- 顶部 Tabs -->
- <el-tabs v-model="editActiveTab" class="drawer-tabs">
- <el-tab-pane :label="t('LSS详情')" name="detail" />
- <el-tab-pane :label="t('摄像头列表')" name="camera" />
- <el-tab-pane :label="t('推币机列表')" name="pusher" />
- </el-tabs>
- <div class="drawer-body">
- <!-- LSS 详情 Tab -->
- <div v-show="editActiveTab === 'detail'" class="lss-detail-form">
- <el-form ref="lssEditFormRef" :model="lssEditForm" label-width="auto">
- <el-form-item :label="t('LSS ID') + ':'">
- <span class="form-value">{{ currentLss?.lssId }}</span>
- </el-form-item>
- <el-form-item :label="t('名称') + ':'" prop="lssName">
- <el-input v-model="lssEditForm.lssName" :placeholder="t('请输入名称')" style="width: 180px" />
- </el-form-item>
- <el-form-item :label="t('地址') + ':'" prop="address">
- <el-input type="textarea" :rows="5" v-model="lssEditForm.address" :placeholder="t('请输入地址')" />
- </el-form-item>
- <el-form-item :label="t('IP') + ':'">
- <span class="form-value">{{ currentLss?.ip }}</span>
- </el-form-item>
- <el-form-item :label="t('心跳') + ':'">
- <span class="heartbeat-status" :class="getHeartbeatClass(currentLss?.status)">
- {{ formatHeartbeat(currentLss) }}
- <span class="heartbeat-dot" :class="getHeartbeatClass(currentLss?.status)"></span>
- </span>
- <el-tooltip placement="right" effect="light">
- <template #content>
- <div class="heartbeat-tooltip">
- <div class="tooltip-title">{{ t('心跳状态') }}:</div>
- <div>{{ t('活跃') }} - {{ t('持续返回中,并且频繁') }}</div>
- <div>{{ t('待机') }} - {{ t('五分钟内有返回') }}</div>
- <div>{{ t('离线') }} - {{ t('五分钟内没有返回') }}</div>
- <div class="tooltip-format">{{ t('表现形式为') }}:</div>
- <div class="tooltip-example">{{ t('Status') }} [yy-mm-dd 00:00:00]</div>
- </div>
- </template>
- <Icon icon="mdi:help-circle" class="heartbeat-info-icon" width="16" height="16" />
- </el-tooltip>
- </el-form-item>
- <el-form-item :label="t('ably') + ':'" prop="ably">
- <div class="textarea-wrapper">
- <el-input
- disabled
- type="textarea"
- :rows="8"
- v-model="lssEditForm.ably"
- :placeholder="t('请输入ably信息')"
- maxlength="1000"
- show-word-limit
- />
- </div>
- </el-form-item>
- </el-form>
- </div>
- <!-- 摄像头列表 Tab -->
- <div v-show="editActiveTab === 'camera'" 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.cameraId"
- :placeholder="t('设备ID')"
- clearable
- style="width: 200px"
- @keyup.enter="handleCameraSearch"
- />
- </el-form-item>
- <el-form-item>
- <el-input
- v-model.trim="cameraSearchForm.cameraName"
- :placeholder="t('名称')"
- clearable
- style="width: 200px"
- @keyup.enter="handleCameraSearch"
- />
- </el-form-item>
- <el-form-item>
- <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
- <el-option :label="t('全部')" value="" />
- <el-option label="active" value="active" />
- <el-option label="hold" value="hold" />
- <el-option label="dead" value="dead" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="handleCameraSearch">
- <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
- {{ t('查询') }}
- </el-button>
- <el-button type="info" @click="handleCameraReset">
- <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
- {{ t('重置') }}
- </el-button>
- <el-button type="primary" @click="handleAddCamera">
- <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
- {{ t('新增') }}
- </el-button>
- </el-form-item>
- </el-form>
- </div>
- <el-table :data="cameraList" stripe :height="cameraTableHeight">
- <template #empty>
- <el-empty :description="t('暂无关联设备')" />
- </template>
- <el-table-column prop="cameraId" :label="t('设备ID')" min-width="100" show-overflow-tooltip />
- <el-table-column prop="cameraName" :label="t('名称')" min-width="100" show-overflow-tooltip />
- <el-table-column :label="t('状态(心跳)')" min-width="140">
- <template #default="{ row }">
- <span :class="['status-text', row.status === 'active' ? 'status-active' : 'status-dead']">
- {{ formatCameraStatus(row) }}
- </span>
- </template>
- </el-table-column>
- <el-table-column :label="t('参数配置')" min-width="80" align="center">
- <template #default="{ row }">
- <el-button type="primary" link @click="handleViewConfig(row)">{{ t('查看') }}</el-button>
- </template>
- </el-table-column>
- <el-table-column :label="t('运行参数')" min-width="80" align="center">
- <template #default="{ row }">
- <el-button type="primary" link @click="handleViewRunParams(row)">{{ t('查看') }}</el-button>
- </template>
- </el-table-column>
- <el-table-column prop="brand" :label="t('厂商')" min-width="90">
- <template #default="{ row }">
- {{ formatBrand(row.vendorName) }}
- </template>
- </el-table-column>
- <el-table-column prop="model" :label="t('型号')" min-width="130" show-overflow-tooltip />
- <el-table-column :label="t('添加时间')" min-width="140">
- <template #default="{ row }">
- {{ formatTime(row.createdAt) }}
- </template>
- </el-table-column>
- <el-table-column :label="t('设备控制')" width="130" align="center">
- <template #default="{ row }">
- <el-button type="primary" link @click="handleEditCamera(row)">
- <Icon icon="mdi:note-edit-outline" width="20" height="20" />
- </el-button>
- <!-- :tooltip="t('查看Cloudflare Stream')" -->
- <el-button type="primary" link @click="handleViewCamera(row)">
- <Icon
- icon="mdi:controller-right"
- :class="['crosshairs-btn', { active: row.streamSn }]"
- width="20"
- height="20"
- />
- </el-button>
- <el-button type="danger" link @click="handleDeleteCamera(row)">
- <Icon icon="mdi:delete" width="20" height="20" />
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="camera-pagination">
- <el-pagination
- v-model:current-page="cameraCurrentPage"
- v-model:page-size="cameraPageSize"
- :page-sizes="[10, 15, 20, 50, 100]"
- :total="cameraTotal"
- layout="total, sizes, prev, pager, next, jumper"
- background
- @size-change="handleCameraSizeChange"
- @current-change="handleCameraPageChange"
- />
- </div>
- </div>
- <!-- 推币机列表 Tab -->
- <div v-show="editActiveTab === 'pusher'" class="tab-content">
- <el-empty :description="t('暂无推币机数据')" />
- </div>
- </div>
- <div v-show="editActiveTab === 'detail'" class="drawer-footer">
- <el-button @click="lssEditDrawerVisible = false">{{ t('取消') }}</el-button>
- <el-button type="primary" :loading="lssUpdating" @click="handleUpdateLss">{{ t('更新') }}</el-button>
- </div>
- </div>
- </el-drawer>
- <!-- 设备列表抽屉 -->
- <el-drawer
- v-model="cameraDrawerVisible"
- :title="`${t('设备列表')} - ${currentLss?.lssName || ''}`"
- direction="rtl"
- size="80%"
- destroy-on-close
- class="device-drawer"
- >
- <el-tabs v-model="deviceActiveTab" class="device-tabs">
- <el-tab-pane :label="t('摄像头列表')" 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.cameraId"
- :placeholder="t('设备ID')"
- clearable
- style="width: 150px"
- @keyup.enter="handleCameraSearch"
- />
- </el-form-item>
- <el-form-item>
- <el-input
- v-model.trim="cameraSearchForm.cameraName"
- :placeholder="t('设备名称')"
- clearable
- style="width: 150px"
- @keyup.enter="handleCameraSearch"
- />
- </el-form-item>
- <el-form-item>
- <el-select v-model="cameraSearchForm.status" :placeholder="t('状态')" clearable style="width: 120px">
- <el-option :label="t('全部')" value="" />
- <el-option label="active" value="active" />
- <el-option label="hold" value="hold" />
- <el-option label="dead" value="dead" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="handleCameraSearch">
- <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
- {{ t('查询') }}
- </el-button>
- <el-button type="info" @click="handleCameraReset">
- <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
- {{ t('重置') }}
- </el-button>
- <el-button type="primary" @click="handleAddCamera">
- <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
- {{ t('新增') }}
- </el-button>
- </el-form-item>
- </el-form>
- </div>
- <el-table :data="cameraList" stripe>
- <template #empty>
- <el-empty :description="t('暂无关联设备')" />
- </template>
- <!-- <el-table-column prop="ip" label="本地IP" min-width="110" /> -->
- <el-table-column prop="cameraId" :label="t('设备ID')" min-width="100" show-overflow-tooltip />
- <el-table-column prop="cameraName" :label="t('名称')" min-width="100" show-overflow-tooltip />
- <el-table-column :label="t('状态(心跳)')" min-width="140">
- <template #default="{ row }">
- <span :class="['status-text', row.status === 'active' ? 'status-active' : 'status-dead']">
- {{ formatCameraStatus(row) }}
- </span>
- </template>
- </el-table-column>
- <el-table-column :label="t('参数配置')" 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="t('运行参数')" 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="vendorName" :label="t('厂商')" min-width="90">
- <template #default="{ row }">
- {{ formatBrand(row.vendorName) }}
- </template>
- </el-table-column>
- <el-table-column prop="model" :label="t('型号')" min-width="130" show-overflow-tooltip />
- <el-table-column :label="t('添加时间')" min-width="140">
- <template #default="{ row }">
- {{ formatTime(row.createdAt) }}
- </template>
- </el-table-column>
- <el-table-column :label="t('设备控制')" min-width="100" align="center" fixed="right">
- <template #default="{ row }">
- <el-button type="primary" link @click="handleEditCamera(row)">
- <Icon icon="mdi:note-edit-outline" width="20" height="20" />
- </el-button>
- <el-button type="danger" link @click="handleDeleteCamera(row)">
- <Icon icon="mdi:delete" width="20" height="20" />
- </el-button>
- <el-button link :class="['crosshairs-btn', { active: !row.streamSn }]" @click="handleViewCamera(row)">
- <Icon icon="mdi:crosshairs" />
- </el-button>
- </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="t('推币机列表')" name="pusher">
- <div class="tab-content-wrapper">
- <div class="camera-toolbar">
- <el-form inline>
- <el-form-item>
- <el-input :placeholder="t('设备ID / 名称')" clearable style="width: 200px" />
- </el-form-item>
- <el-form-item>
- <el-select :placeholder="t('状态')" clearable style="width: 120px">
- <el-option :label="t('全部')" value="" />
- <el-option :label="t('在线')" value="ONLINE" />
- <el-option :label="t('离线')" value="OFFLINE" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary">
- <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
- {{ t('查询') }}
- </el-button>
- <el-button type="info">
- <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
- {{ t('重置') }}
- </el-button>
- </el-form-item>
- </el-form>
- <el-button type="primary">
- <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
- {{ t('新增') }}
- </el-button>
- </div>
- <el-empty :description="t('暂无推币机数据')" />
- </div>
- </el-tab-pane>
- <el-tab-pane :label="t('其他设备')" name="other">
- <div class="tab-content-wrapper">
- <div class="camera-toolbar">
- <el-form inline>
- <el-form-item>
- <el-input :placeholder="t('设备ID / 名称')" clearable style="width: 200px" />
- </el-form-item>
- <el-form-item>
- <el-select :placeholder="t('状态')" clearable style="width: 120px">
- <el-option :label="t('全部')" value="" />
- <el-option :label="t('在线')" value="ONLINE" />
- <el-option :label="t('离线')" value="OFFLINE" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary">
- <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
- {{ t('查询') }}
- </el-button>
- <el-button type="info">
- <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
- {{ t('重置') }}
- </el-button>
- </el-form-item>
- </el-form>
- <el-button type="primary">
- <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
- {{ t('新增') }}
- </el-button>
- </div>
- <el-empty :description="t('暂无其他设备数据')" />
- </div>
- </el-tab-pane>
- </el-tabs>
- </el-drawer>
- <!-- 摄像头编辑抽屉 -->
- <el-drawer
- v-model="cameraDialogVisible"
- :title="isEditCamera ? t('摄像头详情') : t('添加摄像头')"
- direction="rtl"
- size="600px"
- :close-on-click-modal="false"
- :show-close="false"
- destroy-on-close
- class="camera-edit-drawer"
- >
- <el-form ref="cameraFormRef" :model="cameraForm" :rules="cameraRules" label-width="auto">
- <div class="camera-form-container">
- <!-- <el-form-item label="IP 地址" prop="ip">
- <el-input v-model="cameraForm.ip" :disabled="isEditCamera" placeholder="请输入 IP 地址" />
- </el-form-item> -->
- <el-form-item :label="t('设备ID') + ':'" prop="cameraId">
- <el-input
- v-model="cameraForm.cameraId"
- :disabled="isEditCamera"
- :placeholder="t('请输入设备ID')"
- style="max-width: 300px"
- >
- <template v-if="!isEditCamera" #suffix>
- <el-icon class="generate-id-btn" @click="generateCameraId">
- <Icon icon="mdi:refresh" />
- </el-icon>
- </template>
- </el-input>
- </el-form-item>
- <el-form-item :label="t('设备名称') + ':'" prop="cameraName">
- <el-input v-model="cameraForm.cameraName" :placeholder="t('请输入设备名称')" style="max-width: 300px" />
- </el-form-item>
- <el-form-item :label="t('厂商') + ':'" prop="vendorName">
- <el-select
- v-model="cameraForm.vendorName"
- :placeholder="t('请选择摄像头')"
- style="width: 100%; max-width: 300px"
- filterable
- @change="handleVendorSelect"
- >
- <el-option
- v-for="vendor in [
- { id: 'hikvision', name: '海康威视' },
- { id: 'dahua', name: '大华' },
- { id: 'uniview', name: '宇视' },
- { id: 'other', name: '其他' }
- ]"
- :key="vendor.id"
- :label="vendor.name"
- :value="vendor.id"
- />
- </el-select>
- </el-form-item>
- <el-form-item :label="t('型号') + ':'" prop="model">
- <el-input v-model="cameraForm.model" :placeholder="t('请输入型号')" style="max-width: 300px" />
- </el-form-item>
- <!-- <el-form-item label="摄像头型号" prop="cameraId">
- <el-select v-model="cameraForm.selectedVendorId" placeholder="请选择摄像头" style="width: 100%" filterable
- @change="handleVendorSelect">
- <el-option v-for="vendor in cameraVendorList" :key="vendor.id" :label="vendor.name" :value="vendor.id" />
- </el-select>
- </el-form-item> -->
- <!-- <el-form-item label="名称" prop="name">
- <el-input v-model="cameraForm.name" placeholder="请输入名称" />
- </el-form-item> -->
- <!-- <el-form-item label="端口" prop="port">
- <el-input-number v-model="cameraForm.port" :min="1" :max="65535" style="width: 100%" />
- </el-form-item> -->
- <!-- <el-form-item label="用户名" prop="username">
- <el-input v-model="cameraForm.username" placeholder="请输入用户名" />
- </el-form-item>
- <el-form-item label="密码" prop="password">
- <el-input v-model="cameraForm.password" type="password" show-password placeholder="请输入密码" />
- </el-form-item> -->
- <el-form-item :label="t('参数配置') + ':'">
- <CodeEditor
- v-model="cameraForm.paramConfig"
- language="json"
- height="200px"
- :placeholder="t('请输入参数配置 (JSON)')"
- />
- </el-form-item>
- <br />
- <el-form-item :label="t('运行参数') + ':'">
- <CodeEditor
- v-model="cameraForm.runtimeParams"
- language="json"
- height="200px"
- :placeholder="t('设备运行参数 (JSON)')"
- />
- </el-form-item>
- <el-form-item v-if="isEditCamera" :label="t('添加时间') + ':'">
- {{ formatTime(cameraForm.createdAt) }}
- </el-form-item>
- </div>
- </el-form>
- <template #footer>
- <div class="drawer-footer">
- <el-button @click="cameraDialogVisible = false">{{ t('取消') }}</el-button>
- <el-button type="primary" :loading="cameraSubmitting" @click="handleSubmitCamera">
- {{ isEditCamera ? t('更新') : t('添加') }}
- </el-button>
- </div>
- </template>
- </el-drawer>
- <!-- 参数配置/运行参数抽屉 -->
- <el-drawer
- v-model="paramsDialogVisible"
- :title="paramsDialogTitle"
- direction="rtl"
- size="650px"
- :close-on-click-modal="false"
- :show-close="false"
- destroy-on-close
- class="params-drawer"
- >
- <div class="params-content-container">
- <CodeEditor
- v-model="paramsContent"
- language="json"
- height="500px"
- :placeholder="
- paramsDialogType === 'config' ? t('请输入参数配置(JSON 格式)') : t('请输入运行参数(JSON 格式)')
- "
- />
- </div>
- <template #footer>
- <div class="drawer-footer">
- <el-button @click="paramsDialogVisible = false">{{ t('取消') }}</el-button>
- <el-button type="primary" :loading="paramsSubmitting" @click="handleSaveParams">{{ t('更新') }}</el-button>
- </div>
- </template>
- </el-drawer>
- <!-- 扫描设备抽屉 -->
- <el-drawer
- v-model="scanDrawerVisible"
- :title="t('扫描')"
- direction="rtl"
- size="50%"
- destroy-on-close
- class="scan-drawer"
- >
- <div class="scan-drawer-content">
- <div class="scan-toolbar">
- <div class="scan-toolbar-left">
- <el-button v-if="scanMatched" @click="handleRematch">
- <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
- {{ t('再次匹配') }}
- </el-button>
- </div>
- <div class="scan-toolbar-right">
- <el-button @click="credentialDrawerVisible = true">
- <Icon icon="mdi:key-variant" width="16" height="16" style="margin-right: 4px" />
- {{ t('账号配置') }}
- </el-button>
- </div>
- </div>
- <el-table v-loading="scanLoading" :data="discoveredDevices" stripe>
- <template #empty>
- <el-empty :description="t('暂无发现设备')" />
- </template>
- <el-table-column type="index" :label="t('序号')" width="60" align="center" />
- <el-table-column prop="ip" label="IP" min-width="120" show-overflow-tooltip />
- <el-table-column prop="port" :label="t('端口')" width="80" align="center" />
- <el-table-column prop="deviceName" :label="t('设备名称')" min-width="140" show-overflow-tooltip />
- <el-table-column :label="t('匹配状态')" width="100" align="center">
- <template #default="{ row }">
- <Icon
- v-if="row.matchStatus === 'MATCHED'"
- icon="mdi:check-circle"
- width="20"
- height="20"
- style="color: #67c23a"
- />
- <Icon
- v-else-if="row.matchStatus === 'UNMATCHED'"
- icon="mdi:close-circle"
- width="20"
- height="20"
- style="color: #f56c6c"
- />
- <Icon
- v-else-if="row.matchStatus === 'MATCHING'"
- icon="mdi:progress-clock"
- width="20"
- height="20"
- style="color: #e6a23c"
- />
- <span v-else style="color: #909399">{{ t('待匹配') }}</span>
- </template>
- </el-table-column>
- <el-table-column :label="t('操作')" width="80" align="center">
- <template #default="{ row }">
- <el-button
- v-if="row.matchStatus === 'MATCHED' && !row.bound"
- type="primary"
- link
- @click="handleBindDevice(row)"
- >
- {{ t('添加') }}
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <template #footer>
- <div class="drawer-footer">
- <el-button @click="scanDrawerVisible = false">{{ t('取消') }}</el-button>
- <el-button v-if="!scanMatched" type="primary" :loading="matchLoading" @click="handleTriggerMatch">
- {{ t('匹配') }}
- </el-button>
- <el-button v-else type="primary" @click="scanDrawerVisible = false">
- {{ t('完成') }}
- </el-button>
- </div>
- </template>
- </el-drawer>
- <!-- 账号配置抽屉(第二层) -->
- <el-drawer
- v-model="credentialDrawerVisible"
- :title="t('账号配置')"
- direction="rtl"
- size="50%"
- destroy-on-close
- :append-to-body="true"
- class="credential-drawer"
- >
- <div class="credential-content">
- <div class="credential-toolbar">
- <el-form :model="credentialSearchForm" inline>
- <el-form-item>
- <el-input
- v-model.trim="credentialSearchForm.username"
- :placeholder="t('用户名')"
- clearable
- style="width: 150px"
- />
- </el-form-item>
- <el-form-item>
- <el-input
- v-model.trim="credentialSearchForm.password"
- :placeholder="t('密码')"
- clearable
- style="width: 150px"
- />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="loadCredentials">
- <Icon icon="mdi:magnify" width="16" height="16" style="margin-right: 4px" />
- {{ t('查询') }}
- </el-button>
- <el-button type="info" @click="handleCredentialReset">
- <Icon icon="mdi:refresh" width="16" height="16" style="margin-right: 4px" />
- {{ t('重置') }}
- </el-button>
- <el-button type="primary" @click="handleAddCredential">
- <Icon icon="mdi:plus" width="16" height="16" style="margin-right: 4px" />
- {{ t('新增') }}
- </el-button>
- </el-form-item>
- </el-form>
- </div>
- <el-table v-loading="credentialLoading" :data="filteredCredentials" stripe>
- <template #empty>
- <el-empty :description="t('暂无发现设备')" />
- </template>
- <el-table-column prop="username" :label="t('用户名')" min-width="120" show-overflow-tooltip />
- <el-table-column prop="password" :label="t('密码')" min-width="120" show-overflow-tooltip />
- <el-table-column :label="t('设备控制')" width="100" align="center">
- <template #default="{ row }">
- <el-button type="primary" link @click="handleEditCredential(row)">
- <Icon icon="mdi:note-edit-outline" width="20" height="20" />
- </el-button>
- <el-button type="danger" link @click="handleDeleteCredential(row)">
- <Icon icon="mdi:delete" width="20" height="20" />
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </el-drawer>
- <!-- 凭证编辑对话框 -->
- <el-dialog
- v-model="credentialDialogVisible"
- :title="isEditCredential ? t('编辑凭证') : t('新增凭证')"
- width="500px"
- :close-on-click-modal="false"
- :append-to-body="true"
- destroy-on-close
- >
- <el-form ref="credentialFormRef" :model="credentialForm" :rules="credentialRules" label-width="100px">
- <el-form-item :label="t('凭证名称')" prop="name">
- <el-input v-model="credentialForm.name" :placeholder="t('请输入凭证名称')" />
- </el-form-item>
- <el-form-item :label="t('用户名')" prop="username">
- <el-input v-model="credentialForm.username" :placeholder="t('请输入用户名')" />
- </el-form-item>
- <el-form-item :label="t('密码')" prop="password">
- <el-input v-model="credentialForm.password" :placeholder="t('请输入密码')" />
- </el-form-item>
- <el-form-item :label="t('厂商')">
- <el-input v-model="credentialForm.vendor" :placeholder="t('请选择')" />
- </el-form-item>
- <el-form-item :label="t('优先级')">
- <el-input-number v-model="credentialForm.priority" :min="0" :max="999" />
- </el-form-item>
- <el-form-item :label="t('启用')">
- <el-switch v-model="credentialForm.enabled" />
- </el-form-item>
- <el-form-item :label="t('备注')">
- <el-input v-model="credentialForm.remark" type="textarea" :rows="3" :placeholder="t('请输入备注')" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="credentialDialogVisible = false">{{ t('取消') }}</el-button>
- <el-button type="primary" :loading="credentialSubmitting" @click="handleSubmitCredential">
- {{ isEditCredential ? t('更新') : t('添加') }}
- </el-button>
- </template>
- </el-dialog>
- <!-- 分页 -->
- <div class="pagination-container">
- <el-pagination
- v-model:current-page="currentPage"
- v-model:page-size="pageSize"
- :page-sizes="[10, 15, 20, 50, 100]"
- :total="total"
- layout="total, sizes, prev, pager, next, jumper"
- background
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { onMounted, watch } from 'vue'
- import { Icon } from '@iconify/vue'
- import { useI18n } from 'vue-i18n'
- import { formatTime } from '@/utils/dayjs'
- import CodeEditor from '@/components/CodeEditor.vue'
- import {
- formatStatus,
- getStatusTagType,
- formatCameraStatus,
- formatHeartbeat,
- getHeartbeatClass,
- formatBrand
- } from './composables/useFormatters'
- import { useLssList } from './composables/useLssList'
- import { useCameraList } from './composables/useCameraList'
- import { useScanDevices } from './composables/useScanDevices'
- import { useCredentials } from './composables/useCredentials'
- const { t } = useI18n({ useScope: 'global' })
- // ==================== LSS 列表 ====================
- const {
- loading,
- lssList,
- tableRef,
- detailDrawerVisible,
- currentLss,
- lssEditDrawerVisible,
- lssUpdating,
- editActiveTab,
- lssEditFormRef,
- lssEditForm,
- editDrawerSize,
- searchForm,
- currentPage,
- pageSize,
- total,
- getList,
- handleSearch,
- handleReset,
- handleSortChange,
- handleSizeChange,
- handleCurrentChange,
- handleUpdateLss,
- handleDelete
- } = useLssList()
- // ==================== 摄像头列表 ====================
- const {
- cameraDrawerVisible,
- cameraLoading,
- deviceActiveTab,
- cameraList,
- cameraCurrentPage,
- cameraPageSize,
- cameraTotal,
- cameraTableHeight,
- cameraSearchForm,
- cameraDialogVisible,
- cameraFormRef,
- isEditCamera,
- cameraSubmitting,
- cameraForm,
- cameraRules,
- paramsDialogVisible,
- paramsDialogTitle,
- paramsDialogType,
- paramsContent,
- paramsSubmitting,
- loadCameraList,
- handleCameraSearch,
- handleCameraReset,
- handleCameraSizeChange,
- handleCameraPageChange,
- handleVendorSelect,
- generateCameraId,
- handleAddCamera,
- handleEditCamera,
- handleSubmitCamera,
- handleDeleteCamera,
- handleViewCamera,
- handleViewConfig,
- handleViewRunParams,
- handleSaveParams,
- resetCameraSearch
- } = useCameraList(currentLss)
- // ==================== 扫描设备 ====================
- const {
- scanDrawerVisible,
- scanLoading,
- matchLoading,
- scanMatched,
- discoveredDevices,
- handleScanDevices,
- handleTriggerMatch,
- handleRematch,
- handleBindDevice
- } = useScanDevices()
- // ==================== 凭证管理 ====================
- const {
- credentialDrawerVisible,
- credentialLoading,
- credentialSearchForm,
- filteredCredentials,
- credentialDialogVisible,
- isEditCredential,
- credentialSubmitting,
- credentialFormRef,
- credentialForm,
- credentialRules,
- loadCredentials,
- handleCredentialReset,
- handleAddCredential,
- handleEditCredential,
- handleSubmitCredential,
- handleDeleteCredential
- } = useCredentials()
- // ==================== 页面级编排 ====================
- function handleEdit(row: any, tab: 'detail' | 'camera' | 'pusher') {
- currentLss.value = row
- lssEditForm.lssName = row.lssName || ''
- lssEditForm.address = row.address || ''
- lssEditForm.ably = JSON.stringify(row.ably)
- editActiveTab.value = tab
- lssEditDrawerVisible.value = true
- resetCameraSearch()
- loadCameraList()
- }
- // 监听 tab 切换,加载对应数据
- watch(editActiveTab, (newTab) => {
- if (newTab === 'camera' && currentLss.value) {
- resetCameraSearch()
- cameraCurrentPage.value = 1
- loadCameraList()
- }
- })
- onMounted(() => {
- getList()
- })
- </script>
- <style lang="scss" scoped>
- .page-container {
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- }
- .camera-form-container {
- padding: 0 20px;
- }
- .params-content-container {
- padding: 0 20px;
- }
- .search-form {
- flex-shrink: 0;
- margin-bottom: 16px;
- padding: 16px 16px 4px 16px;
- background: #f5f7fa;
- :deep(.el-form-item) {
- margin-bottom: 12px;
- margin-right: 16px;
- }
- :deep(.el-input),
- :deep(.el-select) {
- width: 160px;
- }
- }
- .table-wrapper {
- flex: 1;
- min-height: 0;
- overflow: hidden;
- }
- .pagination-container {
- flex-shrink: 0;
- display: flex;
- justify-content: flex-end;
- padding-top: 16px;
- }
- .camera-toolbar {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 16px;
- :deep(.el-form) {
- .el-form-item {
- margin-bottom: 0;
- margin-right: 12px;
- }
- }
- }
- .camera-pagination {
- margin-top: 16px;
- display: flex;
- justify-content: flex-end;
- }
- .generate-id-btn {
- cursor: pointer;
- color: #909399;
- transition: color 0.2s;
- &:hover {
- color: var(--el-color-primary);
- }
- }
- .status-text {
- font-size: 12px;
- }
- // 十字瞄准按钮样式
- .crosshairs-btn {
- color: #bbbbbb;
- &.active {
- color: var(--color-primary);
- }
- }
- .status-active {
- color: #67c23a;
- }
- .status-hold {
- color: #e6a23c;
- }
- .status-dead {
- color: #f56c6c;
- }
- // LSS 编辑抽屉样式
- .lss-edit-drawer {
- :deep(.el-drawer__body) {
- padding: 0;
- display: flex;
- flex-direction: column;
- height: 100%;
- }
- }
- .drawer-content {
- display: flex;
- flex-direction: column;
- height: 100%;
- }
- .drawer-tabs {
- flex-shrink: 0;
- 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 {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- }
- .lss-detail-form {
- :deep(.el-form-item) {
- margin-bottom: 18px;
- }
- :deep(.el-form-item__label) {
- color: #606266;
- font-size: 14px;
- }
- .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;
- }
- // 设备列表抽屉样式
- .device-drawer {
- :deep(.el-drawer__body) {
- padding: 0;
- }
- }
- .device-tabs {
- height: 100%;
- display: flex;
- flex-direction: column;
- :deep(.el-tabs__header) {
- margin: 0;
- padding: 0 20px;
- flex-shrink: 0;
- border-bottom: 1px solid #e5e7eb;
- }
- :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;
- }
- :deep(.el-tabs__content) {
- flex: 1;
- overflow: hidden;
- padding: 16px;
- }
- :deep(.el-tab-pane) {
- height: 100%;
- overflow-y: auto;
- }
- }
- .tab-content-wrapper {
- height: 100%;
- }
- .drawer-footer {
- flex-shrink: 0;
- display: flex;
- justify-content: flex-end;
- padding: 12px 20px;
- border-top: 1px solid #e5e7eb;
- gap: 12px;
- }
- // 扫描抽屉样式
- .scan-drawer {
- :deep(.el-drawer__body) {
- padding: 0;
- display: flex;
- flex-direction: column;
- }
- }
- .scan-btn {
- color: grey;
- &.scanned {
- color: var(--el-color-primary);
- }
- }
- .scan-drawer-content {
- flex: 1;
- overflow-y: auto;
- padding: 16px;
- }
- .scan-toolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
- }
- // 凭证抽屉样式
- .credential-drawer {
- :deep(.el-drawer__body) {
- padding: 0;
- }
- }
- .credential-content {
- padding: 16px;
- }
- .credential-toolbar {
- margin-bottom: 16px;
- :deep(.el-form) {
- .el-form-item {
- margin-bottom: 0;
- margin-right: 12px;
- }
- }
- }
- // 表格样式
- :deep(.el-table) {
- --el-table-row-hover-bg-color: #f0f0ff;
- .el-table__row--striped td.el-table__cell {
- background-color: #f8f9fc;
- }
- .el-table__header th {
- background-color: #f5f7fa;
- color: #333;
- font-weight: 600;
- }
- }
- </style>
|