|
|
@@ -359,6 +359,37 @@
|
|
|
<view class="progress-fill" :style="{ width: currentBook.progress + '%' }"></view>
|
|
|
</view>
|
|
|
|
|
|
+ <!-- 实时生成状态面板 -->
|
|
|
+ <view v-if="currentBook.status === 'generating'" class="generation-status">
|
|
|
+ <view class="status-header">
|
|
|
+ <text class="status-icon">⚡</text>
|
|
|
+ <text class="status-title">正在生成中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 当前阶段 -->
|
|
|
+ <view class="status-stage">
|
|
|
+ <text class="stage-label">当前阶段:</text>
|
|
|
+ <text class="stage-value">{{ getCurrentStage() }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 错误信息(如果有) -->
|
|
|
+ <view v-if="currentBook.error" class="status-error">
|
|
|
+ <text class="error-icon">⚠️</text>
|
|
|
+ <text class="error-text">{{ currentBook.error }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 自动恢复提示 -->
|
|
|
+ <view v-if="currentBook.error && currentBook.error.includes('自动恢复')" class="status-recovery">
|
|
|
+ <text class="recovery-icon">🔄</text>
|
|
|
+ <text class="recovery-text">系统正在尝试自动恢复...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 提示 -->
|
|
|
+ <view class="status-tip">
|
|
|
+ <text class="tip-text">💡 生成过程可能需要几分钟,请耐心等待</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
<!-- 实时配额显示 -->
|
|
|
<view v-if="currentBook.status === 'generating'" class="quota-monitor">
|
|
|
<text class="quota-monitor-title">🎧 额度监控</text>
|
|
|
@@ -377,6 +408,15 @@
|
|
|
<text class="interrupted-text">生成已中断:额度不足,已保存当前进度</text>
|
|
|
<text class="interrupted-hint">可升级套餐后继续生成</text>
|
|
|
</view>
|
|
|
+
|
|
|
+ <!-- 失败提示(非额度原因) -->
|
|
|
+ <view v-if="currentBook.status === 'failed' && !currentBook?.error?.includes('额度')" class="failed-tip">
|
|
|
+ <text class="failed-icon">❌</text>
|
|
|
+ <text class="failed-text">生成失败</text>
|
|
|
+ <text v-if="currentBook.error" class="failed-error">{{ currentBook.error }}</text>
|
|
|
+ <text class="failed-hint">请检查网络或稍后重试</text>
|
|
|
+ <button class="retry-btn" @click="handleRetryGenerate">🔄 重新生成</button>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 完整内容区(整合大纲+实际内容) -->
|
|
|
@@ -399,10 +439,10 @@
|
|
|
|
|
|
<!-- 章节列表(树形结构,整合大纲信息) -->
|
|
|
<view class="content-tree-list">
|
|
|
- <!-- 章 -->
|
|
|
+ <!-- 章 (只遍历level=1) -->
|
|
|
<view
|
|
|
- v-for="chapter in currentBook.chapters"
|
|
|
- :key="'ch-' + chapter.id"
|
|
|
+ v-for="chapter in chaptersLevel1"
|
|
|
+ :key="chapter.id"
|
|
|
class="content-tree-item content-chapter"
|
|
|
>
|
|
|
<view class="content-tree-row chapter-row">
|
|
|
@@ -429,57 +469,63 @@
|
|
|
<text v-if="chapter.videoUrl" class="media-badge">🎬 视频</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
- <view :class="['chapter-status', chapter.status === 'completed' ? 'done' : 'pending']">
|
|
|
- {{ chapter.status === 'completed' ? '✓' : '○' }}
|
|
|
+ <!-- 章状态 -->
|
|
|
+ <view :class="['chapter-status', getNodeStatusClass(chapter)]">
|
|
|
+ <text class="status-icon">{{ getNodeStatusIcon(chapter) }}</text>
|
|
|
+ <text class="status-text">{{ getNodeStatusText(chapter) }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 节(从大纲获取) -->
|
|
|
- <view v-if="getChapterSections(chapter.number) && getChapterSections(chapter.number).length > 0" class="content-sections-list">
|
|
|
+ <!-- 节(从数据库获取) -->
|
|
|
+ <view v-if="getChapterSectionsFromDB(chapter.id) && getChapterSectionsFromDB(chapter.id).length > 0" class="content-sections-list">
|
|
|
<view
|
|
|
- v-for="(section, sIdx) in getChapterSections(chapter.number)"
|
|
|
- :key="'sec-' + chapter.number + '-' + sIdx"
|
|
|
+ v-for="section in getChapterSectionsFromDB(chapter.id)"
|
|
|
+ :key="'sec-' + section.id"
|
|
|
class="content-tree-item content-section"
|
|
|
>
|
|
|
<view class="content-tree-row section-row">
|
|
|
- <view class="section-num-badge">{{ chapter.number }}.{{ sIdx + 1 }}</view>
|
|
|
+ <view class="section-num-badge">{{ section.number }}</view>
|
|
|
<view class="section-info">
|
|
|
<text class="section-title">{{ section.title }}</text>
|
|
|
<text v-if="section.summary" class="section-summary">{{ section.summary }}</text>
|
|
|
</view>
|
|
|
+ <!-- 节状态 -->
|
|
|
+ <view :class="['section-status', getNodeStatusClass(section)]">
|
|
|
+ <text class="status-icon">{{ getNodeStatusIcon(section) }}</text>
|
|
|
+ <text class="status-text">{{ getNodeStatusText(section) }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 小节(subsections) - 叶子节点,添加点击链接 -->
|
|
|
- <view v-if="section.subsections && section.subsections.length > 0" class="content-subsections-list">
|
|
|
+ <!-- 小节(从数据库获取) -->
|
|
|
+ <view v-if="getSectionSubsectionsFromDB(section.id) && getSectionSubsectionsFromDB(section.id).length > 0" class="content-subsections-list">
|
|
|
<view
|
|
|
- v-for="(sub, subIdx) in section.subsections"
|
|
|
- :key="'sub-' + chapter.number + '-' + sIdx + '-' + subIdx"
|
|
|
+ v-for="subsection in getSectionSubsectionsFromDB(section.id)"
|
|
|
+ :key="'sub-' + subsection.id"
|
|
|
class="content-tree-item content-subsection"
|
|
|
- @click="goToChapterDetail(chapter)"
|
|
|
+ @click="goToChapterDetail(subsection)"
|
|
|
>
|
|
|
<view class="content-tree-row subsection-row">
|
|
|
- <view class="subsection-num-badge">{{ chapter.number }}.{{ sIdx + 1 }}.{{ subIdx + 1 }}</view>
|
|
|
+ <view class="subsection-num-badge">{{ subsection.number }}</view>
|
|
|
<view class="subsection-info">
|
|
|
- <text class="subsection-title">{{ sub.title }}</text>
|
|
|
+ <text class="subsection-title">{{ subsection.title }}</text>
|
|
|
+ <!-- 媒体标识 -->
|
|
|
+ <view class="subsection-media-badges">
|
|
|
+ <text v-if="subsection.audioUrl" class="media-badge-small">🎵 音频</text>
|
|
|
+ <text v-if="subsection.videoUrl" class="media-badge-small">🎬 视频</text>
|
|
|
+ </view>
|
|
|
+ <!-- 内容预览 -->
|
|
|
+ <text v-if="subsection.content" class="subsection-content-preview">
|
|
|
+ {{ subsection.content.substring(0, 50) }}...
|
|
|
+ </text>
|
|
|
</view>
|
|
|
- <view class="content-item-right">
|
|
|
- <text class="expand-icon">→</text>
|
|
|
+ <!-- 小节状态 -->
|
|
|
+ <view :class="['subsection-status', getNodeStatusClass(subsection)]">
|
|
|
+ <text class="status-icon">{{ getNodeStatusIcon(subsection) }}</text>
|
|
|
+ <text class="status-text">{{ getNodeStatusText(subsection) }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
-
|
|
|
- <!-- 如果节没有小节,节本身就是叶子节点,添加点击链接 -->
|
|
|
- <view v-else class="content-tree-item content-subsection" @click="goToChapterDetail(chapter)">
|
|
|
- <view class="content-tree-row subsection-row">
|
|
|
- <view class="subsection-info" style="margin-left: 56rpx;">
|
|
|
- <text class="subsection-title">{{ section.title }}</text>
|
|
|
- </view>
|
|
|
- <view class="content-item-right">
|
|
|
- <text class="expand-icon">→</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -551,6 +597,7 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
|
|
import { onShow } from '@dcloudio/uni-app';
|
|
|
import * as api from '../../utils/book-generator-api';
|
|
|
import type { Book, BookOutline, Chapter } from '../../utils/book-generator-api';
|
|
|
+import { wsService } from '../../utils/websocket';
|
|
|
|
|
|
// 使用相对路径(Nginx 反向代理)
|
|
|
const BASE_URL = '/api';
|
|
|
@@ -775,6 +822,12 @@ const completedChapters = computed(() => {
|
|
|
return currentBook.value.chapters.filter((c) => c.status === 'completed').length;
|
|
|
});
|
|
|
|
|
|
+// 过滤出level=1的章(避免在模板中使用filter导致重复渲染)
|
|
|
+const chaptersLevel1 = computed(() => {
|
|
|
+ if (!currentBook.value?.chapters) return [];
|
|
|
+ return currentBook.value.chapters.filter((c) => c.level === 1);
|
|
|
+});
|
|
|
+
|
|
|
// 计算已使用的音频时长(基于已完成章节的字数)
|
|
|
const usedAudioMinutes = computed(() => {
|
|
|
if (!currentBook.value) return 0;
|
|
|
@@ -913,6 +966,119 @@ function getStatusText(status: string): string {
|
|
|
return map[status] || status;
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 获取当前生成阶段
|
|
|
+ */
|
|
|
+function getCurrentStage(): string {
|
|
|
+ if (!currentBook.value) return '未知';
|
|
|
+
|
|
|
+ const progress = currentBook.value.progress || 0;
|
|
|
+ const error = currentBook.value.error || '';
|
|
|
+
|
|
|
+ // 如果有错误,优先显示
|
|
|
+ if (error) {
|
|
|
+ if (error.includes('AI调用')) return 'AI调用失败,正在重试...';
|
|
|
+ if (error.includes('超时')) return '节点执行超时,正在恢复...';
|
|
|
+ if (error.includes('自动恢复')) return '系统正在自动恢复...';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据进度判断阶段
|
|
|
+ if (progress === 0) return '准备中...';
|
|
|
+ if (progress <= 10) return '正在生成章大纲...';
|
|
|
+ if (progress <= 20) return '正在生成节大纲...';
|
|
|
+ if (progress <= 30) return '正在生成小节大纲...';
|
|
|
+ if (progress <= 90) return '正在生成章节内容...';
|
|
|
+ if (progress <= 95) return '正在生成前言...';
|
|
|
+ if (progress < 100) return '正在生成后记...';
|
|
|
+ if (progress === 100) return '生成完成!';
|
|
|
+
|
|
|
+ return '生成中...';
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取节点状态样式类
|
|
|
+ */
|
|
|
+function getNodeStatusClass(node: any): string {
|
|
|
+ if (node.contentStatus === 'completed') return 'status-completed';
|
|
|
+ if (node.contentStatus === 'generating') return 'status-generating';
|
|
|
+ if (node.contentStatus === 'failed') return 'status-failed';
|
|
|
+ if (node.status === 'completed') return 'status-completed';
|
|
|
+ if (node.status === 'pending') return 'status-pending';
|
|
|
+ return 'status-pending';
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取节点状态图标
|
|
|
+ */
|
|
|
+function getNodeStatusIcon(node: any): string {
|
|
|
+ if (node.contentStatus === 'completed') return '✓';
|
|
|
+ if (node.contentStatus === 'generating') return '⚡';
|
|
|
+ if (node.contentStatus === 'failed') return '✗';
|
|
|
+ if (node.contentError) return '⚠';
|
|
|
+ if (node.status === 'completed') return '✓';
|
|
|
+ if (node.status === 'pending') return '○';
|
|
|
+ return '○';
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取节点状态文本
|
|
|
+ */
|
|
|
+function getNodeStatusText(node: any): string {
|
|
|
+ if (node.contentStatus === 'completed') return '已完成';
|
|
|
+ if (node.contentStatus === 'generating') return '生成中';
|
|
|
+ if (node.contentStatus === 'failed') return '失败';
|
|
|
+ if (node.contentError) return '错误';
|
|
|
+ if (node.status === 'completed') return '大纲完成';
|
|
|
+ if (node.status === 'pending') return '待生成';
|
|
|
+ return '待生成';
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从数据库获取章的节列表(level=2)
|
|
|
+ */
|
|
|
+function getChapterSectionsFromDB(chapterId: string | number) {
|
|
|
+ if (!currentBook.value?.chapters) return [];
|
|
|
+ const chapterIdNum = typeof chapterId === 'string' ? parseInt(chapterId) : chapterId;
|
|
|
+ const result = currentBook.value.chapters.filter((c: any) => {
|
|
|
+ const cParentId = typeof c.parentId === 'string' ? parseInt(c.parentId) : c.parentId;
|
|
|
+ return cParentId === chapterIdNum && c.level === 2;
|
|
|
+ });
|
|
|
+ console.log(`[getChapterSectionsFromDB] chapterId=${chapterId}, 找到${result.length}个节`);
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从数据库获取节的小节列表(level=3)
|
|
|
+ */
|
|
|
+function getSectionSubsectionsFromDB(sectionId: string | number) {
|
|
|
+ if (!currentBook.value?.chapters) return [];
|
|
|
+ const sectionIdNum = typeof sectionId === 'string' ? parseInt(sectionId) : sectionId;
|
|
|
+ const result = currentBook.value.chapters.filter((c: any) => {
|
|
|
+ const cParentId = typeof c.parentId === 'string' ? parseInt(c.parentId) : c.parentId;
|
|
|
+ return cParentId === sectionIdNum && c.level === 3;
|
|
|
+ });
|
|
|
+ console.log(`[getSectionSubsectionsFromDB] sectionId=${sectionId}, 找到${result.length}个小节`);
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 重新生成
|
|
|
+ */
|
|
|
+async function handleRetryGenerate() {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ uni.showModal({
|
|
|
+ title: '确认重新生成',
|
|
|
+ content: '将重新开始生成,是否继续?',
|
|
|
+ success: async (res) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ // 调用LangGraph生成
|
|
|
+ await handleLangGraphGenerate();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
function getChapterStatus(chapterNum: number): string {
|
|
|
const chapter = currentBook.value?.chapters.find((c) => c.number === chapterNum);
|
|
|
if (!chapter) return 'pending';
|
|
|
@@ -1511,6 +1677,36 @@ function goToPublishBook(book: Book) {
|
|
|
// 页面加载
|
|
|
onMounted(() => {
|
|
|
loadBooks();
|
|
|
+
|
|
|
+ // 连接WebSocket接收实时通知
|
|
|
+ try {
|
|
|
+ const token = uni.getStorageSync('token');
|
|
|
+ if (token) {
|
|
|
+ wsService.connect(token);
|
|
|
+
|
|
|
+ // 监听书籍生成通知
|
|
|
+ wsService.on('book_generation_notification', (data: any) => {
|
|
|
+ console.log('[书籍生成通知]', data);
|
|
|
+
|
|
|
+ // 如果是当前正在查看的书籍,更新显示
|
|
|
+ if (currentBook.value && data.bookId === currentBook.value.id.toString()) {
|
|
|
+ // 显示通知消息
|
|
|
+ if (data.message) {
|
|
|
+ uni.showToast({
|
|
|
+ title: data.message,
|
|
|
+ icon: 'none',
|
|
|
+ duration: 3000
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 刷新书籍信息
|
|
|
+ openBook(currentBook.value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[WebSocket] 初始化失败:', error);
|
|
|
+ }
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -2217,6 +2413,295 @@ onMounted(() => {
|
|
|
border-radius: 6rpx;
|
|
|
}
|
|
|
|
|
|
+/* 实时生成状态面板 */
|
|
|
+.generation-status {
|
|
|
+ margin-top: 24rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ border-radius: 16rpx;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.status-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.status-icon {
|
|
|
+ font-size: 36rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+ animation: pulse 2s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.5; }
|
|
|
+}
|
|
|
+
|
|
|
+.status-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.status-stage {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ border-radius: 12rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.stage-label {
|
|
|
+ font-size: 26rpx;
|
|
|
+ opacity: 0.9;
|
|
|
+ margin-right: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.stage-value {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.status-error {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 16rpx;
|
|
|
+ background: rgba(239, 68, 68, 0.3);
|
|
|
+ border-radius: 12rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.error-icon {
|
|
|
+ font-size: 28rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.error-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 1.6;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.status-recovery {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16rpx;
|
|
|
+ background: rgba(251, 191, 36, 0.3);
|
|
|
+ border-radius: 12rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.recovery-icon {
|
|
|
+ font-size: 32rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ from { transform: rotate(0deg); }
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+.recovery-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.status-tip {
|
|
|
+ padding: 12rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+ border-radius: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.tip-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ opacity: 0.9;
|
|
|
+}
|
|
|
+
|
|
|
+/* 失败提示 */
|
|
|
+.failed-tip {
|
|
|
+ margin-top: 24rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
|
|
+ border-radius: 16rpx;
|
|
|
+ border: 2rpx solid #fca5a5;
|
|
|
+}
|
|
|
+
|
|
|
+.failed-icon {
|
|
|
+ font-size: 40rpx;
|
|
|
+ display: block;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.failed-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #dc2626;
|
|
|
+ display: block;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.failed-error {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #991b1b;
|
|
|
+ display: block;
|
|
|
+ padding: 16rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.5);
|
|
|
+ border-radius: 8rpx;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
+.failed-hint {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ display: block;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.retry-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 72rpx;
|
|
|
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-status {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: 80rpx;
|
|
|
+ padding: 8rpx 16rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-status.status-completed {
|
|
|
+ background: #d1fae5;
|
|
|
+ color: #059669;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-status.status-pending {
|
|
|
+ background: #e5e7eb;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-status.status-generating {
|
|
|
+ background: #fef3c7;
|
|
|
+ color: #d97706;
|
|
|
+ animation: pulse-bg 2s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-status.status-failed {
|
|
|
+ background: #fee2e2;
|
|
|
+ color: #dc2626;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse-bg {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.7; }
|
|
|
+}
|
|
|
+
|
|
|
+.status-icon {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.status-text {
|
|
|
+ font-size: 20rpx;
|
|
|
+ margin-top: 4rpx;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* 节状态 */
|
|
|
+.section-status {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: 70rpx;
|
|
|
+ padding: 6rpx 12rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.section-status.status-completed {
|
|
|
+ background: #d1fae5;
|
|
|
+ color: #059669;
|
|
|
+}
|
|
|
+
|
|
|
+.section-status.status-pending {
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.section-status.status-generating {
|
|
|
+ background: #fef3c7;
|
|
|
+ color: #d97706;
|
|
|
+ animation: pulse-bg 2s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.section-status.status-failed {
|
|
|
+ background: #fee2e2;
|
|
|
+ color: #dc2626;
|
|
|
+}
|
|
|
+
|
|
|
+/* 小节状态 */
|
|
|
+.subsection-status {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: 70rpx;
|
|
|
+ padding: 6rpx 12rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.subsection-status.status-completed {
|
|
|
+ background: #d1fae5;
|
|
|
+ color: #059669;
|
|
|
+}
|
|
|
+
|
|
|
+.subsection-status.status-pending {
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.subsection-status.status-generating {
|
|
|
+ background: #fef3c7;
|
|
|
+ color: #d97706;
|
|
|
+ animation: pulse-bg 2s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.subsection-status.status-failed {
|
|
|
+ background: #fee2e2;
|
|
|
+ color: #dc2626;
|
|
|
+}
|
|
|
+
|
|
|
+/* 内容预览 */
|
|
|
+.subsection-content-preview {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ margin-top: 4rpx;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
/* 大纲卡片 */
|
|
|
.outline-theme {
|
|
|
font-size: 24rpx;
|
|
|
@@ -2773,6 +3258,10 @@ onMounted(() => {
|
|
|
.chapter-row {
|
|
|
width: 100%;
|
|
|
cursor: pointer;
|
|
|
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
|
+ padding: 16rpx;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ border-left: 4rpx solid #3b82f6;
|
|
|
}
|
|
|
|
|
|
.chapter-info {
|
|
|
@@ -2839,11 +3328,27 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
/* 节列表 */
|
|
|
+
|
|
|
+/* 小节媒体标识 */
|
|
|
+.subsection-media-badges {
|
|
|
+ display: flex;
|
|
|
+ gap: 8rpx;
|
|
|
+ margin-top: 6rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.media-badge-small {
|
|
|
+ font-size: 18rpx;
|
|
|
+ padding: 2rpx 8rpx;
|
|
|
+ border-radius: 4rpx;
|
|
|
+ background: rgba(79, 70, 229, 0.1);
|
|
|
+ color: #4f46e5;
|
|
|
+}
|
|
|
.content-sections-list {
|
|
|
- margin-left: 64rpx;
|
|
|
+ margin-left: 40rpx;
|
|
|
margin-top: 12rpx;
|
|
|
- padding-left: 20rpx;
|
|
|
- border-left: 2rpx solid #e5e7eb;
|
|
|
+ padding-left: 24rpx;
|
|
|
+ border-left: 3rpx solid #e5e7eb;
|
|
|
+ background: linear-gradient(to right, rgba(243, 244, 246, 0.3), transparent);
|
|
|
}
|
|
|
|
|
|
.content-section {
|
|
|
@@ -2853,6 +3358,10 @@ onMounted(() => {
|
|
|
|
|
|
.section-row {
|
|
|
width: 100%;
|
|
|
+ background: #fafafa;
|
|
|
+ padding: 12rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ border-left: 3rpx solid #10b981;
|
|
|
}
|
|
|
|
|
|
.section-num-badge {
|
|
|
@@ -2889,8 +3398,11 @@ onMounted(() => {
|
|
|
|
|
|
/* 小节列表 */
|
|
|
.content-subsections-list {
|
|
|
- margin-left: 56rpx;
|
|
|
+ margin-left: 32rpx;
|
|
|
margin-top: 8rpx;
|
|
|
+ padding-left: 20rpx;
|
|
|
+ border-left: 2rpx dashed #d1d5db;
|
|
|
+ background: linear-gradient(to right, rgba(209, 213, 219, 0.2), transparent);
|
|
|
}
|
|
|
|
|
|
.content-subsection {
|
|
|
@@ -2900,6 +3412,15 @@ onMounted(() => {
|
|
|
|
|
|
.subsection-row {
|
|
|
width: 100%;
|
|
|
+ padding: 10rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ border-left: 2rpx solid #8b5cf6;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.subsection-row:active {
|
|
|
+ background: #f3f4f6;
|
|
|
+ transform: translateX(4rpx);
|
|
|
}
|
|
|
|
|
|
.subsection-num-badge {
|
|
|
@@ -3206,3 +3727,4 @@ onMounted(() => {
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
+
|