|
|
@@ -0,0 +1,1825 @@
|
|
|
+<template>
|
|
|
+ <view class="page">
|
|
|
+ <!-- 顶部导航栏 -->
|
|
|
+ <view class="nav-bar">
|
|
|
+ <view class="nav-content">
|
|
|
+ <view class="nav-left" @click="goBack">
|
|
|
+ <text class="back-icon">←</text>
|
|
|
+ </view>
|
|
|
+ <text class="page-title">书籍生成</text>
|
|
|
+ <view class="nav-right">
|
|
|
+ <text class="nav-btn" @click="switchTab">📚</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 主内容区 -->
|
|
|
+ <view class="main-content">
|
|
|
+ <!-- 视图1:书籍列表 -->
|
|
|
+ <view v-if="currentView === 'list'" class="list-view">
|
|
|
+ <!-- 创建新书籍按钮 -->
|
|
|
+ <view class="create-card" @click="currentView = 'create'">
|
|
|
+ <text class="create-icon">+</text>
|
|
|
+ <text class="create-text">创建新书籍</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 视频生成入口 -->
|
|
|
+ <view class="video-nav-card" @click="goToVideoGenerator">
|
|
|
+ <view class="video-nav-content">
|
|
|
+ <text class="video-nav-icon">🎬</text>
|
|
|
+ <view class="video-nav-text">
|
|
|
+ <text class="video-nav-title">视频生成</text>
|
|
|
+ <text class="video-nav-desc">图片+音频生成精美视频</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text class="video-nav-arrow">→</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 书籍列表 -->
|
|
|
+ <view v-if="books.length > 0" class="book-list">
|
|
|
+ <view
|
|
|
+ v-for="book in books"
|
|
|
+ :key="book.id"
|
|
|
+ class="book-card"
|
|
|
+ >
|
|
|
+ <view class="book-header" @click="openBook(book)">
|
|
|
+ <text class="book-title">{{ book.title }}</text>
|
|
|
+ <view :class="['status-badge', book.status]">
|
|
|
+ {{ getStatusText(book.status) }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text class="book-desc" @click="openBook(book)">{{ book.description }}</text>
|
|
|
+ <view class="book-meta" @click="openBook(book)">
|
|
|
+ <text>{{ book.totalChapters }}章</text>
|
|
|
+ <text v-if="book.progress > 0">{{ book.progress }}%</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="book.progress > 0 && book.progress < 100" class="progress-bar" @click="openBook(book)">
|
|
|
+ <view class="progress-fill" :style="{ width: book.progress + '%' }"></view>
|
|
|
+ </view>
|
|
|
+ <!-- 批量生成音频按钮 -->
|
|
|
+ <view v-if="book.status === 'completed'" class="book-actions">
|
|
|
+ <button
|
|
|
+ class="audio-btn"
|
|
|
+ :disabled="generatingAudio"
|
|
|
+ @click.stop="handleGenerateAllAudio(book)"
|
|
|
+ >
|
|
|
+ {{ generatingAudio ? '生成中...' : '🎵 生成全部音频' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="video-btn"
|
|
|
+ :disabled="generatingVideo"
|
|
|
+ @click.stop="handleGenerateAllVideo(book)"
|
|
|
+ >
|
|
|
+ {{ generatingVideo ? '生成中...' : '🎬 生成全部视频' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <view v-else class="empty-state">
|
|
|
+ <text class="empty-icon">📖</text>
|
|
|
+ <text class="empty-text">暂无书籍</text>
|
|
|
+ <text class="empty-hint">点击上方按钮创建第一本书</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 视图2:创建书籍 -->
|
|
|
+ <view v-if="currentView === 'create'" class="create-view">
|
|
|
+ <view class="card">
|
|
|
+ <view class="card-header">
|
|
|
+ <text class="card-title">📖 创建新书籍</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 书名 -->
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="form-label">书名 *</text>
|
|
|
+ <input
|
|
|
+ v-model="newBook.title"
|
|
|
+ class="form-input"
|
|
|
+ placeholder="例如:《时间是什么》"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 副标题 -->
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="form-label">副标题</text>
|
|
|
+ <input
|
|
|
+ v-model="newBook.subtitle"
|
|
|
+ class="form-input"
|
|
|
+ placeholder="例如:一本写给青少年的科普书"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 描述/主题 -->
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="form-label">内容描述 *</text>
|
|
|
+ <textarea
|
|
|
+ v-model="newBook.description"
|
|
|
+ class="form-textarea"
|
|
|
+ placeholder="描述这本书的内容、主题、写作目的..."
|
|
|
+ :maxlength="500"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 目标受众 -->
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="form-label">目标受众</text>
|
|
|
+ <view class="chip-group">
|
|
|
+ <view
|
|
|
+ v-for="audience in audiences"
|
|
|
+ :key="audience"
|
|
|
+ :class="['chip', newBook.targetAudience === audience ? 'active' : '']"
|
|
|
+ @click="newBook.targetAudience = audience"
|
|
|
+ >
|
|
|
+ {{ audience }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 写作风格 -->
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="form-label">写作风格</text>
|
|
|
+ <view class="chip-group">
|
|
|
+ <view
|
|
|
+ v-for="style in styles"
|
|
|
+ :key="style"
|
|
|
+ :class="['chip', newBook.style === style ? 'active' : '']"
|
|
|
+ @click="newBook.style = style"
|
|
|
+ >
|
|
|
+ {{ style }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 书籍规模 -->
|
|
|
+ <view class="form-item">
|
|
|
+ <text class="form-label">书籍规模</text>
|
|
|
+ <view class="scale-picker">
|
|
|
+ <view
|
|
|
+ v-for="scale in bookScales"
|
|
|
+ :key="scale.value"
|
|
|
+ :class="['scale-option', { active: newBook.bookScale === scale.value }]"
|
|
|
+ @click="newBook.bookScale = scale.value"
|
|
|
+ >
|
|
|
+ <text class="scale-words">{{ scale.words }}</text>
|
|
|
+ <text class="scale-label">{{ scale.label }}</text>
|
|
|
+ <text class="scale-pages">{{ scale.pages }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="btn-group">
|
|
|
+ <button class="btn-cancel" @click="currentView = 'list'">取消</button>
|
|
|
+ <button
|
|
|
+ class="btn-primary"
|
|
|
+ :disabled="!canCreateBook || creating"
|
|
|
+ @click="createNewBook"
|
|
|
+ >
|
|
|
+ {{ creating ? '创建中...' : '创建书籍' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 视图3:书籍详情/生成 -->
|
|
|
+ <view v-if="currentView === 'detail'" class="detail-view">
|
|
|
+ <!-- 书籍信息 -->
|
|
|
+ <view class="card book-info">
|
|
|
+ <view class="book-title-row">
|
|
|
+ <text class="book-title-large">{{ currentBook?.title }}</text>
|
|
|
+ <view :class="['status-badge', currentBook?.status]">
|
|
|
+ {{ getStatusText(currentBook?.status || 'draft') }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text v-if="currentBook?.subtitle" class="book-subtitle">{{ currentBook.subtitle }}</text>
|
|
|
+ <text class="book-desc">{{ currentBook?.description }}</text>
|
|
|
+ <view class="book-meta-row">
|
|
|
+ <text>章节:{{ currentBook?.totalChapters }} 章</text>
|
|
|
+ <text>预估:{{ currentBook?.estimatedWords || 0 }} 字</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 进度显示 -->
|
|
|
+ <view v-if="currentBook && currentBook.progress > 0" class="card progress-card">
|
|
|
+ <text class="card-title">📊 生成进度</text>
|
|
|
+ <view class="progress-display">
|
|
|
+ <text class="progress-text">{{ currentBook.progress }}%</text>
|
|
|
+ <text class="progress-detail">
|
|
|
+ {{ completedChapters }}/{{ currentBook.totalChapters }} 章
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ <view class="progress-bar-large">
|
|
|
+ <view class="progress-fill" :style="{ width: currentBook.progress + '%' }"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 大纲展示 -->
|
|
|
+ <view v-if="currentBook?.outline" class="card outline-card">
|
|
|
+ <view class="card-header-row">
|
|
|
+ <text class="card-title">📋 书籍大纲</text>
|
|
|
+ <text class="outline-theme">{{ currentBook.outline.mainTheme }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="outline-list">
|
|
|
+ <view
|
|
|
+ v-for="chapter in currentBook.outline.chapters"
|
|
|
+ :key="chapter.number"
|
|
|
+ class="outline-item"
|
|
|
+ >
|
|
|
+ <view class="chapter-num">{{ chapter.number }}</view>
|
|
|
+ <view class="chapter-info">
|
|
|
+ <text class="chapter-title">{{ chapter.title }}</text>
|
|
|
+ <text class="chapter-summary">{{ chapter.summary }}</text>
|
|
|
+ <view class="chapter-points">
|
|
|
+ <text v-for="(point, idx) in chapter.keyPoints.slice(0, 3)" :key="idx" class="point-tag">
|
|
|
+ {{ point }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view :class="['chapter-status', getChapterStatus(chapter.number)]">
|
|
|
+ {{ getChapterStatusText(chapter.number) }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <view class="card action-card">
|
|
|
+ <!-- 未生成大纲:显示所有生成方式 -->
|
|
|
+ <view v-if="!currentBook?.outline && currentBook?.status !== 'planning'" class="btn-col">
|
|
|
+ <view class="btn-row">
|
|
|
+ <button
|
|
|
+ class="action-btn"
|
|
|
+ :disabled="generating"
|
|
|
+ @click="handleGenerateOutline"
|
|
|
+ >
|
|
|
+ {{ generating ? '生成中...' : '📋 生成大纲' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="action-btn primary"
|
|
|
+ :disabled="generating"
|
|
|
+ @click="handleGenerateAll"
|
|
|
+ >
|
|
|
+ {{ generating ? '生成中...' : '🚀 一键生成' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="action-btn langgraph-btn"
|
|
|
+ :disabled="generating"
|
|
|
+ @click="handleLangGraphGenerate"
|
|
|
+ >
|
|
|
+ {{ generating ? '生成中...' : '🤖 LangGraph' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 大纲已生成,显示所有生成方式 -->
|
|
|
+ <view v-if="currentBook?.outline && currentBook.status !== 'generating'" class="btn-col">
|
|
|
+ <view class="btn-row">
|
|
|
+ <button class="action-btn primary" :disabled="generating" @click="handleGenerateAll">
|
|
|
+ {{ generating ? '生成中...' : '🚀 一键生成' }}
|
|
|
+ </button>
|
|
|
+ <button class="action-btn langgraph-btn" :disabled="generating" @click="handleLangGraphGenerate">
|
|
|
+ {{ generating ? '生成中...' : '🤖 LangGraph' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ <view class="btn-row">
|
|
|
+ <button class="action-btn" @click="handleGenerateForeword">前言</button>
|
|
|
+ <button
|
|
|
+ class="action-btn"
|
|
|
+ :disabled="generating"
|
|
|
+ @click="handleGenerateAllChapters"
|
|
|
+ >
|
|
|
+ {{ generating ? '生成中...' : '📝 章节' }}
|
|
|
+ </button>
|
|
|
+ <button class="action-btn" @click="handleGenerateAfterword">后记</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 生成中 -->
|
|
|
+ <view v-if="generating" class="generating-tip">
|
|
|
+ <text>正在生成中,请稍候...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 查看完整内容 -->
|
|
|
+ <button
|
|
|
+ v-if="currentBook?.status === 'completed'"
|
|
|
+ class="action-btn primary full-width"
|
|
|
+ @click="showFullContent"
|
|
|
+ >
|
|
|
+ 📖 查看完整书籍
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 返回列表 -->
|
|
|
+ <button class="action-btn full-width" @click="currentView = 'list'; loadBooks()">
|
|
|
+ 返回书籍列表
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 目录视图 -->
|
|
|
+ <view v-if="currentView === 'toc'" class="toc-view">
|
|
|
+ <!-- 顶部导航 -->
|
|
|
+ <view class="nav-bar">
|
|
|
+ <view class="nav-content">
|
|
|
+ <view class="nav-left" @click="currentView = 'detail'">
|
|
|
+ <text class="back-icon">←</text>
|
|
|
+ </view>
|
|
|
+ <text class="page-title">目录</text>
|
|
|
+ <view class="nav-right"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="toc-content">
|
|
|
+ <!-- 前言入口 -->
|
|
|
+ <view v-if="currentBook?.metadata?.foreword" class="toc-item foreword" @click="showForeword">
|
|
|
+ <text class="toc-label">前言</text>
|
|
|
+ <text class="toc-arrow">→</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 章节列表 -->
|
|
|
+ <view
|
|
|
+ v-for="chapter in currentBook?.chapters"
|
|
|
+ :key="chapter.id"
|
|
|
+ :class="['toc-item', 'chapter', chapter.status === 'completed' ? 'done' : 'pending']"
|
|
|
+ >
|
|
|
+ <view class="toc-left" @click="openChapter(chapter)">
|
|
|
+ <text class="chapter-num">第{{ chapter.number }}章</text>
|
|
|
+ <view class="chapter-info-col">
|
|
|
+ <text class="chapter-title">{{ chapter.title }}</text>
|
|
|
+ <text v-if="chapter.audioUrl" class="audio-status">🎵 已生成音频</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="toc-actions">
|
|
|
+ <!-- 生成音频按钮 -->
|
|
|
+ <button
|
|
|
+ v-if="chapter.status === 'completed' && !chapter.audioUrl"
|
|
|
+ class="mini-audio-btn"
|
|
|
+ :disabled="generatingAudio"
|
|
|
+ @click.stop="handleGenerateChapterAudio(chapter)"
|
|
|
+ >
|
|
|
+ 🎵
|
|
|
+ </button>
|
|
|
+ <!-- 生成视频按钮 -->
|
|
|
+ <button
|
|
|
+ v-if="chapter.status === 'completed' && chapter.audioUrl && !chapter.videoUrl"
|
|
|
+ class="mini-video-btn"
|
|
|
+ :disabled="generatingVideo"
|
|
|
+ @click.stop="handleGenerateChapterVideo(chapter)"
|
|
|
+ >
|
|
|
+ 🎬
|
|
|
+ </button>
|
|
|
+ <text v-if="chapter.videoUrl" class="video-status">🎬 已生成</text>
|
|
|
+ <text class="toc-arrow" @click="openChapter(chapter)">→</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 后记入口 -->
|
|
|
+ <view v-if="currentBook?.metadata?.afterword" class="toc-item afterword" @click="showAfterword">
|
|
|
+ <text class="toc-label">后记</text>
|
|
|
+ <text class="toc-arrow">→</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 章节详情视图 -->
|
|
|
+ <view v-if="currentView === 'chapter-detail'" class="chapter-detail-view">
|
|
|
+ <!-- 顶部导航 -->
|
|
|
+ <view class="nav-bar">
|
|
|
+ <view class="nav-content">
|
|
|
+ <view class="nav-left" @click="currentView = 'toc'">
|
|
|
+ <text class="back-icon">←</text>
|
|
|
+ </view>
|
|
|
+ <text class="page-title">第{{ currentChapter?.number }}章</text>
|
|
|
+ <view class="nav-right"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="chapter-content">
|
|
|
+ <scroll-view class="chapter-scroll" scroll-y>
|
|
|
+ <view class="chapter-header">
|
|
|
+ <text class="chapter-title-large">{{ currentChapter?.title }}</text>
|
|
|
+ <text class="chapter-word-count">{{ currentChapter?.wordCount || 0 }}字</text>
|
|
|
+ </view>
|
|
|
+ <view class="chapter-body">
|
|
|
+ <text class="chapter-text">{{ currentChapter?.content }}</text>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, onMounted } from 'vue';
|
|
|
+import * as api from '../../utils/book-generator-api';
|
|
|
+import type { Book, BookOutline, Chapter } from '../../utils/book-generator-api';
|
|
|
+
|
|
|
+// 使用相对路径(Nginx 反向代理)
|
|
|
+const BASE_URL = '/api';
|
|
|
+
|
|
|
+// 视图状态
|
|
|
+const currentView = ref<'list' | 'create' | 'detail' | 'toc' | 'chapter-detail'>('list');
|
|
|
+
|
|
|
+// 书籍列表
|
|
|
+const books = ref<Book[]>([]);
|
|
|
+
|
|
|
+// 当前书籍
|
|
|
+const currentBook = ref<Book | null>(null);
|
|
|
+
|
|
|
+// 当前查看的章节
|
|
|
+const currentChapter = ref<Chapter | null>(null);
|
|
|
+
|
|
|
+// 创建表单
|
|
|
+const showCreateModal = ref(false);
|
|
|
+const creating = ref(false);
|
|
|
+const newBook = ref({
|
|
|
+ title: '',
|
|
|
+ subtitle: '',
|
|
|
+ description: '',
|
|
|
+ targetAudience: '',
|
|
|
+ style: '',
|
|
|
+ bookScale: 'medium',
|
|
|
+});
|
|
|
+
|
|
|
+// 生成状态
|
|
|
+const generating = ref(false);
|
|
|
+
|
|
|
+// 音频生成状态
|
|
|
+const generatingAudio = ref(false);
|
|
|
+
|
|
|
+// 视频生成状态
|
|
|
+const generatingVideo = ref(false);
|
|
|
+
|
|
|
+// 选项配置
|
|
|
+const audiences = ['不填', '通用', '青少年', '儿童', '大学生', '专业人士', '老年'];
|
|
|
+const styles = ['不填', '通俗易懂', '专业严谨', '轻松幽默', '诗意优美', '故事化'];
|
|
|
+const bookScales = [
|
|
|
+ { value: '800', label: '短文', words: '800字', pages: '约3页' },
|
|
|
+ { value: '2000', label: '短文', words: '2000字', pages: '约8页' },
|
|
|
+ { value: '5000', label: '短文', words: '5000字', pages: '约20页' },
|
|
|
+ { value: '小册子', label: '小册子', words: '1~5万字', pages: '约50~120页' },
|
|
|
+ { value: '标准教程', label: '标准教程', words: '5~15万字', pages: '约150~300页' },
|
|
|
+ { value: '系统教材', label: '系统教材', words: '15~30万字', pages: '约300~550页' },
|
|
|
+ // { value: '专业厚本', label: '专业厚本', words: '30~60万字', pages: '约550~900页' },
|
|
|
+ // { value: '大部头', label: '大部头', words: '60万字以上', pages: '900页以上' },
|
|
|
+];
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+const canCreateBook = computed(() => {
|
|
|
+ return newBook.value.title.trim() && newBook.value.description.trim();
|
|
|
+});
|
|
|
+
|
|
|
+const completedChapters = computed(() => {
|
|
|
+ if (!currentBook.value) return 0;
|
|
|
+ return currentBook.value.chapters.filter((c) => c.status === 'completed').length;
|
|
|
+});
|
|
|
+
|
|
|
+// 方法
|
|
|
+function goBack() {
|
|
|
+ uni.navigateBack();
|
|
|
+}
|
|
|
+
|
|
|
+function switchTab() {
|
|
|
+ uni.switchTab({ url: '/pages/index/index' });
|
|
|
+}
|
|
|
+
|
|
|
+function goToVideoGenerator() {
|
|
|
+ uni.navigateTo({ url: '/pages/video-generator/index' });
|
|
|
+}
|
|
|
+
|
|
|
+function getStatusText(status: string): string {
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ draft: '草稿',
|
|
|
+ planning: '规划中',
|
|
|
+ generating: '生成中',
|
|
|
+ completed: '已完成',
|
|
|
+ failed: '失败',
|
|
|
+ };
|
|
|
+ return map[status] || status;
|
|
|
+}
|
|
|
+
|
|
|
+function getChapterStatus(chapterNum: number): string {
|
|
|
+ const chapter = currentBook.value?.chapters.find((c) => c.number === chapterNum);
|
|
|
+ if (!chapter) return 'pending';
|
|
|
+ return chapter.status === 'completed' ? 'done' : 'pending';
|
|
|
+}
|
|
|
+
|
|
|
+function getChapterStatusText(chapterNum: number): string {
|
|
|
+ const chapter = currentBook.value?.chapters.find((c) => c.number === chapterNum);
|
|
|
+ if (!chapter) return '待生成';
|
|
|
+ return chapter.status === 'completed' ? '✓' : '○';
|
|
|
+}
|
|
|
+
|
|
|
+// 加载书籍列表
|
|
|
+async function loadBooks() {
|
|
|
+ try {
|
|
|
+ const data = await api.getBooks();
|
|
|
+ books.value = data || [];
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载书籍失败:', e);
|
|
|
+ books.value = [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建新书籍
|
|
|
+async function createNewBook() {
|
|
|
+ if (!canCreateBook.value) return;
|
|
|
+
|
|
|
+ creating.value = true;
|
|
|
+ try {
|
|
|
+ await api.createBook({
|
|
|
+ title: newBook.value.title,
|
|
|
+ subtitle: newBook.value.subtitle,
|
|
|
+ description: newBook.value.description,
|
|
|
+ ...(newBook.value.targetAudience ? { targetAudience: newBook.value.targetAudience } : {}),
|
|
|
+ ...(newBook.value.style ? { style: newBook.value.style } : {}),
|
|
|
+ bookScale: newBook.value.bookScale,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 重置表单
|
|
|
+ newBook.value = {
|
|
|
+ title: '',
|
|
|
+ subtitle: '',
|
|
|
+ description: '',
|
|
|
+ targetAudience: '',
|
|
|
+ style: '',
|
|
|
+ bookScale: 'medium',
|
|
|
+ };
|
|
|
+ showCreateModal.value = false;
|
|
|
+ currentView.value = 'list';
|
|
|
+ await loadBooks();
|
|
|
+ uni.showToast({ title: '创建成功', icon: 'success' });
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '创建失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ creating.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 打开书籍
|
|
|
+async function openBook(book: Book) {
|
|
|
+ try {
|
|
|
+ currentBook.value = await api.getBook(book.id);
|
|
|
+ currentView.value = 'detail';
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: '加载失败', icon: 'none' });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成大纲
|
|
|
+async function handleGenerateOutline() {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ generating.value = true;
|
|
|
+ try {
|
|
|
+ const outline = await api.generateOutline(currentBook.value.id);
|
|
|
+ currentBook.value.outline = outline;
|
|
|
+ currentBook.value.status = 'planning';
|
|
|
+ // 初始化章节状态
|
|
|
+ currentBook.value.chapters = outline.chapters.map((c) => ({
|
|
|
+ id: '',
|
|
|
+ bookId: currentBook.value!.id,
|
|
|
+ number: c.number,
|
|
|
+ title: c.title,
|
|
|
+ content: '',
|
|
|
+ wordCount: 0,
|
|
|
+ status: 'pending' as const,
|
|
|
+ }));
|
|
|
+ uni.showToast({ title: '大纲生成成功', icon: 'success' });
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ generating.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 一键生成整本书(大纲+章节+前言+后记)- 异步模式
|
|
|
+let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
+
|
|
|
+async function handleGenerateAll() {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ generating.value = true;
|
|
|
+ currentBook.value.status = 'generating';
|
|
|
+ currentBook.value.progress = 0;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 启动异步生成任务(立即返回)
|
|
|
+ await api.generateBook(currentBook.value.id, {
|
|
|
+ generateForeword: true,
|
|
|
+ generateAfterword: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 显示提示
|
|
|
+ uni.showToast({ title: '生成任务已启动,请稍候...', icon: 'none', duration: 2000 });
|
|
|
+
|
|
|
+ // 开始轮询进度
|
|
|
+ startPollingProgress(currentBook.value.id);
|
|
|
+ } catch (e: any) {
|
|
|
+ generating.value = false;
|
|
|
+ currentBook.value.status = 'failed';
|
|
|
+ uni.showToast({ title: e.message || '启动失败', icon: 'none' });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 轮询进度
|
|
|
+function startPollingProgress(bookId: string) {
|
|
|
+ // 停止之前的轮询
|
|
|
+ stopPollingProgress();
|
|
|
+
|
|
|
+ pollTimer = setInterval(async () => {
|
|
|
+ try {
|
|
|
+ // 直接轮询书籍进度,不依赖工作流
|
|
|
+ const progress = await api.getProgress(bookId);
|
|
|
+
|
|
|
+ if (!progress) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新进度
|
|
|
+ if (currentBook.value) {
|
|
|
+ currentBook.value.progress = progress.progress;
|
|
|
+ currentBook.value.status = progress.status as any;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否完成
|
|
|
+ if (progress.status === 'completed' || progress.status === 'failed') {
|
|
|
+ stopPollingProgress();
|
|
|
+ generating.value = false;
|
|
|
+
|
|
|
+ // 刷新书籍详情
|
|
|
+ currentBook.value = await api.getBook(bookId);
|
|
|
+
|
|
|
+ if (progress.status === 'completed') {
|
|
|
+ uni.showToast({ title: '整本书生成完成!', icon: 'success' });
|
|
|
+ } else if (progress.status === 'failed') {
|
|
|
+ uni.showToast({ title: '生成过程中有错误', icon: 'none' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('轮询进度失败:', e);
|
|
|
+ }
|
|
|
+ }, 3000); // 每3秒轮询一次
|
|
|
+}
|
|
|
+
|
|
|
+// 停止轮询
|
|
|
+function stopPollingProgress() {
|
|
|
+ if (pollTimer) {
|
|
|
+ clearInterval(pollTimer);
|
|
|
+ pollTimer = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// LangGraph 生成
|
|
|
+async function handleLangGraphGenerate() {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ generating.value = true;
|
|
|
+ currentBook.value.status = 'generating';
|
|
|
+ currentBook.value.progress = 0;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = await api.generateWithLangGraph(currentBook.value.id);
|
|
|
+ uni.showToast({ title: 'LangGraph 生成已启动', icon: 'none', duration: 2000 });
|
|
|
+ startPollingProgress(currentBook.value.id);
|
|
|
+ } catch (e: any) {
|
|
|
+ generating.value = false;
|
|
|
+ currentBook.value.status = 'failed';
|
|
|
+ uni.showToast({ title: e.message || '启动失败', icon: 'none' });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成全部章节
|
|
|
+async function handleGenerateAllChapters() {
|
|
|
+ if (!currentBook.value || !currentBook.value.outline) {
|
|
|
+ uni.showToast({ title: '请先生成大纲', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ generating.value = true;
|
|
|
+ currentBook.value.status = 'generating';
|
|
|
+
|
|
|
+ try {
|
|
|
+ for (const outlineChapter of currentBook.value.outline.chapters) {
|
|
|
+ try {
|
|
|
+ const chapter = await api.generateChapter(currentBook.value.id, outlineChapter.number);
|
|
|
+ // 更新章节
|
|
|
+ const idx = currentBook.value.chapters.findIndex((c) => c.number === chapter.number);
|
|
|
+ if (idx >= 0) {
|
|
|
+ currentBook.value.chapters[idx] = chapter;
|
|
|
+ }
|
|
|
+ // 更新进度
|
|
|
+ const completed = currentBook.value.chapters.filter((c) => c.status === 'completed').length;
|
|
|
+ currentBook.value.progress = Math.round((completed / currentBook.value.totalChapters) * 100);
|
|
|
+ } catch (e) {
|
|
|
+ console.error(`生成第${outlineChapter.number}章失败:`, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ currentBook.value.status = 'completed';
|
|
|
+ uni.showToast({ title: '全部章节生成完成', icon: 'success' });
|
|
|
+ } catch (e: any) {
|
|
|
+ currentBook.value.status = 'failed';
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ generating.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成前言
|
|
|
+async function handleGenerateForeword() {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ generating.value = true;
|
|
|
+ try {
|
|
|
+ const foreword = await api.generateForeword(currentBook.value.id);
|
|
|
+ currentBook.value.metadata = currentBook.value.metadata || {};
|
|
|
+ currentBook.value.metadata.foreword = foreword;
|
|
|
+ uni.showToast({ title: '前言生成成功', icon: 'success' });
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ generating.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成后记
|
|
|
+async function handleGenerateAfterword() {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ generating.value = true;
|
|
|
+ try {
|
|
|
+ const afterword = await api.generateAfterword(currentBook.value.id);
|
|
|
+ currentBook.value.metadata = currentBook.value.metadata || {};
|
|
|
+ currentBook.value.metadata.afterword = afterword;
|
|
|
+ uni.showToast({ title: '后记生成成功', icon: 'success' });
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ generating.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 显示完整内容 - 现在显示目录
|
|
|
+async function showFullContent() {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ // 刷新书籍详情获取最新章节
|
|
|
+ currentBook.value = await api.getBook(currentBook.value.id);
|
|
|
+ currentView.value = 'toc';
|
|
|
+}
|
|
|
+
|
|
|
+// 打开章节详情
|
|
|
+function openChapter(chapter: Chapter) {
|
|
|
+ if (chapter.status !== 'completed') {
|
|
|
+ uni.showToast({ title: '该章节尚未生成', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ currentChapter.value = chapter;
|
|
|
+ currentView.value = 'chapter-detail';
|
|
|
+}
|
|
|
+
|
|
|
+// 显示前言
|
|
|
+function showForeword() {
|
|
|
+ if (!currentBook.value?.metadata?.foreword) {
|
|
|
+ uni.showToast({ title: '前言尚未生成', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 创建一个临时章节对象显示前言
|
|
|
+ currentChapter.value = {
|
|
|
+ id: 'foreword',
|
|
|
+ bookId: currentBook.value.id,
|
|
|
+ number: 0,
|
|
|
+ title: '前言',
|
|
|
+ content: currentBook.value.metadata.foreword,
|
|
|
+ wordCount: currentBook.value.metadata.foreword.length,
|
|
|
+ status: 'completed',
|
|
|
+ };
|
|
|
+ currentView.value = 'chapter-detail';
|
|
|
+}
|
|
|
+
|
|
|
+// 显示后记
|
|
|
+function showAfterword() {
|
|
|
+ if (!currentBook.value?.metadata?.afterword) {
|
|
|
+ uni.showToast({ title: '后记尚未生成', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ currentChapter.value = {
|
|
|
+ id: 'afterword',
|
|
|
+ bookId: currentBook.value.id,
|
|
|
+ number: currentBook.value.chapters.length + 1,
|
|
|
+ title: '后记',
|
|
|
+ content: currentBook.value.metadata.afterword,
|
|
|
+ wordCount: currentBook.value.metadata.afterword.length,
|
|
|
+ status: 'completed',
|
|
|
+ };
|
|
|
+ currentView.value = 'chapter-detail';
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 音频生成 ============
|
|
|
+
|
|
|
+/**
|
|
|
+ * 生成单个章节音频
|
|
|
+ */
|
|
|
+async function handleGenerateChapterAudio(chapter: Chapter) {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ generatingAudio.value = true;
|
|
|
+ try {
|
|
|
+ await api.generateChapterAudio(currentBook.value.id, chapter.number, 'cherry');
|
|
|
+ uni.showToast({ title: '音频生成任务已启动', icon: 'none', duration: 2000 });
|
|
|
+
|
|
|
+ // 3秒后刷新书籍信息,检查音频是否生成完成
|
|
|
+ setTimeout(async () => {
|
|
|
+ if (currentBook.value) {
|
|
|
+ currentBook.value = await api.getBook(currentBook.value.id);
|
|
|
+ }
|
|
|
+ }, 3000);
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ generatingAudio.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 批量生成书籍所有章节音频
|
|
|
+ */
|
|
|
+async function handleGenerateAllAudio(book: Book) {
|
|
|
+ generatingAudio.value = true;
|
|
|
+ try {
|
|
|
+ const result = await api.generateAllChaptersAudio(book.id, 'cherry');
|
|
|
+ uni.showToast({
|
|
|
+ title: `已启动 ${result.totalChapters} 个章节的音频生成`,
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2500,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 5秒后刷新书籍信息
|
|
|
+ setTimeout(async () => {
|
|
|
+ await loadBooks();
|
|
|
+ if (currentBook.value?.id === book.id) {
|
|
|
+ currentBook.value = await api.getBook(book.id);
|
|
|
+ }
|
|
|
+ }, 5000);
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ generatingAudio.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ============ 视频生成 ============
|
|
|
+
|
|
|
+/**
|
|
|
+ * 生成单个章节视频
|
|
|
+ */
|
|
|
+async function handleGenerateChapterVideo(chapter: Chapter) {
|
|
|
+ if (!currentBook.value) return;
|
|
|
+
|
|
|
+ generatingVideo.value = true;
|
|
|
+ try {
|
|
|
+ await api.generateChapterVideo(currentBook.value.id, chapter.number);
|
|
|
+ uni.showToast({ title: '视频生成任务已启动', icon: 'none', duration: 2000 });
|
|
|
+
|
|
|
+ // 3秒后刷新书籍信息,检查视频是否生成完成
|
|
|
+ setTimeout(async () => {
|
|
|
+ if (currentBook.value) {
|
|
|
+ currentBook.value = await api.getBook(currentBook.value.id);
|
|
|
+ }
|
|
|
+ }, 3000);
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ generatingVideo.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 批量生成书籍所有章节视频
|
|
|
+ */
|
|
|
+async function handleGenerateAllVideo(book: Book) {
|
|
|
+ generatingVideo.value = true;
|
|
|
+ try {
|
|
|
+ const result = await api.generateAllChaptersVideo(book.id);
|
|
|
+ uni.showToast({
|
|
|
+ title: `已启动 ${result.totalChapters} 个章节的视频生成`,
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2500,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 5秒后刷新书籍信息
|
|
|
+ setTimeout(async () => {
|
|
|
+ await loadBooks();
|
|
|
+ if (currentBook.value?.id === book.id) {
|
|
|
+ currentBook.value = await api.getBook(book.id);
|
|
|
+ }
|
|
|
+ }, 5000);
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ generatingVideo.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 页面加载
|
|
|
+onMounted(() => {
|
|
|
+ loadBooks();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f9fafb;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-bar {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ z-index: 100;
|
|
|
+ background: #ffffff;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.nav-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ height: 88rpx;
|
|
|
+ padding: 0 32rpx;
|
|
|
+ padding-top: env(safe-area-inset-top);
|
|
|
+}
|
|
|
+
|
|
|
+.nav-left,
|
|
|
+.nav-right {
|
|
|
+ width: 80rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.back-icon,
|
|
|
+.nav-btn {
|
|
|
+ font-size: 40rpx;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.page-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.main-content {
|
|
|
+ padding: 120rpx 32rpx 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 卡片样式 */
|
|
|
+.card {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 32rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.card-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+/* 创建卡片 */
|
|
|
+.create-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 16rpx;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 48rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.create-icon {
|
|
|
+ font-size: 48rpx;
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.create-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频导航卡片 */
|
|
|
+.video-nav-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 36rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-icon {
|
|
|
+ font-size: 56rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-text {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-desc {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+.video-nav-arrow {
|
|
|
+ font-size: 40rpx;
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 书籍列表 */
|
|
|
+.book-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.book-card {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 28rpx;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.book-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.book-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.status-badge {
|
|
|
+ font-size: 22rpx;
|
|
|
+ padding: 6rpx 16rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ background: #e5e7eb;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.status-badge.planning,
|
|
|
+.status-badge.generating {
|
|
|
+ background: #fef3c7;
|
|
|
+ color: #d97706;
|
|
|
+}
|
|
|
+
|
|
|
+.status-badge.completed {
|
|
|
+ background: #d1fae5;
|
|
|
+ color: #059669;
|
|
|
+}
|
|
|
+
|
|
|
+.status-badge.failed {
|
|
|
+ background: #fee2e2;
|
|
|
+ color: #dc2626;
|
|
|
+}
|
|
|
+
|
|
|
+.book-desc {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.book-meta {
|
|
|
+ display: flex;
|
|
|
+ gap: 24rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-bar {
|
|
|
+ height: 6rpx;
|
|
|
+ background: #e5e7eb;
|
|
|
+ border-radius: 3rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-fill {
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
|
+ border-radius: 3rpx;
|
|
|
+ transition: width 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+/* 空状态 */
|
|
|
+.empty-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 80rpx 0;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-icon {
|
|
|
+ font-size: 120rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-hint {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单 */
|
|
|
+.form-item {
|
|
|
+ margin-bottom: 28rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #374151;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-input {
|
|
|
+ width: 100%;
|
|
|
+ height: 88rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
+ background: #f9fafb;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.form-textarea {
|
|
|
+ width: 100%;
|
|
|
+ height: 200rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ background: #f9fafb;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.slider-range {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.scale-picker {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12rpx;
|
|
|
+ margin-top: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.scale-option {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20rpx 16rpx;
|
|
|
+ border: 2rpx solid #e5e7eb;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ background: #f9fafb;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.scale-option.active {
|
|
|
+ border-color: #4F46E5;
|
|
|
+ background: #eef2ff;
|
|
|
+}
|
|
|
+
|
|
|
+.scale-label {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #374151;
|
|
|
+ min-width: 160rpx;
|
|
|
+ margin-left: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.scale-option.active .scale-label {
|
|
|
+ color: #4F46E5;
|
|
|
+}
|
|
|
+
|
|
|
+.scale-words {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #374151;
|
|
|
+ min-width: 160rpx;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.scale-pages {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ flex: 1;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+/* 标签组 */
|
|
|
+.chip-group {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.chip {
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+ background: #f3f4f6;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.chip.active {
|
|
|
+ background: #4f46e5;
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 按钮组 */
|
|
|
+.btn-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 20rpx;
|
|
|
+ margin-top: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-cancel,
|
|
|
+.btn-primary {
|
|
|
+ flex: 1;
|
|
|
+ height: 88rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-cancel {
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary[disabled] {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+/* 详情页 */
|
|
|
+.book-info {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.book-title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.book-title-large {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1f2937;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.book-subtitle {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.book-meta-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 24rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+ margin-top: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 进度卡片 */
|
|
|
+.progress-card {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-display {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 16rpx;
|
|
|
+ margin: 16rpx 0;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-text {
|
|
|
+ font-size: 48rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #4f46e5;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-detail {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-bar-large {
|
|
|
+ height: 12rpx;
|
|
|
+ background: #e5e7eb;
|
|
|
+ border-radius: 6rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 大纲卡片 */
|
|
|
+.outline-theme {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.outline-list {
|
|
|
+ margin-top: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.outline-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 20rpx;
|
|
|
+ padding: 20rpx 0;
|
|
|
+ border-bottom: 1px solid #f3f4f6;
|
|
|
+}
|
|
|
+
|
|
|
+.outline-item:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-num {
|
|
|
+ width: 48rpx;
|
|
|
+ height: 48rpx;
|
|
|
+ background: #4f46e5;
|
|
|
+ color: #ffffff;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 24rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-info {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+ margin-bottom: 6rpx;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-summary {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-points {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.point-tag {
|
|
|
+ font-size: 20rpx;
|
|
|
+ padding: 4rpx 12rpx;
|
|
|
+ background: #f3f4f6;
|
|
|
+ color: #6b7280;
|
|
|
+ border-radius: 6rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-status {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-status.done {
|
|
|
+ color: #059669;
|
|
|
+}
|
|
|
+
|
|
|
+/* 操作卡片 */
|
|
|
+.action-card {
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 16rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 88rpx;
|
|
|
+ background: #f3f4f6;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #374151;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn.primary {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn.full-width {
|
|
|
+ width: 100%;
|
|
|
+ flex: none;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn[disabled] {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+.generating-tip {
|
|
|
+ text-align: center;
|
|
|
+ padding: 20rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 26rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* LangGraph 按钮 */
|
|
|
+.action-btn.langgraph-btn {
|
|
|
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
|
+ color: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 全屏弹窗 */
|
|
|
+.modal-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ z-index: 1000;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.content-modal {
|
|
|
+ width: 100%;
|
|
|
+ height: 90vh;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 32rpx 32rpx 0 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 32rpx;
|
|
|
+ border-bottom: 1px solid #f3f4f6;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-close {
|
|
|
+ font-size: 40rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+ padding: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.content-scroll {
|
|
|
+ flex: 1;
|
|
|
+ padding: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.content-body {
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 1.8;
|
|
|
+ color: #374151;
|
|
|
+}
|
|
|
+
|
|
|
+/* ==================== 目录视图 ==================== */
|
|
|
+.toc-view {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: #f9fafb;
|
|
|
+ z-index: 200;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-view .nav-bar {
|
|
|
+ position: relative;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-content {
|
|
|
+ flex: 1;
|
|
|
+ padding: 24rpx 32rpx;
|
|
|
+ padding-top: calc(120rpx + env(safe-area-inset-top));
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 28rpx 32rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.toc-item.foreword,
|
|
|
+.toc-item.afterword {
|
|
|
+ background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.toc-item.foreword .toc-label,
|
|
|
+.toc-item.afterword .toc-label {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #92400e;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-item.chapter {
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-item.chapter.pending {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-item.chapter.done {
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20rpx;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-left .chapter-num {
|
|
|
+ width: 56rpx;
|
|
|
+ height: 56rpx;
|
|
|
+ background: #4f46e5;
|
|
|
+ color: #ffffff;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 24rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-item.chapter.pending .chapter-num {
|
|
|
+ background: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-left .chapter-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.toc-arrow {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+/* ==================== 章节详情视图 ==================== */
|
|
|
+.chapter-detail-view {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: #f9fafb;
|
|
|
+ z-index: 200;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-detail-view .nav-bar {
|
|
|
+ position: relative;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-content {
|
|
|
+ flex: 1;
|
|
|
+ padding-top: calc(88rpx + env(safe-area-inset-top));
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-scroll {
|
|
|
+ height: 100%;
|
|
|
+ padding: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-header {
|
|
|
+ margin-bottom: 32rpx;
|
|
|
+ padding-bottom: 24rpx;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-title-large {
|
|
|
+ display: block;
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1f2937;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-word-count {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-body {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 32rpx;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-text {
|
|
|
+ font-size: 30rpx;
|
|
|
+ line-height: 1.9;
|
|
|
+ color: #374151;
|
|
|
+ text-align: justify;
|
|
|
+}
|
|
|
+
|
|
|
+/* ==================== 音频生成相关样式 ==================== */
|
|
|
+
|
|
|
+/* 书籍卡片操作区 */
|
|
|
+.book-actions {
|
|
|
+ margin-top: 20rpx;
|
|
|
+ padding-top: 20rpx;
|
|
|
+ border-top: 1px solid #f3f4f6;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 72rpx;
|
|
|
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
|
+ border-radius: 12rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-btn[disabled] {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频按钮样式 */
|
|
|
+.video-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 72rpx;
|
|
|
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
|
+ border-radius: 12rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: none;
|
|
|
+ margin-top: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.video-btn[disabled] {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+/* 目录项操作区 */
|
|
|
+.toc-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.chapter-info-col {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-status {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #059669;
|
|
|
+}
|
|
|
+
|
|
|
+.mini-audio-btn {
|
|
|
+ width: 64rpx;
|
|
|
+ height: 64rpx;
|
|
|
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 28rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: none;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.mini-audio-btn[disabled] {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+.mini-video-btn {
|
|
|
+ width: 64rpx;
|
|
|
+ height: 64rpx;
|
|
|
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 28rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: none;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.mini-video-btn[disabled] {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+.video-status {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #f5576c;
|
|
|
+}
|
|
|
+</style>
|