owe
This commit is contained in:
1 parent
9b9f07dea5
commit
2e1974c35d
15 files changed
+406
-169
No files matched your search
@@ -89,12 +89,13 @@ namespace CloudBuilder.Master.Service
|
||||
//bool isAttached = Debugger.IsAttached;
|
||||
foreach (var item in list)
|
||||
{
|
||||
item.Url = FileTypePolicy.PackUrl(service, item.CreatedBy, item.OrderKey, item.TranscodeIndc);
|
||||
item.Url = FileTypePolicy.PackUrl(service, item.CreatedBy, item.OrderKey);
|
||||
}
|
||||
|
||||
data.Datas = list.Select(x => new DmsFileUploadViewDisplayData(x)
|
||||
{
|
||||
FormattedPic = FileTypePolicy.PackPicUrl(service, x.CreatedBy, x.OrderKey)
|
||||
FormattedPic = FileTypePolicy.PackPicUrl(service, x.CreatedBy, x.OrderKey),
|
||||
FormattedM3u8 = FileTypePolicy.PackM3u8Url(service, x.CreatedBy, x.OrderKey)
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@@ -157,12 +158,13 @@ namespace CloudBuilder.Master.Service
|
||||
//bool isAttached = Debugger.IsAttached;
|
||||
foreach (var item in list)
|
||||
{
|
||||
item.Url = FileTypePolicy.PackUrl(service, item.CreatedBy, item.OrderKey, item.TranscodeIndc);
|
||||
item.Url = FileTypePolicy.PackUrl(service, item.CreatedBy, item.OrderKey);
|
||||
}
|
||||
|
||||
data.Datas = list.Select(x => new DmsFileUploadViewDisplayData(x)
|
||||
{
|
||||
FormattedPic = FileTypePolicy.PackPicUrl(service, x.CreatedBy, x.OrderKey)
|
||||
FormattedPic = FileTypePolicy.PackPicUrl(service, x.CreatedBy, x.OrderKey),
|
||||
FormattedM3u8 = FileTypePolicy.PackM3u8Url(service, x.CreatedBy, x.OrderKey)
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@@ -221,12 +223,13 @@ namespace CloudBuilder.Master.Service
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
item.Url = FileTypePolicy.PackUrl(service, item.CreatedBy, item.OrderKey, item.TranscodeIndc);
|
||||
item.Url = FileTypePolicy.PackUrl(service, item.CreatedBy, item.OrderKey);
|
||||
}
|
||||
|
||||
data.Logs = list.Select(x => new DmsFileUploadDeleteLogViewDisplayData(x)
|
||||
{
|
||||
FormattedPic = FileTypePolicy.PackPicUrl(service, x.CreatedBy, x.OrderKey)
|
||||
FormattedPic = FileTypePolicy.PackPicUrl(service, x.CreatedBy, x.OrderKey),
|
||||
FormattedM3u8 = FileTypePolicy.PackM3u8Url(service, x.CreatedBy, x.OrderKey)
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@@ -321,13 +324,14 @@ namespace CloudBuilder.Master.Service
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
item.Url = FileTypePolicy.PackUrl(service, item.CreatedBy, item.OrderKey, item.TranscodeIndc);
|
||||
item.Url = FileTypePolicy.PackUrl(service, item.CreatedBy, item.OrderKey);
|
||||
}
|
||||
|
||||
data.Families = list.Select(x => new FamilyDmsFileViewDisplayData(x)
|
||||
{
|
||||
FileType = FileTypePolicy.GetType(x.OrderKey),
|
||||
FormattedPic = FileTypePolicy.PackPicUrl(service, x.CreatedBy, x.OrderKey)
|
||||
FormattedPic = FileTypePolicy.PackPicUrl(service, x.CreatedBy, x.OrderKey),
|
||||
FormattedM3u8 = FileTypePolicy.PackM3u8Url(service, x.CreatedBy, x.OrderKey)
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -84,11 +84,11 @@ namespace CloudBuilder.Master.Service
|
||||
}
|
||||
if (!string.IsNullOrEmpty(item.Url) && FileTypePolicy.GetType(item.Url!) == FileTypePolicy.AUDIO)
|
||||
{
|
||||
item.FormattedUrl = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Url, YesNoPolicy.NO);
|
||||
item.FormattedUrl = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Url);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(item.Lrc) && FileTypePolicy.GetType(item.Lrc!) == FileTypePolicy.LYRIC)
|
||||
{
|
||||
item.FormattedLrc = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Lrc, YesNoPolicy.NO);
|
||||
item.FormattedLrc = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Lrc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ namespace CloudBuilder.Master.Service
|
||||
//暂时不支持转码
|
||||
DmsFileEntity dmsFileEntity = repositoryDmsFile.DetachedEntities.Where(x => x.OrderKey == item.Url).FirstOrDefault();
|
||||
item.FormattedPic = FileTypePolicy.PackPicUrl(service, item.CreatedBy, item.Url);
|
||||
item.FormattedUrl = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Url, dmsFileEntity.TranscodeIndc);
|
||||
item.FormattedUrl = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Url);
|
||||
item.FormattedM3u8 = FileTypePolicy.PackM3u8Url(service, item.CreatedBy, item.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,6 @@ namespace CloudBuilder.Master.Data
|
||||
public string? IsPause { get; set; }
|
||||
|
||||
public string? FormattedPic { get; set; }
|
||||
public string? FormattedM3u8 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -32,5 +32,7 @@ namespace CloudBuilder.Master.Data
|
||||
public string? IsPause { get; set; }
|
||||
|
||||
public string? FormattedPic { get; set; }
|
||||
|
||||
public string? FormattedM3u8 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -34,5 +34,6 @@ namespace CloudBuilder.Master.Data
|
||||
public string? IsPause { get; set; }
|
||||
|
||||
public string? FormattedPic { get; set; }
|
||||
public string? FormattedM3u8 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -25,5 +25,7 @@ namespace CloudBuilder.Master.Data
|
||||
public string? FormattedUrl { get; set; }
|
||||
|
||||
public string? FormattedPic { get; set; }
|
||||
|
||||
public string? FormattedM3u8 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -78,14 +78,18 @@ namespace CloudBuilder.Security.Policy
|
||||
{
|
||||
return GetType(value) == LYRIC;
|
||||
}
|
||||
|
||||
public static string PackM3u8Url(IApplicationService service, string createdBy, string orderKey)
|
||||
{
|
||||
if (!IsVideo(orderKey)) return null;
|
||||
return PackUrl(service, createdBy, orderKey, YesNoPolicy.YES, false);
|
||||
}
|
||||
public static string PackPicUrl(IApplicationService service, string createdBy, string orderKey)
|
||||
{
|
||||
return PackUrl(service, createdBy, orderKey, YesNoPolicy.YES, true);
|
||||
}
|
||||
public static string PackUrl(IApplicationService service, string createdBy, string orderKey, string transcodeIndc)
|
||||
public static string PackUrl(IApplicationService service, string createdBy, string orderKey)
|
||||
{
|
||||
return PackUrl(service, createdBy, orderKey, transcodeIndc, false);
|
||||
return PackUrl(service, createdBy, orderKey, YesNoPolicy.NO, false);
|
||||
}
|
||||
|
||||
public static string PackUrl(IApplicationService service, string createdBy, string orderKey, string transcodeIndc, bool isPic)
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<template #closeIcon>
|
||||
<slot name="closeIcon">
|
||||
<Space class="ant-modal-operate" @click.stop>
|
||||
<!-- 可选插槽:放在「全屏」左边,供调用方追加自己的按钮。不传则什么都不渲染 -->
|
||||
<slot name="operate" />
|
||||
<FullscreenOutlined v-if="!fullscreenModel" @click="fullscreenModel = true" />
|
||||
<FullscreenExitOutlined v-else @click="restore" />
|
||||
<CloseOutlined @click="closeModal" />
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<dragModal v-model:open="visible" :width="width" :title="title" :footer="null" :mask-closable="false"
|
||||
:fullscreen="fullscreen" wrap-class-name="vpm-wrap">
|
||||
<!-- 弹窗右上角追加「页面全屏」按钮,位置在 DragModal 自带的「全屏」左边。
|
||||
它的作用是把底部控制区隐藏、让画面占满整个弹窗,再点一次恢复 -->
|
||||
<template #operate>
|
||||
<span class="vpm-page-full" :class="{ 'is-on': !controlsVisible }"
|
||||
:title="controlsVisible ? '页面全屏(隐藏控制区)' : '退出页面全屏(显示控制区)'"
|
||||
@click.stop="controlsVisible = !controlsVisible">
|
||||
<EyeOutlined v-if="!controlsVisible" />
|
||||
<EyeInvisibleOutlined v-else />
|
||||
</span>
|
||||
</template>
|
||||
<div class="vpm-body">
|
||||
<VideoPlayer :videos="videos" :start-index="startIndex" :auto-play="true" />
|
||||
<VideoPlayer v-model:controls="controlsVisible" :videos="videos" :start-index="startIndex" :auto-play="true" />
|
||||
</div>
|
||||
</dragModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons-vue";
|
||||
|
||||
import dragModal from "@/components/basic/DragModal.vue";
|
||||
import VideoPlayer from "@/components/basic/VideoPlayer.vue";
|
||||
@@ -37,6 +49,13 @@ const props = withDefaults(
|
||||
const emit = defineEmits(["update:open", "close"]);
|
||||
|
||||
const visible = useVModel(props, "open", emit);
|
||||
|
||||
/**
|
||||
* 底部控制区显隐(右上角「页面全屏」按钮):
|
||||
* 隐藏后播放器的画面区会吃掉控制区占的高度,画面铺满弹窗;再点一次恢复。
|
||||
* 单击画面区、播放器控制栏里的眼睛图标也是同一个动作,状态互通。
|
||||
*/
|
||||
const controlsVisible = ref(true);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -45,6 +64,32 @@ const visible = useVModel(props, "open", emit);
|
||||
height: 100%;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
/* 右上角的「页面全屏」图标。
|
||||
DragModal 的关闭按钮(.ant-modal-close)宽度固定(close-x 50px),
|
||||
把这个图标塞进它的图标排布里会把「全屏」「关闭」挤变形甚至挤出边界,
|
||||
所以用绝对定位挂到关闭按钮左侧:不占它的布局空间。
|
||||
尺寸/垂直对齐按关闭按钮的盒子(高 50px、图标 16px)写死,保证和旁边两个一致 */
|
||||
.vpm-page-full {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: calc(100% + 4px);
|
||||
width: 32px;
|
||||
height: 50px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
margin-right: -10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 已经进入页面全屏(控制区隐藏)时高亮,和播放器里的图标状态保持一致 */
|
||||
.vpm-page-full.is-on {
|
||||
color: #13c2c2;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@@ -62,4 +107,16 @@ const visible = useVModel(props, "open", emit);
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 文件名可能很长:单行显示,超出用 ... 截断,不再折行把标题栏撑高。
|
||||
右侧留出「页面全屏 + 全屏 + 关闭」三个按钮的宽度,避免长标题被按钮压住 */
|
||||
.ant-modal-wrap.vpm-wrap .ant-modal .ant-modal-header {
|
||||
padding-right: 128px;
|
||||
}
|
||||
|
||||
.ant-modal-wrap.vpm-wrap .ant-modal .ant-modal-title {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 画面区:原生 video 只负责解码播放,控制栏全部自绘(与 VinylAudioPlayer 一致) -->
|
||||
<!-- 单击画面区 = 播放/暂停(收起控制区改用「页面全屏」/眼睛按钮) -->
|
||||
<div v-else class="vp-stage" ref="stageRef" @click="toggle">
|
||||
<!-- 不绑 :src:m3u8 要交给 hls.js 解码,播放源统一由 attachSource 挂接 -->
|
||||
<video ref="videoRef" class="vp-media" :class="{ 'is-rot': rotated }" :style="rotStyle"
|
||||
@@ -25,13 +26,15 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 控制区 -->
|
||||
<div v-if="current" class="vp-body">
|
||||
<!-- 控制区:单击画面区可隐藏,隐藏后画面区吃掉这部分空间 -->
|
||||
<div v-if="current && controlsVisible" class="vp-body">
|
||||
<!-- 功能图标行 -->
|
||||
<div class="vp-actions">
|
||||
<button class="vp-act" :class="{ 'is-fav': isFaved }" type="button" title="喜欢" @click="onFav">
|
||||
<HeartFilled v-if="isFaved" />
|
||||
<HeartOutlined v-else />
|
||||
<!-- 原码/转码切换:默认播转码(FormattedM3u8),点这里切到原码(Url) -->
|
||||
<button class="vp-act" :class="{ 'is-on': useOriginal }" type="button" :title="sourceTitle"
|
||||
@click="toggleSource">
|
||||
<VideoCameraOutlined />
|
||||
<span class="vp-badge" :class="{ 'is-on': useOriginal }">{{ useOriginal ? "原码" : "转码" }}</span>
|
||||
</button>
|
||||
<button class="vp-act" type="button" title="下载" @click="onDownload">
|
||||
<DownloadOutlined />
|
||||
@@ -41,7 +44,13 @@
|
||||
<FullscreenExitOutlined v-if="isFullscreen" />
|
||||
<FullscreenOutlined v-else />
|
||||
</button>
|
||||
<button class="vp-act" :class="{ 'is-on': rotated }" type="button" :title="rotTitle"
|
||||
<!-- 隐藏/显示控制区:收起后画面区吃掉这部分空间(弹窗右上角的「页面全屏」也是同一个动作) -->
|
||||
<button class="vp-act" :class="{ 'is-on': !controlsVisible }" type="button"
|
||||
:title="controlsVisible ? '隐藏控制区' : '显示控制区'" @click="toggleControls">
|
||||
<EyeInvisibleOutlined v-if="controlsVisible" />
|
||||
<EyeOutlined v-else />
|
||||
</button>
|
||||
<button class="vp-act" :class="{ 'is-on': rotated }" type="button" title="横竖屏切换"
|
||||
@click="cycleRot">
|
||||
<RotateRightOutlined />
|
||||
<span v-if="rotMode === 'auto'" class="vp-rot-auto">A</span>
|
||||
@@ -147,16 +156,18 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { message } from "ant-design-vue";
|
||||
import Hls from "hls.js";
|
||||
import { request } from "@/utils/request";
|
||||
import type { PublishVideoDetailDisplayData } from "@/data/auto/publishVideoDetailDisplayData";
|
||||
import {
|
||||
HeartOutlined, HeartFilled, DownloadOutlined, FieldTimeOutlined,
|
||||
DownloadOutlined, FieldTimeOutlined,
|
||||
AudioOutlined, AudioMutedOutlined, EllipsisOutlined, RotateRightOutlined,
|
||||
RetweetOutlined, SwapOutlined, StepBackwardFilled, StepForwardFilled,
|
||||
CaretRightFilled, PauseOutlined, UnorderedListOutlined, CloseOutlined,
|
||||
FullscreenOutlined, FullscreenExitOutlined, VideoCameraOutlined,
|
||||
EyeOutlined, EyeInvisibleOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
|
||||
/** 一个视频 */
|
||||
@@ -169,10 +180,14 @@ const props = withDefaults(
|
||||
startIndex?: number;
|
||||
/** 挂上播放源后是否立即播放(弹窗里默认自动播) */
|
||||
autoPlay?: boolean;
|
||||
/** 底部控制区是否显示,可用 v-model:controls 从外部控制(单击画面区也会切换) */
|
||||
controls?: boolean;
|
||||
}>(),
|
||||
{ videos: () => [], startIndex: 0, autoPlay: false },
|
||||
{ videos: () => [], startIndex: 0, autoPlay: false, controls: true },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ (e: "update:controls", value: boolean): void }>();
|
||||
|
||||
/* ---------------- 基础状态 ---------------- */
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
const index = ref(props.startIndex);
|
||||
@@ -184,6 +199,15 @@ const muted = ref(false);
|
||||
const rate = ref(1);
|
||||
const mode = ref<"list" | "one" | "random">("list");
|
||||
const showList = ref(false);
|
||||
/**
|
||||
* 底部控制区是否显示:由控制栏的眼睛图标 / 弹窗右上角的「页面全屏」按钮切换。
|
||||
*
|
||||
* passive: true 是关键:不传的话 useVModel 直接读写 props,
|
||||
* 父组件没绑 v-model:controls 时(例如影视厅 PreviewVideo 页面)
|
||||
* 内部改了值也只会 emit 出去、props 不变,按钮就"点了没反应"。
|
||||
* passive 会保留本地状态,父组件绑了 v-model 时依然双向同步。
|
||||
*/
|
||||
const controlsVisible = useVModel(props, "controls", emit, { passive: true });
|
||||
const menu = ref<"" | "timer" | "volume" | "more">("");
|
||||
const downloaded = ref(false);
|
||||
/** 是否正在缓冲:数据不够时会卡住画面,必须让用户看得见 */
|
||||
@@ -280,6 +304,15 @@ const toggle = () => {
|
||||
if (v.paused) tryPlay();
|
||||
else v.pause();
|
||||
};
|
||||
/**
|
||||
* 切换底部控制区(功能图标行/进度条/主控制)的显示。
|
||||
* 收起后 .vp-stage 的 flex:1 会吃掉这部分高度,画面最大化。
|
||||
* 入口:控制栏里的眼睛图标、弹窗右上角的「页面全屏」按钮。
|
||||
*/
|
||||
const toggleControls = () => {
|
||||
menu.value = ""; // 顺手收起可能打开的弹层(定时/音量/倍速)
|
||||
controlsVisible.value = !controlsVisible.value;
|
||||
};
|
||||
const playAt = (i: number) => {
|
||||
if (i === index.value) {
|
||||
toggle();
|
||||
@@ -320,6 +353,11 @@ const onLoadedMetadata = () => {
|
||||
const v = videoRef.value;
|
||||
if (!v) return;
|
||||
if (isFinite(v.duration)) duration.value = v.duration;
|
||||
// 切换原码/转码后恢复播放位置
|
||||
if (pendingSeek != null) {
|
||||
v.currentTime = pendingSeek;
|
||||
pendingSeek = null;
|
||||
}
|
||||
// 自动识别横竖屏:宽 > 高 视为横屏,auto 模式下旋转90°播放
|
||||
landscape.value = v.videoWidth > v.videoHeight;
|
||||
};
|
||||
@@ -358,7 +396,8 @@ const clearStallTimer = () => {
|
||||
* 第 2 个用 ffmpeg 重封装(不重新编码)即可解决,几乎不吃 CPU。
|
||||
*/
|
||||
const diagnoseStall = async () => {
|
||||
const url = current.value?.FormattedUrl;
|
||||
// 诊断当前真正在播的那个源(转码或原码),不是写死 FormattedUrl
|
||||
const url = sourceUrl.value;
|
||||
if (!url || isHlsSource(url)) return;
|
||||
try {
|
||||
const res = await fetch(url, { headers: { Range: "bytes=0-1" }, credentials: "include" });
|
||||
@@ -487,39 +526,60 @@ const onSleep = (v: number) => {
|
||||
menu.value = "";
|
||||
};
|
||||
|
||||
/* ---------------- 喜欢 / 下载 / 信息 ---------------- */
|
||||
const FAV_KEY = "vp-favs";
|
||||
const loadFavs = (): Set<string> => {
|
||||
try {
|
||||
return new Set<string>(JSON.parse(localStorage.getItem(FAV_KEY) || "[]"));
|
||||
} catch {
|
||||
return new Set<string>();
|
||||
}
|
||||
};
|
||||
const favs = ref<Set<string>>(loadFavs());
|
||||
const isFaved = computed(() => !!current.value && favs.value.has(current.value.FormattedUrl));
|
||||
const onFav = () => {
|
||||
/* ---------------- 原码 / 转码切换 ---------------- */
|
||||
/** false = 播转码(FormattedM3u8,10 秒预览,默认);true = 播原码(Url) */
|
||||
const useOriginal = ref(false);
|
||||
const sourceTitle = computed(() =>
|
||||
useOriginal.value ? "正在播放原码,点击切回转码" : "正在播放转码,点击切换原码"
|
||||
);
|
||||
/** 只有像地址(http(s)://、//、/)的才拿去播,避免把 OrderKey 这类文件名当 URL 请求 */
|
||||
const isPlayableUrl = (u?: string) => !!u && /^(https?:)?\/\//i.test(u);
|
||||
|
||||
/** 当前应使用的播放地址:默认转码;切原码但该视频没有可用原码时回退转码 */
|
||||
const sourceUrl = computed(() => {
|
||||
const t = current.value;
|
||||
if (!t) return "";
|
||||
if (useOriginal.value && isPlayableUrl(t.Url)) return t.Url;
|
||||
return t.FormattedM3u8 || t.FormattedUrl || "";
|
||||
});
|
||||
/** 切换播放源后要恢复到的播放位置(秒) */
|
||||
let pendingSeek: number | null = null;
|
||||
|
||||
const toggleSource = () => {
|
||||
const v = videoRef.value;
|
||||
if (!current.value) return;
|
||||
const s = new Set(favs.value);
|
||||
if (s.has(current.value.FormattedUrl)) s.delete(current.value.FormattedUrl);
|
||||
else s.add(current.value.FormattedUrl);
|
||||
favs.value = s;
|
||||
localStorage.setItem(FAV_KEY, JSON.stringify([...s]));
|
||||
if (useOriginal.value && !current.value.Url) {
|
||||
message.info("该视频没有原码");
|
||||
return;
|
||||
}
|
||||
// 记住播放位置,新源的元数据加载完后再跳回去(见 onLoadedMetadata)
|
||||
pendingSeek = v?.currentTime ?? 0;
|
||||
const wasPlaying = isPlaying.value;
|
||||
useOriginal.value = !useOriginal.value;
|
||||
// 重新挂接播放源:m3u8 走 hls.js,原码 mp4 走 src(内部会销毁上一个 hls 实例)
|
||||
attachSource(sourceUrl.value);
|
||||
if (wasPlaying) tryPlay();
|
||||
};
|
||||
|
||||
const onDownload = async () => {
|
||||
if (!current.value) return;
|
||||
// 优先下载原码;m3u8 是切片清单,下载下来没法直接看
|
||||
const downloadUrl = current.value.Url || current.value.FormattedUrl || sourceUrl.value;
|
||||
// 参考 DmsFileEnquiry.vue:先取回字节流,再用 Blob + <a download> 落盘
|
||||
try {
|
||||
const res = await request<ArrayBuffer>({
|
||||
url: current.value.FormattedUrl,
|
||||
url: downloadUrl,
|
||||
method: "get",
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
const blobUrl = window.URL.createObjectURL(new Blob([res]));
|
||||
const ext = (current.value.FormattedUrl.match(/\.(\w{2,5})(?:\?|#|$)/) || [])[1];
|
||||
const ext = (downloadUrl.match(/\.(\w{2,5})(?:\?|#|$)/) || [])[1];
|
||||
// Title 通常是原始文件名、自带扩展名,先把已有扩展名剥掉再拼,
|
||||
// 否则会得到「xxx.mp4.mp4」这样的双后缀
|
||||
const base = (current.value.Title || "video").replace(/\.[^.]+$/, "");
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
a.download = `${current.value.Title || "video"}.${ext || "mp4"}`;
|
||||
a.download = ext ? `${base}.${ext}` : base;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
@@ -537,13 +597,12 @@ const landscape = ref(false);
|
||||
const rotated = computed(
|
||||
() => rotMode.value === "on" ? true : rotMode.value === "off" ? false : landscape.value
|
||||
);
|
||||
const rotTitle = computed(() => ({
|
||||
auto: "自动旋转(横屏视频转90°)",
|
||||
on: "强制旋转90°",
|
||||
off: "关闭旋转",
|
||||
}[rotMode.value]));
|
||||
const cycleRot = () => {
|
||||
rotMode.value = rotMode.value === "auto" ? "on" : rotMode.value === "on" ? "off" : "auto";
|
||||
// 单击立即翻转画面方向。
|
||||
// 之前是 auto→on→off 轮转,但横屏视频在 auto 和 on 下画面完全相同,
|
||||
// 第一次点击看不出任何变化,用户以为失灵、要按两次才生效。
|
||||
// 这里直接按「当前画面是否已旋转」取反:转着 → 关,没转 → 转。
|
||||
rotMode.value = rotated.value ? "off" : "on";
|
||||
};
|
||||
/** 旋转后 video 宽高对调,横屏视频即可铺满竖版画面区 */
|
||||
const stageRef = ref<HTMLElement | null>(null);
|
||||
@@ -573,14 +632,16 @@ const onFullscreen = async () => {
|
||||
const v = videoRef.value;
|
||||
if (!v) return;
|
||||
try {
|
||||
if (document.fullscreenElement) await document.exitFullscreen();
|
||||
// 只在「video 元素自己在全屏」时才退出;页面全屏(documentElement)时直接切到 video 全屏
|
||||
if (document.fullscreenElement === v) await document.exitFullscreen();
|
||||
else await v.requestFullscreen();
|
||||
} catch {
|
||||
message.error("当前浏览器不支持全屏");
|
||||
}
|
||||
};
|
||||
const onFullscreenChange = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement;
|
||||
// 只认 video 元素自己的全屏,否则弹窗的「页面全屏」会让这里的图标状态错乱
|
||||
isFullscreen.value = document.fullscreenElement === videoRef.value;
|
||||
};
|
||||
|
||||
/* ---------------- 弹层开合 ---------------- */
|
||||
@@ -608,8 +669,9 @@ const prepare = async (t: VideoItem | null, play: boolean) => {
|
||||
if (!t) return;
|
||||
await nextTick();
|
||||
ensureStageRO();
|
||||
// 重新挂接播放源:m3u8 走 hls.js,普通文件走 src(内部会销毁上一个 hls 实例)
|
||||
attachSource(t.FormattedUrl);
|
||||
// 重新挂接播放源:m3u8 走 hls.js,原码 mp4 走 src(内部会销毁上一个 hls 实例)
|
||||
// 播放源按「转码优先」取:默认 FormattedM3u8,切了原码用 Url
|
||||
attachSource(sourceUrl.value);
|
||||
if (play || props.autoPlay) {
|
||||
resumeAfterSwitch = false;
|
||||
tryPlay();
|
||||
@@ -799,6 +861,8 @@ onBeforeUnmount(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ---------- 功能图标行 ---------- */
|
||||
.vp-actions {
|
||||
position: relative;
|
||||
|
||||
@@ -82,6 +82,7 @@ import { Howl } from "howler";
|
||||
import { PublishVideoEntity } from "@/data/auto/publishVideoEntity";
|
||||
import publishVideoService from "@/service/master/publishVideoService";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { request } from "@/utils/request";
|
||||
import fileTypePolicy from "@/policy/custom/fileTypePolicy";
|
||||
import { PublishAudioEntity } from "@/data/auto/publishAudioEntity";
|
||||
import { FamilyFolderFavoriteViewEntity } from "@/data/auto/familyFolderFavoriteViewEntity";
|
||||
@@ -292,18 +293,20 @@ const onChangeFolder = async (fvalue: string) => {
|
||||
onSearchByFilePath(fvalue);
|
||||
};
|
||||
|
||||
/** DMS 记录 → 播放列表项(m3u8 的封面用同名 png 兜底) */
|
||||
/** DMS 记录 → 播放列表项:Url=原码、FormattedM3u8=转码(10秒),封面用同名 png 兜底 */
|
||||
const toVideoItem = (
|
||||
record: DmsFileUploadDeleteLogViewDisplayData,
|
||||
): VideoItem => {
|
||||
const item = new PublishVideoDetailDisplayData();
|
||||
item.Title = record.OrgFileName;
|
||||
item.FormattedUrl = record.Url;
|
||||
item.Url = record.Url;
|
||||
item.FormattedM3u8 = record.FormattedM3u8;
|
||||
item.FormattedUrl = record.FormattedM3u8 || record.Url;
|
||||
item.FormattedPic = (record.FormattedPic || "").replace(/\.m3u8$/, ".png");
|
||||
return item;
|
||||
};
|
||||
|
||||
/** 当前目录里已转码(有 Url)的视频,作为弹窗播放器的播放列表 */
|
||||
/** 当前目录里的视频,作为弹窗播放器的播放列表 */
|
||||
const buildVideoList = (
|
||||
records: DmsFileUploadDeleteLogViewDisplayData[],
|
||||
): VideoItem[] =>
|
||||
@@ -312,7 +315,7 @@ const buildVideoList = (
|
||||
(x) =>
|
||||
x.IsDirectoryIndc != "Y" &&
|
||||
fileTypePolicy.isVideo(x.OrderKey || x.OrgFileName) &&
|
||||
!!x.Url,
|
||||
!!(x.FormattedM3u8 || x.Url),
|
||||
)
|
||||
.map(toVideoItem);
|
||||
|
||||
@@ -328,27 +331,6 @@ const openVideoPlay = (list: VideoItem[], index: number, title: string) => {
|
||||
videoPlayModal.value = true;
|
||||
};
|
||||
|
||||
/** 未转码的视频没有 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 (
|
||||
usernameOrRecord: any,
|
||||
orderKey?: string,
|
||||
@@ -483,8 +465,57 @@ const onShareFamily = async (record: Record<string, any>) => {
|
||||
showModalShare.value = true;
|
||||
};
|
||||
|
||||
const onDownloadFile = async (fid: number, fname: string) => {
|
||||
GetOrgPictureFile(fid, fname, false);
|
||||
/** 按 URL 直接下载(视频走这条:Url 就是原码文件) */
|
||||
const downloadByUrl = async (url: string, fileName: string) => {
|
||||
if (!url) {
|
||||
notification.info({
|
||||
message: "系统提示:",
|
||||
description: "该文件没有可下载的地址",
|
||||
placement: "topRight",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
spinTip.value = "下载中...";
|
||||
// 与 VideoPlayer 一致:先取回字节流,再用 Blob + <a download> 落盘
|
||||
const res = await request<ArrayBuffer>({
|
||||
url,
|
||||
method: "get",
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
const blobUrl = window.URL.createObjectURL(new Blob([res]));
|
||||
const link = document.createElement("a");
|
||||
link.href = blobUrl;
|
||||
link.setAttribute("download", fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => window.URL.revokeObjectURL(blobUrl), 1000);
|
||||
} catch (error) {
|
||||
notification.info({
|
||||
message: "系统提示:",
|
||||
description: "下载失败",
|
||||
placement: "topRight",
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
spinTip.value = "加载中...";
|
||||
}
|
||||
};
|
||||
|
||||
/** 下载一条记录:视频下原码,其它文件走原来的分片合并下载 */
|
||||
const onDownloadFile = async (
|
||||
record: DmsFileUploadDeleteLogViewDisplayData,
|
||||
) => {
|
||||
if (
|
||||
fileTypePolicy.isVideo(record.OrgFileName) ||
|
||||
fileTypePolicy.isVideo(record.OrderKey)
|
||||
) {
|
||||
await downloadByUrl(record.Url, record.OrgFileName);
|
||||
return;
|
||||
}
|
||||
GetOrgPictureFile(record.FileId, record.OrgFileName, false);
|
||||
};
|
||||
|
||||
const GetOrgPictureFile = async (
|
||||
@@ -975,16 +1006,19 @@ const onOpenFile = (record: DmsFileUploadDeleteLogViewDisplayData) => {
|
||||
} else if (fileTypePolicy.isAudio(record.OrgFileName)) {
|
||||
onPlayAudio(toUploadView(record));
|
||||
} else {
|
||||
onDownloadFile(record.FileId, record.OrgFileName);
|
||||
onDownloadFile(record);
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadSelected = (items: DmsFileUploadDeleteLogViewDisplayData[]) => {
|
||||
items.forEach((item) => {
|
||||
const onDownloadSelected = async (
|
||||
items: DmsFileUploadDeleteLogViewDisplayData[],
|
||||
) => {
|
||||
// 逐个串行下载:并发触发多个 <a download> 浏览器会拦截后面的
|
||||
for (const item of items) {
|
||||
if (item.IsDirectoryIndc != "Y") {
|
||||
onDownloadFile(item.FileId, item.OrgFileName);
|
||||
await onDownloadFile(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onShareSelected = (items: DmsFileUploadDeleteLogViewDisplayData[]) => {
|
||||
@@ -1018,15 +1052,9 @@ const onViewPreviewSelected = (items: DmsFileUploadDeleteLogViewDisplayData[]) =
|
||||
};
|
||||
|
||||
/** 播放视频:把当前目录的视频串成播放列表,从点中的那一个开始 */
|
||||
const onUrlVideoPlayItem = async (
|
||||
record: DmsFileUploadDeleteLogViewDisplayData,
|
||||
) => {
|
||||
if (!record.Url) {
|
||||
await onSourceVideoPlay(record);
|
||||
return;
|
||||
}
|
||||
const onUrlVideoPlayItem = (record: DmsFileUploadDeleteLogViewDisplayData) => {
|
||||
const list = buildVideoList(datas.value);
|
||||
let index = list.findIndex((x) => x.FormattedUrl === record.Url);
|
||||
let index = list.findIndex((x) => x.Url === record.Url);
|
||||
if (index < 0) {
|
||||
list.unshift(toVideoItem(record));
|
||||
index = 0;
|
||||
|
||||
@@ -88,6 +88,7 @@ import { Howl } from "howler";
|
||||
import { PublishVideoEntity } from "@/data/auto/publishVideoEntity";
|
||||
import publishVideoService from "@/service/master/publishVideoService";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { request } from "@/utils/request";
|
||||
import fileTypePolicy from "@/policy/custom/fileTypePolicy";
|
||||
import { PublishAudioEntity } from "@/data/auto/publishAudioEntity";
|
||||
import { FamilyFolderFavoriteViewEntity } from "@/data/auto/familyFolderFavoriteViewEntity";
|
||||
@@ -462,16 +463,18 @@ const onChangeFolder = async (fvalue: string) => {
|
||||
onSearchByFilePath(fvalue);
|
||||
};
|
||||
|
||||
/** DMS 记录 → 播放列表项(m3u8 的封面用同名 png 兜底) */
|
||||
/** DMS 记录 → 播放列表项:Url=原码、FormattedM3u8=转码(10秒),封面用同名 png 兜底 */
|
||||
const toVideoItem = (record: DmsFileUploadViewDisplayData): VideoItem => {
|
||||
const item = new PublishVideoDetailDisplayData();
|
||||
item.Title = record.OrgFileName;
|
||||
item.FormattedUrl = record.Url;
|
||||
item.Url = record.Url;
|
||||
item.FormattedM3u8 = record.FormattedM3u8;
|
||||
item.FormattedUrl = record.FormattedM3u8 || record.Url;
|
||||
item.FormattedPic = (record.FormattedPic || "").replace(/\.m3u8$/, ".png");
|
||||
return item;
|
||||
};
|
||||
|
||||
/** 当前目录里已转码(有 Url)的视频,作为弹窗播放器的播放列表 */
|
||||
/** 当前目录里的视频,作为弹窗播放器的播放列表 */
|
||||
const buildVideoList = (
|
||||
records: DmsFileUploadViewDisplayData[],
|
||||
): VideoItem[] =>
|
||||
@@ -480,7 +483,7 @@ const buildVideoList = (
|
||||
(x) =>
|
||||
x.IsDirectoryIndc != "Y" &&
|
||||
fileTypePolicy.isVideo(x.OrderKey || x.OrgFileName) &&
|
||||
!!x.Url,
|
||||
!!(x.FormattedM3u8 || x.Url),
|
||||
)
|
||||
.map(toVideoItem);
|
||||
|
||||
@@ -496,25 +499,6 @@ const openVideoPlay = (list: VideoItem[], index: number, title: string) => {
|
||||
videoPlayModal.value = true;
|
||||
};
|
||||
|
||||
/** 未转码的视频没有 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) => {
|
||||
let username: string;
|
||||
let fname: string;
|
||||
@@ -643,8 +627,53 @@ const onShareFamily = async (records: DmsFileUploadViewDisplayData[]) => {
|
||||
showModalShare.value = true;
|
||||
};
|
||||
|
||||
const onDownloadFile = async (fid: number, fname: string) => {
|
||||
GetOrgPictureFile(fid, fname, false);
|
||||
/** 按 URL 直接下载(视频走这条:Url 就是原码文件) */
|
||||
const downloadByUrl = async (url: string, fileName: string) => {
|
||||
if (!url) {
|
||||
notification.info({
|
||||
message: "系统提示:",
|
||||
description: "该文件没有可下载的地址",
|
||||
placement: "topRight",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
spinTip.value = "下载中...";
|
||||
// 与 VideoPlayer 一致:先取回字节流,再用 Blob + <a download> 落盘
|
||||
const res = await request<ArrayBuffer>({
|
||||
url,
|
||||
method: "get",
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
const blobUrl = window.URL.createObjectURL(new Blob([res]));
|
||||
const link = document.createElement("a");
|
||||
link.href = blobUrl;
|
||||
link.setAttribute("download", fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
// 立刻 revoke 部分浏览器会来不及触发下载,延后释放
|
||||
setTimeout(() => window.URL.revokeObjectURL(blobUrl), 1000);
|
||||
} catch (error) {
|
||||
notification.info({
|
||||
message: "系统提示:",
|
||||
description: "下载失败",
|
||||
placement: "topRight",
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
spinTip.value = "加载中...";
|
||||
}
|
||||
};
|
||||
|
||||
/** 下载一条记录:视频下原码,其它文件走原来的分片合并下载 */
|
||||
const onDownloadFile = async (record: DmsFileUploadViewDisplayData) => {
|
||||
if (fileTypePolicy.isVideo(record.OrgFileName) || fileTypePolicy.isVideo(record.OrderKey)) {
|
||||
await downloadByUrl(record.Url, record.OrgFileName);
|
||||
return;
|
||||
}
|
||||
GetOrgPictureFile(record.FileId, record.OrgFileName, false);
|
||||
};
|
||||
|
||||
const GetOrgPictureFile = async (
|
||||
@@ -1164,16 +1193,17 @@ const onOpenFile = (record: DmsFileUploadViewDisplayData) => {
|
||||
} else if (fileTypePolicy.isAudio(record.OrgFileName)) {
|
||||
onPlayAudio(toUploadView(record));
|
||||
} else {
|
||||
onDownloadFile(record.FileId, record.OrgFileName);
|
||||
onDownloadFile(record);
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadSelected = (items: DmsFileUploadViewDisplayData[]) => {
|
||||
items.forEach((item) => {
|
||||
const onDownloadSelected = async (items: DmsFileUploadViewDisplayData[]) => {
|
||||
// 逐个串行下载:并发触发多个 <a download> 浏览器会拦截后面的
|
||||
for (const item of items) {
|
||||
if (item.IsDirectoryIndc != "Y") {
|
||||
onDownloadFile(item.FileId, item.OrgFileName);
|
||||
await onDownloadFile(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onShareSelected = (items: DmsFileUploadViewDisplayData[]) => {
|
||||
@@ -1231,13 +1261,9 @@ const onViewPreviewSelected = (items: DmsFileUploadViewDisplayData[]) => {
|
||||
};
|
||||
|
||||
/** 播放视频:把当前目录的视频串成播放列表,从点中的那一个开始 */
|
||||
const onUrlVideoPlayItem = async (record: DmsFileUploadViewDisplayData) => {
|
||||
if (!record.Url) {
|
||||
await onSourceVideoPlay(record);
|
||||
return;
|
||||
}
|
||||
const onUrlVideoPlayItem = (record: DmsFileUploadViewDisplayData) => {
|
||||
const list = buildVideoList(datas.value);
|
||||
let index = list.findIndex((x) => x.FormattedUrl === record.Url);
|
||||
let index = list.findIndex((x) => x.Url === record.Url);
|
||||
if (index < 0) {
|
||||
list.unshift(toVideoItem(record));
|
||||
index = 0;
|
||||
|
||||
@@ -74,6 +74,7 @@ import { Howl } from "howler";
|
||||
import { PublishVideoEntity } from "@/data/auto/publishVideoEntity";
|
||||
import publishVideoService from "@/service/master/publishVideoService";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { request } from "@/utils/request";
|
||||
import fileTypePolicy from "@/policy/custom/fileTypePolicy";
|
||||
import { PublishAudioEntity } from "@/data/auto/publishAudioEntity";
|
||||
import { FamilyFolderFavoriteViewEntity } from "@/data/auto/familyFolderFavoriteViewEntity";
|
||||
@@ -292,16 +293,18 @@ const onChangeFolder = async (fvalue: string) => {
|
||||
onSearchByFilePath(fvalue);
|
||||
};
|
||||
|
||||
/** DMS 记录 → 播放列表项(m3u8 的封面用同名 png 兜底) */
|
||||
/** DMS 记录 → 播放列表项:Url=原码、FormattedM3u8=转码(10秒),封面用同名 png 兜底 */
|
||||
const toVideoItem = (record: FamilyDmsFileViewDisplayData): VideoItem => {
|
||||
const item = new PublishVideoDetailDisplayData();
|
||||
item.Title = record.OrgFileName;
|
||||
item.FormattedUrl = record.Url;
|
||||
item.Url = record.Url;
|
||||
item.FormattedM3u8 = record.FormattedM3u8;
|
||||
item.FormattedUrl = record.FormattedM3u8 || record.Url;
|
||||
item.FormattedPic = (record.FormattedPic || "").replace(/\.m3u8$/, ".png");
|
||||
return item;
|
||||
};
|
||||
|
||||
/** 当前目录里已转码(有 Url)的视频,作为弹窗播放器的播放列表 */
|
||||
/** 当前目录里的视频,作为弹窗播放器的播放列表 */
|
||||
const buildVideoList = (
|
||||
records: FamilyDmsFileViewDisplayData[],
|
||||
): VideoItem[] =>
|
||||
@@ -310,7 +313,7 @@ const buildVideoList = (
|
||||
(x) =>
|
||||
x.IsDirectoryIndc != "Y" &&
|
||||
fileTypePolicy.isVideo(x.OrderKey || x.OrgFileName) &&
|
||||
!!x.Url,
|
||||
!!(x.FormattedM3u8 || x.Url),
|
||||
)
|
||||
.map(toVideoItem);
|
||||
|
||||
@@ -326,25 +329,6 @@ const openVideoPlay = (list: VideoItem[], index: number, title: string) => {
|
||||
videoPlayModal.value = true;
|
||||
};
|
||||
|
||||
/** 未转码的视频没有 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) => {
|
||||
let username: string;
|
||||
let fname: string;
|
||||
@@ -473,8 +457,55 @@ const onShareFamily = async (record: Record<string, any>) => {
|
||||
showModalShare.value = true;
|
||||
};
|
||||
|
||||
const onDownloadFile = async (fid: number, fname: string) => {
|
||||
GetOrgPictureFile(fid, fname, false);
|
||||
/** 按 URL 直接下载(视频走这条:Url 就是原码文件) */
|
||||
const downloadByUrl = async (url: string, fileName: string) => {
|
||||
if (!url) {
|
||||
notification.info({
|
||||
message: "系统提示:",
|
||||
description: "该文件没有可下载的地址",
|
||||
placement: "topRight",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
spinTip.value = "下载中...";
|
||||
// 与 VideoPlayer 一致:先取回字节流,再用 Blob + <a download> 落盘
|
||||
const res = await request<ArrayBuffer>({
|
||||
url,
|
||||
method: "get",
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
const blobUrl = window.URL.createObjectURL(new Blob([res]));
|
||||
const link = document.createElement("a");
|
||||
link.href = blobUrl;
|
||||
link.setAttribute("download", fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => window.URL.revokeObjectURL(blobUrl), 1000);
|
||||
} catch (error) {
|
||||
notification.info({
|
||||
message: "系统提示:",
|
||||
description: "下载失败",
|
||||
placement: "topRight",
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
spinTip.value = "加载中...";
|
||||
}
|
||||
};
|
||||
|
||||
/** 下载一条记录:视频下原码,其它文件走原来的分片合并下载 */
|
||||
const onDownloadFile = async (record: FamilyDmsFileViewDisplayData) => {
|
||||
if (
|
||||
fileTypePolicy.isVideo(record.OrgFileName) ||
|
||||
fileTypePolicy.isVideo(record.OrderKey)
|
||||
) {
|
||||
await downloadByUrl(record.Url, record.OrgFileName);
|
||||
return;
|
||||
}
|
||||
GetOrgPictureFile(record.FileId, record.OrgFileName, false);
|
||||
};
|
||||
|
||||
const GetOrgPictureFile = async (
|
||||
@@ -1076,16 +1107,17 @@ const onOpenFile = (record: FamilyDmsFileViewDisplayData) => {
|
||||
} else if (fileTypePolicy.isAudio(record.OrgFileName)) {
|
||||
onPlayAudio(toUploadView(record));
|
||||
} else {
|
||||
onDownloadFile(record.FileId, record.OrgFileName);
|
||||
onDownloadFile(record);
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadSelected = (items: FamilyDmsFileViewDisplayData[]) => {
|
||||
items.forEach((item) => {
|
||||
const onDownloadSelected = async (items: FamilyDmsFileViewDisplayData[]) => {
|
||||
// 逐个串行下载:并发触发多个 <a download> 浏览器会拦截后面的
|
||||
for (const item of items) {
|
||||
if (item.IsDirectoryIndc != "Y") {
|
||||
onDownloadFile(item.FileId, item.OrgFileName);
|
||||
await onDownloadFile(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onShareSelected = (items: FamilyDmsFileViewDisplayData[]) => {
|
||||
@@ -1137,13 +1169,9 @@ const onViewPreviewSelected = (items: FamilyDmsFileViewDisplayData[]) => {
|
||||
};
|
||||
|
||||
/** 播放视频:把当前目录的视频串成播放列表,从点中的那一个开始 */
|
||||
const onUrlVideoPlayItem = async (record: FamilyDmsFileViewDisplayData) => {
|
||||
if (!record.Url) {
|
||||
await onSourceVideoPlay(record);
|
||||
return;
|
||||
}
|
||||
const onUrlVideoPlayItem = (record: FamilyDmsFileViewDisplayData) => {
|
||||
const list = buildVideoList(datas.value);
|
||||
let index = list.findIndex((x) => x.FormattedUrl === record.Url);
|
||||
let index = list.findIndex((x) => x.Url === record.Url);
|
||||
if (index < 0) {
|
||||
list.unshift(toVideoItem(record));
|
||||
index = 0;
|
||||
|
||||
@@ -36,6 +36,21 @@ onMounted(async () => {
|
||||
await loadVideos();
|
||||
});
|
||||
|
||||
/**
|
||||
* 发布专辑详情(FindPublishVideoDefault / FindByKey)里,Url 存的是
|
||||
* OrderKey(形如 xxxx-3-3.mp4 的文件名),不是可播放地址;
|
||||
* 真正的原码地址在 FormattedUrl 上(后端 3 参 PackUrl 现在固定返回原码)。
|
||||
* 这里把 Url 补成地址,否则播放器切「原码」时会拿文件名当地址请求、直接 404。
|
||||
* 网盘列表模式(mode=list)的 Url 本身就是完整地址,不受影响。
|
||||
*/
|
||||
const normalizeDetails = (details: PublishVideoDetailDisplayData[]) =>
|
||||
(details ?? []).map((d) => {
|
||||
if (d.Url && !/^(https?:)?\/\//i.test(d.Url)) {
|
||||
d.Url = d.FormattedUrl || "";
|
||||
}
|
||||
return d;
|
||||
});
|
||||
|
||||
const loadVideos = async () => {
|
||||
try {
|
||||
// 模式一:从网盘中心加载某个路径下的所有视频
|
||||
@@ -65,7 +80,9 @@ const loadVideos = async () => {
|
||||
for (const item of dr.Datas) {
|
||||
const detail = new PublishVideoDetailDisplayData();
|
||||
detail.Title = item.OrgFileName;
|
||||
detail.FormattedUrl = item.Url;
|
||||
detail.Url = item.Url; // 原码
|
||||
detail.FormattedM3u8 = item.FormattedM3u8; // 转码(10秒)
|
||||
detail.FormattedUrl = item.FormattedM3u8 || item.Url;
|
||||
// m3u8 通常配套同名 jpg/png 封面,这里把后缀替换成 .png 兜底
|
||||
detail.FormattedPic = (item.FormattedPic || "").replace(/\.m3u8$/, ".png");
|
||||
videos.value.push(detail);
|
||||
@@ -75,12 +92,12 @@ const loadVideos = async () => {
|
||||
// 默认发布视频
|
||||
else if (!publishName.value || publishName.value.length === 0) {
|
||||
const data: PublishVideoData = await publishVideoService.FindPublishVideoDefault();
|
||||
videos.value = data?.Details ?? [];
|
||||
videos.value = normalizeDetails(data?.Details ?? []);
|
||||
}
|
||||
// 按 key 加载
|
||||
else {
|
||||
const data: PublishVideoData = await publishVideoService.FindByKey(publishName.value);
|
||||
videos.value = data?.Details ?? [];
|
||||
videos.value = normalizeDetails(data?.Details ?? []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
Reference in new issue
Block a user