123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589 |
- import React, { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
- import { Button, Nav, Modal, Form, Dropdown, Row, Col, Container, Card, ListGroup, Badge, Table, Alert } from 'react-bootstrap';
- import { useHistory } from 'react-router-dom';
- import './home.css'
- import { toast } from 'react-toastify';
- import VideoDownload from '../../components/videoDownload';
- import SubtitleUpload from '../../components/subtitleUpload';
- import AudioUpload from '../../components/audioUpload';
- import VideoUpload from '../../components/videoUpload';
- import { bookService, bookInfoService, getDbPath } from '../../db';
- import CozeApiSettings from '../../components/CozeApiSettings';
- import { hasValidToken } from '../../utils/cozeConfig';
- const Home = forwardRef((props, ref) => {
- const [showProject, setShowProject] = useState(false);
- const [projects, setProjects] = useState([]);
- const [loading, setLoading] = useState(false);
- const [showApiSettings, setShowApiSettings] = useState(false);
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [projectToDelete, setProjectToDelete] = useState(null);
- const [showAudioPlayer, setShowAudioPlayer] = useState(false);
- const [currentAudio, setCurrentAudio] = useState(null);
- const [projectAudioFile, setProjectAudioFile] = useState(null);
- const [projectVideoFile, setProjectVideoFile] = useState(null);
- const history = useHistory();
- const handleAddProject = () => {
- setShowProject(true);
- };
- // 处理API设置
- const handleOpenApiSettings = () => {
- setShowApiSettings(true);
- };
- const handleCloseApiSettings = () => {
- setShowApiSettings(false);
- };
- // 加载项目列表
- const loadProjects = async () => {
- try {
- setLoading(true);
- const books = await bookService.getAllBooks();
- setProjects(books);
- } catch (error) {
- console.error('加载项目列表失败:', error);
- toast.error('加载项目列表失败');
- } finally {
- setLoading(false);
- }
- };
- // 查看项目详情
- const viewProjectDetail = (projectId) => {
- history.push(`/project/${projectId}`);
- };
- // 下载SRT字幕
- const downloadSubtitleSRT = async (projectId, projectTitle) => {
- try {
- setLoading(true);
- // 获取项目详情
- const segments = await bookInfoService.getBookInfoByBookId(projectId);
-
- if (!segments || segments.length === 0) {
- toast.error('该项目没有字幕内容可下载');
- return;
- }
-
- // 生成SRT格式内容
- const srtContent = segments
- .sort((a, b) => a.segment_id - b.segment_id)
- .map((segment) => {
- return `${segment.segment_id}\n${segment.start_time} --> ${segment.end_time}\n${segment.text}\n`;
- })
- .join('\n');
-
- // 创建Blob对象
- const blob = new Blob([srtContent], { type: 'text/plain;charset=utf-8' });
-
- // 创建下载链接
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = `${projectTitle || '项目'}.srt`;
-
- // 触发下载
- document.body.appendChild(link);
- link.click();
-
- // 清理
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
-
- toast.success('字幕下载成功');
- } catch (error) {
- console.error('下载字幕失败:', error);
- toast.error(`下载字幕失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 下载纯文本文案
- const downloadPlainText = async (projectId, projectTitle) => {
- try {
- setLoading(true);
- // 获取项目详情
- const segments = await bookInfoService.getBookInfoByBookId(projectId);
-
- if (!segments || segments.length === 0) {
- toast.error('该项目没有文本内容可下载');
- return;
- }
-
- // 提取所有纯文本内容(不包含时间信息)
- const textContent = segments
- .sort((a, b) => a.segment_id - b.segment_id)
- .map(segment => segment.text)
- .join('\n\n');
-
- // 创建Blob对象
- const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8' });
-
- // 创建下载链接
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = `${projectTitle || '项目'}_文案.txt`;
-
- // 触发下载
- document.body.appendChild(link);
- link.click();
-
- // 清理
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
-
- toast.success('文案下载成功');
- } catch (error) {
- console.error('下载文案失败:', error);
- toast.error(`下载文案失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 复制文案到剪贴板
- const copyProjectText = async (projectId, projectTitle) => {
- try {
- setLoading(true);
- // 获取项目详情
- const segments = await bookInfoService.getBookInfoByBookId(projectId);
-
- if (!segments || segments.length === 0) {
- toast.error('该项目没有文本内容可复制');
- return;
- }
-
- // 提取所有纯文本内容(不包含时间信息)
- const textContent = segments
- .sort((a, b) => a.segment_id - b.segment_id)
- .map(segment => segment.text)
- .join('\n\n');
-
- // 复制到剪贴板
- await navigator.clipboard.writeText(textContent);
-
- toast.success('文案已复制到剪贴板');
- } catch (error) {
- console.error('复制文案失败:', error);
- toast.error(`复制文案失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 返回主页
- const backToHome = () => {
- setShowProject(false);
- loadProjects(); // 刷新项目列表
- };
- // 格式化时间
- const formatTime = (timeString) => {
- return timeString.replace(',', '.');
- };
- // 计算持续时间(秒)
- const calculateDuration = (startTime, endTime) => {
- const parseTime = (time) => {
- const [hours, minutes, seconds] = time.replace(',', '.').split(':').map(parseFloat);
- return hours * 3600 + minutes * 60 + seconds;
- };
- const startSeconds = parseTime(startTime);
- const endSeconds = parseTime(endTime);
- return (endSeconds - startSeconds).toFixed(2);
- };
- // 打开数据库路径
- const openDatabasePath = async () => {
- try {
- if (window.electron && window.electron.files) {
- // 获取数据库路径
- const dbPath = await getDbPath();
- if (!dbPath) {
- toast.error('无法获取数据库路径');
- return;
- }
-
- console.log('数据库路径:', dbPath);
-
- // 获取数据库所在目录
- let dbDir = dbPath;
- const lastSlashIndex = Math.max(dbPath.lastIndexOf('/'), dbPath.lastIndexOf('\\'));
- if (lastSlashIndex !== -1) {
- dbDir = dbPath.substring(0, lastSlashIndex);
- }
-
- console.log('数据库目录:', dbDir);
-
- // 打开文件夹
- window.electron.files.showItemInFolder(dbDir);
- toast.success('已打开数据库所在文件夹');
- } else {
- toast.info('此功能仅在桌面应用中可用');
- }
- } catch (error) {
- console.error('打开数据库路径失败:', error);
- toast.error(`打开数据库路径失败: ${error.message}`);
- }
- };
- // 打开资源文件夹
- const openResourcesPath = async () => {
- try {
- if (window.electron && window.electron.ipcRenderer) {
- // 获取资源目录路径
- const result = await window.electron.ipcRenderer.invoke('get-resources-path');
- if (!result.success) {
- toast.error(`无法获取资源目录路径: ${result.error}`);
- return;
- }
-
- console.log('资源目录路径:', result.path);
-
- // 打开文件夹
- window.electron.files.showItemInFolder(result.path);
- toast.success('已打开资源文件夹');
- } else {
- toast.info('此功能仅在桌面应用中可用');
- }
- } catch (error) {
- console.error('打开资源文件夹失败:', error);
- toast.error(`打开资源文件夹失败: ${error.message}`);
- }
- };
- // 删除项目
- const handleDeleteProject = async (project) => {
- setProjectToDelete(project);
- setShowDeleteConfirm(true);
- };
- // 确认删除项目
- const confirmDeleteProject = async () => {
- if (!projectToDelete) return;
-
- try {
- setLoading(true);
- await bookService.deleteBook(projectToDelete.id);
- toast.success(`项目 "${projectToDelete.title}" 已删除`);
- setShowDeleteConfirm(false);
- setProjectToDelete(null);
- await loadProjects(); // 刷新项目列表
- } catch (error) {
- console.error('删除项目失败:', error);
- toast.error(`删除项目失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 取消删除
- const cancelDeleteProject = () => {
- setShowDeleteConfirm(false);
- setProjectToDelete(null);
- };
- // 播放音频
- const playAudio = (projectId, audioFileName) => {
- // 由于实际开发中,音频文件可能存储在特定位置,这里仅为示例,使用音频文件名
- // 实际应用中可能需要通过其他方式获取真实的音频文件路径
- setCurrentAudio({
- projectId,
- fileName: audioFileName
- });
- setShowAudioPlayer(true);
- };
- // 关闭音频播放器
- const closeAudioPlayer = () => {
- setShowAudioPlayer(false);
- setCurrentAudio(null);
- };
- // 组件挂载时加载项目列表
- useEffect(() => {
- loadProjects();
- }, []);
- return (
- <>
- <div className='home-container'>
- <Container>
- {!showProject ? (
- <>
- <div className="header-container">
- <div className="d-flex justify-content-between align-items-center">
- <Button
- variant="primary"
- onClick={handleAddProject}
- className="add-project-btn"
- >
- 添加项目
- </Button>
-
- <Button
- variant={hasValidToken() ? "outline-success" : "outline-warning"}
- onClick={handleOpenApiSettings}
- className="api-settings-btn"
- >
- {hasValidToken() ? "Coze API设置 ✓" : "配置Coze API"}
- </Button>
- </div>
- </div>
-
- {!hasValidToken() && (
- <Alert variant="warning" className="mt-3">
- <Alert.Heading>请配置Coze API Token</Alert.Heading>
- <p>
- 您尚未配置Coze API Token,文本转描述词功能将无法使用。
- 请点击右上角的"配置Coze API"按钮进行设置。
- </p>
- </Alert>
- )}
-
- <div className="welcome-container">
- {projects.length > 0 ? (
- <div className="project-list-container">
- <h4 className="mb-3">项目列表</h4>
-
- <div className="mb-3 d-flex gap-2">
- <Button
- variant="outline-secondary"
- size="sm"
- onClick={openDatabasePath}
- >
- <i className="bi bi-folder"></i> 打开数据库文件夹
- </Button>
-
- <Button
- variant="outline-secondary"
- size="sm"
- onClick={openResourcesPath}
- >
- <i className="bi bi-folder"></i> 打开资源文件夹
- </Button>
- </div>
-
- <div className="table-responsive">
- <Table striped hover className="project-table">
- <thead>
- <tr>
- <th>项目名称</th>
- <th>创建时间</th>
- <th>字幕文件</th>
- <th>音频文件</th>
- <th>视频文件</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
- {projects.map((project) => (
- <tr key={project.id}>
- <td className="project-title-cell">{project.title}</td>
- <td>{new Date(project.created_at).toLocaleString()}</td>
- <td>
- {project.subtitle_path ? (
- <Badge variant="info">{project.subtitle_path}</Badge>
- ) : (
- <span className="text-muted">无</span>
- )}
- </td>
- <td>
- {project.audio_path ? (
- <div className="file-path-badge" title={project.audio_path}>
- <Badge bg="success">
- {project.audio_path.length > 15
- ? project.audio_path.substring(0, 6) + '...' + project.audio_path.substring(project.audio_path.length - 6)
- : project.audio_path}
- </Badge>
- </div>
- ) : (
- <span className="text-muted">无</span>
- )}
- </td>
- <td>
- {project.video_path ? (
- <div className="file-path-badge" title={project.video_path}>
- <Badge bg="primary">
- {project.video_path.length > 15
- ? project.video_path.substring(0, 6) + '...' + project.video_path.substring(project.video_path.length - 6)
- : project.video_path}
- </Badge>
- </div>
- ) : (
- <span className="text-muted">无</span>
- )}
- </td>
- <td className="action-buttons">
- <Button
- variant="outline-primary"
- size="sm"
- onClick={() => viewProjectDetail(project.id)}
- >
- 查看详情
- </Button>
- <Button
- variant="outline-info"
- size="sm"
- onClick={() => downloadSubtitleSRT(project.id, project.title)}
- >
- 下载字幕
- </Button>
- <Button
- variant="outline-warning"
- size="sm"
- onClick={() => copyProjectText(project.id, project.title)}
- >
- 复制文案
- </Button>
- {project.audio_path && (
- <Button
- variant="outline-info"
- size="sm"
- onClick={() => playAudio(project.id, project.audio_path)}
- >
- 播放音频
- </Button>
- )}
- <Button
- variant="outline-danger"
- size="sm"
- onClick={() => handleDeleteProject(project)}
- >
- 删除
- </Button>
- </td>
- </tr>
- ))}
- </tbody>
- </Table>
- </div>
- </div>
- ) : (
- <p className="text-center text-muted">点击左上角"添加项目"按钮开始创建新项目</p>
- )}
- </div>
- </>
- ) : (
- <>
- <div className="header-container">
- <Button
- variant="outline-secondary"
- onClick={backToHome}
- className="back-btn"
- >
- 返回
- </Button>
- </div>
- <AudioUpload onAudioSelected={(audioFile) => setProjectAudioFile(audioFile)} />
-
- <VideoUpload onVideoSelected={(videoFile) => setProjectVideoFile(videoFile)} />
- <SubtitleUpload
- onProjectCreated={backToHome}
- projectAudioFile={projectAudioFile}
- projectVideoFile={projectVideoFile}
- />
- </>
- )}
- </Container>
- </div>
-
- {/* API设置对话框 */}
- <Modal
- show={showApiSettings}
- onHide={handleCloseApiSettings}
- size="lg"
- centered
- >
- <Modal.Header closeButton>
- <Modal.Title>Coze API设置</Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <CozeApiSettings onSave={handleCloseApiSettings} />
- </Modal.Body>
- </Modal>
-
- {/* 删除确认对话框 */}
- <Modal
- show={showDeleteConfirm}
- onHide={cancelDeleteProject}
- centered
- >
- <Modal.Header closeButton>
- <Modal.Title>确认删除</Modal.Title>
- </Modal.Header>
- <Modal.Body>
- {projectToDelete && (
- <p>您确定要删除项目 "{projectToDelete.title}" 吗?此操作将同时删除所有相关的分镜数据,且不可恢复!</p>
- )}
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={cancelDeleteProject}>
- 取消
- </Button>
- <Button
- variant="danger"
- onClick={confirmDeleteProject}
- disabled={loading}
- >
- {loading ? '删除中...' : '确认删除'}
- </Button>
- </Modal.Footer>
- </Modal>
-
- {/* 音频播放器模态框 */}
- <Modal
- show={showAudioPlayer}
- onHide={closeAudioPlayer}
- centered
- >
- <Modal.Header closeButton>
- <Modal.Title>音频播放</Modal.Title>
- </Modal.Header>
- <Modal.Body>
- {currentAudio && (
- <div className="audio-player-container">
- <p>项目音频: {currentAudio.fileName}</p>
- <div className="audio-player">
- <audio
- controls
- autoPlay
- className="w-100"
- src={`${currentAudio.fileName}`} // 实际开发中需要提供真实的音频文件URL
- >
- 您的浏览器不支持音频播放
- </audio>
- </div>
- <div className="text-muted mt-2">
- <small>注意: 这里仅展示文件名,实际应用中需要处理真实的音频文件路径</small>
- </div>
- </div>
- )}
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={closeAudioPlayer}>
- 关闭
- </Button>
- </Modal.Footer>
- </Modal>
- </>
- );
- });
- export default Home;
|