Files
jh-military-admin/src/views/device/components/useVideoDialog.ts
T
dzc 86533f0c87
build / task (16.x, npm, macOS-latest) (push) Canceled after 0s
build / task (16.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, npm, windows-latest) (push) Canceled after 0s
build / task (16.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (16.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (16.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (16.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, yarn, windows-latest) (push) Canceled after 0s
build / task (18.x, npm, macOS-latest) (push) Canceled after 0s
build / task (18.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, npm, windows-latest) (push) Canceled after 0s
build / task (18.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (18.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (18.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (18.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, yarn, windows-latest) (push) Canceled after 0s
build / task (20.x, npm, macOS-latest) (push) Canceled after 0s
build / task (20.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, npm, windows-latest) (push) Canceled after 0s
build / task (20.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (20.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (20.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (20.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, yarn, windows-latest) (push) Canceled after 0s
fix: 完善视频播放关闭流程和弹窗通道交互
2026-10-27 15:35:21 +08:00

211 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
import * as DeviceManageApi from '/@/api/device/deviceManage';
import * as DeviceChannelApi from '/@/api/device/deviceChannel';
import { useMessage } from '/@/hooks/message';
export interface VideoChannel {
channelCode: string;
channelName?: string;
channelSeq?: string | number;
deviceName?: string;
}
export type ChannelSelectionMode = 'single' | 'multiple';
interface VideoDialogOptions {
mode: 'preview' | 'replay';
getPlayOptions: () => Record<string, string | number> | null;
}
interface DialogPlayer {
startReal: (channels: Record<string, unknown>[]) => Promise<void>;
startPlayback: (channels: Record<string, unknown>[]) => Promise<void>;
closeVideo: () => Promise<void>;
setTotal?: (total: number) => void;
destroy?: () => void;
updateVisible: (visible: boolean) => void;
}
export function useVideoDialog(options: VideoDialogOptions) {
const dialogVisible = ref(false);
const loading = ref(false);
const loadError = ref('');
const channels = ref<VideoChannel[]>([]);
const selectedCodes = ref<string[]>([]);
const playingCodes = ref<string[]>([]);
const selectionMode = ref<ChannelSelectionMode>('single');
const playerRef = ref<DialogPlayer>();
const deviceName = ref('');
const currentPage = ref(1);
const pageSize = 16;
const divisionOptions = [1, 4, 9, 16];
let sourceType = '';
let sourceCode = '';
let session = 0;
let playRequest = 0;
let appliedOptions: Record<string, string | number> = {};
const playingChannels = computed(() => {
const codes = new Set(playingCodes.value);
return channels.value.filter((channel) => codes.has(channel.channelCode));
});
const pageChannels = computed(() => playingChannels.value.slice((currentPage.value - 1) * pageSize, currentPage.value * pageSize));
const displayedCodes = computed(() => pageChannels.value.map((channel) => channel.channelCode));
const division = computed(() => divisionOptions.find((count) => count >= pageChannels.value.length) || pageSize);
const playingSummary = computed(() => {
if (!playingChannels.value.length) return '尚未选择播放通道';
if (playingChannels.value.length === 1) return playingChannels.value[0].channelName || '未命名通道';
const summary = `共 ${playingChannels.value.length} 个通道`;
return playingChannels.value.length > pageSize
? `${summary} · 当前第 ${(currentPage.value - 1) * pageSize + 1}–${Math.min(currentPage.value * pageSize, playingChannels.value.length)} 路`
: summary;
});
async function playPage() {
const request = ++playRequest;
const activeSession = session;
const list = pageChannels.value.map((channel, snum) => ({
...appliedOptions,
channelId: channel.channelCode,
channelName: channel.channelName,
snum,
}));
await nextTick();
const player = playerRef.value;
if (!player || !dialogVisible.value || request !== playRequest) return;
try {
// 切换前清空旧窗口,避免取消选择的通道继续播放。
await player.closeVideo();
if (!dialogVisible.value || activeSession !== session || request !== playRequest || player !== playerRef.value) return;
if (options.mode === 'preview') {
// 预览保留插件云台入口,分页在弹窗状态栏统一显示。
player.setTotal?.(list.length);
await player.startReal(list);
} else await player.startPlayback(list);
} catch {
if (dialogVisible.value && request === playRequest) useMessage().error('视频播放失败,请重试');
}
}
async function playSelected() {
if (loading.value || !selectedCodes.value.length) return;
const nextOptions = options.getPlayOptions();
if (!nextOptions) return;
// 翻页沿用本次筛选快照,不受尚未提交的表单修改影响。
appliedOptions = { ...nextOptions };
playingCodes.value = [...selectedCodes.value];
currentPage.value = 1;
await playPage();
}
function changeSelection(codes: string[]) {
const available = new Set(channels.value.map((channel) => channel.channelCode));
selectedCodes.value = [...new Set(codes)].filter((code) => available.has(code));
if (selectionMode.value === 'single') {
selectedCodes.value = selectedCodes.value.slice(0, 1);
void playSelected();
}
}
function changeSelectionMode(mode: ChannelSelectionMode) {
selectionMode.value = mode;
if (mode === 'single') {
selectedCodes.value = [selectedCodes.value[0] || playingCodes.value[0] || channels.value[0]?.channelCode].filter(Boolean);
void playSelected();
}
}
async function changePage(page: number) {
currentPage.value = Math.max(1, Math.min(page, Math.ceil(playingChannels.value.length / pageSize) || 1));
await playPage();
}
function closeDialog() {
++session;
++playRequest;
dialogVisible.value = false;
loading.value = false;
playerRef.value?.updateVisible(false);
if (playerRef.value?.destroy) playerRef.value.destroy();
else void playerRef.value?.closeVideo();
}
async function openDialog(type: string, code: string) {
if (!type || !code) return;
closeDialog();
const activeSession = ++session;
sourceType = type;
sourceCode = code;
channels.value = [];
selectedCodes.value = [];
playingCodes.value = [];
deviceName.value = '';
loadError.value = '';
selectionMode.value = 'single';
currentPage.value = 1;
dialogVisible.value = true;
loading.value = true;
try {
const query: Record<string, string> = {};
if (type === 'device') {
// 预览入口传设备 ID,回放入口传设备编码。
const result = options.mode === 'preview' ? await DeviceManageApi.getObj(code) : await DeviceManageApi.getByCode({ code });
if (activeSession !== session) return;
if (result.code != 0 || !result.data?.deviceCode) throw new Error('获取设备信息失败');
query.deviceCode = result.data.deviceCode;
deviceName.value = result.data.deviceName || '';
} else if (type === 'channel') query.channelCode = code;
else if (type === 'playbackGroup') query.playbackGroupChannelCode = code;
else throw new Error('不支持的视频来源');
const result = await DeviceChannelApi.list(query);
if (activeSession !== session) return;
if (result.code != 0) throw new Error('加载通道失败,请重试');
const seen = new Set<string>();
channels.value = (Array.isArray(result.data) ? result.data : [])
.filter((channel: VideoChannel) => {
const channelCode = channel.channelCode == null ? '' : String(channel.channelCode);
if (!channelCode || seen.has(channelCode)) return false;
seen.add(channelCode);
return true;
})
.map((channel: VideoChannel) => ({
...channel,
channelCode: String(channel.channelCode),
}));
deviceName.value ||= channels.value[0]?.deviceName || (type === 'playbackGroup' ? '回放分组' : '设备视频');
selectedCodes.value = channels.value.slice(0, 1).map((channel) => channel.channelCode);
} catch (error) {
if (activeSession === session) loadError.value = error instanceof Error ? error.message : '加载通道失败,请重试';
} finally {
if (activeSession === session) loading.value = false;
}
if (activeSession === session && dialogVisible.value) void playSelected();
}
onBeforeUnmount(closeDialog);
return {
dialogVisible,
channels,
selectedCodes,
selectionMode,
loading,
loadError,
playingCodes,
displayedCodes,
playingChannels,
deviceName,
playerRef,
currentPage,
pageSize,
division,
divisionOptions,
playingSummary,
openDialog,
closeDialog,
changeSelection,
changeSelectionMode,
playSelected,
changePage,
reloadChannels: () => openDialog(sourceType, sourceCode),
};
}