This commit is contained in:
itcsx committed 2026-09-21 21:31:16 +08:00
1 parent 7c8af2fb08
commit c10f8347e2
31 files changed
+358 -292

No files matched your search

+4 -3
View File
@@ -219,7 +219,7 @@ const requestChunk = async (
percent: percentBefore,
current: index,
total: chunks.length,
text: `网络异常,正在重试(${attempt + 1}/${retries})...`,
text: `网络异常,正在重试(${attempt + 1}/${retries})... ${percentBefore}%`,
});
await sleep(props.retryDelay * Math.pow(2, attempt));
}
@@ -262,10 +262,11 @@ const uploadChunk = async (
percent,
current: index,
total: chunks.length,
// 单文件、多文件都统一用百分比(按字节算的整体进度),不再显示「第几片/共几片」
text:
progress.fileCount > 1
? `上传中(第 ${progress.fileNo}/${progress.fileCount} 个,分片 ${index}/${chunks.length}) ${percent}%`
: `上传中(${index}/${chunks.length})...`,
? `上传中(第 ${progress.fileNo}/${progress.fileCount} 个) ${percent}%`
: `上传中 ${percent}%`,
});
if (index < chunks.length) {
+2 -1
View File
@@ -39,7 +39,7 @@
<template v-else-if="isVideo(record.OrderKey)">
<div class="video-thumb">
<img :src="record.FormattedPic" :title="record.OrgFileName" />
<img :src="record.FormattedPic" :title="record.OrgFileName" @error="retryImage($event)" />
<div class="play-overlay">
<PlayCircleOutlined />
</div>
@@ -109,6 +109,7 @@ import {
AppstoreOutlined,
} from "@ant-design/icons-vue";
import { DmsFileUploadViewDisplayData } from "@/data/auto/dmsFileUploadViewDisplayData";
import { retryImage } from "@/utils/imageRetry";
const props = defineProps<{
datas: DmsFileUploadViewDisplayData[];
@@ -80,6 +80,7 @@
<img
:src="record.FormattedPic"
:title="record.OrgFileName"
@error="retryImage($event)"
/>
<div class="play-overlay">
<PlayCircleOutlined />
@@ -192,6 +193,7 @@ import {
} from "@ant-design/icons-vue";
import { FamilyDmsFileViewDisplayData } from "@/data/auto/familyDmsFileViewDisplayData";
import { notification } from "ant-design-vue";
import { retryImage } from "@/utils/imageRetry";
const props = defineProps<{
datas: FamilyDmsFileViewDisplayData[];
selectedKeys: FamilyDmsFileViewDisplayData[];
@@ -58,20 +58,20 @@
</a-tooltip>
</a-col>
<a-col :span="2" v-if="!isDirectorySelected && isSingleItem && isSingleVideo">
<a-tooltip title="播放">
<a-tooltip>
<a-button class="nav-btn" @click="onPlayVideo"><cust-icon-font size="16"
type="icon-foldervideo" /></a-button>
</a-tooltip>
</a-col>
<a-col :span="2" v-if="!isDirectorySelected && isSingleItem && isSingleAudio" style="margin-left: 4px">
<a-tooltip v-if="selectedItem.IsPlay != 'Y'" title="播放">
<a-tooltip v-if="selectedItem.IsPlay != 'Y'">
<a-button class="nav-btn" :icon="h(PlayCircleOutlined, { style: { color: 'red' } })"
@click="onPlayAudio"></a-button>
</a-tooltip>
<a-tooltip v-else-if="selectedItem.IsPause != 'Y'" title="暂停">
<a-tooltip v-else-if="selectedItem.IsPause != 'Y'">
<a-button class="nav-btn" :icon="h(PauseCircleOutlined)" @click="onPauseAudio"></a-button>
</a-tooltip>
<a-tooltip v-else title="继续">
<a-tooltip v-else>
<a-button class="nav-btn" :icon="h(PlayCircleOutlined, { style: { color: 'red' } })"
@click="onContinueAudio"></a-button>
</a-tooltip>
@@ -85,9 +85,8 @@
</a-col>
</a-row>
<FileUpload v-if="canCreate" ref="fileUploadRef" :action="uploadUrl" :headers="headers"
:resolve-file-path="resolveFilePath" :multiple="multiple" @start="onUploadStart"
@progress="onUploadProgress" @success="onUploadSuccess" @complete="onUploadComplete"
@interrupted="onUploadInterrupted" @error="onUploadError" />
:resolve-file-path="resolveFilePath" :multiple="multiple" @start="onUploadStart" @progress="onUploadProgress"
@success="onUploadSuccess" @complete="onUploadComplete" @interrupted="onUploadInterrupted" @error="onUploadError" />
</template>
<script setup lang="ts">
@@ -4,7 +4,7 @@
<a-row>
<a-col flex="300px">
<a-row justify="start">
<a-col :span="3" style="margin-left: 8px">
<a-col :span="3" style="margin-left: 8px">
<a-tooltip title="还原">
<a-button class="nav-btn" :icon="h(UndoOutlined)" style="color:green;"
:disabled="!hasSelection" @click="$emit('restore', selectedItems)"></a-button>
@@ -12,33 +12,36 @@
</a-col>
<a-col :span="3">
<a-tooltip title="下载">
<a-button class="nav-btn" :icon="h(DownloadOutlined)" :disabled="!hasSelection || isDirectorySelected || !isSingleItem"
<a-button class="nav-btn" :icon="h(DownloadOutlined)"
:disabled="!hasSelection || isDirectorySelected || !isSingleItem"
@click="onDownload"></a-button>
</a-tooltip>
</a-col>
<a-col :span="3" v-if="hasSelection">
<a-col :span="3" v-if="hasSelection">
<a-tooltip title="删除">
<a-popconfirm title="确定是否删除?" ok-text="是" cancel-text="否" @confirm="$emit('delete', selectedItems)">
<a-button class="nav-btn" :icon="h(DeleteOutlined)" style="color:red;"
:disabled="!canCreate" ></a-button></a-popconfirm>
<a-popconfirm title="确定是否删除?" ok-text="是" cancel-text="否"
@confirm="$emit('delete', selectedItems)">
<a-button class="nav-btn" :icon="h(DeleteOutlined)" style="color:red;"
:disabled="!canCreate"></a-button></a-popconfirm>
</a-tooltip>
</a-col>
<a-col :span="3" v-if="!isDirectorySelected && isSingleItem && isSingleVideo">
<a-tooltip title="播放">
<a-tooltip>
<a-button class="nav-btn" @click="onPlayVideo"><cust-icon-font size="16"
type="icon-foldervideo" /></a-button>
</a-tooltip>
</a-col>
<a-col :span="3" v-if="!isDirectorySelected && isSingleItem && isSingleAudio" >
<a-tooltip v-if="selectedItem.IsPlay != 'Y'" title="播放">
<a-col :span="3" v-if="!isDirectorySelected && isSingleItem && isSingleAudio">
<a-tooltip v-if="selectedItem.IsPlay != 'Y'">
<a-button class="nav-btn" :icon="h(PlayCircleOutlined, { style: { color: 'red' } })"
@click="onPlayAudio"></a-button>
</a-tooltip>
<a-tooltip v-else-if="selectedItem.IsPause != 'Y'" title="暂停">
<a-button class="nav-btn" :icon="h(PauseCircleOutlined)" @click="onPauseAudio"></a-button>
<a-tooltip v-else-if="selectedItem.IsPause != 'Y'">
<a-button class="nav-btn" :icon="h(PauseCircleOutlined)"
@click="onPauseAudio"></a-button>
</a-tooltip>
<a-tooltip v-else title="继续">
<a-button class="nav-btn" :icon="h(PlayCircleOutlined, { style: { color: 'red' } })"
<a-tooltip v-else>
<a-button class="nav-btn" :icon="h(PlayCircleOutlined, { style: { color: 'red' } })"
@click="onContinueAudio"></a-button>
</a-tooltip>
</a-col>
@@ -182,20 +185,20 @@ const onContinueAudio = () => {
<style scoped>
.nav-btn {
width: 28px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
width: 28px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.nav-btn:hover:not(:disabled) {
background: #e6f4ff;
color: #1677ff;
background: #e6f4ff;
color: #1677ff;
}
.nav-btn:disabled {
color: #bfbfbf;
color: #bfbfbf;
}
</style>
@@ -24,20 +24,20 @@
</a-tooltip>
</a-col>
<a-col :span="3" v-if="!isDirectorySelected && isSingleItem && isSingleVideo">
<a-tooltip title="播放">
<a-tooltip >
<a-button class="nav-btn" @click="onPlayVideo"><cust-icon-font size="16"
type="icon-foldervideo" /></a-button>
</a-tooltip>
</a-col>
<a-col :span="3" v-if="!isDirectorySelected && isSingleItem && isSingleAudio">
<a-tooltip v-if="selectedItem.IsPlay != 'Y'" title="播放">
<a-tooltip v-if="selectedItem.IsPlay != 'Y'">
<a-button class="nav-btn" :icon="h(PlayCircleOutlined, { style: { color: 'red' } })"
@click="onPlayAudio"></a-button>
</a-tooltip>
<a-tooltip v-else-if="selectedItem.IsPause != 'Y'" title="暂停">
<a-tooltip v-else-if="selectedItem.IsPause != 'Y'" >
<a-button class="nav-btn" :icon="h(PauseCircleOutlined)" @click="onPauseAudio"></a-button>
</a-tooltip>
<a-tooltip v-else title="继续">
<a-tooltip v-else >
<a-button class="nav-btn" :icon="h(PlayCircleOutlined, { style: { color: 'red' } })"
@click="onContinueAudio"></a-button>
</a-tooltip>
+33 -7
View File
@@ -19,7 +19,8 @@
<a-spin size="large" />
<span>加载中…</span>
</div>
<button v-if="!isPlaying && !buffering" class="vp-bigplay" type="button" title="播放" @click.stop="toggle">
<button v-if="!isPlaying && !buffering" class="vp-bigplay" type="button" aria-label="播放"
@click.stop="toggle">
<CaretRightFilled />
</button>
</div>
@@ -107,7 +108,7 @@
<button class="vp-ctl" type="button" title="上一个" @click="goPrev">
<StepBackwardFilled />
</button>
<button class="vp-play" type="button" :title="isPlaying ? '暂停' : '播放'" @click="toggle">
<button class="vp-play" type="button" :aria-label="isPlaying ? '暂停' : '播放'" @click="toggle">
<PauseOutlined v-if="isPlaying" />
<CaretRightFilled v-else />
</button>
@@ -161,11 +162,20 @@ import {
/** 一个视频 */
export type VideoItem = PublishVideoDetailDisplayData;
const props = withDefaults(defineProps<{ videos?: VideoItem[] }>(), { videos: () => [] });
const props = withDefaults(
defineProps<{
videos?: VideoItem[];
/** 打开时从第几个开始播(点列表里的某一个进来时用) */
startIndex?: number;
/** 挂上播放源后是否立即播放(弹窗里默认自动播) */
autoPlay?: boolean;
}>(),
{ videos: () => [], startIndex: 0, autoPlay: false },
);
/* ---------------- 基础状态 ---------------- */
const videoRef = ref<HTMLVideoElement | null>(null);
const index = ref(0);
const index = ref(props.startIndex);
const isPlaying = ref(false);
const currentTime = ref(0);
const duration = ref(0);
@@ -589,7 +599,8 @@ const fmt = (t: number) => {
};
/* ---------------- 联动 ---------------- */
watch(current, async (t) => {
/** 切到某个视频:重置状态 → 挂播放源 → 按需播放 */
const prepare = async (t: VideoItem | null, play: boolean) => {
currentTime.value = 0;
duration.value = 0;
bufferedEnd.value = 0;
@@ -599,11 +610,13 @@ watch(current, async (t) => {
ensureStageRO();
// 重新挂接播放源:m3u8 走 hls.js,普通文件走 src(内部会销毁上一个 hls 实例)
attachSource(t.FormattedUrl);
if (resumeAfterSwitch) {
if (play || props.autoPlay) {
resumeAfterSwitch = false;
tryPlay();
}
});
};
watch(current, (t) => prepare(t, resumeAfterSwitch));
// 播放列表默认收起,由用户点击列表按钮展开
watch(
@@ -613,6 +626,17 @@ watch(
}
);
// 外部指定了从第几个开始播(弹窗没销毁又换了视频时也要跟随)
watch(
() => props.startIndex,
(i) => {
if (i >= 0 && i < videos.value.length && i !== index.value) {
resumeAfterSwitch = true;
index.value = i;
}
}
);
watch([volume, muted], () => {
const v = videoRef.value;
if (!v) return;
@@ -625,6 +649,8 @@ watch(rate, () => {
let timerId = 0;
onMounted(() => {
// 列表在挂载时就已就绪的情况(例如弹窗里先备好数据再打开),watch 不会触发,这里补一次
if (current.value) prepare(current.value, false);
document.addEventListener("click", onDocClick);
document.addEventListener("fullscreenchange", onFullscreenChange);
timerId = window.setInterval(() => {
-3
View File
@@ -11,7 +11,6 @@ import {
setupAntd,
setupAssets,
setupCustomComponents,
setupVueVideoPlayer,
setupVuePhoto,
} from "./plugins";
@@ -20,8 +19,6 @@ const app = createApp(App);
function setupPlugins() {
// 注册全局常用的ant-design-vue组件
setupAntd(app);
// 注册全局常用的VueVideoPlayer组件
setupVueVideoPlayer(app);
// 注册全局常用的VuePhoto组件
setupVuePhoto(app);
// 引入静态资源
-1
View File
@@ -1,5 +1,4 @@
export { setupAntd } from "@/plugins/antd";
export { setupAssets } from "@/plugins/assets";
export { setupCustomComponents } from "@/plugins/customComponents";
export { setupVueVideoPlayer } from "@/plugins/video";
export { setupVuePhoto } from "@/plugins/photo";
-7
View File
@@ -1,7 +0,0 @@
import vue3videoPlay from "vue3-video-play";
import "vue3-video-play/dist/style.css";
import type { App } from "vue";
export function setupVueVideoPlayer(app: App<Element>) {
app.use(vue3videoPlay);
}
@@ -21,7 +21,8 @@
onVisibleChange: setPicVisible,
}" :src="imageSrc" />
<dmsFolder @on-submit="onReload"></dmsFolder>
<videoPlay></videoPlay>
<VideoPlayModal v-model:open="videoPlayModal" :videos="videoPlayList" :start-index="videoPlayIndex"
:title="videoPlayTitle" :fullscreen="width < 640" />
<dragModal v-model:open="showModalShare" width="60%" title="分享管理查询" :mask-closable="false" :fullscreen="width < 640"
@ok="handleSelectShare">
<DmsFileShareModal ref="shareEnquiry"></DmsFileShareModal>
@@ -77,7 +78,6 @@ import { notification } from "ant-design-vue";
import { SearchCriteriaResultData } from "@/data/auto/searchCriteriaResultData";
import dmsFolder from "./DmsFolderModal.vue";
import { orderBy } from "lodash-es";
import VideoPlay from "./PlayVideo.vue";
import { Howl } from "howler";
import { PublishVideoEntity } from "@/data/auto/publishVideoEntity";
import publishVideoService from "@/service/master/publishVideoService";
@@ -92,6 +92,9 @@ import { PublishPhotoEntity } from "@/data/auto/publishPhotoEntity";
import MyComputer from "@/components/basic/MyComputer.vue";
import Toolbar from "@/components/basic/Toolbar.vue";
import OperatorStationDelete from "@/components/basic/OperatorStationDelete.vue";
import VideoPlayModal from "@/components/basic/VideoPlayModal.vue";
import type { VideoItem } from "@/components/basic/VideoPlayer.vue";
import { PublishVideoDetailDisplayData } from "@/data/auto/publishVideoDetailDisplayData";
interface BreadcrumbItem {
value: string;
@@ -137,9 +140,8 @@ const dmsFolderIsRename = ref<boolean>(false);
const dmsFolderIsMove = ref<boolean>(false);
const entity = ref<DmsFileEntity>(new DmsFileEntity());
const videoPlayModal = ref<boolean>(false);
const videoSrc = ref<string>("");
const videoPoster = ref<string>("");
const videoType = ref<string>("");
const videoPlayList = ref<VideoItem[]>([]);
const videoPlayIndex = ref<number>(0);
const videoPlayTitle = ref<string>("");
const visible = ref<boolean>(false);
@@ -239,11 +241,6 @@ provide("dmsFolderTitle", dmsFolderTitle);
provide("dmsFolderIsRename", dmsFolderIsRename);
provide("dmsFolderIsMove", dmsFolderIsMove);
provide("DmsFileEntity", entity);
provide("videoPlayModal", videoPlayModal);
provide("videoPlayTitle", videoPlayTitle);
provide("videoSrc", videoSrc);
provide("videoPoster", videoPoster);
provide("videoType", videoType);
provide("isModalPublishVideo", showModalPublishVideo);
provide("isModalPublishAudio", showModalPublishAudio);
provide("isModalPublishPhoto", showModalPublishPhoto);
@@ -295,29 +292,61 @@ const onChangeFolder = async (fvalue: string) => {
onSearchByFilePath(fvalue);
};
const onUrlVideoPlay = (url: string, title: string, poster: string) => {
/** DMS 记录 → 播放列表项(m3u8 的封面用同名 png 兜底) */
const toVideoItem = (
record: DmsFileUploadDeleteLogViewDisplayData,
): VideoItem => {
const item = new PublishVideoDetailDisplayData();
item.Title = record.OrgFileName;
item.FormattedUrl = record.Url;
item.FormattedPic = (record.FormattedPic || "").replace(/\.m3u8$/, ".png");
return item;
};
/** 当前目录里已转码(有 Url)的视频,作为弹窗播放器的播放列表 */
const buildVideoList = (
records: DmsFileUploadDeleteLogViewDisplayData[],
): VideoItem[] =>
records
.filter(
(x) =>
x.IsDirectoryIndc != "Y" &&
fileTypePolicy.isVideo(x.OrderKey || x.OrgFileName) &&
!!x.Url,
)
.map(toVideoItem);
/** 打开视频弹窗 */
const openVideoPlay = (list: VideoItem[], index: number, title: string) => {
sound.value?.stop();
datas.value.forEach((element) => {
element.IsPlay = "";
});
videoPoster.value = poster;
videoType.value = "m3u8";
videoSrc.value = url;
videoPlayList.value = list;
videoPlayIndex.value = index < 0 ? 0 : index;
videoPlayTitle.value = title || "视频播放";
videoPlayModal.value = true;
videoPlayTitle.value = title;
};
const onVideoPlay = async (fid: number) => {
dmsFileService
.FindDeleteLogM3u8Path(fid)
.then((res) => {
videoType.value = "video/mp4";
videoSrc.value = res;
videoPlayModal.value = true;
})
.catch(() => {
videoPlayModal.value = false;
/** 未转码的视频没有 Url,直接取源片路径播放 */
const onSourceVideoPlay = async (
record: DmsFileUploadDeleteLogViewDisplayData,
) => {
try {
loading.value = true;
const url = await dmsFileService.FindDeleteLogM3u8Path(record.FileId);
const item = toVideoItem(record);
item.FormattedUrl = url;
openVideoPlay([item], 0, record.OrgFileName);
} catch (error) {
notification.info({
message: "系统提示:",
description: "视频加载失败",
placement: "topRight",
});
} finally {
loading.value = false;
}
};
const onPreviewFile = async (
@@ -531,7 +560,7 @@ const onViewPic = () => {
} else if (fileTypePolicy.isImage(keys.value[0].OrgFileName)) {
onPreviewFile(keys.value[0].CreatedBy, keys.value[0].OrderKey);
} else if (fileTypePolicy.isVideo(keys.value[0].OrgFileName)) {
onVideoPlay(keys.value[0].FileId);
onUrlVideoPlayItem(keys.value[0]);
} else {
notification.info({
message: "系统提示:",
@@ -942,7 +971,7 @@ const onOpenFile = (record: DmsFileUploadDeleteLogViewDisplayData) => {
if (fileTypePolicy.isImage(record.OrgFileName)) {
onPreviewFile(record.CreatedBy, record.OrderKey);
} else if (fileTypePolicy.isVideo(record.OrgFileName)) {
onVideoPlay(record.FileId);
onUrlVideoPlayItem(record);
} else if (fileTypePolicy.isAudio(record.OrgFileName)) {
onPlayAudio(toUploadView(record));
} else {
@@ -988,12 +1017,21 @@ const onViewPreviewSelected = (items: DmsFileUploadDeleteLogViewDisplayData[]) =
}
};
const onUrlVideoPlayItem = (record: DmsFileUploadDeleteLogViewDisplayData) => {
onUrlVideoPlay(
record.Url,
record.OrgFileName,
record.FormattedPic.replace(".m3u8", ".png"),
);
/** 播放视频:把当前目录的视频串成播放列表,从点中的那一个开始 */
const onUrlVideoPlayItem = async (
record: DmsFileUploadDeleteLogViewDisplayData,
) => {
if (!record.Url) {
await onSourceVideoPlay(record);
return;
}
const list = buildVideoList(datas.value);
let index = list.findIndex((x) => x.FormattedUrl === record.Url);
if (index < 0) {
list.unshift(toVideoItem(record));
index = 0;
}
openVideoPlay(list, index, record.OrgFileName);
};
const onPlayAudioItem = (record: DmsFileUploadDeleteLogViewDisplayData) => {
+88 -38
View File
@@ -23,7 +23,8 @@
onVisibleChange: setPicVisible,
}" :src="imageSrc" />
<dmsFolder @on-submit="onReload" @submit-start="loading = true" @submit-end="loading = false"></dmsFolder>
<videoPlay></videoPlay>
<VideoPlayModal v-model:open="videoPlayModal" :videos="videoPlayList" :start-index="videoPlayIndex"
:title="videoPlayTitle" :fullscreen="width < 640" />
<dragModal v-model:open="showModalShare" width="60%" title="分享管理查询" :mask-closable="false" :fullscreen="width < 640"
@ok="handleSelectShare">
<DmsFileShareModal ref="shareEnquiry"></DmsFileShareModal>
@@ -83,7 +84,6 @@ import dmsFolder from "./DmsFolderModal.vue";
import { useLoginProfileStore } from "@/store/modules/loginProfile";
import { WebSitePolicy } from "@/policy/custom/webSitePolicy";
import { orderBy } from "lodash-es";
import VideoPlay from "./PlayVideo.vue";
import { Howl } from "howler";
import { PublishVideoEntity } from "@/data/auto/publishVideoEntity";
import publishVideoService from "@/service/master/publishVideoService";
@@ -98,6 +98,9 @@ import { PublishPhotoEntity } from "@/data/auto/publishPhotoEntity";
import MyComputer from "@/components/basic/MyComputer.vue";
import Toolbar from "@/components/basic/Toolbar.vue";
import OperatorStation from "@/components/basic/OperatorStation.vue";
import VideoPlayModal from "@/components/basic/VideoPlayModal.vue";
import type { VideoItem } from "@/components/basic/VideoPlayer.vue";
import { PublishVideoDetailDisplayData } from "@/data/auto/publishVideoDetailDisplayData";
interface BreadcrumbItem {
value: string;
@@ -163,9 +166,8 @@ const dmsFolderIsRename = ref<boolean>(false);
const dmsFolderIsMove = ref<boolean>(false);
const entity = ref<DmsFileEntity>(new DmsFileEntity());
const videoPlayModal = ref<boolean>(false);
const videoSrc = ref<string>("");
const videoPoster = ref<string>("");
const videoType = ref<string>("");
const videoPlayList = ref<VideoItem[]>([]);
const videoPlayIndex = ref<number>(0);
const videoPlayTitle = ref<string>("");
const visible = ref<boolean>(false);
@@ -198,6 +200,7 @@ const toUploadView = (r: Record<string, any>): DmsFileUploadViewDisplayData =>
onUnmounted(() => {
sound.value?.stop();
clearRefreshTimers();
});
const play = (src: string) => {
@@ -264,11 +267,6 @@ provide("dmsFolderTitle", dmsFolderTitle);
provide("dmsFolderIsRename", dmsFolderIsRename);
provide("dmsFolderIsMove", dmsFolderIsMove);
provide("DmsFileEntity", entity);
provide("videoPlayModal", videoPlayModal);
provide("videoPlayTitle", videoPlayTitle);
provide("videoSrc", videoSrc);
provide("videoPoster", videoPoster);
provide("videoType", videoType);
provide("isModalPublishVideo", showModalPublishVideo);
provide("isModalPublishAudio", showModalPublishAudio);
provide("isModalPublishPhoto", showModalPublishPhoto);
@@ -313,15 +311,32 @@ const onUploadSuccess = () => {
spinTip.value = "上传完成,等待服务端处理...";
};
/** 一批文件全部传完:给后端留点时间合并分片,文件越多等得越久 */
/** 上传后延迟刷新的定时器,离开页面/重新开始时清掉,避免野刷新 */
let refreshTimers: number[] = [];
const clearRefreshTimers = () => {
refreshTimers.forEach((t) => clearTimeout(t));
refreshTimers = [];
};
/**
* 一批文件全部传完:给后端留点时间合并分片,文件越多等得越久。
* 视频封面(jpg)由 ffmpeg 生成、可能比合并更慢,所以首刷之后再补刷两次——
* 每次查询服务端都会重新拼 ?v=,src 变了 <img> 才会重新请求,封面就能补上来。
*/
const onUploadComplete = (payload: { count?: number }) => {
const count = payload?.count ?? 1;
const sec = 2500 + (count - 1) * 1000;
spinTip.value = "加载中...";
setTimeout(() => {
onReSearch();
loading.value = false;
}, sec);
clearRefreshTimers();
refreshTimers.push(
window.setTimeout(() => {
onReSearch();
loading.value = false;
}, sec),
);
refreshTimers.push(window.setTimeout(onReSearch, sec + 3000));
refreshTimers.push(window.setTimeout(onReSearch, sec + 6000));
};
/** 中断时的断点信息,弹窗里展示用 */
@@ -395,29 +410,57 @@ const onChangeFolder = async (fvalue: string) => {
onSearchByFilePath(fvalue);
};
const onUrlVideoPlay = (url: string, title: string, poster: string) => {
/** DMS 记录 → 播放列表项(m3u8 的封面用同名 png 兜底) */
const toVideoItem = (record: DmsFileUploadViewDisplayData): VideoItem => {
const item = new PublishVideoDetailDisplayData();
item.Title = record.OrgFileName;
item.FormattedUrl = record.Url;
item.FormattedPic = (record.FormattedPic || "").replace(/\.m3u8$/, ".png");
return item;
};
/** 当前目录里已转码(有 Url)的视频,作为弹窗播放器的播放列表 */
const buildVideoList = (
records: DmsFileUploadViewDisplayData[],
): VideoItem[] =>
records
.filter(
(x) =>
x.IsDirectoryIndc != "Y" &&
fileTypePolicy.isVideo(x.OrderKey || x.OrgFileName) &&
!!x.Url,
)
.map(toVideoItem);
/** 打开视频弹窗 */
const openVideoPlay = (list: VideoItem[], index: number, title: string) => {
sound.value?.stop();
datas.value.forEach((element) => {
element.IsPlay = "";
});
videoPoster.value = poster;
videoType.value = "m3u8";
videoSrc.value = url;
videoPlayList.value = list;
videoPlayIndex.value = index < 0 ? 0 : index;
videoPlayTitle.value = title || "视频播放";
videoPlayModal.value = true;
videoPlayTitle.value = title;
};
const onVideoPlay = async (fid: number) => {
dmsFileService
.FindSourceVideoPath(fid)
.then((res) => {
videoType.value = "video/mp4";
videoSrc.value = res;
videoPlayModal.value = true;
})
.catch(() => {
videoPlayModal.value = false;
/** 未转码的视频没有 Url,直接取源片路径播放 */
const onSourceVideoPlay = async (record: DmsFileUploadViewDisplayData) => {
try {
loading.value = true;
const url = await dmsFileService.FindSourceVideoPath(record.FileId);
const item = toVideoItem(record);
item.FormattedUrl = url;
openVideoPlay([item], 0, record.OrgFileName);
} catch (error) {
notification.info({
message: "系统提示:",
description: "视频加载失败",
placement: "topRight",
});
} finally {
loading.value = false;
}
};
const onPreviewFile = async (usernameOrRecord: any, orderKey?: string) => {
@@ -626,7 +669,7 @@ const onViewPic = () => {
} else if (fileTypePolicy.isImage(keys.value[0].OrgFileName)) {
onPreviewFile(keys.value[0].CreatedBy, keys.value[0].OrderKey);
} else if (fileTypePolicy.isVideo(keys.value[0].OrgFileName)) {
onVideoPlay(keys.value[0].FileId);
onUrlVideoPlayItem(keys.value[0]);
} else {
notification.info({
message: "系统提示:",
@@ -1065,7 +1108,7 @@ const onOpenFile = (record: DmsFileUploadViewDisplayData) => {
if (fileTypePolicy.isImage(record.OrgFileName)) {
onPreviewFile(record.CreatedBy, record.OrderKey);
} else if (fileTypePolicy.isVideo(record.OrgFileName)) {
onVideoPlay(record.FileId);
onUrlVideoPlayItem(record);
} else if (fileTypePolicy.isAudio(record.OrgFileName)) {
onPlayAudio(toUploadView(record));
} else {
@@ -1135,12 +1178,19 @@ const onViewPreviewSelected = (items: DmsFileUploadViewDisplayData[]) => {
}
};
const onUrlVideoPlayItem = (record: DmsFileUploadViewDisplayData) => {
onUrlVideoPlay(
record.Url,
record.OrgFileName,
record.FormattedPic.replace(".m3u8", ".png"),
);
/** 播放视频:把当前目录的视频串成播放列表,从点中的那一个开始 */
const onUrlVideoPlayItem = async (record: DmsFileUploadViewDisplayData) => {
if (!record.Url) {
await onSourceVideoPlay(record);
return;
}
const list = buildVideoList(datas.value);
let index = list.findIndex((x) => x.FormattedUrl === record.Url);
if (index < 0) {
list.unshift(toVideoItem(record));
index = 0;
}
openVideoPlay(list, index, record.OrgFileName);
};
const onPlayAudioItem = (record: DmsFileUploadViewDisplayData) => {
+65 -33
View File
@@ -21,7 +21,8 @@
onVisibleChange: setPicVisible,
}" :src="imageSrc" />
<dmsFolder @on-submit="onReload" @submit-start="loading = true" @submit-end="loading = false"></dmsFolder>
<videoPlay></videoPlay>
<VideoPlayModal v-model:open="videoPlayModal" :videos="videoPlayList" :start-index="videoPlayIndex"
:title="videoPlayTitle" :fullscreen="width < 640" />
<dragModal v-model:open="showModalShare" width="60%" title="分享管理查询" :mask-closable="false" :fullscreen="width < 640"
@ok="handleSelectShare">
<DmsFileShareModal ref="shareEnquiry"></DmsFileShareModal>
@@ -69,7 +70,6 @@ import { notification } from "ant-design-vue";
import { SearchCriteriaResultData } from "@/data/auto/searchCriteriaResultData";
import dmsFolder from "./DmsFolderModal.vue";
import { orderBy } from "lodash-es";
import VideoPlay from "./PlayVideo.vue";
import { Howl } from "howler";
import { PublishVideoEntity } from "@/data/auto/publishVideoEntity";
import publishVideoService from "@/service/master/publishVideoService";
@@ -84,6 +84,9 @@ import { PublishPhotoEntity } from "@/data/auto/publishPhotoEntity";
import MyComputerFamily from "@/components/basic/MyComputerFamily.vue";
import Toolbar from "@/components/basic/Toolbar.vue";
import OperatorStationFamily from "@/components/basic/OperatorStationFamily.vue";
import VideoPlayModal from "@/components/basic/VideoPlayModal.vue";
import type { VideoItem } from "@/components/basic/VideoPlayer.vue";
import { PublishVideoDetailDisplayData } from "@/data/auto/publishVideoDetailDisplayData";
import { FamilyDmsFileViewEntity } from "@/data/auto/familyDmsFileViewEntity.ts";
import FamilyGroupService from "@/service/master/familyGroupService";
@@ -131,9 +134,8 @@ const dmsFolderIsRename = ref<boolean>(false);
const dmsFolderIsMove = ref<boolean>(false);
const entity = ref<DmsFileEntity>(new DmsFileEntity());
const videoPlayModal = ref<boolean>(false);
const videoSrc = ref<string>("");
const videoPoster = ref<string>("");
const videoType = ref<string>("");
const videoPlayList = ref<VideoItem[]>([]);
const videoPlayIndex = ref<number>(0);
const videoPlayTitle = ref<string>("");
const visible = ref<boolean>(false);
const currentFamilyName = ref<string>("");
@@ -238,11 +240,6 @@ provide("DmsFileEntity", entity);
provide("dmsFolderIsFamilyGroup", true);
provide("dmsFolderFamilyName", currentFamilyName);
provide("videoPlayModal", videoPlayModal);
provide("videoPlayTitle", videoPlayTitle);
provide("videoSrc", videoSrc);
provide("videoPoster", videoPoster);
provide("videoType", videoType);
provide("isModalPublishVideo", showModalPublishVideo);
provide("isModalPublishAudio", showModalPublishAudio);
provide("isModalPublishPhoto", showModalPublishPhoto);
@@ -295,29 +292,57 @@ const onChangeFolder = async (fvalue: string) => {
onSearchByFilePath(fvalue);
};
const onUrlVideoPlay = (url: string, title: string, poster: string) => {
/** DMS 记录 → 播放列表项(m3u8 的封面用同名 png 兜底) */
const toVideoItem = (record: FamilyDmsFileViewDisplayData): VideoItem => {
const item = new PublishVideoDetailDisplayData();
item.Title = record.OrgFileName;
item.FormattedUrl = record.Url;
item.FormattedPic = (record.FormattedPic || "").replace(/\.m3u8$/, ".png");
return item;
};
/** 当前目录里已转码(有 Url)的视频,作为弹窗播放器的播放列表 */
const buildVideoList = (
records: FamilyDmsFileViewDisplayData[],
): VideoItem[] =>
records
.filter(
(x) =>
x.IsDirectoryIndc != "Y" &&
fileTypePolicy.isVideo(x.OrderKey || x.OrgFileName) &&
!!x.Url,
)
.map(toVideoItem);
/** 打开视频弹窗 */
const openVideoPlay = (list: VideoItem[], index: number, title: string) => {
sound.value?.stop();
datas.value.forEach((element) => {
element.IsPlay = "";
});
videoPoster.value = poster;
videoType.value = "m3u8";
videoSrc.value = url;
videoPlayList.value = list;
videoPlayIndex.value = index < 0 ? 0 : index;
videoPlayTitle.value = title || "视频播放";
videoPlayModal.value = true;
videoPlayTitle.value = title;
};
const onVideoPlay = async (fid: number) => {
dmsFileService
.FindSourceVideoPath(fid)
.then((res) => {
videoType.value = "video/mp4";
videoSrc.value = res;
videoPlayModal.value = true;
})
.catch(() => {
videoPlayModal.value = false;
/** 未转码的视频没有 Url,直接取源片路径播放 */
const onSourceVideoPlay = async (record: FamilyDmsFileViewDisplayData) => {
try {
loading.value = true;
const url = await dmsFileService.FindSourceVideoPath(record.FileId);
const item = toVideoItem(record);
item.FormattedUrl = url;
openVideoPlay([item], 0, record.OrgFileName);
} catch (error) {
notification.info({
message: "系统提示:",
description: "视频加载失败",
placement: "topRight",
});
} finally {
loading.value = false;
}
};
const onPreviewFile = async (usernameOrRecord: any, orderKey?: string) => {
@@ -531,7 +556,7 @@ const onViewPic = () => {
} else if (fileTypePolicy.isImage(keys.value[0].OrgFileName)) {
onPreviewFile(keys.value[0].CreatedBy, keys.value[0].OrderKey);
} else if (fileTypePolicy.isVideo(keys.value[0].OrgFileName)) {
onVideoPlay(keys.value[0].FileId);
onUrlVideoPlayItem(keys.value[0]);
} else {
notification.info({
message: "系统提示:",
@@ -1047,7 +1072,7 @@ const onOpenFile = (record: FamilyDmsFileViewDisplayData) => {
if (fileTypePolicy.isImage(record.OrgFileName)) {
onPreviewFile(record.CreatedBy, record.OrderKey);
} else if (fileTypePolicy.isVideo(record.OrgFileName)) {
onVideoPlay(record.FileId);
onUrlVideoPlayItem(record);
} else if (fileTypePolicy.isAudio(record.OrgFileName)) {
onPlayAudio(toUploadView(record));
} else {
@@ -1111,12 +1136,19 @@ const onViewPreviewSelected = (items: FamilyDmsFileViewDisplayData[]) => {
}
};
const onUrlVideoPlayItem = (record: FamilyDmsFileViewDisplayData) => {
onUrlVideoPlay(
record.Url,
record.OrgFileName,
record.FormattedPic.replace(".m3u8", ".png"),
);
/** 播放视频:把当前目录的视频串成播放列表,从点中的那一个开始 */
const onUrlVideoPlayItem = async (record: FamilyDmsFileViewDisplayData) => {
if (!record.Url) {
await onSourceVideoPlay(record);
return;
}
const list = buildVideoList(datas.value);
let index = list.findIndex((x) => x.FormattedUrl === record.Url);
if (index < 0) {
list.unshift(toVideoItem(record));
index = 0;
}
openVideoPlay(list, index, record.OrgFileName);
};
const onPlayAudioItem = (record: FamilyDmsFileViewDisplayData) => {
-82
View File
@@ -1,82 +0,0 @@
<template>
<dragModal :width="modal.width" v-model:open="modal.showModal" :title="title" :mask-closable="false">
<vue3VideoPlay ref="videoPlayerM3u8" v-if="videoType == 'm3u8'" :title="title" :src="modal.videoSrc" type="m3u8"
:autoPlay="true" />
<vue3VideoPlay ref="videoPlayerMp4" v-else :title="title" :src="modal.videoSrc" type="video/mp4"
:autoPlay="true" />
<template #footer>
<a-button type="primary" @click="handleSubmit">关闭</a-button>
</template>
</dragModal>
</template>
<script setup lang="ts">
import { watch, watchEffect, inject, reactive, ref, VNode, h, unref, onMounted, onBeforeMount } from "vue";
import dragModal from "@/components/basic/DragModal.vue"
const modal = reactive({
loading: false,
showModal: false,
videoSrc: '',
width: '850px'
});
const videoPlayerM3u8 = ref()
const videoPlayerMp4 = ref()
const title = ref(inject<string>("videoPlayTitle", '视频播放'));
const videoPlayModal = inject<boolean>("videoPlayModal");
const videoSrc = inject<string>("videoSrc");
modal.showModal = videoPlayModal!;
modal.videoSrc = videoSrc!;
import { bsInfo } from "@/utils/browser"
if (bsInfo.size.sm) {
modal.width = '350px';
}
else {
modal.width = '850px';
}
const videoType = ref(inject<string>("videoType", 'm3u8'));
//const videoSrc = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
// 视频播放器配置
const options = reactive({
src: modal.videoSrc, //视频源
})
const handleSubmit = async () => {
try {
modal.loading = true;
modal.showModal = false;
// console.log('-----------close video player -----------')
if (videoType.value == 'm3u8') {
videoPlayerM3u8.value?.pause();
videoPlayerM3u8.value?.removeAttribute('src');
videoPlayerM3u8.value?.load();
}
else {
videoPlayerMp4.value?.pause();
videoPlayerMp4.value?.removeAttribute('src');
videoPlayerMp4.value?.load();
}
} catch (error) {
} finally {
modal.loading = false;
}
};
</script>
<style scoped></style>
-1
View File
@@ -1,4 +1,3 @@
/// <reference types="vite/client" />
declare module "vue3-video-play";
declare module "@worstone/vue-aplayer";
declare module "wow.js";