Files
douyin/app/src/pages/workplace/RecordTable.vue
T

1363 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<!-- 优化查询区域调整布局时间博主名称标题放一行宽度自适应 -->
<div class="query-container">
<a-form layout="inline" :model="quaryData" class="query-form">
<!-- 第一行时间选择器组 + 博主名称 + 标题合并为一行自适应宽度 -->
<div class="form-row form-main-row">
<a-form-item label="同步日期" class="form-item form-item-date">
<a-range-picker v-model:value="value1" :ranges="ranges" :locale="locale" @change="datePicked" class="range-picker" />
</a-form-item>
<a-form-item label="发布日期" class="form-item form-item-date">
<a-range-picker v-model:value="value2" :ranges="ranges2" :locale="locale" @change="datePicked2" class="range-picker" />
</a-form-item>
<a-form-item label="博主名称" ref="author" name="author" class="form-item form-item-input">
<a-input v-model:value="quaryData.author" class="query-input" placeholder="请输入博主名称" />
</a-form-item>
<a-form-item label="标题" ref="title" name="title" class="form-item form-item-input">
<a-input v-model:value="quaryData.title" class="query-input" placeholder="请输入标题" />
</a-form-item>
</div>
<!-- 第二行单选组 + 按钮组 -->
<div class="form-row form-actions-row">
<a-form-item label="视频类型" class="form-item radio-group-item">
<a-radio-group v-model:value="quaryData.viedoType" button-style="solid" @change="onViedoTypeChanged" class="video-type-radio">
<a-radio-button value="*">全部</a-radio-button>
<a-radio-button value="1">喜欢的</a-radio-button>
<a-radio-button value="2">收藏的</a-radio-button>
<a-radio-button value="3">关注的</a-radio-button>
<a-radio-button value="4" v-if="showImageViedo">图文视频</a-radio-button>
</a-radio-group>
</a-form-item>
<a-button type="primary" @click="GetRecords" class="query-button">
<SearchOutlined />查询
</a-button>
<a-form-item class="form-item batch-operation-item" style="margin-left:20px;">
<a-switch v-model:checked="isBatchMode" checked-children="批量操作" un-checked-children="批量操作" class="batch-switch" />
</a-form-item>
<a-form-item class="form-item button-group-item">
<a-space size="middle" class="button-group">
<a-button success @click="handleBatchShare" class="delete-button" v-if="isBatchMode" :disabled="selectedRowKeys.length === 0 || isSyncing">
<ShareAltOutlined />
分享选中
</a-button>
<a-button type="danger" @click="handleBatchDelete" class="delete-button" v-if="isBatchMode" :disabled="selectedRowKeys.length === 0 || isSyncing">
<SyncOutlined />
重新同步
</a-button>
</a-space>
</a-form-item>
<!-- 按钮代码 -->
<a-form-item class="form-item delete-btn-2-wrapper">
<a-button danger @click="handShowDeleteVideos" class="delete-button-2">
<ClearOutlined /> <!-- 注意首字母大写Antd图标命名规范 -->
已删除
</a-button>
</a-form-item>
</div>
</a-form>
</div>
<!-- 已删除视频-抽屉 -->
<a-drawer title="已删除视频" size="large" :visible="deleteVideoShow" @close="onDeleteVideoClose">
<template #extra>
</template>
<a-list size="small" bordered :data-source="deleteVideos">
<template #renderItem="{item, index}">
<a-list-item>
<span>
{{index+1}}. {{ item.videoTitle }}
</span>
<a-button type="text" size="small" class="copy-delete-video-btn" @click="(e) => copyVideoPath(item.videoSavePath)">
<CopyOutlined /> 复制
</a-button>
</a-list-item>
</template>
</a-list>
</a-drawer>
<!-- 视频播放弹窗 - 保持原有 -->
<a-modal v-model:visible="isModalOpen" :width="900" :mask-closable="false" :footer="null" @cancel="handleCancel" :body-style="{ padding: '0', overflow: 'hidden', backgroundColor: '#fff' }" :style="{
borderRadius: '8px',
maxWidth: '85vw',
maxHeight: '80vh',
minWidth: '500px',
minHeight: '400px'
}" :mask-style="{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }">
<!-- 自定义弹窗标题替代原来的:title属性 -->
<template #title>
<span class="modal-title-with-tooltip" :title="formatFilePath(currentVideoInfo?.videoSavePath)">
{{ playingTitle }}
</span>
</template>
<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 class="info-item">
<a-popover placement="bottom">
<template #content>
<p>{{formatPathSeparator(currentVideoInfo?.videoSavePath)}}</p>
</template>
<a-button type="link" size="small" @click="copyVideoPath(formatPathSeparator(currentVideoInfo?.videoSavePath))" class="copy-path-btn">
复制路径
</a-button>
</a-popover>
</div>
</div>
</div>
</a-modal>
<!-- 表格 - 增加复选框和操作列 -->
<a-table :columns="columns" :data-source="dataSource" bordered :pagination="pagination" @change="handleTableChange" :loading="loading" :row-selection="isBatchMode ? rowSelection : null" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'videoTitle'">
<a class="video-title-link" :title="record.videoTitle || '无标题'" @click="handleVideoClick(record)" @mouseenter="handleTitleMouseEnter" @mouseleave="handleTitleMouseLeave">
{{ formatVideoTitle(record.videoTitle) }}
</a>
</template>
<template v-if="column.key === 'operation'">
<a-space size="small">
<a-button type="link" @click="handleReDownload(record)" :disabled="isSyncing">
<SyncOutlined />
重新同步
</a-button>
<a-button type="link" @click="handleShare(record)" :disabled="!record.id">
<ShareAltOutlined />
分享
</a-button>
<a-button type="link" danger @click="handleDelete(record)" :disabled="!record.id">
<DeleteOutlined />
删除
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted, nextTick, watch, computed } from 'vue';
import { useApiStore } from '@/store';
import type { UnwrapRef } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
import { message, Modal } from 'ant-design-vue';
import CryptoJS from 'crypto-js';
import {
SearchOutlined,
SyncOutlined,
ShareAltOutlined,
ClearOutlined,
CopyOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue';
// 类型定义
type RangeValue = [Dayjs, Dayjs];
interface DataItem {
id?: string; // 视频ID(后端返回的字段,用于拼接播放地址)
videoTitle?: string; // 视频标题
syncTimeStr?: string; // 同步时间
viedoTypeStr?: string; // 同步类型
author?: string; // 博主
viedoCate?: string; // 视频类型
dyUser?: string; // CK名称
fileHash?: string;
authorId?: string;
videoSavePath: string;
createTimeStr?: string; // 发布时间
}
interface QuaryParam {
dates?: string[];
dates2?: string[];
pageIndex: number;
pageSize: number;
author: string;
title: string;
viedoType: string;
fileHash: string;
authorId: string;
}
// 引入dayjs中文包
import 'dayjs/locale/zh-cn';
import { forEach } from 'lodash';
dayjs.locale('zh-cn');
// 批量操作相关状态
const isBatchMode = ref(false); // 批量操作开关状态
const selectedRowKeys = ref<string[]>([]); // 选中的行ID集合
// 表格行选择器类型定义(对齐 Ant Design Vue 3.x 规范)
interface CustomTableRowSelection<T> {
type: 'checkbox' | 'radio';
selectedRowKeys: string[] | number[];
onChange?: (
selectedRowKeys: string[] | number[],
selectedRows: T[],
info: { type: 'select' | 'unselect' | 'selectAll' | 'unselectAll' | 'clear' }
) => void;
preserveSelectedRowKeys?: boolean;
getCheckboxProps?: (record: T) => { disabled?: boolean };
}
// ✅ 修复:用计算属性实现响应式绑定(解决 checkbox 选中卡顿)
const rowSelection = computed<CustomTableRowSelection<DataItem>>(() => ({
type: 'checkbox',
selectedRowKeys: selectedRowKeys.value, // 计算属性自动同步选中状态
onChange: (selectedKeys, selectedRows) => {
selectedRowKeys.value = selectedKeys as string[];
console.log('选中的行ID', selectedRowKeys.value);
console.log('选中的行数据:', selectedRows);
},
preserveSelectedRowKeys: false,
getCheckboxProps: (record) => ({
disabled: isSyncing.value, // 同步时禁用复选框,避免冲突
}),
}));
// 表格列配置(优化:临时注释 fixed: right 避免渲染冲突)
const columns = ref([
{
title: '同步时间',
dataIndex: 'syncTimeStr',
align: 'center',
width: 180,
},
{
title: '发布时间',
dataIndex: 'createTimeStr',
align: 'center',
width: 180,
},
{
title: '同步类型',
dataIndex: 'viedoTypeStr',
align: 'center',
width: 120,
},
{
title: '博主',
dataIndex: 'author',
align: 'center',
width: 150,
},
{
title: '视频类型',
dataIndex: 'viedoCate',
width: 200,
align: 'center',
},
{
title: '视频标题',
dataIndex: 'videoTitle',
align: 'left',
width: 350,
},
{
title: 'CK名称',
dataIndex: 'dyUser',
align: 'center',
width: 120,
},
{
title: '操作',
key: 'operation',
align: 'center',
width: 180,
// fixed: 'right', // 注释:避免固定列导致的重绘卡顿,如需使用可后续调试
},
]);
// 监听批量操作开关状态变化,清空选中状态+强制表格重绘
watch(isBatchMode, (isOpen) => {
if (!isOpen) {
selectedRowKeys.value = [];
// 强制表格重新渲染,解决状态残留问题
nextTick(() => {
const tableEl = document.querySelector('.ant-table') as HTMLElement;
if (tableEl) {
tableEl.setAttribute('key', Date.now().toString());
}
});
}
});
// 基础状态(优化:删除冗余的 datas 响应式数组)
const loading = ref(false);
const showImageViedo = ref(true);
const dataSource = ref<DataItem[]>([]); // 直接用 ref 数组存储表格数据,减少响应式嵌套
// 查询参数
const value1 = ref<RangeValue>();
const ranges = {
今天: [dayjs(), dayjs()] as RangeValue,
本月: [dayjs(), dayjs().endOf('month')] as RangeValue,
};
const value2 = ref<RangeValue>();
const ranges2 = {
今天: [dayjs(), dayjs()] as RangeValue,
本月: [dayjs(), dayjs().endOf('month')] as RangeValue,
};
const quaryData: UnwrapRef<QuaryParam> = reactive({
pageIndex: 0,
pageSize: 20,
author: '',
title: '',
viedoType: '*',
authorId: '',
fileHash: '',
});
// 分页配置
const pagination = ref({
current: 1,
defaultPageSize: 10,
total: 0,
showSizeChanger: true, // 强制显示「每页显示数量」下拉框(关键修复)
showTotal: () => `共 ${0} 条`,
// showQuickJumper: true, // 显示快速跳转输入框(可选,增强体验)
pageSizeOptions: ['10', '20', '50', '100'], // 自定义每页条数选项(可选)
showSizeChange: (current, pageSize) => {
// 可选:监听每页条数变化,重置当前页为第1页(避免最后一页数据不足的问题)
pagination.value.current = 1;
pagination.value.defaultPageSize = pageSize;
GetRecords();
},
});
// 视频播放相关配置
const DEFAULT_LOW_VOLUME = 0.3;
const isVideoLoading = ref(false); // 视频加载状态
const isSyncing = ref(false); // 同步状态
const currentVideoInfo = ref<DataItem | null>(null); // 当前播放视频信息
// 视频弹窗相关状态
const isModalOpen = ref(false);
const videoRef = ref<HTMLVideoElement | null>(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('');
let videoProgressListener: ((e: Event) => void) | null = null; // 进度监听器
/** 格式化存储路径(过长时中间省略) */
const formatFilePath = (filePath?: string) => {
if (!filePath) return '暂无存储路径信息';
// 路径超过80字符时,保留前40和后30字符,中间用...省略
if (filePath.length > 80) {
return `${filePath.slice(0, 40)}...${filePath.slice(-30)}`;
}
return filePath;
};
// -------------------------- 核心工具方法 --------------------------
const formatPathSeparator = (path: string | undefined) => {
if (!path) return path; // 处理空路径情况
// 正则表达式 /\\/g 表示全局匹配所有反斜杠
return path.replace(/\\/g, '/');
};
/** 格式化表格视频标题:超过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'));
}
if (value2.value) {
quaryData.dates2 = value2.value.map((date) => date.format('YYYY-MM-DD')); // 修复:之前误写为value1
}
useApiStore()
.VideoPageList(quaryData)
.then((res) => {
loading.value = false;
if (res.code === 0) {
dataSource.value = res.data.data; // 直接更新 ref 数组,优化响应式
pagination.value.current = res.data.pageIndex;
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('获取数据失败,请稍后重试');
});
};
/** 立即同步 */
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 datePicked2 = (_, dateArry: RangeValue) => {
quaryData.dates2 = dateArry.map((date) => date.format('YYYY-MM-DD'));
console.log('选择的发布日期范围:', quaryData.dates2);
};
/** 表格分页/排序变化事件 */
const handleTableChange = (paginationObj: any) => {
pagination.value.current = paginationObj.current;
pagination.value.defaultPageSize = paginationObj.pageSize;
// 分页变化时清空选中状态(跨页不保留)
if (isBatchMode.value) {
selectedRowKeys.value = [];
}
GetRecords();
};
/** 获取系统配置 */
// const getConfig = () => {
// useApiStore()
// .apiGetConfig()
// .then((res) => {
// if (res.code === 0) {
// showImageViedo.value = res.data.downImageVideoFromEnv;
// } else {
// message.warning(`获取配置失败: ${res.message}`);
// }
// GetRecords();
// })
// .catch((error) => {
// console.error('获取配置失败:', error);
// message.error('获取配置失败,请稍后重试');
// });
// };
/** 视频类型切换事件 */
const onViedoTypeChanged = () => {
GetRecords();
};
// -------------------------- 视频播放相关方法 --------------------------
/** 点击视频标题播放 */
const handleVideoClick = (record: DataItem) => {
if (!record.id) {
message.warning('该视频暂无播放地址');
return;
}
// 保存当前视频信息
currentVideoInfo.value = record;
console.log(currentVideoInfo);
videoId.value = record.id;
playingTitle.value = formatModalTitle(record.videoTitle);
// 重置错误状态
hasError.value = false;
// 显示弹窗(触发watch加载视频)
isModalOpen.value = true;
};
/** 加载视频(优化:简化逻辑,避免内存泄漏) */
const loadVideo = () => {
if (!videoRef.value || !videoId.value) return;
isVideoLoading.value = true;
// 移除之前的监听器
if (videoProgressListener) {
videoRef.value.removeEventListener('progress', videoProgressListener);
videoProgressListener = null;
}
// 拼接视频地址(添加时间戳避免缓存)
const timestamp = new Date().getTime();
videoUrl.value = `${import.meta.env.VITE_API_URL}api/Video/play/${videoId.value}?t=${timestamp}`;
// 直接赋值src并加载
videoRef.value.src = videoUrl.value;
// 重新绑定进度监听器
videoProgressListener = handleVideoProgress;
videoRef.value.addEventListener('progress', videoProgressListener);
// 触发加载
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;
}
}
};
/** 暂停视频并释放资源 */
const pauseVideo = () => {
if (!videoRef.value) return;
const video = videoRef.value;
// 暂停播放
video.pause();
// 移除监听器
if (videoProgressListener) {
video.removeEventListener('progress', videoProgressListener);
videoProgressListener = null;
}
// 清空src
video.src = '';
// 重置状态
isVideoLoading.value = false;
};
/** 视频错误处理 */
const handleVideoError = (e: Event) => {
const video = e.target as HTMLVideoElement;
const errorCode = video.error?.code;
const errorMap: Record<number, string> = {
1: '视频加载中断',
2: '网络错误(跨域未配置/后端服务未启动/接口不可用)',
3: '视频解码失败(格式不支持或文件损坏)',
4: '视频格式不支持',
5: '视频文件不存在或后端权限不足',
};
if (!video.src) {
errorMessage.value = '视频地址为空,请重试';
} else {
errorMessage.value = `加载失败:${errorMap[errorCode as number] || '未知错误'}(视频ID${videoId.value}`;
}
hasError.value = true;
isVideoLoading.value = false;
console.error('视频播放错误详情:', video.error);
};
/** 关闭视频弹窗 */
const handleCancel = () => {
// 暂停视频并释放资源
pauseVideo();
// 立即关闭弹窗
isModalOpen.value = false;
// 延迟重置状态
setTimeout(() => {
currentVideoInfo.value = null;
videoUrl.value = '';
videoId.value = '';
playingTitle.value = '';
}, 100);
};
// 监听弹窗状态,加载/释放视频
watch(
isModalOpen,
(isOpen) => {
if (isOpen) {
// 弹窗打开时,延迟加载视频(给DOM渲染时间)
nextTick(() => {
loadVideo();
});
} else {
// 弹窗关闭时,立即暂停视频
pauseVideo();
}
},
{ immediate: false }
);
// -------------------------- 批量操作和操作列事件 --------------------------
/** 批量删除事件 */
const handleBatchDelete = () => {
if (selectedRowKeys.value.length === 0) {
message.warning('请先选择要删除的视频');
return;
}
Modal.confirm({
title: '确认重新下载吗',
content: `您确定要重新下载选中的 ${selectedRowKeys.value.length} 条视频数据吗?`,
okText: '确认重新下载',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
reDownload({ ids: selectedRowKeys.value });
},
});
};
const deleteVideoShow = ref(false);
const handShowDeleteVideos = () => {
deleteVideoShow.value = true;
getDeleteViedos();
};
const deleteVideos = ref([]);
const getDeleteViedos = () => {
useApiStore()
.GetDeleteViedos()
.then((res) => {
deleteVideos.value = res.data;
});
};
const onDeleteVideoClose = (e) => {
deleteVideoShow.value = false;
};
const reDownload = (param: object) => {
try {
loading.value = true;
console.log('执行批量删除,选中ID', selectedRowKeys.value);
useApiStore()
.ReDownViedos(param)
.then((res) => {
loading.value = false;
if (res.code === 0) {
message.success('删除成功,下次任务执行时会重新下载');
// 刷新数据并清空选中状态
GetRecords();
selectedRowKeys.value = [];
} else {
message.warning(res.message || '获取数据失败');
}
})
.catch((error) => {
loading.value = false;
});
} catch (error) {
console.error('批量删除失败:', error);
message.error('删除失败,请稍后重试');
} finally {
loading.value = false;
}
};
/** 重新下载事件 */
const handleReDownload = (record: DataItem) => {
if (!record.id) {
message.warning('视频ID不存在,无法重新下载');
return;
}
try {
loading.value = true;
const _ids = [record.id];
reDownload({ ids: _ids });
} catch (error) {
console.error('重新下载失败:', error);
message.error('重新下载失败,请稍后重试');
loading.value = false;
}
};
const handleBatchShare = () => {
const matchedItems = dataSource.value.filter((item) => selectedRowKeys.value.includes(item.id));
try {
// console.log('执行分享操作,视频ID', record.id, '视频标题:', record.videoTitle);
// 生成分享链接
const currentDomain = window.location.origin;
let shareUrl = '';
matchedItems.forEach((record) => {
let k = CryptoJS.MD5(record.fileHash + record.authorId).toString();
shareUrl += `${currentDomain}/share/${record.id}/${k}
`;
});
copyToClipboard(shareUrl, '分享链接已复制到剪贴板!');
} catch (error) {
console.error('分享失败:', error);
message.error('分享功能异常,请稍后重试');
}
};
// 复制链接到剪贴板(兼容生产环境)
const copyToClipboard = async (shareUrl: string, msg: string) => {
try {
// 方案1:优先使用 navigator.clipboard(现代浏览器+HTTPS环境)
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(shareUrl);
message.success(msg);
} else {
// 方案2:降级使用 document.execCommand(兼容HTTP/旧浏览器)
const textarea = document.createElement('textarea');
// 隐藏文本域(避免影响页面布局)
textarea.style.position = 'absolute';
textarea.style.top = '-9999px';
textarea.style.left = '-9999px';
// 设置要复制的内容
textarea.value = shareUrl;
document.body.appendChild(textarea);
// 选中并复制
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea); // 清理DOM
if (success) {
message.success(msg);
} else {
// 方案3:最终降级 - 显示链接让用户手动复制
throw new Error('自动复制失败');
}
}
} catch (error) {
console.warn('复制失败,触发手动复制方案:', error);
// 最终降级:显示链接弹窗
Modal.info({
title: '视频分享',
content: `
<p>分享链接:<a href="${shareUrl}" target="_blank" rel="noopener noreferrer">${shareUrl}</a></p>
<p style="margin-top: 8px; color: #666;">请手动复制链接后分享给他人</p>
`,
okText: '已复制',
onOk: () => {},
});
}
};
/** 分享事件 */
const handleShare = (record: DataItem) => {
if (!record.id) {
message.warning('视频ID不存在,无法分享');
return;
}
try {
const currentDomain = window.location.origin;
// console.log('执行分享操作,视频ID', record.id, '视频标题:', record.videoTitle);
// 生成分享链接
let k = CryptoJS.MD5(record.fileHash + record.authorId).toString();
const shareUrl = `${currentDomain}/share/${record.id}/${k}`;
copyToClipboard(shareUrl, '分享链接已复制到剪贴板!');
} catch (error) {
console.error('分享失败:', error);
message.error('分享功能异常,请稍后重试');
}
};
//视频删除不再下载
const handleDelete = (record: DataItem) => {
Modal.confirm({
title: '确认删除',
content: `您确定要删除这条视频数据吗?此操作不可撤销,以后也不会再下载!!!`,
okText: '确认删除',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
try {
useApiStore()
.DeleteVideo(record.id)
.then((res) => {
if (res.code == 0) {
message.success('删除成功,再也不会下载!!!');
} else {
message.error('删除失败');
}
GetRecords();
});
} catch (error) {
console.error('删除失败', error);
message.error('视频删除失败,请稍后再试');
}
},
});
};
// 新增:复制视频路径方法
const copyVideoPath = (path?: string) => {
if (!path) {
message.warning('暂无视频存储路径');
return;
}
copyToClipboard(path, '视频保存路径已复制到剪贴板!');
};
// -------------------------- 页面初始化 --------------------------
onMounted(() => {
// getConfig();
GetRecords();
});
</script>
<style>
/* 新增:优化视频元素的过渡效果,避免关闭时的视觉卡顿 */
.video-element {
width: 100%;
height: auto;
max-height: 420px;
min-height: 250px;
background-color: #000;
object-fit: contain;
opacity: 1;
transition: opacity 0.2s ease-in-out; /* 缩短过渡时间 */
will-change: opacity; /* 告诉浏览器提前优化渲染 */
}
/* 新增:查询区域样式优化 */
.query-container {
margin: 16px 0;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.query-form {
width: 100%;
}
.form-row {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 12px;
}
.form-row:last-child {
margin-bottom: 0;
}
.form-item {
margin-bottom: 0 !important;
margin-right: 24px !important;
display: flex;
align-items: center;
}
/* 核心修改:主查询行自适应布局 */
.form-main-row {
display: flex;
flex-wrap: nowrap; /* 禁止换行 */
align-items: center;
width: 100%;
overflow: hidden; /* 防止溢出 */
}
/* 日期选择器项:固定基础宽度,自适应收缩 */
.form-item-date {
flex: 0 1 280px; /* 不放大,可缩小,基础宽度280px */
min-width: 220px; /* 最小宽度,防止过度收缩 */
}
/* 输入框项:自适应拉伸填充剩余空间 */
.form-item-input {
flex: 1 1 auto; /* 可放大,可缩小,自动宽度 */
min-width: 180px; /* 最小宽度,保证可用性 */
}
/* 日期选择器自适应宽度 */
.range-picker {
width: 100% !important; /* 占满父容器宽度 */
min-width: 200px !important;
}
/* 输入框自适应宽度 */
.query-input {
width: 100% !important; /* 占满父容器宽度 */
min-width: 160px !important;
}
/* 新增:批量操作开关样式 */
.batch-operation-item {
margin-left: 20px !important;
}
.batch-switch {
--ant-switch-height: 24px;
--ant-switch-width: 80px;
}
/* 新增:删除按钮样式 */
.delete-button {
min-width: 100px;
}
/* 单选组样式 */
.video-type-radio {
display: flex;
flex-wrap: wrap;
}
.radio-group-item {
flex: 1;
min-width: 300px;
}
/* 按钮组样式 - 关键修改:保持原有布局 */
.button-group-item {
margin-left: 8px !important; /* 仅保留少量间距,不使用auto */
margin-right: 0 !important;
display: flex !important;
align-items: center !important;
}
.button-group {
display: flex;
gap: 12px;
}
.query-button,
.sync-button {
min-width: 100px;
}
/* 核心修复:操作行布局 - 关键修改 */
.form-actions-row {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
min-height: 40px;
box-sizing: border-box;
/* 移除之前的padding-right,避免影响其他按钮 */
padding-right: 0 !important;
}
/* 已删除按钮容器 - 独立定位,不影响其他按钮 */
.delete-btn-2-wrapper {
margin-left: auto !important; /* 自动靠右,不影响左侧按钮 */
margin-right: 0 !important;
padding: 0 !important;
width: 100px !important;
height: 32px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* 响应式调整:屏幕较小时允许主查询行换行 */
@media (max-width: 1440px) {
.form-main-row {
flex-wrap: wrap; /* 允许换行 */
}
.form-item-date,
.form-item-input {
margin-bottom: 12px !important; /* 换行后添加底部间距 */
}
}
@media (max-width: 1200px) {
.form-actions-row {
flex-wrap: wrap; /* 允许其他元素换行 */
min-height: 60px; /* 增大行高 */
}
.batch-operation-item {
margin-left: 20px !important;
margin-top: 8px !important;
}
/* 响应式下按钮组调整 */
.button-group-item {
margin-left: 20px !important;
margin-top: 8px !important;
}
/* 已删除按钮在小屏幕下换行显示 */
.delete-btn-2-wrapper {
margin-left: 20px !important;
margin-top: 8px !important;
margin-right: 0 !important;
width: auto !important;
}
}
@media (max-width: 992px) {
.form-item {
margin-right: 16px !important;
}
}
@media (max-width: 768px) {
.form-item-date,
.form-item-input {
flex: 1 1 100%; /* 占满整行 */
min-width: unset;
}
.button-group {
width: 100%;
justify-content: space-between;
}
.query-button,
.sync-button,
.delete-button {
flex: 1;
margin: 0 4px;
}
}
/* 原有样式保持不变 */
.video-container {
position: relative;
border-bottom: 1px solid #e8e8e8;
overflow: hidden;
max-height: 420px;
}
.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;
flex: 1;
align-items: center;
font-size: 14px;
line-height: 1.6;
flex-wrap: nowrap;
}
.info-label {
color: #666666;
margin-right: 8px;
white-space: nowrap;
font-weight: 500;
}
.info-value {
color: #333333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 8px;
}
/* 新增:复制路径按钮样式 */
.copy-path-btn {
padding: 0 6px !important;
height: 24px !important;
font-size: 12px !important;
white-space: nowrap;
}
.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;
overflow: hidden !important;
max-width: 85vw !important;
max-height: 80vh !important;
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;
}
: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;
}
: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;
}
: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;
}
/* 新增:表格复选框列样式调整 */
:deep(.ant-table-selection-column) {
width: 50px !important;
text-align: center !important;
}
/* 新增:操作列按钮样式 */
:deep(.ant-space-item button) {
padding: 0 8px !important;
height: 28px !important;
font-size: 13px !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;
}
/* 响应式下操作列调整 */
:deep(.ant-table-column-has-fix-right) {
right: 0 !important;
}
}
@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;
}
/* 移动端操作列换行显示 */
:deep(.ant-space) {
flex-direction: column !important;
align-items: flex-start !important;
gap: 4px !important;
}
}
/* 弹窗标题悬停样式 */
.modal-title-with-tooltip {
position: relative;
cursor: help; /* 鼠标变为帮助图标,提示可悬停 */
padding: 2px 0;
}
/* 可选:添加下划线动画增强交互提示 */
.modal-title-with-tooltip:hover {
text-decoration: underline;
text-underline-offset: 4px;
text-decoration-color: #1890ff;
text-decoration-thickness: 1px;
}
/* 已删除视频条目样式 */
.delete-video-text {
flex: 1; /* 文本占满剩余空间 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
/* 复制按钮样式 */
.copy-delete-video-btn {
padding: 0 4px !important;
height: 24px !important;
font-size: 12px !important;
color: #1890ff !important;
}
.copy-delete-video-btn:hover {
color: #40a9ff !important;
background-color: #f0f9ff !important;
}
/* 列表项布局调整 */
:deep(.ant-list-item) {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 8px 16px !important;
}
</style>