同步记录中打开视频播放

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
+1 -1
View File
@@ -1,2 +1,2 @@
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-space>
<a-button type="primary" @click="GetRecords">查询</a-button>
<!-- 核心修改点绑定 disabled 属性 -->
<a-button type="danger" @click="StartNow" :disabled="isSyncing">
<!-- 可选添加加载状态提示 -->
<template #icon>
<a-spin v-if="isSyncing" size="small" />
</template>
@@ -30,24 +28,98 @@
</a-space>
</a-form-item>
</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">
<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>
</div>
</template>
<script lang="ts" setup>
import { defineComponent, reactive, ref } from 'vue';
import { reactive, ref, onMounted, nextTick, watch } from 'vue';
import { useApiStore } from '@/store';
import type { UnwrapRef } from 'vue';
import { onMounted } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
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];
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';
dayjs.locale('zh-cn');
// ... (columns, loading, DataItem, datas, showImageViedo, QuaryParam, value1, ranges, quaryData 的定义保持不变)
// 表格列配置
const columns = ref([
{
title: '同步时间',
@@ -77,6 +149,7 @@ const columns = ref([
title: '视频标题',
dataIndex: 'videoTitle',
align: 'left',
width: 350, // 确保标题单元格有足够宽度
},
{
title: 'CK名称',
@@ -85,26 +158,20 @@ const columns = ref([
width: 200,
},
]);
// 基础状态
const loading = ref(false);
interface DataItem {}
const datas: UnwrapRef<DataItem[]> = reactive([]);
const showImageViedo = ref(false);
interface QuaryParam {
dates?: string[];
pageIndex: number;
pageSize: number;
author: string;
tag: string;
viedoType: string;
}
const dataSource = ref(datas);
// 查询参数
const value1 = ref<RangeValue>();
const ranges = {
今天: [dayjs(), dayjs()] as RangeValue,
本月: [dayjs(), dayjs().endOf('month')] as RangeValue,
};
const quaryData: UnwrapRef<QuaryParam> = reactive({
datas: [],
pageIndex: 0,
pageSize: 20,
author: '',
@@ -112,10 +179,71 @@ const quaryData: UnwrapRef<QuaryParam> = reactive({
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 = () => {
loading.value = true;
quaryData.pageIndex = pagination.value.current;
quaryData.pageSize = pagination.value.defaultPageSize;
if (value1.value) {
quaryData.dates = value1.value.map((date) => date.format('YYYY-MM-DD'));
}
useApiStore()
.VideoPageList(quaryData)
.then((res) => {
@@ -126,22 +254,57 @@ const GetRecords = () => {
pagination.value.defaultPageSize = res.data.pageSize;
pagination.value.total = 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();
getConfig(); // 在 onMounted 中调用 getConfig
});
const pagination = ref({
current: 1,
defaultPageSize: 10,
total: 0,
showTotal: () => `${0}`,
});
};
// 获取配置
const getConfig = () => {
useApiStore()
.apiGetConfig()
@@ -149,67 +312,372 @@ const getConfig = () => {
if (res.code === 0) {
showImageViedo.value = res.data.downImageVideoFromEnv;
} else {
// 可以在这里添加配置获取失败的处理
message.warning(`获取配置失败: ${res.message}`);
}
})
.catch((error) => {
console.error('获取配置失败:', error);
message.error('获取配置失败,请稍后重试');
});
};
const handleTableChange = (e) => {
pagination.value.current = e.current;
pagination.value.defaultPageSize = e.defaultPageSize;
// 通常 total 是由后端返回的,这里不需要手动设置
GetRecords();
};
// 核心修改点:添加一个 ref 来控制按钮状态
const isSyncing = ref(false);
const StartNow = () => {
// 如果正在同步中,则直接返回,防止重复点击
if (isSyncing.value) {
// 视频点击事件处理 - 核心修改:使用formatModalTitle处理弹窗标题长度
const handleVideoClick = (record: DataItem) => {
if (!record.id) {
message.warning('该视频暂无播放地址');
return;
}
message.success('请耐心等待,同步任务正在启动...');
isSyncing.value = true; // 开始同步,禁用按钮
useApiStore()
.StartJobNow()
.then((res) => {
// 根据后端返回的状态码判断是否真正成功
if (res.code === 0) {
message.success('同步任务启动成功!');
} else {
message.error(`同步任务启动失败: ${res.message || '未知错误'}`);
}
})
.catch((error) => {
console.error('同步任务API调用失败:', error);
message.error('同步任务启动失败,请检查网络或联系管理员。');
})
.finally(() => {
isSyncing.value = false; // 无论成功失败,都恢复按钮状态
});
// 保存当前视频信息
currentVideoInfo.value = record;
videoId.value = record.id;
// 弹窗标题显示格式化后的标题(超过40字符显示省略号)
playingTitle.value = formatModalTitle(record.videoTitle);
isModalOpen.value = true;
// 显示加载状态
isVideoLoading.value = true;
// 重置错误状态
hasError.value = false;
};
const datePicked = (ref, dateArry) => {
// 注意:dateArry 是 Dayjs 对象数组,如果后端需要字符串,需要格式化
// quaryData.dates = dateArry.map(date => date.format('YYYY-MM-DD'));
quaryData.dates = dateArry;
console.log(dateArry);
// 监听弹窗状态,加载默认视频
watch(
isModalOpen,
(isOpen) => {
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) => {
quaryData.viedoType = e.target.value;
pagination.value.current = 1; // 切换类型后,重置页码到第一页
// 拼接后端视频接口地址(对应 VideoController 的 StreamVideo 方法)
videoUrl.value = `${import.meta.env.VITE_API_URL}api/Video/play/${videoId.value}`;
// 重新加载视频(解决切换视频不刷新的问题)
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();
};
getConfig();
});
</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>
+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 {
// playViedo,
deleteCookie,
UpdateConfig,
apiCheckInitStatus,