ow
This commit is contained in:
1 parent
501c92d87c
commit
f1a88acb89
9 files changed
+1037
-162
No files matched your search
@@ -1357,7 +1357,7 @@ namespace CloudBuilder.Master.Service
|
|||||||
{
|
{
|
||||||
if (File.Exists(fileName)) File.Delete(fileName);
|
if (File.Exists(fileName)) File.Delete(fileName);
|
||||||
}
|
}
|
||||||
string m3u8FilePath = fullName.Replace(fileExtension, ".png");
|
string m3u8FilePath = fullName.Replace(fileExtension, FileTypePolicy.JPG);
|
||||||
//生成m3u8(暂不生成,只生成截图)
|
//生成m3u8(暂不生成,只生成截图)
|
||||||
TsToM3u8(dmsFile.OrderKey, fullName, m3u8FilePath);
|
TsToM3u8(dmsFile.OrderKey, fullName, m3u8FilePath);
|
||||||
}
|
}
|
||||||
@@ -1405,7 +1405,14 @@ namespace CloudBuilder.Master.Service
|
|||||||
public void TsToM3u8(string orderKey, string mp4Url, string m3u8Url, int videoLength = 6)
|
public void TsToM3u8(string orderKey, string mp4Url, string m3u8Url, int videoLength = 6)
|
||||||
{
|
{
|
||||||
string para = string.Format(service.Configuration[ServiceSettingsPolicy.FfmpegPictrue], mp4Url, m3u8Url);
|
string para = string.Format(service.Configuration[ServiceSettingsPolicy.FfmpegPictrue], mp4Url, m3u8Url);
|
||||||
|
try
|
||||||
|
{
|
||||||
RunMyProcess(service.Configuration[ServiceSettingsPolicy.FfmpegPath], para);
|
RunMyProcess(service.Configuration[ServiceSettingsPolicy.FfmpegPath], para);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
new FileLog(service.Configuration).WriteToFile(ex.Message);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
para = string.Format(service.Configuration["Ffmpeg:Arguments"], mp4Url, videoLength, m3u8Url);
|
para = string.Format(service.Configuration["Ffmpeg:Arguments"], mp4Url, videoLength, m3u8Url);
|
||||||
//不直接转码视频文件,影响服务器性能,改为在闭时自动生成
|
//不直接转码视频文件,影响服务器性能,改为在闭时自动生成
|
||||||
@@ -1483,8 +1490,10 @@ namespace CloudBuilder.Master.Service
|
|||||||
/// <param name="Parameters"></param>
|
/// <param name="Parameters"></param>
|
||||||
public void RunMyProcess(string exePath, string Parameters)
|
public void RunMyProcess(string exePath, string Parameters)
|
||||||
{
|
{
|
||||||
|
FileLog log = new FileLog(service.Configuration);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
log.WriteToFile(exePath + Parameters);
|
||||||
if (string.IsNullOrEmpty(exePath)) return;
|
if (string.IsNullOrEmpty(exePath)) return;
|
||||||
Cli.Wrap(exePath)
|
Cli.Wrap(exePath)
|
||||||
.WithArguments(Parameters)
|
.WithArguments(Parameters)
|
||||||
@@ -1492,6 +1501,7 @@ namespace CloudBuilder.Master.Service
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
log.WriteToFile(ex.Message);
|
||||||
throw new ValidatedException(ex.Message);
|
throw new ValidatedException(ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,18 +88,10 @@ namespace CloudBuilder.Master.Service
|
|||||||
{
|
{
|
||||||
var upload = ServiceSettingsPolicy.GetDmsFileUploadDirectory(service).Replace(phy, "");
|
var upload = ServiceSettingsPolicy.GetDmsFileUploadDirectory(service).Replace(phy, "");
|
||||||
|
|
||||||
var filePathRel = Path.Combine(upload, item.CreatedBy, "m3u8", "");
|
//暂时不支持转码
|
||||||
var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/');
|
//DmsFileEntity dmsFileEntity = repositoryDmsFile.DetachedEntities.Where(x => x.OrderKey == item.Url).FirstOrDefault();
|
||||||
item.FormattedUrl = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + '/' + Path.GetFileNameWithoutExtension(item.Url) + "/main.m3u8";
|
item.FormattedPic = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Url, YesNoPolicy.YES);
|
||||||
|
item.FormattedUrl = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Url, YesNoPolicy.NO);
|
||||||
item.FormattedPic = item.FormattedUrl.Replace("main.m3u8", "main.png");
|
|
||||||
|
|
||||||
DmsFileEntity dmsFileEntity = repositoryDmsFile.DetachedEntities.Where(x => x.OrderKey == Path.GetFileNameWithoutExtension(item.Url)).FirstOrDefault();
|
|
||||||
string transcodeIndc = YesNoPolicy.NO;
|
|
||||||
if (dmsFileEntity != null) transcodeIndc = dmsFileEntity.TranscodeIndc;
|
|
||||||
item.FormattedUrl = FileTypePolicy.PackUrl(service, item.CreatedBy, item.Url, transcodeIndc);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ namespace CloudBuilder.Security.Policy
|
|||||||
public const string DOCUMENT = "D";
|
public const string DOCUMENT = "D";
|
||||||
public const string LYRIC = "L";
|
public const string LYRIC = "L";
|
||||||
|
|
||||||
|
public const string JPG = ".jpg";
|
||||||
|
|
||||||
public static FileTypePolicy instance = null;
|
public static FileTypePolicy instance = null;
|
||||||
|
|
||||||
public FileTypePolicy(IContext context) : base(context)
|
public FileTypePolicy(IContext context) : base(context)
|
||||||
@@ -115,7 +117,7 @@ namespace CloudBuilder.Security.Policy
|
|||||||
var upload = (ServiceSettingsPolicy.GetDmsFileBackupDirectory(service) ?? "").Replace(phy ?? "", "");
|
var upload = (ServiceSettingsPolicy.GetDmsFileBackupDirectory(service) ?? "").Replace(phy ?? "", "");
|
||||||
var filePathRel = Path.Combine(upload, createdBy, fileExtension.Replace(".", ""), "");
|
var filePathRel = Path.Combine(upload, createdBy, fileExtension.Replace(".", ""), "");
|
||||||
var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/";
|
var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/";
|
||||||
url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + Path.GetFileNameWithoutExtension(orderKey) + ".png";
|
url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + Path.GetFileNameWithoutExtension(orderKey) + JPG;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,35 +5,17 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|
||||||
# 此階段是用來組建服務專案
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj", "CloudBuilder.Web.Entry/"]
|
|
||||||
RUN dotnet restore "./CloudBuilder.Web.Entry/CloudBuilder.Web.Entry.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/CloudBuilder.Web.Entry"
|
|
||||||
RUN dotnet build "./CloudBuilder.Web.Entry.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
# 此階段可用來發佈要複製到最終階段的服務專案
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
RUN dotnet publish "./CloudBuilder.Web.Entry.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
# 此階段用於生產環境,或以一般模式從 VS 執行時 (未使用偵錯設定時的預設值)
|
# 此階段用於生產環境,或以一般模式從 VS 執行時 (未使用偵錯設定時的預設值)
|
||||||
FROM base AS final
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
COPY ./dist ./dist
|
|
||||||
ENTRYPOINT ["dotnet", "CloudBuilder.Web.Entry.dll"]
|
ENTRYPOINT ["dotnet", "CloudBuilder.Web.Entry.dll"]
|
||||||
|
|
||||||
#version: "3"
|
#version: "3"
|
||||||
#services:
|
#services:
|
||||||
#cb:
|
#cb:
|
||||||
#image: mcr.microsoft.com/dotnet/runtime-deps:8.0
|
#build: .
|
||||||
#container_name: CloudBuilder.Web.Entry
|
#container_name: youjiala.com
|
||||||
#restart: always
|
#restart: always
|
||||||
#ports:
|
#ports:
|
||||||
#- "5200:8080"
|
#- "5200:8080"
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2</_PublishTargetUrl>
|
<_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2</_PublishTargetUrl>
|
||||||
<History>True|2026-09-16T05:56:51.8724546Z||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||;</History>
|
<History>True|2026-09-17T10:11:11.5364547Z||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||;</History>
|
||||||
<LastFailureDetails />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
},
|
},
|
||||||
"Ffmpeg": {
|
"Ffmpeg": {
|
||||||
"Path": "D:\\NET8\\Exe\\ffmpeg\\bin\\ffmpeg.exe", //视频转码
|
"Path": "D:\\NET8\\Exe\\ffmpeg\\bin\\ffmpeg.exe", //视频转码
|
||||||
"Pictrue": " -i {0} -ss 00:00:2.000 -frames:v 1 {1}",
|
"Pictrue": " -i {0} -ss 00:00:2.000 -frames:v 1 -vf \"scale=1280:-1\" -q:v 4 {1}", //" -i {0} -ss 00:00:2.000 -frames:v 1 {1}",
|
||||||
"Arguments": " -i {0} -c:v libx264 -vf scale=1280:-1 -start_number 0 -hls_time {1} -hls_list_size 0 -f hls {2}" //0-源路径,1-目标路径,2-存放多少秒 1280×720,852*480
|
"Arguments": " -i {0} -c:v libx264 -vf scale=1280:-1 -start_number 0 -hls_time {1} -hls_list_size 0 -f hls {2}" //0-源路径,1-目标路径,2-存放多少秒 1280×720,852*480
|
||||||
},
|
},
|
||||||
"CloudBuilder.Topshelf": {
|
"CloudBuilder.Topshelf": {
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,967 @@
|
|||||||
|
<template>
|
||||||
|
<div class="vp">
|
||||||
|
<!-- 空列表 -->
|
||||||
|
<div v-if="!current" class="vp-empty">
|
||||||
|
<VideoCameraOutlined />
|
||||||
|
<p>暂无视频</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 画面区:原生 video 只负责解码播放,控制栏全部自绘(与 VinylAudioPlayer 一致) -->
|
||||||
|
<div v-else class="vp-stage" ref="stageRef" @click="toggle">
|
||||||
|
<video ref="videoRef" class="vp-media" :class="{ 'is-rot': rotated }" :style="rotStyle"
|
||||||
|
:src="current.FormattedUrl" :poster="current.FormattedPic"
|
||||||
|
preload="metadata" playsinline @timeupdate="onTimeUpdate" @loadedmetadata="onLoadedMetadata"
|
||||||
|
@durationchange="onLoadedMetadata" @ended="onEnded" @play="isPlaying = true"
|
||||||
|
@pause="isPlaying = false" @error="onError"></video>
|
||||||
|
<button v-if="!isPlaying" class="vp-bigplay" type="button" title="播放" @click.stop="toggle">
|
||||||
|
<CaretRightFilled />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制区 -->
|
||||||
|
<div v-if="current" 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 />
|
||||||
|
</button>
|
||||||
|
<button class="vp-act" type="button" title="下载" @click="onDownload">
|
||||||
|
<DownloadOutlined />
|
||||||
|
<span class="vp-badge" :class="{ 'is-on': downloaded }">{{ downloaded ? "ON" : "OFF" }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="vp-act" :class="{ 'is-on': isFullscreen }" type="button" title="全屏" @click="onFullscreen">
|
||||||
|
<FullscreenExitOutlined v-if="isFullscreen" />
|
||||||
|
<FullscreenOutlined v-else />
|
||||||
|
</button>
|
||||||
|
<button class="vp-act" :class="{ 'is-on': rotated }" type="button" :title="rotTitle"
|
||||||
|
@click="cycleRot">
|
||||||
|
<RotateRightOutlined />
|
||||||
|
<span v-if="rotMode === 'auto'" class="vp-rot-auto">A</span>
|
||||||
|
</button>
|
||||||
|
<button class="vp-act" :class="{ 'is-on': sleepAt > 0 || stopAfterEnd }" type="button" title="定时关闭"
|
||||||
|
@click.stop="toggleMenu('timer')">
|
||||||
|
<FieldTimeOutlined />
|
||||||
|
</button>
|
||||||
|
<button class="vp-act" :class="{ 'is-muted': isMuted }" type="button" title="音量"
|
||||||
|
@click.stop="toggleMenu('volume')">
|
||||||
|
<AudioMutedOutlined v-if="isMuted" />
|
||||||
|
<AudioOutlined v-else />
|
||||||
|
</button>
|
||||||
|
<button class="vp-act" type="button" title="更多" @click.stop="toggleMenu('more')">
|
||||||
|
<EllipsisOutlined class="vp-rot90" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 弹层:定时关闭 -->
|
||||||
|
<div v-if="menu === 'timer'" class="vp-pop">
|
||||||
|
<button v-for="o in timerOptions" :key="o.value" type="button"
|
||||||
|
:class="{ 'is-on': sleepMinutes === o.value }" @click="onSleep(o.value)">
|
||||||
|
{{ o.label }}<template v-if="sleepMinutes === o.value && sleepLeft">({{ sleepLeft }})</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 弹层:音量 -->
|
||||||
|
<div v-if="menu === 'volume'" class="vp-pop vp-pop-vol">
|
||||||
|
<span class="vp-vol-num">{{ Math.round(volume * 100) }}</span>
|
||||||
|
<div class="vp-vol-track" ref="volRef" @pointerdown="onVolDown" @pointermove="onVolMove"
|
||||||
|
@pointerup="onVolUp" @pointercancel="onVolUp">
|
||||||
|
<span class="vp-vol-fill" :style="{ height: volume * 100 + '%' }"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 弹层:更多(倍速) -->
|
||||||
|
<div v-if="menu === 'more'" class="vp-pop">
|
||||||
|
<button v-for="r in rateOptions" :key="r" type="button" :class="{ 'is-on': rate === r }"
|
||||||
|
@click="rate = r">倍速 {{ r }}x</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度 -->
|
||||||
|
<div class="vp-progress" ref="barRef" @pointerdown="onBarDown" @pointermove="onBarMove" @pointerup="onBarUp"
|
||||||
|
@pointercancel="onBarUp">
|
||||||
|
<div class="vp-bar">
|
||||||
|
<span class="vp-bar-fill" :style="{ width: displayRatio * 100 + '%' }"></span>
|
||||||
|
<span class="vp-bar-thumb" :style="{ left: displayRatio * 100 + '%' }"></span>
|
||||||
|
</div>
|
||||||
|
<div class="vp-times">
|
||||||
|
<span>{{ fmt(displayTime) }}</span>
|
||||||
|
<span>{{ fmt(duration) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主控制 -->
|
||||||
|
<div class="vp-controls">
|
||||||
|
<button class="vp-ctl" type="button" :title="modeTitle" @click="cycleMode">
|
||||||
|
<RetweetOutlined v-if="mode !== 'random'" />
|
||||||
|
<SwapOutlined v-else />
|
||||||
|
<span v-if="mode === 'one'" class="vp-mode-one">1</span>
|
||||||
|
</button>
|
||||||
|
<button class="vp-ctl" type="button" title="上一个" @click="goPrev">
|
||||||
|
<StepBackwardFilled />
|
||||||
|
</button>
|
||||||
|
<button class="vp-play" type="button" :title="isPlaying ? '暂停' : '播放'" @click="toggle">
|
||||||
|
<PauseOutlined v-if="isPlaying" />
|
||||||
|
<CaretRightFilled v-else />
|
||||||
|
</button>
|
||||||
|
<button class="vp-ctl" type="button" title="下一个" @click="goNext(false)">
|
||||||
|
<StepForwardFilled />
|
||||||
|
</button>
|
||||||
|
<button class="vp-ctl" type="button" title="播放列表" @click="showList = !showList">
|
||||||
|
<UnorderedListOutlined />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放列表 -->
|
||||||
|
<transition name="vp-sheet">
|
||||||
|
<section v-if="showList && videos.length" class="vp-list">
|
||||||
|
<header class="vp-list-head">
|
||||||
|
<span>播放列表({{ videos.length }})</span>
|
||||||
|
<button type="button" title="关闭" @click="showList = false">
|
||||||
|
<CloseOutlined />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<ul class="vp-list-body">
|
||||||
|
<li v-for="(v, i) in videos" :key="i" :class="{ 'is-active': i === index }" @click="playAt(i)">
|
||||||
|
<span class="vp-list-idx">
|
||||||
|
<span v-if="i === index && isPlaying" class="vp-eq is-on"><i></i><i></i><i></i></span>
|
||||||
|
<template v-else>{{ i + 1 }}</template>
|
||||||
|
</span>
|
||||||
|
<span class="vp-list-name">{{ v.Title }}</span>
|
||||||
|
<span class="vp-list-author">{{ v.Author }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import { request } from "@/utils/request";
|
||||||
|
import type { PublishVideoDetailDisplayData } from "@/data/auto/publishVideoDetailDisplayData";
|
||||||
|
import {
|
||||||
|
HeartOutlined, HeartFilled, DownloadOutlined, FieldTimeOutlined,
|
||||||
|
AudioOutlined, AudioMutedOutlined, EllipsisOutlined, RotateRightOutlined,
|
||||||
|
RetweetOutlined, SwapOutlined, StepBackwardFilled, StepForwardFilled,
|
||||||
|
CaretRightFilled, PauseOutlined, UnorderedListOutlined, CloseOutlined,
|
||||||
|
FullscreenOutlined, FullscreenExitOutlined, VideoCameraOutlined,
|
||||||
|
} from "@ant-design/icons-vue";
|
||||||
|
|
||||||
|
/** 一个视频 */
|
||||||
|
export type VideoItem = PublishVideoDetailDisplayData;
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ videos?: VideoItem[] }>(), { videos: () => [] });
|
||||||
|
|
||||||
|
/* ---------------- 基础状态 ---------------- */
|
||||||
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||||
|
const index = ref(0);
|
||||||
|
const isPlaying = ref(false);
|
||||||
|
const currentTime = ref(0);
|
||||||
|
const duration = ref(0);
|
||||||
|
const volume = ref(0.9);
|
||||||
|
const muted = ref(false);
|
||||||
|
const rate = ref(1);
|
||||||
|
const mode = ref<"list" | "one" | "random">("list");
|
||||||
|
const showList = ref(false);
|
||||||
|
const menu = ref<"" | "timer" | "volume" | "more">("");
|
||||||
|
const downloaded = ref(false);
|
||||||
|
/** 切换后是否接着播(点上一个/下一个/列表点视频时为 true) */
|
||||||
|
let resumeAfterSwitch = false;
|
||||||
|
|
||||||
|
const videos = computed(() => props.videos || []);
|
||||||
|
const current = computed<VideoItem | null>(() => videos.value[index.value] ?? null);
|
||||||
|
const isMuted = computed(() => muted.value || volume.value === 0);
|
||||||
|
const modeTitle = computed(() => ({ list: "列表循环", one: "单集循环", random: "随机播放" }[mode.value]));
|
||||||
|
const rateOptions = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
||||||
|
|
||||||
|
/* ---------------- 播放控制 ---------------- */
|
||||||
|
const tryPlay = () => {
|
||||||
|
// 自动播放被浏览器拦截时静默失败,等用户点播放键
|
||||||
|
videoRef.value?.play().catch(() => { });
|
||||||
|
};
|
||||||
|
const toggle = () => {
|
||||||
|
const v = videoRef.value;
|
||||||
|
if (!v || !current.value) return;
|
||||||
|
if (v.paused) tryPlay();
|
||||||
|
else v.pause();
|
||||||
|
};
|
||||||
|
const playAt = (i: number) => {
|
||||||
|
if (i === index.value) {
|
||||||
|
toggle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resumeAfterSwitch = true;
|
||||||
|
index.value = i;
|
||||||
|
};
|
||||||
|
const goPrev = () => {
|
||||||
|
const n = videos.value.length;
|
||||||
|
if (!n) return;
|
||||||
|
resumeAfterSwitch = true;
|
||||||
|
index.value = (index.value - 1 + n) % n;
|
||||||
|
};
|
||||||
|
const goNext = (auto: boolean) => {
|
||||||
|
const n = videos.value.length;
|
||||||
|
if (!n) return;
|
||||||
|
resumeAfterSwitch = true;
|
||||||
|
if (auto && mode.value === "random" && n > 1) {
|
||||||
|
let r = index.value;
|
||||||
|
while (r === index.value) r = Math.floor(Math.random() * n);
|
||||||
|
index.value = r;
|
||||||
|
} else {
|
||||||
|
index.value = (index.value + 1) % n;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const cycleMode = () => {
|
||||||
|
mode.value = mode.value === "list" ? "one" : mode.value === "one" ? "random" : "list";
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- video 事件 ---------------- */
|
||||||
|
const onTimeUpdate = () => {
|
||||||
|
const v = videoRef.value;
|
||||||
|
if (!v || dragRatio.value !== null) return;
|
||||||
|
currentTime.value = v.currentTime;
|
||||||
|
};
|
||||||
|
const onLoadedMetadata = () => {
|
||||||
|
const v = videoRef.value;
|
||||||
|
if (!v) return;
|
||||||
|
if (isFinite(v.duration)) duration.value = v.duration;
|
||||||
|
// 自动识别横竖屏:宽 > 高 视为横屏,auto 模式下旋转90°播放
|
||||||
|
landscape.value = v.videoWidth > v.videoHeight;
|
||||||
|
};
|
||||||
|
const onEnded = () => {
|
||||||
|
if (stopAfterEnd.value) {
|
||||||
|
stopAfterEnd.value = false;
|
||||||
|
sleepMinutes.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode.value === "one") {
|
||||||
|
const v = videoRef.value;
|
||||||
|
if (v) {
|
||||||
|
v.currentTime = 0;
|
||||||
|
tryPlay();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goNext(true);
|
||||||
|
};
|
||||||
|
const onError = () => {
|
||||||
|
// 视频地址拉不起来时只静默处理,避免打断页面
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- 进度条(支持鼠标/触摸拖动) ---------------- */
|
||||||
|
const barRef = ref<HTMLElement | null>(null);
|
||||||
|
let draggingBar = false;
|
||||||
|
const dragRatio = ref<number | null>(null);
|
||||||
|
const ratioFromEvent = (e: PointerEvent) => {
|
||||||
|
const el = barRef.value;
|
||||||
|
if (!el) return 0;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
return Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));
|
||||||
|
};
|
||||||
|
const onBarDown = (e: PointerEvent) => {
|
||||||
|
if (!current.value || !duration.value) return;
|
||||||
|
draggingBar = true;
|
||||||
|
dragRatio.value = ratioFromEvent(e);
|
||||||
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
|
};
|
||||||
|
const onBarMove = (e: PointerEvent) => {
|
||||||
|
if (draggingBar) dragRatio.value = ratioFromEvent(e);
|
||||||
|
};
|
||||||
|
const onBarUp = (e: PointerEvent) => {
|
||||||
|
if (!draggingBar) return;
|
||||||
|
draggingBar = false;
|
||||||
|
const r = ratioFromEvent(e);
|
||||||
|
if (videoRef.value && duration.value > 0) videoRef.value.currentTime = r * duration.value;
|
||||||
|
dragRatio.value = null;
|
||||||
|
};
|
||||||
|
const displayRatio = computed(
|
||||||
|
() => dragRatio.value ?? (duration.value > 0 ? currentTime.value / duration.value : 0)
|
||||||
|
);
|
||||||
|
const displayTime = computed(() =>
|
||||||
|
dragRatio.value !== null && duration.value > 0 ? dragRatio.value * duration.value : currentTime.value
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---------------- 音量 ---------------- */
|
||||||
|
const volRef = ref<HTMLElement | null>(null);
|
||||||
|
let draggingVol = false;
|
||||||
|
const volFromEvent = (e: PointerEvent) => {
|
||||||
|
const el = volRef.value;
|
||||||
|
if (!el) return;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
volume.value = Math.min(1, Math.max(0, 1 - (e.clientY - r.top) / r.height));
|
||||||
|
muted.value = false;
|
||||||
|
};
|
||||||
|
const onVolDown = (e: PointerEvent) => {
|
||||||
|
draggingVol = true;
|
||||||
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
|
volFromEvent(e);
|
||||||
|
};
|
||||||
|
const onVolMove = (e: PointerEvent) => {
|
||||||
|
if (draggingVol) volFromEvent(e);
|
||||||
|
};
|
||||||
|
const onVolUp = () => {
|
||||||
|
draggingVol = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- 定时关闭 ---------------- */
|
||||||
|
const timerOptions = [
|
||||||
|
{ label: "不开启", value: 0 },
|
||||||
|
{ label: "15 分钟", value: 15 },
|
||||||
|
{ label: "30 分钟", value: 30 },
|
||||||
|
{ label: "60 分钟", value: 60 },
|
||||||
|
{ label: "播完当前", value: -1 },
|
||||||
|
];
|
||||||
|
const sleepMinutes = ref(0);
|
||||||
|
const sleepAt = ref(0);
|
||||||
|
const stopAfterEnd = ref(false);
|
||||||
|
const nowTick = ref(Date.now());
|
||||||
|
const sleepLeft = computed(() => {
|
||||||
|
if (sleepAt.value <= nowTick.value) return "";
|
||||||
|
const s = Math.ceil((sleepAt.value - nowTick.value) / 1000);
|
||||||
|
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
||||||
|
});
|
||||||
|
const onSleep = (v: number) => {
|
||||||
|
sleepMinutes.value = v;
|
||||||
|
stopAfterEnd.value = v === -1;
|
||||||
|
sleepAt.value = v > 0 ? Date.now() + v * 60000 : 0;
|
||||||
|
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 = () => {
|
||||||
|
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]));
|
||||||
|
};
|
||||||
|
const onDownload = async () => {
|
||||||
|
if (!current.value) return;
|
||||||
|
// 参考 DmsFileEnquiry.vue:先取回字节流,再用 Blob + <a download> 落盘
|
||||||
|
try {
|
||||||
|
const res = await request<ArrayBuffer>({
|
||||||
|
url: current.value.FormattedUrl,
|
||||||
|
method: "get",
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
});
|
||||||
|
const blobUrl = window.URL.createObjectURL(new Blob([res]));
|
||||||
|
const ext = (current.value.FormattedUrl.match(/\.(\w{2,5})(?:\?|#|$)/) || [])[1];
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = blobUrl;
|
||||||
|
a.download = `${current.value.Title || "video"}.${ext || "mp4"}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
setTimeout(() => window.URL.revokeObjectURL(blobUrl), 1000);
|
||||||
|
downloaded.value = true;
|
||||||
|
} catch {
|
||||||
|
message.error("下载失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/* ---------------- 旋转90° ---------------- */
|
||||||
|
/** auto:横屏视频自动转90°(默认);on:强制旋转;off:关闭 */
|
||||||
|
const rotMode = ref<"auto" | "on" | "off">("auto");
|
||||||
|
/** 当前视频是否横屏(宽 > 高),由 loadedmetadata 检测 */
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
/** 旋转后 video 宽高对调,横屏视频即可铺满竖版画面区 */
|
||||||
|
const stageRef = ref<HTMLElement | null>(null);
|
||||||
|
const stageSize = ref({ w: 0, h: 0 });
|
||||||
|
let stageRO: ResizeObserver | null = null;
|
||||||
|
const rotStyle = computed(() =>
|
||||||
|
rotated.value && stageSize.value.h > 0
|
||||||
|
? { width: stageSize.value.h + "px", height: stageSize.value.w + "px" }
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
const syncStageSize = () => {
|
||||||
|
const el = stageRef.value;
|
||||||
|
if (el) stageSize.value = { w: el.clientWidth, h: el.clientHeight };
|
||||||
|
};
|
||||||
|
/** stage 在 v-else 分支里,数据到了才挂载,所以延后观察尺寸变化 */
|
||||||
|
const ensureStageRO = () => {
|
||||||
|
const el = stageRef.value;
|
||||||
|
if (!el || stageRO || typeof ResizeObserver === "undefined") return;
|
||||||
|
stageRO = new ResizeObserver(syncStageSize);
|
||||||
|
stageRO.observe(el);
|
||||||
|
syncStageSize();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- 全屏 ---------------- */
|
||||||
|
const isFullscreen = ref(false);
|
||||||
|
const onFullscreen = async () => {
|
||||||
|
const v = videoRef.value;
|
||||||
|
if (!v) return;
|
||||||
|
try {
|
||||||
|
if (document.fullscreenElement) await document.exitFullscreen();
|
||||||
|
else await v.requestFullscreen();
|
||||||
|
} catch {
|
||||||
|
message.error("当前浏览器不支持全屏");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onFullscreenChange = () => {
|
||||||
|
isFullscreen.value = !!document.fullscreenElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- 弹层开合 ---------------- */
|
||||||
|
const toggleMenu = (m: "" | "timer" | "volume" | "more") => {
|
||||||
|
menu.value = menu.value === m ? "" : m;
|
||||||
|
};
|
||||||
|
const onDocClick = (e: MouseEvent) => {
|
||||||
|
const t = e.target as HTMLElement;
|
||||||
|
if (menu.value && !t.closest(".vp-pop") && !t.closest(".vp-act")) menu.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- 时间格式 ---------------- */
|
||||||
|
const fmt = (t: number) => {
|
||||||
|
const v = isFinite(t) && t > 0 ? t : 0;
|
||||||
|
return `${String(Math.floor(v / 60)).padStart(2, "0")}:${String(Math.floor(v % 60)).padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- 联动 ---------------- */
|
||||||
|
watch(current, async (t) => {
|
||||||
|
currentTime.value = 0;
|
||||||
|
duration.value = 0;
|
||||||
|
downloaded.value = false;
|
||||||
|
if (!t) return;
|
||||||
|
await nextTick();
|
||||||
|
ensureStageRO();
|
||||||
|
// 换了 src 后主动 load,保证 poster 与时长重新计算
|
||||||
|
videoRef.value?.load();
|
||||||
|
if (resumeAfterSwitch) {
|
||||||
|
resumeAfterSwitch = false;
|
||||||
|
tryPlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放列表默认收起,由用户点击列表按钮展开
|
||||||
|
watch(
|
||||||
|
() => videos.value.length,
|
||||||
|
(n) => {
|
||||||
|
if (index.value >= n) index.value = 0;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch([volume, muted], () => {
|
||||||
|
const v = videoRef.value;
|
||||||
|
if (!v) return;
|
||||||
|
v.volume = volume.value;
|
||||||
|
v.muted = muted.value || volume.value === 0;
|
||||||
|
});
|
||||||
|
watch(rate, () => {
|
||||||
|
if (videoRef.value) videoRef.value.playbackRate = rate.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
let timerId = 0;
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("click", onDocClick);
|
||||||
|
document.addEventListener("fullscreenchange", onFullscreenChange);
|
||||||
|
timerId = window.setInterval(() => {
|
||||||
|
nowTick.value = Date.now();
|
||||||
|
if (sleepAt.value && nowTick.value >= sleepAt.value) {
|
||||||
|
sleepAt.value = 0;
|
||||||
|
sleepMinutes.value = 0;
|
||||||
|
videoRef.value?.pause();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener("click", onDocClick);
|
||||||
|
document.removeEventListener("fullscreenchange", onFullscreenChange);
|
||||||
|
if (timerId) clearInterval(timerId);
|
||||||
|
stageRO?.disconnect();
|
||||||
|
videoRef.value?.pause();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vp {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
background:
|
||||||
|
radial-gradient(130% 55% at 50% -8%, rgba(255, 255, 255, 0.18), transparent 60%),
|
||||||
|
linear-gradient(178deg, #9aa8a3 0%, #7f9d9a 28%, #4d9093 54%, #12818f 78%, #0b7280 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp button {
|
||||||
|
font-family: inherit;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 原生 video 仅作解码,画面铺满画面区并按比例留边 */
|
||||||
|
.vp-media {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转90°:宽高对调(内联 style 提供),绕中心旋转 */
|
||||||
|
.vp-media.is-rot {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全屏时浏览器自己接管布局,还原为正常方向 */
|
||||||
|
.vp-media:fullscreen {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
position: static !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 空状态 ---------- */
|
||||||
|
.vp-empty {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 46px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-empty p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 主体布局 ---------- */
|
||||||
|
.vp-body {
|
||||||
|
position: relative;
|
||||||
|
padding: 14px 24px 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-bigplay {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 2;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(6, 56, 62, 0.5);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.55);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.35);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-bigplay:active {
|
||||||
|
transform: translate(-50%, -50%) scale(0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 画面区 ---------- */
|
||||||
|
.vp-stage {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 功能图标行 ---------- */
|
||||||
|
.vp-actions {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-act {
|
||||||
|
position: relative;
|
||||||
|
font-size: 22px;
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-act.is-on {
|
||||||
|
color: #7ff3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-act.is-fav {
|
||||||
|
color: #ff5b7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-act.is-muted {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-rot90 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转按钮的 A 角标 = auto 模式 */
|
||||||
|
.vp-rot-auto {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #7ff3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -16px;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 2px 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-badge.is-on {
|
||||||
|
color: #0b7280;
|
||||||
|
background: #7ff3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 均衡器小图标 */
|
||||||
|
.vp-eq {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-eq i {
|
||||||
|
width: 3px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-eq i:nth-child(2) {
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-eq i:nth-child(3) {
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-eq.is-on i {
|
||||||
|
animation: vp-eq 0.9s ease-in-out infinite;
|
||||||
|
transform-origin: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-eq.is-on i:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-eq.is-on i:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes vp-eq {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scaleY(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scaleY(1.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 弹层 ---------- */
|
||||||
|
.vp-pop {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 12px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 122px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(6, 40, 44, 0.92);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-pop button {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-pop button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-pop button.is-on {
|
||||||
|
color: #7ff3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-pop-vol {
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 14px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vol-num {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vol-track {
|
||||||
|
position: relative;
|
||||||
|
width: 6px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vol-fill {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #7ff3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 进度 ---------- */
|
||||||
|
.vp-progress {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 10px 2px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-bar {
|
||||||
|
position: relative;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-bar-fill {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-bar-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 26px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #fff;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-times {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 主控制 ---------- */
|
||||||
|
.vp-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ctl {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-mode-one {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #7ff3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-play {
|
||||||
|
width: 68px;
|
||||||
|
height: 68px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
color: #0e6d7a;
|
||||||
|
font-size: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.35);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-play:active {
|
||||||
|
transform: scale(0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 播放列表 ---------- */
|
||||||
|
.vp-list {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 8;
|
||||||
|
max-height: 72%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
background: rgba(7, 38, 42, 0.94);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-head button {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 0 14px;
|
||||||
|
list-style: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-body li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-body li:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-body li.is-active {
|
||||||
|
color: #7ff3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-idx {
|
||||||
|
flex: none;
|
||||||
|
width: 22px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-name {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-list-author {
|
||||||
|
flex: none;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-sheet-enter-active,
|
||||||
|
.vp-sheet-leave-active {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-sheet-enter-from,
|
||||||
|
.vp-sheet-leave-to {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 窄屏微调 ---------- */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.vp-body {
|
||||||
|
padding: 12px 18px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-play {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,93 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<vue3VideoPlay v-if="options.type == 'm3u8'" :width="videoWidth" :title="title" :src="modal.videoSrc"
|
<div class="pv-page" :style="{
|
||||||
:poster="modal.poster" type="m3u8" @ended="handleEnded" :autoPlay="false" />
|
height: bsInfo.height - 120 + 'px',
|
||||||
<vue3VideoPlay v-else :width="videoWidth" :title="title" :src="modal.videoSrc" :poster="modal.poster"
|
}">
|
||||||
type="video/mp4" @ended="handleEnded" :autoPlay="false" />
|
<VideoPlayer :videos="videos" />
|
||||||
<div class="list-container" :style="'height:' + listHeight + 'px;'">
|
|
||||||
<a-list bordered :data-source="modal.data.Details">
|
|
||||||
<template #renderItem="{ item, index }">
|
|
||||||
<a-list-item slot="renderItem" slot-scope="item, index" @click="handleChange(index)">
|
|
||||||
<span
|
|
||||||
:style="modal.index == index ? 'color: black;font-weight: bold;font-size: 24px;' : 'font-size: 20px;'">
|
|
||||||
{{ index + 1
|
|
||||||
}}.{{
|
|
||||||
item.Title }}</span>
|
|
||||||
</a-list-item>
|
|
||||||
</template>
|
|
||||||
<template #header>
|
|
||||||
<div>播放列表</div>
|
|
||||||
</template>
|
|
||||||
</a-list>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {
|
import { ref, onMounted } from "vue";
|
||||||
watch,
|
import { useRoute } from "vue-router";
|
||||||
computed,
|
|
||||||
inject,
|
import VideoPlayer from "@/components/basic/VideoPlayer.vue";
|
||||||
reactive,
|
import type { VideoItem } from "@/components/basic/VideoPlayer.vue";
|
||||||
ref,
|
|
||||||
VNode,
|
|
||||||
h,
|
|
||||||
unref,
|
|
||||||
onMounted,
|
|
||||||
onBeforeMount,
|
|
||||||
} from "vue";
|
|
||||||
import dragModal from "@/components/basic/DragModal.vue";
|
|
||||||
import { PublishVideoData } from "@/data/auto/publishVideoData";
|
|
||||||
import { useRoute, useRouter } from "vue-router";
|
|
||||||
import publishVideoService from "@/service/master/publishVideoService";
|
|
||||||
import { bsInfo } from "@/utils/browser";
|
import { bsInfo } from "@/utils/browser";
|
||||||
|
import publishVideoService from "@/service/master/publishVideoService";
|
||||||
|
import { PublishVideoData } from "@/data/auto/publishVideoData";
|
||||||
import { PublishVideoDetailDisplayData } from "@/data/auto/publishVideoDetailDisplayData";
|
import { PublishVideoDetailDisplayData } from "@/data/auto/publishVideoDetailDisplayData";
|
||||||
import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria";
|
import { DmsFileSearchCriteria } from "@/data/auto/dmsFileSearchCriteria";
|
||||||
|
import dmsFileService from "@/service/master/dmsFileService";
|
||||||
import { DmsFileUploadViewDisplayData } from "@/data/auto/dmsFileUploadViewDisplayData";
|
import { DmsFileUploadViewDisplayData } from "@/data/auto/dmsFileUploadViewDisplayData";
|
||||||
import { SearchFilter } from "@/data/auto/searchFilter";
|
import { SearchFilter } from "@/data/auto/searchFilter";
|
||||||
import { FileTypePolicy } from "@/policy/custom/fileTypePolicy";
|
import { FileTypePolicy } from "@/policy/custom/fileTypePolicy";
|
||||||
import { DmsFileResultData } from "@/data/auto/dmsFileResultData";
|
import { DmsFileResultData } from "@/data/auto/dmsFileResultData";
|
||||||
import dmsFileService from "@/service/master/dmsFileService";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const publishName = ref<string>(route.params.id as string);
|
const publishName = ref<string>(route.params.id as string);
|
||||||
const mode = ref<string>(route.params.mode as string)
|
const mode = ref<string>(route.params.mode as string);
|
||||||
|
|
||||||
const videoWidth = computed(() => {
|
// 视频列表:与 VinylAudioPlayer 的 tracks 一致,组件负责 UI,页面只负责数据
|
||||||
if (bsInfo.size.sm) return bsInfo.width;
|
const videos = ref<VideoItem[]>([]);
|
||||||
else return bsInfo.width - 600;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const listHeight = computed(() => {
|
|
||||||
if (bsInfo.size.sm) return bsInfo.height - 550;
|
|
||||||
else return bsInfo.height - 560;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const modal = reactive({
|
|
||||||
loading: false,
|
|
||||||
showModal: false,
|
|
||||||
videoSrc: "",
|
|
||||||
poster: "",
|
|
||||||
index: 0,
|
|
||||||
data: new PublishVideoData(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const title = "有家视频";
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 加载歌曲信息
|
await loadVideos();
|
||||||
loadVideos();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadVideos = async () => {
|
const loadVideos = async () => {
|
||||||
try {
|
try {
|
||||||
if (mode.value == "list") {
|
// 模式一:从网盘中心加载某个路径下的所有视频
|
||||||
let c: DmsFileSearchCriteria = new DmsFileSearchCriteria();
|
if (mode.value === "list") {
|
||||||
|
const c = new DmsFileSearchCriteria();
|
||||||
c.limit = 100;
|
c.limit = 100;
|
||||||
c.page = 1;
|
c.page = 1;
|
||||||
|
|
||||||
let filter: SearchFilter;
|
let filter: SearchFilter;
|
||||||
filter = new SearchFilter();
|
filter = new SearchFilter();
|
||||||
filter.Column = DmsFileUploadViewDisplayData.DB_FILE_PATH;
|
filter.Column = DmsFileUploadViewDisplayData.DB_FILE_PATH;
|
||||||
@@ -95,6 +51,7 @@ const loadVideos = async () => {
|
|||||||
filter.Operator = "single";
|
filter.Operator = "single";
|
||||||
filter.Value = publishName.value;
|
filter.Value = publishName.value;
|
||||||
c.Filter.push(filter);
|
c.Filter.push(filter);
|
||||||
|
|
||||||
filter = new SearchFilter();
|
filter = new SearchFilter();
|
||||||
filter.Column = DmsFileUploadViewDisplayData.DB_FILE_TYPE;
|
filter.Column = DmsFileUploadViewDisplayData.DB_FILE_TYPE;
|
||||||
filter.Type = "SingleSelection";
|
filter.Type = "SingleSelection";
|
||||||
@@ -102,76 +59,41 @@ const loadVideos = async () => {
|
|||||||
filter.Value = FileTypePolicy.VIDEO;
|
filter.Value = FileTypePolicy.VIDEO;
|
||||||
c.Filter.push(filter);
|
c.Filter.push(filter);
|
||||||
|
|
||||||
let dr: DmsFileResultData = await dmsFileService.FindBySearchCriteria(c);
|
const dr: DmsFileResultData = await dmsFileService.FindBySearchCriteria(c);
|
||||||
|
|
||||||
if (dr && dr.Datas && dr.Datas.length > 0) {
|
if (dr && dr.Datas && dr.Datas.length > 0) {
|
||||||
|
for (const item of dr.Datas) {
|
||||||
modal.data.Details = [];
|
const detail = new PublishVideoDetailDisplayData();
|
||||||
let detail: PublishVideoDetailDisplayData;
|
detail.Title = item.OrgFileName;
|
||||||
for (let i = 0; i < dr.Datas.length; i++) {
|
detail.FormattedUrl = item.Url;
|
||||||
detail = new PublishVideoDetailDisplayData()
|
// m3u8 通常配套同名 jpg/png 封面,这里把后缀替换成 .png 兜底
|
||||||
detail.Title = dr.Datas[i].OrgFileName;
|
detail.FormattedPic = (item.FormattedPic || "").replace(/\.m3u8$/, ".png");
|
||||||
detail.FormattedUrl = dr.Datas[i].Url;
|
videos.value.push(detail);
|
||||||
detail.FormattedPic = dr.Datas[i].FormattedPic.replace('.m3u8', '.png');
|
|
||||||
modal.data.Details.push(detail);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (!publishName.value || publishName.value.length == 0) {
|
// 默认发布视频
|
||||||
modal.data = await publishVideoService.FindPublishVideoDefault();
|
else if (!publishName.value || publishName.value.length === 0) {
|
||||||
} else {
|
const data: PublishVideoData = await publishVideoService.FindPublishVideoDefault();
|
||||||
modal.data = await publishVideoService.FindByKey(publishName.value);
|
videos.value = data?.Details ?? [];
|
||||||
|
}
|
||||||
|
// 按 key 加载
|
||||||
|
else {
|
||||||
|
const data: PublishVideoData = await publishVideoService.FindByKey(publishName.value);
|
||||||
|
videos.value = data?.Details ?? [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error); // for debug
|
console.log(error);
|
||||||
}
|
|
||||||
|
|
||||||
if (modal.data && modal.data.Details && modal.data.Details.length > 0) {
|
|
||||||
options.type = modal.data.Details[0].FormattedUrl.indexOf(".m3u8") > 0 ? "m3u8" : "video/mp4";
|
|
||||||
modal.videoSrc = modal.data.Details[0].FormattedUrl;
|
|
||||||
modal.poster = modal.data.Details[0].FormattedPic;
|
|
||||||
modal.index = 0;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (index: number) => {
|
|
||||||
modal.index = index;
|
|
||||||
options.type = modal.data.Details[index].FormattedUrl.indexOf(".m3u8") > 0 ? "m3u8" : "video/mp4";
|
|
||||||
modal.videoSrc = modal.data.Details[index].FormattedUrl;
|
|
||||||
modal.poster = modal.data.Details[index].FormattedPic;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnded = () => {
|
|
||||||
// 视频播放结束后,自动播放下一个视频
|
|
||||||
const index = modal.index + 1;
|
|
||||||
if (index < modal.data.Details.length) {
|
|
||||||
modal.index = index;
|
|
||||||
options.type = modal.data.Details[index].FormattedUrl.indexOf(".m3u8") > 0 ? "m3u8" : "video/mp4";
|
|
||||||
modal.videoSrc = modal.data.Details[index].FormattedUrl;
|
|
||||||
modal.poster = modal.data.Details[index].FormattedPic;
|
|
||||||
} else {
|
|
||||||
// 所有视频播放完毕,跳转到下一个页面
|
|
||||||
//router.push("/master/video-publish");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//const videoSrc = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
|
|
||||||
// 视频播放器配置
|
|
||||||
const options = reactive({
|
|
||||||
src: modal.videoSrc, //视频源
|
|
||||||
type: "m3u8", //视频类型
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.layout-content-view {
|
.pv-page {
|
||||||
padding-top: 0 !important;
|
width: 100%;
|
||||||
padding-left: 0 !important;
|
/* 外层 .layout-content-view 只有 padding-left: 8px,这里补上右侧
|
||||||
}
|
同样的间距,播放器才会左右居中而不是偏右贴边 */
|
||||||
|
padding-right: 8px;
|
||||||
.list-container {
|
box-sizing: border-box;
|
||||||
overflow-y: scroll;
|
|
||||||
/* 添加滚动条 */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user