123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- import { Button, Modal, Upload, Progress, message, Form, Input, Space } from "antd";
- import { UploadOutlined, ReloadOutlined, PlusOutlined } from "@ant-design/icons";
- import React, { useState, useEffect, useCallback } from "react";
- import OSSSDK from "../../../../../utils/OSSSDK";
- import http from "../../../../../http";
- import styles from "./index.module.css";
- import type { UploadFile, UploadProps } from "antd/es/upload/interface";
- import { getAccessToken } from "../../../../../http/sso";
- import { adFileUpload, getTempStsToken, uploadPublishVideo } from "../../../../../http/api";
- interface UploadVideoModalProps {
- visible: boolean;
- onClose: () => void;
- onOk?: (videoInfo: any) => void;
- isLoading?: boolean;
- videoInfo?: any;
- }
- interface UploadStatus {
- isUploading: boolean;
- isUploaded: boolean;
- isError: boolean;
- errType?: string;
- }
- interface UploadCreds {
- host: string;
- hosts: string[];
- fileName: string;
- upload: string;
- accessKeyId: string;
- accessKeySecret: string;
- securityToken: string;
- expiration: string;
- }
- const UploadVideoModal: React.FC<UploadVideoModalProps> = ({
- visible,
- onClose,
- onOk,
- videoInfo
- }) => {
- // 视频文件状态
- const [videoFile, setVideoFile] = useState<File & { localUrl?: string } | null>(null);
- const [videoUploadProgress, setVideoUploadProgress] = useState(0);
- const [videoUploadStatus, setVideoUploadStatus] = useState<UploadStatus>({
- isUploading: false,
- isUploaded: false,
- isError: false
- });
- // 封面文件状态
- const [coverFileList, setCoverFileList] = useState<UploadFile[]>([]);
- const [coverUploadStatus, setCoverUploadStatus] = useState<UploadStatus>({
- isUploading: false,
- isUploaded: false,
- isError: false
- });
- // OSS相关状态
- const [videoCreds, setVideoCreds] = useState<UploadCreds | null>(null);
- const [videoUploader, setVideoUploader] = useState<OSSSDK | null>(null);
- const [videoUrl, setVideoUrl] = useState('');
-
- // 重试相关状态
- const [speedTimer, setSpeedTimer] = useState<NodeJS.Timeout | null>(null);
- // 表单引用
- const [form] = Form.useForm();
- const [isEditMode, setIsEditMode] = useState(false);
- // 视频预览
- const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
- const [isVideoHovering, setIsVideoHovering] = useState(false);
- // 发布视频loading状态
- const [publishLoading, setPublishLoading] = useState(false);
- // 重置状态
- const resetStates = useCallback(() => {
- // 重置视频文件状态
- setVideoFile(null);
- setVideoUploadProgress(0);
- setVideoUploadStatus({
- isUploading: false,
- isUploaded: false,
- isError: false
- });
- // 重置封面文件状态
- setCoverFileList([]);
- setCoverUploadStatus({
- isUploading: false,
- isUploaded: false,
- isError: false
- });
- // 重置OSS状态
- setVideoCreds(null);
- setVideoUploader(null);
- setVideoUrl('');
- setIsEditMode(false);
- if (speedTimer) {
- clearInterval(speedTimer);
- setSpeedTimer(null);
- }
- form.resetFields();
- }, [speedTimer, form]);
- // 组件卸载时清理
- useEffect(() => {
- return () => {
- if (speedTimer) clearInterval(speedTimer);
- if (videoUploader) {
- videoUploader.cancelUpload();
- }
- };
- }, [speedTimer, videoUploader]);
- useEffect(() => {
- if (videoFile) {
- startVideoUpload(videoFile);
- }
- }, [videoFile]);
- // 当videoInfo发生变化时,初始化表单和状态
- useEffect(() => {
- if (visible && videoInfo) {
- setIsEditMode(true);
- setVideoUploadProgress(100);
- // 填充表单数据
- form.setFieldsValue({
- title: videoInfo.title,
- });
- // 设置视频URL和状态
- setVideoUrl(videoInfo.videoUrl);
- setVideoUploadStatus({
- isUploading: false,
- isUploaded: true,
- isError: false
- });
- // 设置封面文件
- if (videoInfo.coverUrl) {
- setCoverFileList([{
- uid: '-1',
- name: videoInfo.coverName || 'cover.jpg',
- status: 'done',
- url: videoInfo.coverUrl,
- response: {
- data: {
- fileUrl: videoInfo.coverUrl
- }
- }
- }]);
- setCoverUploadStatus({
- isUploading: false,
- isUploaded: true,
- isError: false
- });
- }
- // 对于编辑模式,我们需要创建一个模拟的videoFile对象来显示视频预览
- if (videoInfo.videoUrl) {
- // 提取文件名作为显示名称
- const fileName = videoInfo.videoName || videoInfo.videoUrl.split('/').pop() || 'video.mp4';
- setVideoFile({
- localUrl: videoInfo.videoUrl,
- name: fileName,
- type: 'video/mp4',
- size: 0 // 实际项目中可能需要从服务器获取文件大小
- } as File & { localUrl?: string });
- }
- }
- }, [visible, videoInfo, form]);
- // 获取上传凭证
- const getSignature = async (fileType: number, uploadId?: string): Promise<UploadCreds> => {
- try {
- const params: any = { fileType };
- if (uploadId) {
- params.uploadId = uploadId;
- }
- // 这里需要根据实际API接口调整
- const response = await http.post<any>(getTempStsToken, params);
- if (response.code === 0) {
- const credsData = response.data;
- if (fileType === 2) { // 视频文件
- setVideoUrl(credsData.fileName);
- }
- return credsData;
- } else {
- throw new Error(response.data.msg || '获取签名失败');
- }
- } catch (error) {
- console.error('获取签名失败:', error);
- throw error;
- }
- };
- // 初始化视频上传器
- const initVideoUploader = async (creds: UploadCreds): Promise<OSSSDK> => {
- if (!videoFile) {
- throw new Error('视频文件不存在');
- }
- const uploader = new OSSSDK(videoFile, creds, (checkpoint: any[]) => {
- // 更新上传进度
- const progress = Number((checkpoint[checkpoint.length - 1].percent * 100).toFixed(2));
- setVideoUploadProgress(progress);
- });
- return uploader;
- };
- // 开始视频上传
- const startVideoUpload = async (file?: File) => {
- const targetFile = file || videoFile;
- if (!targetFile) {
- message.error('请先选择视频文件');
- return;
- }
- try {
- setVideoUploadStatus(prev => ({ ...prev, isUploading: true, isError: false }));
- // 获取上传凭证
- const uploadCreds = await getSignature(2); // 2表示视频文件
- setVideoCreds(uploadCreds);
- // 初始化上传器
- const uploaderInstance = await initVideoUploader(uploadCreds);
- setVideoUploader(uploaderInstance);
- // 开始上传
- await uploaderInstance.multipartUpload();
- // 上传完成
- setVideoUploadStatus(prev => ({ ...prev, isUploading: false, isUploaded: true }));
- if (!isEditMode) {
- message.success('视频上传成功');
- }
- } catch (error: any) {
- console.error('视频上传失败:', error);
- setVideoUploadStatus(prev => ({ ...prev, isUploading: false, isError: true, errType: 'uploadError' }));
- message.error('视频上传失败,请重试');
- }
- };
- // 封面上传处理函数
- const handleCoverUploadChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
- // 只保留最新的文件
- setCoverFileList(newFileList.slice(-1));
- // 如果上传成功,更新状态
- if (newFileList.length > 0 && newFileList[0].status === 'done') {
- setCoverUploadStatus(prev => ({ ...prev, isUploaded: true, isError: false }));
- message.success('封面上传成功');
- } else if (newFileList.length > 0 && newFileList[0].status === 'error') {
- setCoverUploadStatus(prev => ({ ...prev, isError: true }));
- message.error('封面上传失败');
- } else if (newFileList.length > 0 && newFileList[0].status === 'uploading') {
- setCoverUploadStatus(prev => ({ ...prev, isUploading: true, isError: false }));
- }
- };
- // 封面文件验证
- const checkCoverFile = (file: UploadFile) => {
- if ((file.size || 0) > 5 * 1024 * 1024) {
- message.error('图片大小不能超过5MB');
- return Upload.LIST_IGNORE; // 阻止上传
- }
- return true; // 允许上传
- };
- // 重试视频上传
- const handleVideoRetry = async () => {
- setVideoUploadStatus(prev => ({ ...prev, isError: false }));
-
- if (videoUploader && videoCreds) {
- try {
- setVideoUploadStatus(prev => ({ ...prev, isUploading: true }));
- // 重新获取凭证
- const newCreds = await getSignature(2, videoCreds.upload);
- setVideoCreds(newCreds);
- videoUploader.updateConfig(newCreds);
-
- // 断点续传
- await videoUploader.resumeMultipartUpload();
-
- setVideoUploadStatus(prev => ({ ...prev, isUploading: false, isUploaded: true }));
- message.success('视频上传成功');
-
- } catch (error) {
- console.error('重试视频上传失败:', error);
- setVideoUploadStatus(prev => ({ ...prev, isUploading: false, isError: true }));
- message.error('重试失败');
- }
- }
- };
- // 取消视频上传
- const cancelVideoUpload = () => {
- if (videoUploader) {
- videoUploader.cancelUpload();
- }
- setVideoUploadStatus(prev => ({ ...prev, isUploading: false }));
- message.info('已取消视频上传');
- };
- // 发布视频
- const publishVideo = async () => {
- if (!videoUploadStatus.isUploaded) {
- message.warning('请等待视频上传完成');
- return;
- }
- // 表单校验
- try {
- await form.validateFields();
- } catch (error) {
- message.warning('请填写完整的视频信息');
- return;
- }
- // 校验封面是否上传
- if (coverFileList.length === 0 || !coverUploadStatus.isUploaded) {
- message.warning('请上传视频封面');
- return;
- }
- try {
- // 设置loading状态为true
- setPublishLoading(true);
-
- const formData = form.getFieldsValue();
- const publishData = {
- ...formData,
- videoUrl: isEditMode ? videoUrl : (videoCreds?.fileName || videoUrl),
- coverUrl: coverFileList.length > 0 ? coverFileList[0].response.data.fileUrl : '',
- fileExtensions: 'mp4', // 可以根据文件类型动态设置
- ...(isEditMode && videoInfo?.videoId && { videoId: videoInfo.videoId })
- };
- // 这里需要根据实际API接口调整
- const response = await http.post<any>(uploadPublishVideo, publishData);
- if (response.code === 0) {
- message.success('发布成功');
- onOk?.(response.data);
- onClose();
- resetStates();
- } else {
- message.error(response.msg || '发布失败');
- }
- } catch (error) {
- console.error('发布失败:', error);
- message.error('发布失败,请重试');
- } finally {
- // 请求结束后(无论成功或失败),设置loading状态为false
- setPublishLoading(false);
- }
- };
- // 视频文件上传前处理
- const beforeVideoUpload = (file: File & { localUrl?: string }) => {
- // 验证文件类型
- const isVideo = file.type.startsWith('video/');
- if (!isVideo) {
- message.error('只能上传视频文件!');
- return false;
- }
- // 验证文件大小 (例如:限制500MB)
- const isLt500M = file.size / 1024 / 1024 < 500;
- if (!isLt500M) {
- message.error('视频大小不能超过500MB!');
- return false;
- }
-
- file.localUrl = URL.createObjectURL(file);
- setVideoFile(file);
- setVideoUploadProgress(0);
- setVideoUploadStatus({
- isUploading: false,
- isUploaded: false,
- isError: false
- });
-
- return false; // 阻止自动上传
- };
- // 删除视频文件
- const handleVideoRemove = () => {
- setVideoFile(null);
- setVideoUploadProgress(0);
- setVideoUploadStatus({
- isUploading: false,
- isUploaded: false,
- isError: false
- });
- if (videoUploader) {
- videoUploader.cancelUpload();
- }
- };
- // 删除封面文件
- const handleCoverRemove = () => {
- setCoverFileList([]);
- setCoverUploadStatus({
- isUploading: false,
- isUploaded: false,
- isError: false
- });
- };
- // 获取视频进度文本
- const getVideoProgressText = () => {
- if (videoUploadStatus.isError) {
- return '上传失败,点击重试重新上传';
- } else if (!videoUploadStatus.isUploading && videoUploadStatus.isUploaded) {
- return '上传完成';
- } else if (!videoUploadStatus.isUploading && !videoUploadStatus.isUploaded) {
- return '等待中...';
- } else {
- return '';
- }
- };
- // 获取封面进度文本
- const getCoverProgressText = () => {
- if (coverUploadStatus.isError) {
- return '上传失败,请重新选择文件';
- } else if (!coverUploadStatus.isUploading && coverUploadStatus.isUploaded) {
- return '上传完成';
- } else if (coverFileList.length > 0 && coverFileList[0].status === 'uploading') {
- return '上传中...';
- } else {
- return '';
- }
- };
- return (
- <>
- <Modal
- open={visible}
- onCancel={() => {
- onClose();
- resetStates();
- }}
- footer={null}
- width={800}
- title={isEditMode ? "修改视频" : "上传视频"}
- destroyOnHidden
- >
- <div className={styles['upload-video-modal']}>
- {/* 视频上传区域:上传成功后隐藏选择模块 */}
- {!videoFile && (
- <div className={styles['upload-section']}>
- <h4>视频文件</h4>
- <Upload
- fileList={[]}
- beforeUpload={beforeVideoUpload}
- onRemove={handleVideoRemove}
- accept="video/*"
- maxCount={1}
- showUploadList={false}
- >
- <Button icon={<UploadOutlined />} disabled={videoUploadStatus.isUploading}>
- 选择视频文件
- </Button>
- </Upload>
- </div>
- )}
- {/* 视频上传进度区域 */}
- {videoFile && (
- <div className={styles['upload-progress-section']}>
- <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
- <div
- style={{ width: 104, height: 104, background: '#fafafa', border: '1px dashed #d9d9d9', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', position: 'relative' }}
- onMouseEnter={() => setIsVideoHovering(true)}
- onMouseLeave={() => setIsVideoHovering(false)}
- >
- <video src={videoFile.localUrl} style={{ width: '100%', height: '100%', objectFit: 'cover' }} muted />
- {!videoUploadStatus.isUploading && isVideoHovering && (
- <div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.35)' }}>
- <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, display: 'flex', justifyContent: 'space-between', padding: 8 }}>
- <Button size="small" type="primary" onClick={() => setVideoPreviewOpen(true)}>
- 预览
- </Button>
- <Button size="small" danger onClick={handleVideoRemove}>
- 删除
- </Button>
- </div>
- </div>
- )}
- </div>
- <div style={{ flex: 1 }}>
- <div className={styles['progress-info']}>
- <span className={styles['file-name']}>{videoFile.name}</span>
- </div>
- <Progress
- percent={videoUploadProgress}
- status={videoUploadStatus.isError ? 'exception' : 'active'}
- strokeColor={videoUploadStatus.isError ? '#F2584F' : '#FF4383'}
- />
- {!isEditMode ? <div className={styles['progress-text']}>{getVideoProgressText()}</div> : null}
- </div>
- </div>
- </div>
- )}
- {/* 封面上传区域 */}
- <div className={styles['upload-section']}>
- <h4>封面图片</h4>
- <Upload
- action={adFileUpload}
- headers={{
- token: getAccessToken()
- }}
- accept="image/*"
- listType="picture-card"
- beforeUpload={checkCoverFile}
- onChange={handleCoverUploadChange}
- fileList={coverFileList}
- showUploadList={{ showPreviewIcon: false }}
- maxCount={1}
- data={{ fileType: 'PICTURE' }}
- onRemove={handleCoverRemove}
- >
- {coverFileList.length >= 1 ? null : (
- <button style={{ border: 0, background: 'none' }} type="button">
- <PlusOutlined />
- <div style={{ marginTop: 8 }}>
- {coverUploadStatus.isUploaded ? '已选择封面' : '上传封面'}
- </div>
- </button>
- )}
- </Upload>
-
- {!isEditMode && getCoverProgressText() && (
- <div className={styles['progress-text']}>{getCoverProgressText()}</div>
- )}
- </div>
- {/* 视频信息编辑区域 */}
- <div className={styles['video-info-section']}>
- <Form form={form} layout="vertical">
- <Form.Item
- label="视频标题"
- name="title"
- rules={[{ required: true, message: '请输入视频标题' }]}
- >
- <Input placeholder="请输入视频标题" />
- </Form.Item>
- </Form>
- </div>
- {/* 操作按钮区域 */}
- <div className={styles['action-section']}>
- <div className={styles['right-actions']}>
- <Space>
- {/* 视频上传操作 */}
- {videoFile && (
- <>
- {videoUploadStatus.isUploading && (
- <Button onClick={cancelVideoUpload}>
- 取消视频上传
- </Button>
- )}
-
- {videoUploadStatus.isError && (
- <Button
- type="primary"
- danger
- icon={<ReloadOutlined />}
- onClick={handleVideoRetry}
- className={styles['retry-btn']}
- >
- 重试视频
- </Button>
- )}
- {!videoUploadStatus.isUploading && videoUploadStatus.isUploaded && (
- <Button
- danger
- onClick={handleVideoRemove}
- >
- 删除已上传视频
- </Button>
- )}
- </>
- )}
- {/* 发布按钮 */}
- {videoUploadStatus.isUploaded && (
- <Button
- type="primary"
- onClick={publishVideo}
- loading={publishLoading}
- >
- 发布视频
- </Button>
- )}
- </Space>
- </div>
- </div>
- </div>
- </Modal>
- {/* 视频预览弹窗 */}
- <Modal
- open={videoPreviewOpen}
- onCancel={() => setVideoPreviewOpen(false)}
- footer={null}
- width={720}
- title="预览视频"
- destroyOnHidden
- >
- <video src={videoFile?.localUrl} style={{ height: '100%', margin: '0 auto' }} controls />
- </Modal>
- </>
- );
- };
- export default UploadVideoModal;
|