DeviceView.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. <template>
  2. <!-- 设备画面组件:右侧面板上方,包含操作按钮和 2×4 监控画面网格 -->
  3. <div class="device-view">
  4. <div class="device-view-content">
  5. <!-- 顶部操作按钮:最新热源 / 最新闯入 / 最新烟雾 / 关闭声光报警 -->
  6. <div class="view-actions">
  7. <button
  8. v-for="btn in actions"
  9. :key="btn.key"
  10. type="button"
  11. :class="['action-btn', { active: activeAction === btn.key }]"
  12. @click="onAction(btn.key)"
  13. >
  14. {{ btn.label }}
  15. </button>
  16. </div>
  17. <!-- 2×4 监控画面网格 -->
  18. <div class="view-grid">
  19. <div class="view-cell">
  20. <div class="media-wrap">
  21. <div v-if="renderPlayer" ref="playerRef" style="display: flex; width: 100%; height: 100%; overflow: hidden; pointer-events: none"></div>
  22. </div>
  23. </div>
  24. </div>
  25. <div class="view-pagination">
  26. <a-pagination
  27. size="small"
  28. :current="pagination.current"
  29. :page-size="pagination.pageSize"
  30. :total="total"
  31. :show-size-changer="false"
  32. :show-less-items="true"
  33. :hide-on-single-page="false"
  34. @change="onPageChange"
  35. />
  36. </div>
  37. </div>
  38. <!-- 最新热源 / 最新闯入 报警图片弹窗 -->
  39. <Teleport to="body">
  40. <div v-if="latestAlarmVisible" class="latest-alarm-mask" @click.self="closeLatestAlarmPopup">
  41. <div :class="['latest-alarm-panel', { expanded: latestAlarmExpanded }]">
  42. <div class="latest-alarm-frame">
  43. <div class="latest-alarm-title-tab">{{ latestAlarmTitle }}</div>
  44. <div class="latest-alarm-toolbar">
  45. <button type="button" class="latest-alarm-icon-btn" title="放大" @click="toggleLatestAlarmExpand">
  46. <svg viewBox="0 0 16 16" width="14" height="14">
  47. <rect x="3" y="3" width="7" height="7" fill="none" stroke="currentColor" stroke-width="1.3" />
  48. <path
  49. d="M9 3 H13 V7 M13 3 L9.5 6.5 M7 9 L3 13 M3 9 V13 H7"
  50. fill="none"
  51. stroke="currentColor"
  52. stroke-width="1.3"
  53. stroke-linecap="round"
  54. stroke-linejoin="round"
  55. />
  56. </svg>
  57. </button>
  58. <button type="button" class="latest-alarm-icon-btn" title="关闭" @click="closeLatestAlarmPopup">
  59. <svg viewBox="0 0 16 16" width="14" height="14">
  60. <rect x="2.5" y="2.5" width="8" height="8" fill="none" stroke="currentColor" stroke-width="1.3" />
  61. <path
  62. d="M8 8 L13.5 13.5 M10.5 13.5 H13.5 V10.5"
  63. fill="none"
  64. stroke="currentColor"
  65. stroke-width="1.3"
  66. stroke-linecap="round"
  67. stroke-linejoin="round"
  68. />
  69. </svg>
  70. </button>
  71. </div>
  72. <div class="latest-alarm-image-wrap">
  73. <img v-if="latestAlarmData.imageUrl" class="latest-alarm-image" :src="latestAlarmData.imageUrl" :alt="latestAlarmTitle" />
  74. <div v-else class="latest-alarm-image-empty">暂无告警图像</div>
  75. <div class="latest-alarm-overlay bottom-right">{{ latestAlarmData.deviceName || '--' }}</div>
  76. </div>
  77. <div class="latest-alarm-footer">
  78. <span>告警时间:{{ latestAlarmData.alarmTime || '--' }}</span>
  79. <span class="latest-alarm-sep">|</span>
  80. <span>告警设备:{{ latestAlarmData.deviceName || '--' }}</span>
  81. <span class="latest-alarm-sep">|</span>
  82. <span>告警类型:{{ latestAlarmData.alarmType || '--' }}</span>
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. </Teleport>
  88. </div>
  89. </template>
  90. <script setup lang="ts">
  91. import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
  92. import dayjs from 'dayjs';
  93. import { useCamera } from '/@/hooks/system/useCameraPianation';
  94. import { useMessage } from '/@/hooks/web/useMessage';
  95. import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
  96. import { dscAlarmLogList } from '../hsqHome.api';
  97. /** 操作按钮数据结构 */
  98. interface ActionItem {
  99. key: string;
  100. label: string;
  101. }
  102. /** 最新报警弹窗展示数据 */
  103. interface LatestAlarmPopupData {
  104. imageUrl: string;
  105. alarmTime: string;
  106. deviceName: string;
  107. alarmType: string;
  108. }
  109. /** 事件类型编码:热源 / 闯入 */
  110. const EVENT_TYPE_MAP: Record<string, { code: string; title: string; typeLabel: string }> = {
  111. heat: { code: '10001', title: '最新热源告警图', typeLabel: '热源报警' },
  112. intrusion: { code: '10002', title: '最新闯入告警图', typeLabel: '闯入报警' },
  113. };
  114. let props = defineProps({
  115. deviceId: {
  116. type: String,
  117. default: '',
  118. },
  119. });
  120. /** 组件自定义事件 */
  121. const emit = defineEmits<{
  122. (e: 'action', key: string): void;
  123. (e: 'select', channel: any): void;
  124. }>();
  125. const { getCamera, removeCamera, cameraList, openRegionPreview, Total: total } = useCamera();
  126. const playerRef = ref();
  127. const renderPlayer = ref(true);
  128. /** 网格分页:2×4,每页 8 路画面;autoPlayStart/Count 控制自动播放区间;onCameraSelect 选中回调 */
  129. const pagination = reactive<{
  130. current: number;
  131. pageSize: number;
  132. autoPlayStart?: number;
  133. autoPlayCount?: number;
  134. onCameraSelect?: (camera: any, index: number) => void;
  135. }>({
  136. current: 1,
  137. pageSize: 8,
  138. onCameraSelect: handleCameraSelect,
  139. });
  140. /** 顶部操作按钮列表 */
  141. const actions: ActionItem[] = [
  142. { key: 'heat', label: '最新热源' },
  143. { key: 'intrusion', label: '最新闯入' },
  144. { key: 'smoke', label: '最新烟雾' },
  145. { key: 'alarmOff', label: '关闭声光报警' },
  146. ];
  147. /** 当前激活的操作按钮 */
  148. const activeAction = ref('');
  149. /** 当前选中的通道 ID / 名称 */
  150. const selectedId = ref('');
  151. /** 当前选中的画面下标 */
  152. const selectedIndex = ref(-1);
  153. /** 取流播放模式:visible 前4播后4停;infrared 前4停后4播 */
  154. const streamPlayMode = ref<'visible' | 'infrared' | null>(null);
  155. /** 各画面本地焦距缩放比例(不调接口) */
  156. const viewZoomMap = reactive<Record<number, number>>({});
  157. /** 各画面本地焦点模糊程度(px,不调接口) */
  158. const viewFocusBlurMap = reactive<Record<number, number>>({});
  159. /** 各画面本地光圈亮度系数(1 为正常,不调接口) */
  160. const viewIrisBrightMap = reactive<Record<number, number>>({});
  161. const ZOOM_STEP = 0.15;
  162. const ZOOM_MIN = 1;
  163. const ZOOM_MAX = 3;
  164. const FOCUS_BLUR_STEP = 0.4;
  165. const FOCUS_BLUR_MIN = 0;
  166. const FOCUS_BLUR_MAX = 4;
  167. const IRIS_BRIGHT_STEP = 0.12;
  168. const IRIS_BRIGHT_MIN = 0.4;
  169. const IRIS_BRIGHT_MAX = 2;
  170. const { createMessage } = useMessage();
  171. /** 最新报警弹窗 */
  172. const latestAlarmVisible = ref(false);
  173. const latestAlarmExpanded = ref(false);
  174. const latestAlarmTitle = ref('最新热源告警图');
  175. const latestAlarmLoading = ref(false);
  176. const latestAlarmData = reactive<LatestAlarmPopupData>({
  177. imageUrl: '',
  178. alarmTime: '',
  179. deviceName: '',
  180. alarmType: '',
  181. });
  182. /** 解析报警记录中的图片地址 */
  183. function resolveAlarmImageUrl(record: Record<string, any>) {
  184. const raw = record.picName || record.picName2 || '';
  185. if (!raw) return '';
  186. const text = String(raw);
  187. if (/^https?:\/\//i.test(text) || text.startsWith('data:')) return text;
  188. // 兼容路径中带盘符前缀的情况(与报警详情一致)
  189. const path = text.includes('h') ? text.substring(text.indexOf('h')) : text;
  190. return getFileAccessHttpUrl(path.replace(/\\/g, '/'));
  191. }
  192. /** 从列表中过滤出指定类型的最新一条记录 */
  193. function pickLatestAlarmRecord(records: any[], eventCode: string) {
  194. const matched = (records || []).filter((item) => String(item?.eventType) === eventCode);
  195. if (!matched.length) return null;
  196. return matched.slice().sort((a, b) => dayjs(b.createTime || b.alarmTime || 0).valueOf() - dayjs(a.createTime || a.alarmTime || 0).valueOf())[0];
  197. }
  198. /** 点击最新热源 / 最新闯入:请求列表后展示对应最新报警图 */
  199. async function openLatestAlarmByType(key: 'heat' | 'intrusion') {
  200. if (latestAlarmLoading.value) return;
  201. const meta = EVENT_TYPE_MAP[key];
  202. if (!meta) return;
  203. latestAlarmLoading.value = true;
  204. try {
  205. const res = await dscAlarmLogList({ pageNo: 1, pageSize: 1000 });
  206. const records = res?.records || [];
  207. console.log('报警图片列表', records);
  208. const latest = pickLatestAlarmRecord(records, meta.code);
  209. if (!latest) {
  210. createMessage.warning(`暂无${meta.typeLabel}数据`);
  211. return;
  212. }
  213. latestAlarmTitle.value = meta.title;
  214. latestAlarmData.imageUrl = resolveAlarmImageUrl(latest);
  215. latestAlarmData.alarmTime = String(latest.createTime || latest.alarmTime || '');
  216. latestAlarmData.deviceName = String(latest.deviceName || latest.devId || latest.strname || '');
  217. latestAlarmData.alarmType = meta.typeLabel;
  218. latestAlarmExpanded.value = false;
  219. latestAlarmVisible.value = true;
  220. } catch (e) {
  221. console.error('获取最新报警记录失败', e);
  222. createMessage.error('获取最新报警记录失败');
  223. } finally {
  224. latestAlarmLoading.value = false;
  225. }
  226. }
  227. function toggleLatestAlarmExpand() {
  228. latestAlarmExpanded.value = !latestAlarmExpanded.value;
  229. }
  230. function closeLatestAlarmPopup() {
  231. latestAlarmVisible.value = false;
  232. latestAlarmExpanded.value = false;
  233. }
  234. /** 点击操作按钮,触发 action 事件 */
  235. function onAction(key: string) {
  236. activeAction.value = key;
  237. if (key === 'heat' || key === 'intrusion') {
  238. openLatestAlarmByType(key);
  239. }
  240. emit('action', key);
  241. }
  242. /** 选中监控通道,触发 select 事件 */
  243. function handleCameraSelect(camera: any, index: number) {
  244. selectedIndex.value = index;
  245. selectedId.value = camera?.id || camera?.name || String(index);
  246. const deviceid = camera?.deviceid || camera?.deviceId || camera?.deviceID || camera?.devId || '';
  247. emit('select', {
  248. ...camera,
  249. index,
  250. deviceid: deviceid != null && deviceid !== '' ? String(deviceid) : '',
  251. ip: camera?.ip || camera?.strip || '',
  252. });
  253. applyViewLensStyles();
  254. }
  255. /** 对当前选中画面做本地焦距缩放(不调接口) */
  256. function zoomSelectedView(direction: 1 | -1) {
  257. if (selectedIndex.value < 0) return;
  258. const index = selectedIndex.value;
  259. const current = viewZoomMap[index] ?? 1;
  260. const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Number((current + direction * ZOOM_STEP).toFixed(2))));
  261. viewZoomMap[index] = next;
  262. applyViewLensStyles();
  263. }
  264. /**
  265. * 对当前选中画面做本地焦点调节(不调接口)
  266. * direction > 0 焦点后调(更模糊);direction < 0 焦点前调(更清晰)
  267. */
  268. function focusSelectedView(direction: 1 | -1) {
  269. if (selectedIndex.value < 0) return;
  270. const index = selectedIndex.value;
  271. const current = viewFocusBlurMap[index] ?? 0;
  272. const next = Math.min(FOCUS_BLUR_MAX, Math.max(FOCUS_BLUR_MIN, Number((current + direction * FOCUS_BLUR_STEP).toFixed(2))));
  273. viewFocusBlurMap[index] = next;
  274. applyViewLensStyles();
  275. }
  276. /**
  277. * 对当前选中画面做本地光圈调节(不调接口)
  278. * direction > 0 光圈扩大(更亮);direction < 0 光圈缩小(更暗)
  279. */
  280. function irisSelectedView(direction: 1 | -1) {
  281. if (selectedIndex.value < 0) return;
  282. const index = selectedIndex.value;
  283. const current = viewIrisBrightMap[index] ?? 1;
  284. const next = Math.min(IRIS_BRIGHT_MAX, Math.max(IRIS_BRIGHT_MIN, Number((current + direction * IRIS_BRIGHT_STEP).toFixed(2))));
  285. viewIrisBrightMap[index] = next;
  286. applyViewLensStyles();
  287. }
  288. /** 获取画面实际绘制节点(video/canvas),避免操作外层容器 */
  289. function getPaintNode(item: HTMLElement) {
  290. const media = (item.querySelector('video, canvas') as HTMLElement | null) || (item.querySelector('.liveVideo') as HTMLElement | null);
  291. if (!media || media === item) return null;
  292. return media.matches('video, canvas') ? media : (media.querySelector('video, canvas') as HTMLElement | null) || media;
  293. }
  294. /** 把焦距缩放 + 焦点模糊 + 光圈亮度应用到对应画面内容(不改变外层容器尺寸) */
  295. function applyViewLensStyles() {
  296. const root = playerRef.value as HTMLElement | undefined;
  297. if (!root) return;
  298. const items = Array.from(root.querySelectorAll('.live-video-item')) as HTMLElement[];
  299. items.forEach((item, index) => {
  300. item.style.overflow = 'hidden';
  301. item.style.transform = '';
  302. const paintNode = getPaintNode(item);
  303. if (!paintNode) return;
  304. const scale = viewZoomMap[index] ?? 1;
  305. const blur = viewFocusBlurMap[index] ?? 0;
  306. const bright = viewIrisBrightMap[index] ?? 1;
  307. const filters: string[] = [];
  308. if (bright !== 1) filters.push(`brightness(${bright})`);
  309. if (blur > 0) filters.push(`blur(${blur}px)`);
  310. paintNode.style.transformOrigin = 'center center';
  311. paintNode.style.transition = 'transform 0.15s ease, filter 0.15s ease';
  312. paintNode.style.transform = scale === 1 ? 'none' : `scale(${scale})`;
  313. paintNode.style.filter = filters.length ? filters.join(' ') : 'none';
  314. });
  315. }
  316. /** 同步分页上的自动播放区间 */
  317. function syncPaginationPlayRange() {
  318. if (streamPlayMode.value === 'visible') {
  319. pagination.autoPlayStart = 0;
  320. pagination.autoPlayCount = 4;
  321. } else if (streamPlayMode.value === 'infrared') {
  322. pagination.autoPlayStart = 4;
  323. pagination.autoPlayCount = 4;
  324. } else {
  325. pagination.autoPlayStart = undefined;
  326. pagination.autoPlayCount = undefined;
  327. }
  328. }
  329. /** 按当前取流模式校正播放/停止状态 */
  330. function applyStreamPlayState() {
  331. if (!streamPlayMode.value) return;
  332. const root = playerRef.value as HTMLElement | undefined;
  333. if (!root) return;
  334. const videos = Array.from(root.querySelectorAll('video')) as HTMLVideoElement[];
  335. videos.forEach((video, index) => {
  336. const shouldPlay = streamPlayMode.value === 'visible' ? index < 4 : index >= 4;
  337. if (shouldPlay) {
  338. video.play?.().catch(() => {});
  339. } else {
  340. video.pause?.();
  341. }
  342. });
  343. }
  344. /** 手动勾选可见光取流:前4播放,后4停止 */
  345. async function enableVisibleLightPlayMode() {
  346. streamPlayMode.value = 'visible';
  347. syncPaginationPlayRange();
  348. await nextTick();
  349. applyStreamPlayState();
  350. }
  351. /** 手动勾选红外取流:前4停止,后4播放 */
  352. async function enableInfraredPlayMode() {
  353. streamPlayMode.value = 'infrared';
  354. syncPaginationPlayRange();
  355. await nextTick();
  356. applyStreamPlayState();
  357. }
  358. /**
  359. * 勾选设备 ID 预检:能查到摄像头才用它过滤
  360. * 后端 devIdList 过滤查不到数据时退回不带过滤,保证画面能展示
  361. */
  362. async function resolveUsableDevIdList(raw: string) {
  363. const ids = String(raw ?? '')
  364. .split(',')
  365. .map((id) => id.trim())
  366. .filter((id) => id !== '' && id !== 'undefined' && id !== 'null')
  367. .join(',');
  368. if (!ids) return '';
  369. try {
  370. const res = await cameraList({ devIdList: ids, pageNo: 1, pageSize: pagination.pageSize });
  371. if (res?.records?.length) return ids;
  372. console.warn('[DeviceView] 按勾选设备ID查询无结果,退回全部摄像头。devIdList =', ids);
  373. } catch (e) {
  374. console.warn('[DeviceView] 按勾选设备ID查询异常,退回全部摄像头', e);
  375. }
  376. return '';
  377. }
  378. /** 加载当前页监控画面 */
  379. async function loadCameraList() {
  380. syncPaginationPlayRange();
  381. const devIdList = await resolveUsableDevIdList(props.deviceId);
  382. await getCamera(devIdList, playerRef, renderPlayer, pagination);
  383. if (streamPlayMode.value) {
  384. await nextTick();
  385. // 等待播放器挂载完成后再校正播放状态
  386. setTimeout(() => {
  387. applyStreamPlayState();
  388. applyViewLensStyles();
  389. }, 300);
  390. } else {
  391. await nextTick();
  392. setTimeout(() => applyViewLensStyles(), 300);
  393. }
  394. }
  395. /** 分页切换 */
  396. async function onPageChange(page: number) {
  397. pagination.current = page;
  398. selectedIndex.value = -1;
  399. selectedId.value = '';
  400. Object.keys(viewZoomMap).forEach((key) => delete viewZoomMap[Number(key)]);
  401. Object.keys(viewFocusBlurMap).forEach((key) => delete viewFocusBlurMap[Number(key)]);
  402. Object.keys(viewIrisBrightMap).forEach((key) => delete viewIrisBrightMap[Number(key)]);
  403. await loadCameraList();
  404. }
  405. onMounted(async () => {
  406. await loadCameraList();
  407. });
  408. watch(
  409. () => props.deviceId,
  410. async () => {
  411. pagination.current = 1;
  412. selectedIndex.value = -1;
  413. selectedId.value = '';
  414. Object.keys(viewZoomMap).forEach((key) => delete viewZoomMap[Number(key)]);
  415. Object.keys(viewFocusBlurMap).forEach((key) => delete viewFocusBlurMap[Number(key)]);
  416. Object.keys(viewIrisBrightMap).forEach((key) => delete viewIrisBrightMap[Number(key)]);
  417. await loadCameraList();
  418. }
  419. );
  420. onBeforeUnmount(() => {
  421. removeCamera(playerRef);
  422. });
  423. defineExpose({
  424. enableVisibleLightPlayMode,
  425. enableInfraredPlayMode,
  426. applyStreamPlayState,
  427. zoomSelectedView,
  428. focusSelectedView,
  429. irisSelectedView,
  430. openRegionPreview,
  431. });
  432. </script>
  433. <style lang="less" scoped>
  434. @import '/@/design/theme.less';
  435. // 设备画面主容器
  436. .device-view {
  437. --image-bg: url('@/assets/images/home-container/configurable/hsq/7-5.png');
  438. --dv-accent: #00d4ff;
  439. --dv-accent-soft: rgba(0, 212, 255, 0.45);
  440. --dv-text: #d8f4ff;
  441. position: relative;
  442. width: 100%;
  443. height: 100%;
  444. padding: 15px;
  445. box-sizing: border-box;
  446. }
  447. .device-view-content {
  448. width: 100%;
  449. height: 100%;
  450. display: flex;
  451. flex-direction: column;
  452. overflow: hidden;
  453. color: var(--dv-text);
  454. user-select: none;
  455. // margin: 15px;
  456. }
  457. // 顶部操作按钮栏
  458. .view-actions {
  459. display: flex;
  460. flex-wrap: wrap;
  461. align-items: center;
  462. gap: 12px;
  463. margin-bottom: 14px;
  464. flex-shrink: 0;
  465. .action-btn {
  466. min-width: 96px;
  467. height: 30px;
  468. padding: 0 14px;
  469. border: 1px solid #269ee1;
  470. border-radius: 2px;
  471. background: #185f8e;
  472. color: #fff;
  473. font-size: 13px;
  474. letter-spacing: 0.5px;
  475. cursor: pointer;
  476. box-shadow: 0 0 6px rgba(0, 200, 255, 0.25), inset 0 0 8px rgba(0, 120, 180, 0.12);
  477. transition: color 0.2s, background 0.2s, box-shadow 0.2s;
  478. &:hover,
  479. &.active {
  480. color: #fff;
  481. background: rgba(0, 90, 140, 0.45);
  482. }
  483. &:active {
  484. transform: translateY(1px);
  485. }
  486. }
  487. }
  488. // 2×4 监控画面网格
  489. .view-grid {
  490. position: relative;
  491. flex: 1;
  492. min-height: 0;
  493. display: flex;
  494. overflow: hidden;
  495. }
  496. .view-cell {
  497. flex: 1;
  498. min-height: 0;
  499. cursor: pointer;
  500. }
  501. .media-wrap {
  502. height: 100%;
  503. box-sizing: border-box;
  504. }
  505. .view-pagination {
  506. display: flex;
  507. justify-content: flex-end;
  508. align-items: center;
  509. flex-shrink: 0;
  510. height: 36px;
  511. padding: 0 4px;
  512. }
  513. :deep(.zxm-pagination) {
  514. color: #cfe9ff;
  515. .zxm-pagination-item,
  516. .zxm-pagination-prev,
  517. .zxm-pagination-next,
  518. .zxm-pagination-jump-prev,
  519. .zxm-pagination-jump-next {
  520. min-width: 26px;
  521. height: 26px;
  522. line-height: 24px;
  523. border: 1px solid rgba(58, 225, 255, 0.45);
  524. background: rgba(16, 64, 100, 0.85);
  525. color: #cfe9ff;
  526. }
  527. .zxm-pagination-item a,
  528. .zxm-pagination-prev button,
  529. .zxm-pagination-next button {
  530. color: #cfe9ff;
  531. }
  532. .zxm-pagination-item-active {
  533. border-color: #01fefc;
  534. background: rgba(32, 166, 169, 0.85);
  535. a {
  536. color: #fff;
  537. }
  538. }
  539. .zxm-pagination-disabled .zxm-pagination-item-link,
  540. .zxm-pagination-disabled button {
  541. color: rgba(207, 233, 255, 0.35);
  542. border-color: rgba(58, 225, 255, 0.2);
  543. }
  544. }
  545. // 无画面占位提示
  546. .media-empty {
  547. height: 100%;
  548. display: flex;
  549. align-items: center;
  550. justify-content: center;
  551. }
  552. :deep(#LivePlayerBox) {
  553. display: flex;
  554. flex-direction: row;
  555. justify-content: flex-start;
  556. flex-wrap: wrap;
  557. pointer-events: none;
  558. gap: 10px;
  559. .live-video-item {
  560. width: 349px;
  561. height: 220px;
  562. padding: 12px 8px;
  563. box-sizing: border-box;
  564. pointer-events: auto;
  565. cursor: pointer;
  566. background-size: 100% 100%;
  567. border: 1px solid transparent;
  568. border-radius: 2px;
  569. transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
  570. /* 变焦时容器尺寸不变,仅裁剪放大后的画面 */
  571. overflow: hidden;
  572. position: relative;
  573. flex: 0 0 349px;
  574. &:hover {
  575. border-color: rgba(1, 254, 252, 0.45);
  576. box-shadow: 0 0 8px rgba(0, 183, 255, 0.25);
  577. }
  578. /* 选中态:青色描边高亮,与监控界面风格一致 */
  579. &.is-selected {
  580. border-color: #01fefc;
  581. box-shadow: 0 0 10px rgba(1, 254, 252, 0.45), inset 0 0 12px rgba(0, 183, 255, 0.12);
  582. background-color: rgba(0, 90, 140, 0.18);
  583. }
  584. /* 内层媒体区:占满内容区,作为裁剪视口 */
  585. .liveVideo,
  586. video,
  587. canvas {
  588. display: block;
  589. width: 100% !important;
  590. height: 100% !important;
  591. max-width: 100%;
  592. max-height: 100%;
  593. object-fit: cover;
  594. transform-origin: center center;
  595. }
  596. }
  597. .liveVideo {
  598. width: 100% !important;
  599. height: 100% !important;
  600. padding: 0 !important;
  601. align-self: auto !important;
  602. background-size: 100% 100% !important;
  603. pointer-events: none;
  604. overflow: hidden;
  605. }
  606. .video-parent {
  607. pointer-events: auto !important;
  608. align-self: auto !important;
  609. overflow: hidden;
  610. width: 100%;
  611. height: 100%;
  612. }
  613. }
  614. ::-webkit-scrollbar {
  615. display: none;
  616. }
  617. /* 最新热源 / 最新闯入 弹窗(风格对齐报警自动弹窗) */
  618. .latest-alarm-mask {
  619. position: fixed;
  620. inset: 0;
  621. z-index: 2200;
  622. display: flex;
  623. align-items: center;
  624. justify-content: center;
  625. background: rgba(0, 8, 20, 0.55);
  626. }
  627. .latest-alarm-panel {
  628. width: min(720px, 92vw);
  629. max-height: 90vh;
  630. &.expanded {
  631. width: min(1080px, 96vw);
  632. }
  633. }
  634. .latest-alarm-frame {
  635. position: relative;
  636. padding: 18px 12px 0;
  637. background: linear-gradient(180deg, rgba(6, 28, 48, 0.98) 0%, rgba(4, 16, 32, 0.98) 100%);
  638. border: 1px solid rgba(0, 210, 255, 0.85);
  639. box-shadow: 0 0 0 2px rgba(8, 40, 70, 0.95), 0 0 22px rgba(0, 200, 255, 0.35), inset 0 0 0 1px rgba(0, 180, 255, 0.35);
  640. clip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 10px, 100% calc(100% - 10px), calc(100% - 10px) 100%, 10px 100%, 0 calc(100% - 10px), 0 10px);
  641. }
  642. .latest-alarm-title-tab {
  643. position: absolute;
  644. top: -1px;
  645. left: 50%;
  646. transform: translateX(-50%);
  647. min-width: 168px;
  648. height: 28px;
  649. padding: 0 28px;
  650. display: flex;
  651. align-items: center;
  652. justify-content: center;
  653. color: #fff;
  654. font-size: 14px;
  655. font-weight: 600;
  656. letter-spacing: 1px;
  657. background: linear-gradient(180deg, #2ad4e8 0%, #1498b8 55%, #0c6f8a 100%);
  658. clip-path: polygon(12px 0, calc(100% - 12px) 0, 100% 100%, 0 100%);
  659. box-shadow: 0 0 10px rgba(0, 200, 255, 0.45);
  660. z-index: 2;
  661. }
  662. .latest-alarm-toolbar {
  663. position: absolute;
  664. top: 8px;
  665. right: 12px;
  666. display: flex;
  667. gap: 8px;
  668. z-index: 3;
  669. }
  670. .latest-alarm-icon-btn {
  671. width: 22px;
  672. height: 22px;
  673. padding: 0;
  674. border: none;
  675. background: transparent;
  676. color: #e8f4ff;
  677. cursor: pointer;
  678. display: inline-flex;
  679. align-items: center;
  680. justify-content: center;
  681. &:hover {
  682. color: #01fefc;
  683. }
  684. }
  685. .latest-alarm-image-wrap {
  686. position: relative;
  687. margin-top: 14px;
  688. height: 380px;
  689. border: 1px solid rgba(0, 183, 255, 0.45);
  690. background: #000;
  691. overflow: hidden;
  692. .latest-alarm-panel.expanded & {
  693. height: 560px;
  694. }
  695. }
  696. .latest-alarm-image {
  697. width: 100%;
  698. height: 100%;
  699. object-fit: contain;
  700. display: block;
  701. }
  702. .latest-alarm-image-empty {
  703. width: 100%;
  704. height: 100%;
  705. display: flex;
  706. align-items: center;
  707. justify-content: center;
  708. color: rgba(200, 220, 235, 0.55);
  709. font-size: 14px;
  710. }
  711. .latest-alarm-overlay {
  712. position: absolute;
  713. color: #fff;
  714. font-size: 13px;
  715. text-shadow: 0 0 4px rgba(0, 0, 0, 0.85);
  716. pointer-events: none;
  717. &.bottom-right {
  718. right: 10px;
  719. bottom: 10px;
  720. }
  721. }
  722. .latest-alarm-footer {
  723. display: flex;
  724. flex-wrap: wrap;
  725. align-items: center;
  726. gap: 8px;
  727. padding: 10px 8px 12px;
  728. color: #cfe9ff;
  729. font-size: 13px;
  730. }
  731. .latest-alarm-sep {
  732. color: rgba(1, 254, 252, 0.55);
  733. }
  734. </style>