同步记录中打开视频播放

This commit is contained in:
jianzhichu
2025-11-26 23:52:49 +08:00
parent 22a65b9833
commit 6bedb30602
9 changed files with 742 additions and 90 deletions
+74 -16
View File
@@ -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" // 默认二进制流
};
} }
} }
} }
+1
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
VITE_BASE_URL=/ VITE_BASE_URL=/
VITE_API_URL=http://localhost VITE_API_URL=/
+540 -72
View File
@@ -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>
+8
View File
@@ -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,
+1
View File
@@ -65,6 +65,7 @@
<ItemGroup> <ItemGroup>
<Folder Include="db\" /> <Folder Include="db\" />
<Folder Include="wwwroot\" />
</ItemGroup> </ItemGroup>
+10
View File
@@ -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);
}
} }
+106
View File
@@ -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>