同步记录中打开视频播放
This commit is contained in:
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Drawing.Printing;
|
using System.Drawing.Printing;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace dy.net.Controllers
|
namespace dy.net.Controllers
|
||||||
{
|
{
|
||||||
@@ -53,28 +54,85 @@ namespace dy.net.Controllers
|
|||||||
data
|
data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpGet]
|
/// <summary>
|
||||||
public IActionResult Index()
|
/// 播放视频
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vid"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("play/{vid}")]
|
||||||
|
public async Task<IActionResult> StreamVideo([FromRoute] string vid)
|
||||||
{
|
{
|
||||||
List<MyClass> myClasses = new List<MyClass>();
|
try
|
||||||
foreach (var drive in DriveInfo.GetDrives())
|
|
||||||
{
|
{
|
||||||
myClasses.Add(new MyClass { name=drive.Name, x1= drive.TotalSize, x2= drive.TotalFreeSpace });
|
var viedo = await dyCollectVideoService.GetById(vid);
|
||||||
Serilog.Log.Debug($"Drive: {drive.Name}, Total Size: {drive.TotalSize}, Free Space: {drive.TotalFreeSpace}");
|
|
||||||
|
if(viedo == null)
|
||||||
|
{
|
||||||
|
return NotFound($"视频不存在:{vid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 拼接完整物理路径(配置路径 + 文件名)
|
||||||
|
string videoFullPath = viedo.VideoSavePath;
|
||||||
|
|
||||||
|
// 2. 验证文件是否存在
|
||||||
|
if (!System.IO.File.Exists(videoFullPath))
|
||||||
|
{
|
||||||
|
return NotFound($"视频文件不存在:{videoFullPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 获取文件信息(大小、类型)
|
||||||
|
var fileInfo = new FileInfo(videoFullPath);
|
||||||
|
long fileSize = fileInfo.Length;
|
||||||
|
string contentType = GetContentType(videoFullPath); // 自动识别视频 MIME 类型
|
||||||
|
|
||||||
|
// 4. 处理分片请求(前端视频标签自动发起,支持断点续传)
|
||||||
|
if (Request.Headers.ContainsKey("Range") && long.TryParse(Request.Headers.Range.ToString().Split('=')[1].Split('-')[0], out long start))
|
||||||
|
{
|
||||||
|
// 分片起始位置(前端请求的起始字节)
|
||||||
|
long end = Math.Min(start + 1024 * 1024 * 2, fileSize - 1); // 每片 2MB(可调整)
|
||||||
|
long chunkSize = end - start + 1;
|
||||||
|
|
||||||
|
// 5. 设置分片响应头
|
||||||
|
Response.StatusCode = StatusCodes.Status206PartialContent;
|
||||||
|
Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileSize}");
|
||||||
|
Response.Headers.Add("Accept-Ranges", "bytes");
|
||||||
|
Response.Headers.Add("Content-Length", chunkSize.ToString());
|
||||||
|
|
||||||
|
// 6. 读取分片并返回流
|
||||||
|
var stream = new FileStream(videoFullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
|
||||||
|
stream.Seek(start, SeekOrigin.Begin);
|
||||||
|
return new FileStreamResult(stream, contentType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 完整文件请求(兼容旧浏览器)
|
||||||
|
return PhysicalFile(videoFullPath, contentType, enableRangeProcessing: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, $"视频加载失败:{ex.Message}");
|
||||||
}
|
}
|
||||||
//var disk=DiskInfoHelper.GetDockerHostTotalDiskSpaceGB();
|
|
||||||
return Ok(JsonConvert.SerializeObject(myClasses)+"\r\n"+JsonConvert.SerializeObject(myClasses.Sum(x=>x.x1)+"\r\n"+ JsonConvert.SerializeObject(myClasses.Sum(x => x.x2))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
public class MyClass
|
/// 辅助方法:根据文件名获取 MIME 类型(确保前端正确识别视频格式)
|
||||||
|
/// </summary>
|
||||||
|
private string GetContentType(string filename)
|
||||||
{
|
{
|
||||||
public string name { get; set; }
|
string extension = Path.GetExtension(filename).ToLowerInvariant();
|
||||||
|
return extension switch
|
||||||
public long x1 { get; set; }
|
{
|
||||||
|
".mp4" => "video/mp4",
|
||||||
public long x2 { get; set; }
|
".webm" => "video/webm",
|
||||||
|
".ogg" => "video/ogg",
|
||||||
|
".mov" => "video/quicktime",
|
||||||
|
".avi" => "video/x-msvideo",
|
||||||
|
_ => "application/octet-stream" // 默认二进制流
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ ____/ _|_)_____/ _| _| \_|\____|
|
|||||||
// API路由映射
|
// API路由映射
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
// 生产环境启用SPA
|
// 生产环境启用SPA
|
||||||
if (!environment.IsDevelopment())
|
if (!environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
||||||
<History>True|2025-11-25T16:00:42.1507258Z||;True|2025-11-26T00:00:17.0107229+08:00||;True|2025-11-25T23:42:07.4349629+08:00||;False|2025-11-25T23:41:56.9328658+08:00||;True|2025-11-25T23:19:32.5262917+08:00||;True|2025-11-25T14:08:53.3850967+08:00||;True|2025-11-24T23:53:54.4283003+08:00||;True|2025-11-24T23:41:15.8248332+08:00||;True|2025-11-24T23:33:57.1844427+08:00||;True|2025-11-24T23:31:54.9228836+08:00||;True|2025-11-24T23:24:15.8681502+08:00||;True|2025-11-24T23:22:57.9046229+08:00||;True|2025-11-24T23:18:04.1131647+08:00||;True|2025-11-24T22:47:54.1336448+08:00||;True|2025-11-24T22:47:18.3613833+08:00||;True|2025-11-24T08:17:50.3994607+08:00||;True|2025-11-24T08:07:00.9951205+08:00||;True|2025-11-23T23:34:50.0030826+08:00||;True|2025-11-23T23:32:36.7452616+08:00||;True|2025-11-22T23:18:56.3202345+08:00||;True|2025-11-22T22:52:51.7203302+08:00||;True|2025-11-22T22:52:42.9620946+08:00||;True|2025-11-22T22:52:09.8257640+08:00||;True|2025-11-22T22:39:31.5894141+08:00||;True|2025-11-22T22:31:11.0704815+08:00||;True|2025-11-22T22:20:36.4579131+08:00||;True|2025-11-22T22:19:10.2281364+08:00||;True|2025-11-22T19:34:45.9336901+08:00||;True|2025-11-12T23:06:38.7834109+08:00||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;True|2024-01-02T23:40:38.2001198+08:00||;True|2024-01-02T23:08:36.7230444+08:00||;True|2024-01-02T22:53:43.9658255+08:00||;True|2024-01-02T22:25:38.5545279+08:00||;</History>
|
<History>True|2025-11-26T15:48:02.3957186Z||;True|2025-11-26T23:43:06.8154188+08:00||;False|2025-11-26T23:42:05.9191485+08:00||;True|2025-11-26T23:30:11.1295861+08:00||;True|2025-11-26T00:00:42.1507258+08:00||;True|2025-11-26T00:00:17.0107229+08:00||;True|2025-11-25T23:42:07.4349629+08:00||;False|2025-11-25T23:41:56.9328658+08:00||;True|2025-11-25T23:19:32.5262917+08:00||;True|2025-11-25T14:08:53.3850967+08:00||;True|2025-11-24T23:53:54.4283003+08:00||;True|2025-11-24T23:41:15.8248332+08:00||;True|2025-11-24T23:33:57.1844427+08:00||;True|2025-11-24T23:31:54.9228836+08:00||;True|2025-11-24T23:24:15.8681502+08:00||;True|2025-11-24T23:22:57.9046229+08:00||;True|2025-11-24T23:18:04.1131647+08:00||;True|2025-11-24T22:47:54.1336448+08:00||;True|2025-11-24T22:47:18.3613833+08:00||;True|2025-11-24T08:17:50.3994607+08:00||;True|2025-11-24T08:07:00.9951205+08:00||;True|2025-11-23T23:34:50.0030826+08:00||;True|2025-11-23T23:32:36.7452616+08:00||;True|2025-11-22T23:18:56.3202345+08:00||;True|2025-11-22T22:52:51.7203302+08:00||;True|2025-11-22T22:52:42.9620946+08:00||;True|2025-11-22T22:52:09.8257640+08:00||;True|2025-11-22T22:39:31.5894141+08:00||;True|2025-11-22T22:31:11.0704815+08:00||;True|2025-11-22T22:20:36.4579131+08:00||;True|2025-11-22T22:19:10.2281364+08:00||;True|2025-11-22T19:34:45.9336901+08:00||;True|2025-11-12T23:06:38.7834109+08:00||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;</History>
|
||||||
<LastFailureDetails />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
VITE_BASE_URL=/
|
VITE_BASE_URL=/
|
||||||
VITE_API_URL=http://localhost
|
VITE_API_URL=/
|
||||||
|
|||||||
@@ -19,9 +19,7 @@
|
|||||||
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button type="primary" @click="GetRecords">查询</a-button>
|
<a-button type="primary" @click="GetRecords">查询</a-button>
|
||||||
<!-- 核心修改点:绑定 disabled 属性 -->
|
|
||||||
<a-button type="danger" @click="StartNow" :disabled="isSyncing">
|
<a-button type="danger" @click="StartNow" :disabled="isSyncing">
|
||||||
<!-- 可选:添加加载状态提示 -->
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<a-spin v-if="isSyncing" size="small" />
|
<a-spin v-if="isSyncing" size="small" />
|
||||||
</template>
|
</template>
|
||||||
@@ -30,24 +28,98 @@
|
|||||||
</a-space>
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
|
<!-- 视频播放弹窗 - 简化视频信息 + 优化样式 -->
|
||||||
|
<a-modal v-model:visible="isModalOpen" :title="playingTitle" :width="900" :mask-closable="false" :footer="null" @cancel="handleCancel" :body-style="{ padding: '0', overflow: 'hidden', backgroundColor: '#fff' }" :style="{
|
||||||
|
borderRadius: '8px',
|
||||||
|
maxWidth: '85vw', // 最大宽度不超过视口85%(防止超屏)
|
||||||
|
maxHeight: '80vh', // 最大高度不超过视口80%
|
||||||
|
minWidth: '500px', // 最小宽度兜底(避免过小)
|
||||||
|
minHeight: '380px' // 最小高度兜底
|
||||||
|
}" :mask-style="{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }">
|
||||||
|
<!-- 视频容器:增加加载遮罩层 -->
|
||||||
|
<div class="video-container">
|
||||||
|
<!-- 加载遮罩层 - 视频加载中显示 -->
|
||||||
|
<div v-if="isVideoLoading" class="loading-overlay">
|
||||||
|
<a-spin size="large" tip="视频加载中..." />
|
||||||
|
<p class="loading-tip">请稍候,正在为您准备视频...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误提示 - 视频加载失败显示 -->
|
||||||
|
<div v-else-if="hasError" class="error-container">
|
||||||
|
<a-alert type="error" showIcon :message="errorMessage" description="建议尝试:1. 检查网络连接 2. 刷新页面重试 3. 联系管理员" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频播放器:添加尺寸限制和比例保持 -->
|
||||||
|
<video ref="videoRef" class="video-element" controls preload="metadata" :autoplay="autoPlay" :muted="autoMuted" @error="handleVideoError" @loadeddata="() => isVideoLoading = false" @waiting="() => isVideoLoading = true" @canplay="() => isVideoLoading = false" :style="{ opacity: isVideoLoading || hasError ? 0 : 1, transition: 'opacity 0.3s ease' }">
|
||||||
|
<source :src="videoUrl" type="video/mp4" />
|
||||||
|
您的浏览器不支持 HTML5 视频播放,请升级浏览器。
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频信息栏:仅保留同步时间和视频类型 + 优化样式 -->
|
||||||
|
<div v-if="currentVideoInfo" class="video-info-bar">
|
||||||
|
<div class="info-container">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">同步时间:</span>
|
||||||
|
<span class="info-value">{{ currentVideoInfo.syncTimeStr || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">视频类型:</span>
|
||||||
|
<span class="info-value">{{ currentVideoInfo.viedoCate || '未知' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 表格:视频标题超长省略 + 悬停显示完整标题(修复编译报错) -->
|
||||||
<a-table :columns="columns" :data-source="dataSource" bordered :pagination="pagination" @change="handleTableChange" :loading="loading">
|
<a-table :columns="columns" :data-source="dataSource" bordered :pagination="pagination" @change="handleTableChange" :loading="loading">
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.dataIndex === 'videoTitle'">
|
||||||
|
<!-- 核心修复:将内联箭头函数改为方法调用,避免build报错 -->
|
||||||
|
<a class="video-title-link" :title="record.videoTitle || '无标题'" @click="handleVideoClick(record)" @mouseenter="handleTitleMouseEnter" @mouseleave="handleTitleMouseLeave">
|
||||||
|
{{ formatVideoTitle(record.videoTitle) }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineComponent, reactive, ref } from 'vue';
|
import { reactive, ref, onMounted, nextTick, watch } from 'vue';
|
||||||
import { useApiStore } from '@/store';
|
import { useApiStore } from '@/store';
|
||||||
import type { UnwrapRef } from 'vue';
|
import type { UnwrapRef } from 'vue';
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
|
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
|
||||||
import { message, Spin } from 'ant-design-vue'; // 引入 Spin 用于显示加载图标
|
import { message, Spin, Alert } from 'ant-design-vue';
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
type RangeValue = [Dayjs, Dayjs];
|
type RangeValue = [Dayjs, Dayjs];
|
||||||
|
interface DataItem {
|
||||||
|
id?: string; // 视频ID(后端返回的字段,用于拼接播放地址)
|
||||||
|
videoTitle?: string; // 视频标题
|
||||||
|
syncTimeStr?: string; // 同步时间
|
||||||
|
viedoTypeStr?: string; // 同步类型
|
||||||
|
author?: string; // 博主
|
||||||
|
viedoCate?: string; // 视频类型
|
||||||
|
dyUser?: string; // CK名称
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuaryParam {
|
||||||
|
dates?: string[];
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
author: string;
|
||||||
|
tag: string;
|
||||||
|
viedoType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引入dayjs中文包
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
// ... (columns, loading, DataItem, datas, showImageViedo, QuaryParam, value1, ranges, quaryData 的定义保持不变)
|
// 表格列配置
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
{
|
{
|
||||||
title: '同步时间',
|
title: '同步时间',
|
||||||
@@ -77,6 +149,7 @@ const columns = ref([
|
|||||||
title: '视频标题',
|
title: '视频标题',
|
||||||
dataIndex: 'videoTitle',
|
dataIndex: 'videoTitle',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
width: 350, // 确保标题单元格有足够宽度
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'CK名称',
|
title: 'CK名称',
|
||||||
@@ -85,26 +158,20 @@ const columns = ref([
|
|||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 基础状态
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
interface DataItem {}
|
|
||||||
const datas: UnwrapRef<DataItem[]> = reactive([]);
|
const datas: UnwrapRef<DataItem[]> = reactive([]);
|
||||||
const showImageViedo = ref(false);
|
const showImageViedo = ref(false);
|
||||||
interface QuaryParam {
|
const dataSource = ref(datas);
|
||||||
dates?: string[];
|
|
||||||
pageIndex: number;
|
|
||||||
pageSize: number;
|
|
||||||
author: string;
|
|
||||||
tag: string;
|
|
||||||
viedoType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
const value1 = ref<RangeValue>();
|
const value1 = ref<RangeValue>();
|
||||||
const ranges = {
|
const ranges = {
|
||||||
今天: [dayjs(), dayjs()] as RangeValue,
|
今天: [dayjs(), dayjs()] as RangeValue,
|
||||||
本月: [dayjs(), dayjs().endOf('month')] as RangeValue,
|
本月: [dayjs(), dayjs().endOf('month')] as RangeValue,
|
||||||
};
|
};
|
||||||
const quaryData: UnwrapRef<QuaryParam> = reactive({
|
const quaryData: UnwrapRef<QuaryParam> = reactive({
|
||||||
datas: [],
|
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
author: '',
|
author: '',
|
||||||
@@ -112,10 +179,71 @@ const quaryData: UnwrapRef<QuaryParam> = reactive({
|
|||||||
viedoType: '*',
|
viedoType: '*',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = ref({
|
||||||
|
current: 1,
|
||||||
|
defaultPageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showTotal: () => `共 ${0} 条`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 视频播放相关配置
|
||||||
|
const DEFAULT_LOW_VOLUME = 0.3; // 默认低音量(与示例一致)
|
||||||
|
// 视频播放相关状态
|
||||||
|
const isVideoLoading = ref(false); // 视频加载状态(控制加载动画显示)
|
||||||
|
const videoErrorMsg = ref(''); // 视频错误提示
|
||||||
|
const isSyncing = ref(false);
|
||||||
|
|
||||||
|
// 当前播放视频信息
|
||||||
|
const currentVideoInfo = ref<DataItem | null>(null);
|
||||||
|
|
||||||
|
// 状态管理(视频弹窗相关)
|
||||||
|
const isModalOpen = ref(false); // 弹窗显示状态
|
||||||
|
const videoRef = ref(null); // 视频元素引用
|
||||||
|
const videoUrl = ref(''); // 当前播放视频地址
|
||||||
|
const hasError = ref(false); // 错误状态
|
||||||
|
const errorMessage = ref(''); // 错误信息
|
||||||
|
const autoPlay = ref(true); // 弹窗打开自动播放
|
||||||
|
const autoMuted = ref(true); // 自动播放时静音(浏览器政策要求)
|
||||||
|
const videoId = ref('');
|
||||||
|
const playingTitle = ref('');
|
||||||
|
|
||||||
|
// -------------------------- 核心方法:标题格式化 --------------------------
|
||||||
|
/** 格式化表格视频标题:超过20字符显示省略号 */
|
||||||
|
const formatVideoTitle = (title?: string) => {
|
||||||
|
if (!title) return '无标题';
|
||||||
|
return title.length > 20 ? `${title.slice(0, 20)}...` : title;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 格式化弹窗标题:超过40字符显示省略号 */
|
||||||
|
const formatModalTitle = (title?: string) => {
|
||||||
|
if (!title) return '视频播放';
|
||||||
|
return title.length > 40 ? `${title.slice(0, 40)}...` : title;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 标题鼠标进入事件:添加下划线 */
|
||||||
|
const handleTitleMouseEnter = (e: Event) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
target.style.textDecoration = 'underline';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 标题鼠标离开事件:移除下划线 */
|
||||||
|
const handleTitleMouseLeave = (e: Event) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
target.style.textDecoration = 'none';
|
||||||
|
};
|
||||||
|
// ----------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 查询表格数据
|
||||||
const GetRecords = () => {
|
const GetRecords = () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
quaryData.pageIndex = pagination.value.current;
|
quaryData.pageIndex = pagination.value.current;
|
||||||
quaryData.pageSize = pagination.value.defaultPageSize;
|
quaryData.pageSize = pagination.value.defaultPageSize;
|
||||||
|
|
||||||
|
if (value1.value) {
|
||||||
|
quaryData.dates = value1.value.map((date) => date.format('YYYY-MM-DD'));
|
||||||
|
}
|
||||||
|
|
||||||
useApiStore()
|
useApiStore()
|
||||||
.VideoPageList(quaryData)
|
.VideoPageList(quaryData)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@@ -126,22 +254,57 @@ const GetRecords = () => {
|
|||||||
pagination.value.defaultPageSize = res.data.pageSize;
|
pagination.value.defaultPageSize = res.data.pageSize;
|
||||||
pagination.value.total = res.data.total;
|
pagination.value.total = res.data.total;
|
||||||
pagination.value.showTotal = () => `共 ${res.data.total} 条`;
|
pagination.value.showTotal = () => `共 ${res.data.total} 条`;
|
||||||
|
} else {
|
||||||
|
message.warning(res.message || '获取数据失败');
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
loading.value = false;
|
||||||
|
console.error('获取表格数据失败:', error);
|
||||||
|
message.error('获取数据失败,请稍后重试');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
// 立即同步
|
||||||
|
const StartNow = () => {
|
||||||
|
if (isSyncing.value) return;
|
||||||
|
|
||||||
|
message.success('请耐心等待,同步任务正在启动...');
|
||||||
|
isSyncing.value = true;
|
||||||
|
|
||||||
|
useApiStore()
|
||||||
|
.StartJobNow()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
message.success('同步任务启动成功!');
|
||||||
|
GetRecords();
|
||||||
|
} else {
|
||||||
|
message.error(`同步任务启动失败: ${res.message || '未知错误'}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('同步任务API调用失败:', error);
|
||||||
|
message.error('同步任务启动失败,请检查网络或联系管理员');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isSyncing.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日期选择器变化事件
|
||||||
|
const datePicked = (_, dateArry: RangeValue) => {
|
||||||
|
quaryData.dates = dateArry.map((date) => date.format('YYYY-MM-DD'));
|
||||||
|
console.log('选择的日期范围:', quaryData.dates);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格分页变化事件
|
||||||
|
const handleTableChange = (paginationObj: any) => {
|
||||||
|
pagination.value.current = paginationObj.current;
|
||||||
|
pagination.value.defaultPageSize = paginationObj.pageSize;
|
||||||
GetRecords();
|
GetRecords();
|
||||||
getConfig(); // 在 onMounted 中调用 getConfig
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const pagination = ref({
|
|
||||||
current: 1,
|
|
||||||
defaultPageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
showTotal: () => `共 ${0} 条`,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
const getConfig = () => {
|
const getConfig = () => {
|
||||||
useApiStore()
|
useApiStore()
|
||||||
.apiGetConfig()
|
.apiGetConfig()
|
||||||
@@ -149,67 +312,372 @@ const getConfig = () => {
|
|||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
showImageViedo.value = res.data.downImageVideoFromEnv;
|
showImageViedo.value = res.data.downImageVideoFromEnv;
|
||||||
} else {
|
} else {
|
||||||
// 可以在这里添加配置获取失败的处理
|
message.warning(`获取配置失败: ${res.message}`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('获取配置失败:', error);
|
console.error('获取配置失败:', error);
|
||||||
|
message.error('获取配置失败,请稍后重试');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTableChange = (e) => {
|
// 视频点击事件处理 - 核心修改:使用formatModalTitle处理弹窗标题长度
|
||||||
pagination.value.current = e.current;
|
const handleVideoClick = (record: DataItem) => {
|
||||||
pagination.value.defaultPageSize = e.defaultPageSize;
|
if (!record.id) {
|
||||||
// 通常 total 是由后端返回的,这里不需要手动设置
|
message.warning('该视频暂无播放地址');
|
||||||
GetRecords();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 核心修改点:添加一个 ref 来控制按钮状态
|
|
||||||
const isSyncing = ref(false);
|
|
||||||
|
|
||||||
const StartNow = () => {
|
|
||||||
// 如果正在同步中,则直接返回,防止重复点击
|
|
||||||
if (isSyncing.value) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 保存当前视频信息
|
||||||
message.success('请耐心等待,同步任务正在启动...');
|
currentVideoInfo.value = record;
|
||||||
isSyncing.value = true; // 开始同步,禁用按钮
|
videoId.value = record.id;
|
||||||
|
// 弹窗标题显示格式化后的标题(超过40字符显示省略号)
|
||||||
useApiStore()
|
playingTitle.value = formatModalTitle(record.videoTitle);
|
||||||
.StartJobNow()
|
isModalOpen.value = true;
|
||||||
.then((res) => {
|
// 显示加载状态
|
||||||
// 根据后端返回的状态码判断是否真正成功
|
isVideoLoading.value = true;
|
||||||
if (res.code === 0) {
|
// 重置错误状态
|
||||||
message.success('同步任务启动成功!');
|
hasError.value = false;
|
||||||
} else {
|
|
||||||
message.error(`同步任务启动失败: ${res.message || '未知错误'}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('同步任务API调用失败:', error);
|
|
||||||
message.error('同步任务启动失败,请检查网络或联系管理员。');
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
isSyncing.value = false; // 无论成功失败,都恢复按钮状态
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const datePicked = (ref, dateArry) => {
|
// 监听弹窗状态,加载默认视频
|
||||||
// 注意:dateArry 是 Dayjs 对象数组,如果后端需要字符串,需要格式化
|
watch(
|
||||||
// quaryData.dates = dateArry.map(date => date.format('YYYY-MM-DD'));
|
isModalOpen,
|
||||||
quaryData.dates = dateArry;
|
(isOpen) => {
|
||||||
console.log(dateArry);
|
if (isOpen) {
|
||||||
|
loadVideo();
|
||||||
|
} else {
|
||||||
|
pauseVideo();
|
||||||
|
// 关闭弹窗时重置状态
|
||||||
|
currentVideoInfo.value = null;
|
||||||
|
videoUrl.value = '';
|
||||||
|
isVideoLoading.value = false; // 重置加载状态
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleCancel = () => {
|
||||||
|
isModalOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataSource = ref(datas);
|
// 加载视频
|
||||||
|
const loadVideo = () => {
|
||||||
|
hasError.value = false;
|
||||||
|
isVideoLoading.value = true; // 开始加载,显示加载动画
|
||||||
|
|
||||||
const onViedoTypeChanged = (e) => {
|
// 拼接后端视频接口地址(对应 VideoController 的 StreamVideo 方法)
|
||||||
quaryData.viedoType = e.target.value;
|
videoUrl.value = `${import.meta.env.VITE_API_URL}api/Video/play/${videoId.value}`;
|
||||||
pagination.value.current = 1; // 切换类型后,重置页码到第一页
|
|
||||||
|
// 重新加载视频(解决切换视频不刷新的问题)
|
||||||
|
nextTick(() => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
// 监听视频加载进度
|
||||||
|
videoRef.value.addEventListener('progress', handleVideoProgress);
|
||||||
|
videoRef.value.load();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 视频加载进度处理(可选:显示加载百分比)
|
||||||
|
const handleVideoProgress = (e: Event) => {
|
||||||
|
const video = e.target as HTMLVideoElement;
|
||||||
|
if (video.buffered.length > 0) {
|
||||||
|
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
|
||||||
|
const duration = video.duration;
|
||||||
|
// 当缓冲达到总时长的90%以上时,可以提前隐藏加载动画
|
||||||
|
if (duration > 0 && bufferedEnd / duration > 0.9) {
|
||||||
|
isVideoLoading.value = false;
|
||||||
|
video.removeEventListener('progress', handleVideoProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暂停视频
|
||||||
|
const pauseVideo = () => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
(videoRef.value as HTMLVideoElement).pause();
|
||||||
|
(videoRef.value as HTMLVideoElement).removeEventListener('progress', handleVideoProgress); // 移除进度监听
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 视频错误处理
|
||||||
|
const handleVideoError = (e: Event) => {
|
||||||
|
hasError.value = true;
|
||||||
|
const video = e.target as HTMLVideoElement;
|
||||||
|
const errorCode = video.error?.code;
|
||||||
|
|
||||||
|
// 错误码映射(参考 HTML5 视频错误标准)
|
||||||
|
const errorMap: Record<number, string> = {
|
||||||
|
1: '视频加载中断',
|
||||||
|
2: '网络错误(跨域未配置/后端服务未启动/接口不可用)',
|
||||||
|
3: '视频解码失败(格式不支持或文件损坏)',
|
||||||
|
4: '视频格式不支持',
|
||||||
|
5: '视频文件不存在或后端权限不足',
|
||||||
|
};
|
||||||
|
|
||||||
|
errorMessage.value = `加载失败:${errorMap[errorCode as number] || '未知错误'}(视频ID:${videoId.value})`;
|
||||||
|
isVideoLoading.value = false; // 错误时隐藏加载态
|
||||||
|
console.error('视频播放错误详情:', video.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修复缺失的方法
|
||||||
|
const onViedoTypeChanged = () => {
|
||||||
|
// 可根据需要添加类型切换后的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面挂载时初始化
|
||||||
|
onMounted(() => {
|
||||||
GetRecords();
|
GetRecords();
|
||||||
};
|
getConfig();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
|
/* 视频容器样式 - 与表格风格统一 */
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid #e8e8e8; /* 与表格边框一致 */
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频播放器样式 - 优化响应式和过渡效果 */
|
||||||
|
.video-element {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 420px;
|
||||||
|
min-height: 250px;
|
||||||
|
background-color: #000; /* 加载时显示黑色背景,提升体验 */
|
||||||
|
object-fit: contain; /* 保持视频比例,不拉伸 */
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载遮罩层 - 优化居中效果和样式 */
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7); /* 半透明黑色背景,突出加载动画 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载提示文字样式 */
|
||||||
|
.loading-tip {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误容器样式(优化错误显示) */
|
||||||
|
.error-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频信息栏:简化样式 + 优化布局 */
|
||||||
|
.video-info-bar {
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #f8f9fa; /* 淡灰色背景,更清爽 */
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 40px; /* 两个信息项之间的间距 */
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap; /* 响应式适配,小屏幕自动换行 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #666666; /* 标签深灰色,更醒目 */
|
||||||
|
margin-right: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #333333; /* 数值深色,保证可读性 */
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频标题链接样式(统一提取到CSS,避免内联样式冲突) */
|
||||||
|
.video-title-link {
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗标题样式优化:确保超长时不会撑破弹窗 */
|
||||||
|
:deep(.ant-modal-title) {
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: #1f2937 !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
max-width: calc(100% - 40px) !important; /* 预留关闭按钮空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式深度优化 - 与主风格完全统一 */
|
||||||
|
:deep(.ant-modal) {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.1) !important; /* Ant Design标准阴影 */
|
||||||
|
overflow: hidden !important;
|
||||||
|
max-width: 85vw !important; /* 最大宽度不超过视口85% */
|
||||||
|
max-height: 80vh !important; /* 最大高度不超过视口80% */
|
||||||
|
min-width: 500px !important; /* 最小宽度兜底 */
|
||||||
|
min-height: 380px !important; /* 最小高度兜底 */
|
||||||
|
width: 900px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-header) {
|
||||||
|
border-bottom: 1px solid #e8e8e8 !important;
|
||||||
|
padding: 16px 24px !important;
|
||||||
|
border-radius: 8px 8px 0 0 !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-close) {
|
||||||
|
color: #8c8c8c !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
width: 40px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-close:hover) {
|
||||||
|
color: #1890ff !important;
|
||||||
|
background-color: #f0f9ff !important; /* 与Ant Design按钮hover背景一致 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-content) {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-modal-mask) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5) !important;
|
||||||
|
backdrop-filter: blur(2px) !important; /* 增加毛玻璃效果,提升质感 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载组件样式优化 - 与Ant Design统一 */
|
||||||
|
:deep(.ant-spin-dot) {
|
||||||
|
color: #1890ff !important;
|
||||||
|
font-size: 36px !important; /* 放大加载动画,更醒目 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-spin-tip) {
|
||||||
|
color: #ffffff !important; /* 加载提示文字白色,与深色背景对比 */
|
||||||
|
font-size: 16px !important;
|
||||||
|
margin-top: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误提示样式优化 - 与Ant Design警告组件统一 */
|
||||||
|
:deep(.ant-alert-error) {
|
||||||
|
border: none !important;
|
||||||
|
background-color: #fff2f0 !important;
|
||||||
|
color: #ff4d4f !important;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-alert-icon) {
|
||||||
|
color: #ff4d4f !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
margin-right: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格单元格 hover 效果保持一致 */
|
||||||
|
:deep(.ant-table-tbody tr:hover td) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式优化:保持各屏幕尺寸下的一致性 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.video-element {
|
||||||
|
max-height: 380px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.video-element {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
.info-container {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
:deep(.ant-modal) {
|
||||||
|
width: 95% !important;
|
||||||
|
min-width: 320px !important;
|
||||||
|
min-height: 320px !important;
|
||||||
|
}
|
||||||
|
/* 移动端弹窗标题适配 */
|
||||||
|
:deep(.ant-modal-title) {
|
||||||
|
max-width: calc(100% - 30px) !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
}
|
||||||
|
/* 移动端加载动画适配 */
|
||||||
|
:deep(.ant-spin-dot) {
|
||||||
|
font-size: 28px !important;
|
||||||
|
}
|
||||||
|
.loading-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.video-element {
|
||||||
|
min-height: 220px;
|
||||||
|
}
|
||||||
|
.video-info-bar {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
.info-container {
|
||||||
|
gap: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
/* 移动端弹窗标题适配 */
|
||||||
|
:deep(.ant-modal-title) {
|
||||||
|
max-width: calc(100% - 25px) !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -162,8 +162,16 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// async function playViedo(id: string) {
|
||||||
|
// return http.request<any, Response<any>>('/api/video/play/' + id, 'get').then(r => {
|
||||||
|
// return r.data;
|
||||||
|
// }).finally(() => {
|
||||||
|
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// playViedo,
|
||||||
deleteCookie,
|
deleteCookie,
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
apiCheckInitStatus,
|
apiCheckInitStatus,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="db\" />
|
<Folder Include="db\" />
|
||||||
|
<Folder Include="wwwroot\" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,16 @@ namespace dy.net.service
|
|||||||
return await _dyCollectVideoRepository.GetUperLastViedoFileName(AuthorId, ViedoNameSimplify);
|
return await _dyCollectVideoRepository.GetUperLastViedoFileName(AuthorId, ViedoNameSimplify);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID获取视频信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<DouyinVideo> GetById(string id)
|
||||||
|
{
|
||||||
|
return await _dyCollectVideoRepository.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HTML 视频播放器(对接非 wwwroot 视频)</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #42b983;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #359469;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin-top: 15px;
|
||||||
|
color: #dc3545;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="video-container">
|
||||||
|
<video id="videoPlayer" controls preload="metadata" autoplay muted>
|
||||||
|
<source id="videoSource" type="video/mp4">
|
||||||
|
您的浏览器不支持 HTML5 视频播放,请升级浏览器。
|
||||||
|
</video>
|
||||||
|
<div id="errorMsg" class="error-message"></div>
|
||||||
|
<div class="video-list">
|
||||||
|
<button onclick="changeVideo()">播放视频 1</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 1. 后端 API 基础地址(与 Program.cs 中 app.Run() 一致)
|
||||||
|
const BASE_URL = 'http://localhost:5025';
|
||||||
|
|
||||||
|
// 2. 视频接口 URL(对应后端 VideoController 的 StreamVideo 方法)
|
||||||
|
const getVideoUrl = () => `${BASE_URL}/api/Video/play/1993346988078411776`;
|
||||||
|
|
||||||
|
// 3. DOM 元素
|
||||||
|
const videoPlayer = document.getElementById('videoPlayer');
|
||||||
|
const videoSource = document.getElementById('videoSource');
|
||||||
|
const errorMsg = document.getElementById('errorMsg');
|
||||||
|
|
||||||
|
// 4. 初始加载视频
|
||||||
|
window.onload = () => changeVideo();
|
||||||
|
|
||||||
|
// 5. 切换视频
|
||||||
|
function changeVideo() {
|
||||||
|
errorMsg.style.display = 'none';
|
||||||
|
const videoUrl = getVideoUrl();
|
||||||
|
videoSource.src = videoUrl;
|
||||||
|
videoPlayer.load(); // 重新加载视频
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 错误处理(文件不存在、权限不足、跨域等)
|
||||||
|
videoPlayer.onerror = (e) => {
|
||||||
|
const errorMap = {
|
||||||
|
1: '视频加载中断',
|
||||||
|
2: '网络错误(跨域未配置或后端服务未启动)',
|
||||||
|
3: '视频解码失败(格式不支持)',
|
||||||
|
4: '视频格式不支持',
|
||||||
|
5: '视频文件不存在或后端权限不足'
|
||||||
|
};
|
||||||
|
const errorText = errorMap[e.target.error.code] || '未知错误';
|
||||||
|
errorMsg.textContent = `加载失败:${errorText}(文件:${videoSource.src.split('/').pop()})`;
|
||||||
|
errorMsg.style.display = 'block';
|
||||||
|
console.error('错误详情:', e.target.error);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user