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
211 lines
7.5 KiB
TypeScript
211 lines
7.5 KiB
TypeScript
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),
|
||
};
|
||
}
|