|
|
@@ -15,28 +15,26 @@
|
|
|
|
|
|
<!-- 主内容区 -->
|
|
|
<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="create-card" @click="goToCreate">
|
|
|
+ <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 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>
|
|
|
- <text class="video-nav-arrow">→</text>
|
|
|
</view>
|
|
|
+ <text class="video-nav-arrow">→</text>
|
|
|
+ </view>
|
|
|
|
|
|
- <!-- 书籍列表 -->
|
|
|
- <view v-if="books.length > 0" class="book-list">
|
|
|
+ <!-- 书籍列表 -->
|
|
|
+ <view v-if="books.length > 0" class="book-list">
|
|
|
<view
|
|
|
v-for="book in books"
|
|
|
:key="book.id"
|
|
|
@@ -44,8 +42,8 @@
|
|
|
>
|
|
|
<view class="book-header" @click="openBook(book)">
|
|
|
<text class="book-title">{{ book.title }}</text>
|
|
|
- <view :class="['status-badge', book.status]">
|
|
|
- {{ getStatusText(book.status) }}
|
|
|
+ <view :class="['status-badge', book.isPublished ? 'published' : book.status]">
|
|
|
+ {{ book.isPublished ? '已公开' : getStatusText(book.status) }}
|
|
|
</view>
|
|
|
</view>
|
|
|
<text class="book-desc" @click="openBook(book)">{{ book.description }}</text>
|
|
|
@@ -56,23 +54,48 @@
|
|
|
<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.chapters && book.chapters.length > 0" class="book-actions">
|
|
|
<button
|
|
|
class="audio-btn"
|
|
|
- :disabled="generatingAudio"
|
|
|
+ :disabled="generatingAudio[book.id]"
|
|
|
@click.stop="handleGenerateAllAudio(book)"
|
|
|
>
|
|
|
- {{ generatingAudio ? '生成中...' : '🎵 生成全部音频' }}
|
|
|
+ <text v-if="generatingAudio[book.id]">🎵 生成中...</text>
|
|
|
+ <text v-else-if="getBookAudioStatus(book).status === 'completed'">🔄 重新生成音频</text>
|
|
|
+ <text v-else-if="getBookAudioStatus(book).status === 'partial'">🎵 继续生成音频({{ getBookAudioStatus(book).completed }}/{{ getBookAudioStatus(book).total }})</text>
|
|
|
+ <text v-else>🎵 生成全部音频</text>
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="merge-audio-btn"
|
|
|
+ :disabled="generatingAudio[book.id] || mergingAudio[book.id] || !canMergeAudio(book)"
|
|
|
+ @click.stop="handleMergeChapterAudio(book)"
|
|
|
+ >
|
|
|
+ <text v-if="mergingAudio[book.id]">🔄 合并中...</text>
|
|
|
+ <text v-else-if="canMergeAudio(book)">🔊 合并音频</text>
|
|
|
+ <text v-else>🔒 需先完成音频生成</text>
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="merge-video-btn"
|
|
|
+ :disabled="generatingVideo[book.id] || mergingVideo[book.id] || !canMergeVideo(book)"
|
|
|
+ @click.stop="handleMergeChapterVideo(book)"
|
|
|
+ >
|
|
|
+ <text v-if="mergingVideo[book.id]">🔄 合并中...</text>
|
|
|
+ <text v-else-if="canMergeVideo(book)">🎬 合并视频</text>
|
|
|
+ <text v-else>🔒 需先完成视频生成</text>
|
|
|
</button>
|
|
|
<button
|
|
|
class="video-btn"
|
|
|
- :disabled="generatingVideo"
|
|
|
+ :disabled="generatingVideo[book.id]"
|
|
|
@click.stop="handleGenerateAllVideo(book)"
|
|
|
>
|
|
|
- {{ generatingVideo ? '生成中...' : '🎬 生成全部视频' }}
|
|
|
+ <text v-if="generatingVideo[book.id]">🎬 生成中...</text>
|
|
|
+ <text v-else-if="getBookVideoStatus(book).status === 'completed'">🔄 重新生成视频</text>
|
|
|
+ <text v-else-if="getBookVideoStatus(book).status === 'partial'">🎬 继续生成视频({{ getBookVideoStatus(book).completed }}/{{ getBookVideoStatus(book).total }})</text>
|
|
|
+ <text v-else>🎬 生成全部视频</text>
|
|
|
</button>
|
|
|
<button
|
|
|
+ v-if="false"
|
|
|
class="publish-all-btn"
|
|
|
@click.stop="goToPublishBook(book)"
|
|
|
>
|
|
|
@@ -95,860 +118,42 @@
|
|
|
<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>
|
|
|
- <text class="form-hint-small">点击模板自动填充所有配置,也可手动修改</text>
|
|
|
- <view class="template-grid">
|
|
|
- <view
|
|
|
- v-for="template in quickTemplates"
|
|
|
- :key="template.name"
|
|
|
- class="template-card"
|
|
|
- @click="applyTemplate(template)"
|
|
|
- >
|
|
|
- <text class="template-icon">{{ template.icon }}</text>
|
|
|
- <text class="template-name">{{ template.name }}</text>
|
|
|
- <text class="template-desc">{{ template.desc }}</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 重要提示 -->
|
|
|
- <view class="important-tip">
|
|
|
- <view class="tip-header">
|
|
|
- <text class="tip-icon">💡</text>
|
|
|
- <text class="tip-title">重要提示</text>
|
|
|
- </view>
|
|
|
- <text class="tip-content">
|
|
|
- 知识难度和面向人群会显著影响生成内容的风格和深度。同样的知识,面向不同人群,表达方式完全不同:
|
|
|
- </text>
|
|
|
- <view class="tip-examples">
|
|
|
- <text class="tip-example">• 面向小学生:形象、简单、通俗易懂</text>
|
|
|
- <text class="tip-example">• 面向大学生:系统、专业、有理论深度</text>
|
|
|
- <text class="tip-example">• 面向研究生:前沿、深入、有研究价值</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 知识难度 -->
|
|
|
- <view class="form-item highlight-item">
|
|
|
- <view class="label-with-badge">
|
|
|
- <text class="form-label">知识难度</text>
|
|
|
- <view class="required-badge">重要</view>
|
|
|
- </view>
|
|
|
- <text class="form-hint">选择适合的知识深度,AI会根据难度调整内容深度和专业程度</text>
|
|
|
- <view class="chip-group">
|
|
|
- <view
|
|
|
- v-for="level in knowledgeLevels"
|
|
|
- :key="level.value"
|
|
|
- :class="['chip', 'level-chip', newBook.knowledgeLevel === level.value ? 'active' : '']"
|
|
|
- @click="newBook.knowledgeLevel = level.value"
|
|
|
- >
|
|
|
- <text class="chip-icon">{{ level.icon }}</text>
|
|
|
- <text class="chip-text">{{ level.label }}</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 面向人群 -->
|
|
|
- <view class="form-item highlight-item">
|
|
|
- <view class="label-with-badge">
|
|
|
- <text class="form-label">面向人群</text>
|
|
|
- <view class="required-badge">重要</view>
|
|
|
- </view>
|
|
|
- <text class="form-hint">选择目标读者,AI会根据人群调整语言风格和表达方式</text>
|
|
|
- <view class="chip-group">
|
|
|
- <view
|
|
|
- v-for="audience in audiences"
|
|
|
- :key="audience.value"
|
|
|
- :class="['chip', 'audience-chip', newBook.targetAudience === audience.value ? 'active' : '']"
|
|
|
- @click="newBook.targetAudience = audience.value"
|
|
|
- >
|
|
|
- <text class="chip-icon">{{ audience.icon }}</text>
|
|
|
- <text class="chip-text">{{ audience.label }}</text>
|
|
|
- </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>
|
|
|
- <text class="form-hint-small">选择行业领域,AI会使用相关的案例和术语</text>
|
|
|
- <view class="chip-group">
|
|
|
- <view
|
|
|
- v-for="industry in industries"
|
|
|
- :key="industry.value"
|
|
|
- :class="['chip', newBook.industry === industry.value ? 'active' : '']"
|
|
|
- @click="newBook.industry = industry.value"
|
|
|
- >
|
|
|
- <text class="chip-icon">{{ industry.icon }}</text>
|
|
|
- <text class="chip-text">{{ industry.label }}</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 特殊要求 (多选) -->
|
|
|
- <view class="form-item">
|
|
|
- <text class="form-label">特殊要求</text>
|
|
|
- <text class="form-hint-small">可多选,AI会在生成时包含这些元素</text>
|
|
|
- <view class="chip-group">
|
|
|
- <view
|
|
|
- v-for="feature in specialFeatures"
|
|
|
- :key="feature.value"
|
|
|
- :class="['chip', 'feature-chip', newBook.specialFeatures.includes(feature.value) ? 'active' : '']"
|
|
|
- @click="toggleSpecialFeature(feature.value)"
|
|
|
- >
|
|
|
- <text class="chip-icon">{{ feature.icon }}</text>
|
|
|
- <text class="chip-text">{{ feature.label }}</text>
|
|
|
- </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="selectBookScale(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 v-if="bookEstimate && bookEstimate.words && bookEstimate.audioMinutes" class="estimate-card">
|
|
|
- <view class="estimate-header">
|
|
|
- <text class="estimate-title">📊 生成预估</text>
|
|
|
- </view>
|
|
|
- <view class="estimate-row">
|
|
|
- <text class="estimate-label">预估字数:</text>
|
|
|
- <text class="estimate-value">{{ bookEstimate.words.min }}~{{ bookEstimate.words.max }}字</text>
|
|
|
- </view>
|
|
|
- <view class="estimate-row">
|
|
|
- <text class="estimate-label">预估时长:</text>
|
|
|
- <text class="estimate-value">{{ bookEstimate.audioMinutes.min }}~{{ bookEstimate.audioMinutes.max }}分钟</text>
|
|
|
- </view>
|
|
|
- <view class="estimate-row">
|
|
|
- <text class="estimate-label">预估章节:</text>
|
|
|
- <text class="estimate-value">约{{ bookEstimate.estimatedChapters }}章</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 配额检查结果 -->
|
|
|
- <view v-if="quotaCheck" class="quota-check">
|
|
|
- <view v-if="quotaCheck.allowed" class="quota-ok">
|
|
|
- <text class="quota-icon">✅</text>
|
|
|
- <text class="quota-text">额度充足,可生成</text>
|
|
|
- </view>
|
|
|
- <view v-else class="quota-warning">
|
|
|
- <text class="quota-icon">⚠️</text>
|
|
|
- <text class="quota-text">{{ quotaCheck.reason }}</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 详细配额信息 -->
|
|
|
- <view class="quota-detail">
|
|
|
- <text class="quota-info">您当前额度:{{ quotaCheck.quota.remainingMinutes }}分钟 / {{ quotaCheck.quota.totalMinutes }}分钟</text>
|
|
|
- <view v-if="quotaCheck.costEstimate.overageMinutes > 0" class="quota-overage">
|
|
|
- <text>预计超出:{{ quotaCheck.costEstimate.overageMinutes }}分钟</text>
|
|
|
- <text class="quota-price">额外费用:¥{{ quotaCheck.costEstimate.estimatedPrice }}</text>
|
|
|
- </view>
|
|
|
- </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 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>
|
|
|
- <view class="quota-monitor-row">
|
|
|
- <text>已用:{{ usedAudioMinutes }}分钟</text>
|
|
|
- <text>剩余:{{ remainingAudioMinutes }}分钟</text>
|
|
|
- </view>
|
|
|
- <view v-if="overageMinutes > 0" class="quota-overage-warning">
|
|
|
- <text>⚠️ 已超出配额 {{ overageMinutes }} 分钟</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 中断提示 -->
|
|
|
- <view v-if="currentBook.status === 'failed' && currentBook?.error?.includes('额度')" class="interrupted-tip">
|
|
|
- <text class="interrupted-icon">⚠️</text>
|
|
|
- <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>
|
|
|
-
|
|
|
- <!-- 完整内容区(整合大纲+实际内容) -->
|
|
|
- <view v-if="currentBook?.chapters && currentBook.chapters.length > 0" class="card content-card">
|
|
|
- <view class="card-header-row">
|
|
|
- <text class="card-title">📖 完整内容</text>
|
|
|
- <text v-if="currentBook.outline?.mainTheme" class="outline-theme">{{ currentBook.outline.mainTheme }}</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 前言 -->
|
|
|
- <view v-if="currentBook?.metadata?.foreword" class="content-item foreword" @click="viewForeword">
|
|
|
- <view class="content-item-left">
|
|
|
- <text class="content-label">前言</text>
|
|
|
- <text class="content-word-count">{{ currentBook?.metadata?.foreword?.length || 0 }}字</text>
|
|
|
- </view>
|
|
|
- <view class="content-item-right">
|
|
|
- <text class="expand-icon">→</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 章节列表(树形结构,整合大纲信息) -->
|
|
|
- <view class="content-tree-list">
|
|
|
- <!-- 章 (只遍历level=1) -->
|
|
|
- <view
|
|
|
- v-for="chapter in chaptersLevel1"
|
|
|
- :key="chapter.id"
|
|
|
- class="content-tree-item content-chapter"
|
|
|
- >
|
|
|
- <view class="content-tree-row chapter-row">
|
|
|
- <view class="chapter-num-badge">{{ chapter.number }}</view>
|
|
|
- <view class="chapter-info">
|
|
|
- <text class="chapter-title">{{ chapter.title }}</text>
|
|
|
- <!-- 显示大纲概要 -->
|
|
|
- <text v-if="getChapterOutline(chapter.number)?.summary" class="chapter-outline-summary">
|
|
|
- {{ getChapterOutline(chapter.number)?.summary }}
|
|
|
- </text>
|
|
|
- <!-- 显示核心知识点 -->
|
|
|
- <view v-if="getChapterOutline(chapter.number)?.keyPoints" class="chapter-key-points">
|
|
|
- <text
|
|
|
- v-for="(point, idx) in getChapterOutline(chapter.number)?.keyPoints?.slice(0, 3) || []"
|
|
|
- :key="idx"
|
|
|
- class="point-tag"
|
|
|
- >
|
|
|
- {{ point }}
|
|
|
- </text>
|
|
|
- </view>
|
|
|
- <!-- 媒体标识 -->
|
|
|
- <view class="chapter-media-badges">
|
|
|
- <text v-if="chapter.audioUrl" class="media-badge">🎵 音频</text>
|
|
|
- <text v-if="chapter.videoUrl" class="media-badge">🎬 视频</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- <!-- 章状态 -->
|
|
|
- <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="getChapterSectionsFromDB(chapter.id) && getChapterSectionsFromDB(chapter.id).length > 0" class="content-sections-list">
|
|
|
- <view
|
|
|
- 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">{{ 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>
|
|
|
-
|
|
|
- <!-- 小节(从数据库获取) -->
|
|
|
- <view v-if="getSectionSubsectionsFromDB(section.id) && getSectionSubsectionsFromDB(section.id).length > 0" class="content-subsections-list">
|
|
|
- <view
|
|
|
- v-for="subsection in getSectionSubsectionsFromDB(section.id)"
|
|
|
- :key="'sub-' + subsection.id"
|
|
|
- class="content-tree-item content-subsection"
|
|
|
- @click="goToChapterDetail(subsection)"
|
|
|
- >
|
|
|
- <view class="content-tree-row subsection-row">
|
|
|
- <view class="subsection-num-badge">{{ subsection.number }}</view>
|
|
|
- <view class="subsection-info">
|
|
|
- <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="['subsection-status', getNodeStatusClass(subsection)]">
|
|
|
- <text class="status-icon">{{ getNodeStatusIcon(subsection) }}</text>
|
|
|
- <text class="status-text">{{ getNodeStatusText(subsection) }}</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </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">{{ chapter.title }}</text>
|
|
|
- </view>
|
|
|
- <view class="content-item-right">
|
|
|
- <text class="expand-icon">→</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 后记 -->
|
|
|
- <view v-if="currentBook?.metadata?.afterword" class="content-item afterword" @click="viewAfterword">
|
|
|
- <view class="content-item-left">
|
|
|
- <text class="content-label">后记</text>
|
|
|
- <text class="content-word-count">{{ currentBook?.metadata?.afterword?.length || 0 }}字</text>
|
|
|
- </view>
|
|
|
- <view class="content-item-right">
|
|
|
- <text class="expand-icon">→</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 操作按钮 -->
|
|
|
- <view class="card action-card">
|
|
|
- <!-- LangGraph 生成按钮 -->
|
|
|
- <view class="btn-col">
|
|
|
- <view class="btn-row">
|
|
|
- <button
|
|
|
- class="action-btn langgraph-btn"
|
|
|
- :disabled="generating"
|
|
|
- @click="handleLangGraphGenerate"
|
|
|
- >
|
|
|
- {{ generating ? '生成中...' : '🤖 开始生成' }}
|
|
|
- </button>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 生成中 -->
|
|
|
- <view v-if="generating" class="generating-tip">
|
|
|
- <text>正在生成中,请稍候...</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 无章节警告 -->
|
|
|
- <view v-if="currentBook?.status === 'completed' && (!currentBook.chapters || currentBook.chapters.length === 0)" class="warning-tip">
|
|
|
- <text class="warning-icon">⚠️</text>
|
|
|
- <text class="warning-text">该书籍状态标记为已完成,但没有任何章节内容。</text>
|
|
|
- <text class="warning-hint">请尝试点击上方按钮重新生成。</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 返回列表 -->
|
|
|
- <button class="action-btn full-width" @click="backToList">
|
|
|
- 返回书籍列表
|
|
|
- </button>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
|
|
+import { ref, onMounted } 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';
|
|
|
-
|
|
|
-// 视图状态
|
|
|
-const currentView = ref<'list' | 'create' | 'detail' | 'toc' | 'chapter-detail'>('list');
|
|
|
-
|
|
|
-// 监听视图变化
|
|
|
-watch(currentView, (newView) => {
|
|
|
- if (newView === 'create') {
|
|
|
- // 打开创建视图时,加载默认规模的预估
|
|
|
- nextTick(() => {
|
|
|
- selectBookScale(newBook.value.bookScale);
|
|
|
- });
|
|
|
- }
|
|
|
-});
|
|
|
+import type { Book } from '../../utils/book-generator-api';
|
|
|
|
|
|
// 书籍列表
|
|
|
const books = ref<Book[]>([]);
|
|
|
+const isLoadingBooks = ref(false);
|
|
|
|
|
|
-// 当前书籍
|
|
|
-const currentBook = ref<Book | null>(null);
|
|
|
-
|
|
|
-// 创建表单
|
|
|
-const showCreateModal = ref(false);
|
|
|
-const creating = ref(false);
|
|
|
-const newBook = ref({
|
|
|
- title: '',
|
|
|
- subtitle: '',
|
|
|
- description: '',
|
|
|
- targetAudience: '',
|
|
|
- knowledgeLevel: '', // 知识难度
|
|
|
- industry: '', // 行业领域
|
|
|
- specialFeatures: [] as string[], // 特殊要求(多选)
|
|
|
- style: '',
|
|
|
- bookScale: 'medium',
|
|
|
-});
|
|
|
-
|
|
|
-// 生成状态
|
|
|
-const generating = ref(false);
|
|
|
-
|
|
|
-// 书籍预估信息
|
|
|
-const bookEstimate = ref<{
|
|
|
- scale: string;
|
|
|
- words: { min: number; max: number; avg: number };
|
|
|
- audioMinutes: { min: number; max: number; avg: number };
|
|
|
- estimatedChapters: number;
|
|
|
-} | null>(null);
|
|
|
-
|
|
|
-// 配额检查结果
|
|
|
-const quotaCheck = ref<{
|
|
|
- allowed: boolean;
|
|
|
- reason: string | null;
|
|
|
- estimatedWords: { min: number; max: number; avg: number };
|
|
|
- estimatedAudioMinutes: { min: number; max: number; avg: number };
|
|
|
- quota: {
|
|
|
- totalMinutes: number;
|
|
|
- usedMinutes: number;
|
|
|
- remainingMinutes: number;
|
|
|
- overageEnabled: boolean;
|
|
|
- overagePrice: number;
|
|
|
- };
|
|
|
- costEstimate: {
|
|
|
- inQuotaMinutes: number;
|
|
|
- overageMinutes: number;
|
|
|
- estimatedPrice: number;
|
|
|
- displayText: string;
|
|
|
- };
|
|
|
-} | null>(null);
|
|
|
-
|
|
|
-// 音频生成状态
|
|
|
-const generatingAudio = ref(false);
|
|
|
-
|
|
|
-// 视频生成状态
|
|
|
-const generatingVideo = ref(false);
|
|
|
-
|
|
|
-// 书籍公开状态
|
|
|
-const togglingPublish = ref<Record<string, boolean>>({});
|
|
|
-
|
|
|
-// 选项配置
|
|
|
-// 知识难度选项
|
|
|
-const knowledgeLevels = [
|
|
|
- { value: '入门', label: '入门级', icon: '🌱', desc: '零基础,简单易懂' },
|
|
|
- { value: '基础', label: '基础级', icon: '📚', desc: '基本概念,循序渐进' },
|
|
|
- { value: '进阶', label: '进阶级', icon: '📈', desc: '深入讲解,系统全面' },
|
|
|
- { value: '高级', label: '高级级', icon: '🎯', desc: '专业深入,前沿技术' },
|
|
|
- { value: '专家', label: '专家级', icon: '🔬', desc: '研究级别,底层原理' },
|
|
|
-];
|
|
|
-
|
|
|
-// 面向人群选项
|
|
|
-const audiences = [
|
|
|
- { value: '儿童', label: '儿童', icon: '👶', desc: '6-12岁,形象生动' },
|
|
|
- { value: '青少年', label: '青少年', icon: '🧑', desc: '13-18岁,通俗易懂' },
|
|
|
- { value: '大学生', label: '大学生', icon: '🎓', desc: '系统专业,有理论' },
|
|
|
- { value: '专业人士', label: '专业人士', icon: '💼', desc: '从业者,实用深入' },
|
|
|
- { value: '研究生', label: '研究生', icon: '🔍', desc: '研究级别,前沿深入' },
|
|
|
- { value: '大众读者', label: '大众读者', icon: '👥', desc: '通俗普及,有趣味' },
|
|
|
-];
|
|
|
-
|
|
|
-// 行业领域选项
|
|
|
-const industries = [
|
|
|
- { value: 'IT/计算机', label: 'IT/计算机', icon: '💻', desc: '编程、软件、互联网' },
|
|
|
- { value: '金融', label: '金融', icon: '💰', desc: '投资、银行、证券' },
|
|
|
- { value: '医学', label: '医学', icon: '🏥', desc: '医疗、健康、临床' },
|
|
|
- { value: '教育', label: '教育', icon: '📚', desc: '教学、学习、心理学' },
|
|
|
- { value: '文学', label: '文学', icon: '✍️', desc: '小说、散文、传记' },
|
|
|
- { value: '科普', label: '科普', icon: '🔬', desc: '科学、自然、探索' },
|
|
|
- { value: '商业', label: '商业', icon: '📊', desc: '管理、营销、创业' },
|
|
|
- { value: '艺术', label: '艺术', icon: '🎨', desc: '设计、音乐、影视' },
|
|
|
-];
|
|
|
-
|
|
|
-// 特殊要求选项
|
|
|
-const specialFeatures = [
|
|
|
- { value: '案例分析', label: '案例分析', icon: '📖' },
|
|
|
- { value: '代码示例', label: '代码示例', icon: '💻' },
|
|
|
- { value: '习题练习', label: '习题练习', icon: '✍️' },
|
|
|
- { value: '图表插图', label: '图表插图', icon: '📊' },
|
|
|
- { value: '项目实战', label: '项目实战', icon: '🎯' },
|
|
|
- { value: '思维导图', label: '思维导图', icon: '🧠' },
|
|
|
- { value: '总结回顾', label: '总结回顾', icon: '📝' },
|
|
|
-];
|
|
|
-
|
|
|
-// 快速模板
|
|
|
-const quickTemplates = [
|
|
|
- {
|
|
|
- name: '大学教材',
|
|
|
- icon: '🎓',
|
|
|
- desc: '面向大学生,系统专业,有案例习题',
|
|
|
- config: {
|
|
|
- knowledgeLevel: '进阶',
|
|
|
- targetAudience: '大学生',
|
|
|
- industry: '教育',
|
|
|
- specialFeatures: ['案例分析', '习题练习', '总结回顾'],
|
|
|
- style: '专业严谨',
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- name: '技术教程',
|
|
|
- icon: '💻',
|
|
|
- desc: '面向初学者,有代码示例和实战项目',
|
|
|
- config: {
|
|
|
- knowledgeLevel: '入门',
|
|
|
- targetAudience: '初学者',
|
|
|
- industry: 'IT/计算机',
|
|
|
- specialFeatures: ['代码示例', '项目实战', '案例分析'],
|
|
|
- style: '通俗易懂',
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- name: '科普读物',
|
|
|
- icon: '🔬',
|
|
|
- desc: '面向大众,通俗易懂,有趣味性',
|
|
|
- config: {
|
|
|
- knowledgeLevel: '入门',
|
|
|
- targetAudience: '大众读者',
|
|
|
- industry: '科普',
|
|
|
- specialFeatures: ['案例分析', '图表插图'],
|
|
|
- style: '轻松幽默',
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- name: '商业书籍',
|
|
|
- icon: '📊',
|
|
|
- desc: '面向从业者,有真实案例,实用性强',
|
|
|
- config: {
|
|
|
- knowledgeLevel: '进阶',
|
|
|
- targetAudience: '专业人士',
|
|
|
- industry: '商业',
|
|
|
- specialFeatures: ['案例分析', '总结回顾'],
|
|
|
- style: '专业严谨',
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- name: '儿童读物',
|
|
|
- icon: '👶',
|
|
|
- desc: '面向儿童,形象生动,有趣味性',
|
|
|
- config: {
|
|
|
- knowledgeLevel: '入门',
|
|
|
- targetAudience: '儿童',
|
|
|
- industry: '教育',
|
|
|
- specialFeatures: ['图表插图'],
|
|
|
- style: '轻松幽默',
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- name: '学术研究',
|
|
|
- icon: '🔍',
|
|
|
- desc: '面向研究生,前沿深入,有研究价值',
|
|
|
- config: {
|
|
|
- knowledgeLevel: '专家',
|
|
|
- targetAudience: '研究生',
|
|
|
- industry: 'IT/计算机',
|
|
|
- specialFeatures: ['案例分析', '总结回顾'],
|
|
|
- style: '专业严谨',
|
|
|
- },
|
|
|
- },
|
|
|
-];
|
|
|
-
|
|
|
-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(() => {
|
|
|
- // 免费版需要检查配额
|
|
|
- if (quotaCheck.value && !quotaCheck.value.allowed) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- 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;
|
|
|
-});
|
|
|
-
|
|
|
-// 过滤出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;
|
|
|
- const totalWords = currentBook.value.chapters
|
|
|
- .filter((c) => c.status === 'completed')
|
|
|
- .reduce((sum, c) => sum + (c.wordCount || 0), 0);
|
|
|
- return Math.ceil(totalWords / 150); // 150字/分钟
|
|
|
-});
|
|
|
-
|
|
|
-// 计算剩余配额
|
|
|
-const remainingAudioMinutes = computed(() => {
|
|
|
- if (!quotaInfo.value) return 0;
|
|
|
- return Math.max(0, quotaInfo.value.totalMinutes - quotaInfo.value.usedMinutes);
|
|
|
-});
|
|
|
-
|
|
|
-// 计算超出配额分钟数
|
|
|
-const overageMinutes = computed(() => {
|
|
|
- if (!currentBook.value) return 0;
|
|
|
- const used = usedAudioMinutes.value;
|
|
|
- if (!quotaInfo.value) return 0;
|
|
|
- return Math.max(0, used - quotaInfo.value.totalMinutes);
|
|
|
-});
|
|
|
-
|
|
|
-// 配额信息(从 API 获取)
|
|
|
-const quotaInfo = ref<{
|
|
|
- totalMinutes: number;
|
|
|
- usedMinutes: number;
|
|
|
- remainingMinutes: number;
|
|
|
- overageEnabled: boolean;
|
|
|
-} | null>(null);
|
|
|
-
|
|
|
-// 配额加载状态
|
|
|
-let quotaLoading = false;
|
|
|
-let quotaLastLoadTime = 0;
|
|
|
-const QUOTA_CACHE_DURATION = 30000; // 30秒缓存;
|
|
|
+// 音频/视频生成状态
|
|
|
+const generatingAudio = ref<Record<string, boolean> >({});
|
|
|
+const generatingVideo = ref<Record<string, boolean> >({});
|
|
|
+const mergingAudio = ref<Record<string, boolean> >({});
|
|
|
+const mergingVideo = ref<Record<string, boolean> >({});
|
|
|
+const togglingPublish = ref<Record<string, boolean> >({});
|
|
|
|
|
|
-// 切换特殊要求
|
|
|
-function toggleSpecialFeature(feature: string) {
|
|
|
- const features = newBook.value.specialFeatures;
|
|
|
- const index = features.indexOf(feature);
|
|
|
- if (index > -1) {
|
|
|
- features.splice(index, 1);
|
|
|
- } else {
|
|
|
- features.push(feature);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 应用模板
|
|
|
-function applyTemplate(template: typeof quickTemplates[0]) {
|
|
|
- const config = template.config;
|
|
|
- newBook.value.knowledgeLevel = config.knowledgeLevel;
|
|
|
- newBook.value.targetAudience = config.targetAudience;
|
|
|
- newBook.value.industry = config.industry;
|
|
|
- newBook.value.specialFeatures = [...config.specialFeatures];
|
|
|
- newBook.value.style = config.style;
|
|
|
-
|
|
|
- uni.showToast({
|
|
|
- title: `已应用${template.name}模板`,
|
|
|
- icon: 'success',
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 选择书籍规模时加载预估
|
|
|
-async function selectBookScale(scale: string) {
|
|
|
- newBook.value.bookScale = scale;
|
|
|
-
|
|
|
- try {
|
|
|
- const response = await uni.request({
|
|
|
- url: `/api/book-generator/langgraph/estimate?scale=${encodeURIComponent(scale)}&userId=1`,
|
|
|
- method: 'GET'
|
|
|
- });
|
|
|
-
|
|
|
- const res = response.data as any;
|
|
|
- if (res.code === 0 && res.data && res.data.words && res.data.audioMinutes) {
|
|
|
- bookEstimate.value = {
|
|
|
- scale: res.data.scale,
|
|
|
- words: res.data.words,
|
|
|
- audioMinutes: res.data.audioMinutes,
|
|
|
- estimatedChapters: res.data.estimatedChapters
|
|
|
- };
|
|
|
-
|
|
|
- // 如果有配额检查结果
|
|
|
- if (res.data.quotaCheck) {
|
|
|
- quotaCheck.value = res.data.quotaCheck;
|
|
|
- }
|
|
|
- } else {
|
|
|
- console.warn('预估数据不完整:', res);
|
|
|
- bookEstimate.value = null;
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- console.error('加载预估失败:', e);
|
|
|
- bookEstimate.value = null;
|
|
|
- quotaCheck.value = null;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 从大纲中获取章节的 sections
|
|
|
-function getChapterSections(chapterNumber: number) {
|
|
|
- if (!currentBook.value?.outline?.chapters) return [];
|
|
|
- const chapter = currentBook.value.outline.chapters.find(c => c.number === chapterNumber);
|
|
|
- return chapter?.sections || [];
|
|
|
-}
|
|
|
-
|
|
|
-// 从大纲中获取章节信息(包含summary和keyPoints)
|
|
|
-function getChapterOutline(chapterNumber: number) {
|
|
|
- if (!currentBook.value?.outline?.chapters) return null;
|
|
|
- return currentBook.value.outline.chapters.find(c => c.number === chapterNumber) || null;
|
|
|
-}
|
|
|
-
|
|
|
-// 方法
|
|
|
function goBack() {
|
|
|
- // 如果在详情页,返回列表;否则返回上一页
|
|
|
- if (currentView.value === 'detail') {
|
|
|
- backToList();
|
|
|
- } else {
|
|
|
+ const pages = getCurrentPages();
|
|
|
+ if (pages.length > 1) {
|
|
|
uni.navigateBack();
|
|
|
+ } else {
|
|
|
+ uni.switchTab({ url: '/pages/index/index' });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function switchTab() {
|
|
|
- uni.switchTab({ url: '/pages/index/index' });
|
|
|
+ uni.switchTab({ url: '/pages/book-generator/index' });
|
|
|
+}
|
|
|
+
|
|
|
+function goToCreate() {
|
|
|
+ uni.navigateTo({ url: '/pages/book-generator/create' });
|
|
|
}
|
|
|
|
|
|
function goToVideoGenerator() {
|
|
|
@@ -966,2765 +171,183 @@ 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 getBookLeafLevel(book: Book): number {
|
|
|
+ const chapters = book.chapters || [];
|
|
|
+ if (chapters.length === 0) return 0;
|
|
|
+ const maxLevel = Math.max(...chapters.map(c => c.level || 0));
|
|
|
+ return maxLevel > 0 ? maxLevel : 1;
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 获取节点状态文本
|
|
|
- */
|
|
|
-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 '待生成';
|
|
|
+function getBookLeafNodes(book: Book, leafLevel: number): any[] {
|
|
|
+ return (book.chapters || []).filter(c => c.level === leafLevel);
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 从数据库获取章的节列表(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;
|
|
|
+function getBookAudioStatus(book: Book) {
|
|
|
+ const leafLevel = getBookLeafLevel(book);
|
|
|
+ const leafNodes = getBookLeafNodes(book, leafLevel);
|
|
|
+ const total = leafNodes.length;
|
|
|
+ const completed = leafNodes.filter(n => n.audioUrl).length;
|
|
|
+ let status: 'none' | 'partial' | 'completed' = 'none';
|
|
|
+ if (completed === total && total > 0) status = 'completed';
|
|
|
+ else if (completed > 0) status = 'partial';
|
|
|
+ if (generatingAudio.value[book.id]) status = 'generating';
|
|
|
+ return { total, completed, status };
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 从数据库获取节的小节列表(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;
|
|
|
+function getBookVideoStatus(book: Book) {
|
|
|
+ const leafLevel = getBookLeafLevel(book);
|
|
|
+ const leafNodes = getBookLeafNodes(book, leafLevel);
|
|
|
+ const total = leafNodes.length;
|
|
|
+ const completed = leafNodes.filter(n => n.videoUrl).length;
|
|
|
+ let status: 'none' | 'partial' | 'completed' = 'none';
|
|
|
+ if (completed === total && total > 0) status = 'completed';
|
|
|
+ else if (completed > 0) status = 'partial';
|
|
|
+ if (generatingVideo.value[book.id]) status = 'generating';
|
|
|
+ return { total, completed, status };
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 重新生成
|
|
|
- */
|
|
|
-async function handleRetryGenerate() {
|
|
|
- if (!currentBook.value) return;
|
|
|
-
|
|
|
- uni.showModal({
|
|
|
- title: '确认重新生成',
|
|
|
- content: '将重新开始生成,是否继续?',
|
|
|
- success: async (res) => {
|
|
|
- if (res.confirm) {
|
|
|
- // 调用LangGraph生成
|
|
|
- await handleLangGraphGenerate();
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
+function canMergeAudio(book: Book): boolean {
|
|
|
+ const leafLevel = getBookLeafLevel(book);
|
|
|
+ if (leafLevel <= 1) return false;
|
|
|
+ const leafNodes = getBookLeafNodes(book, leafLevel);
|
|
|
+ return leafNodes.every(n => n.audioUrl) && leafNodes.length > 0;
|
|
|
}
|
|
|
|
|
|
-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 canMergeVideo(book: Book): boolean {
|
|
|
+ const leafLevel = getBookLeafLevel(book);
|
|
|
+ if (leafLevel <= 1) return false;
|
|
|
+ const leafNodes = getBookLeafNodes(book, leafLevel);
|
|
|
+ return leafNodes.every(n => n.videoUrl) && leafNodes.length > 0;
|
|
|
}
|
|
|
|
|
|
-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() {
|
|
|
+ if (isLoadingBooks.value) return;
|
|
|
+ isLoadingBooks.value = true;
|
|
|
try {
|
|
|
- const data = await api.getBooks();
|
|
|
- books.value = data || [];
|
|
|
+ books.value = await api.getBooks() || [];
|
|
|
} catch (e) {
|
|
|
console.error('加载书籍失败:', e);
|
|
|
books.value = [];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 创建新书籍
|
|
|
-async function createNewBook() {
|
|
|
- if (!canCreateBook.value) return;
|
|
|
-
|
|
|
- // 显示确认弹窗
|
|
|
- const configSummary: string[] = [];
|
|
|
- if (newBook.value.knowledgeLevel) configSummary.push(`知识难度:${newBook.value.knowledgeLevel}`);
|
|
|
- if (newBook.value.targetAudience) configSummary.push(`面向人群:${newBook.value.targetAudience}`);
|
|
|
- if (newBook.value.industry) configSummary.push(`行业领域:${newBook.value.industry}`);
|
|
|
- if (newBook.value.specialFeatures.length > 0) configSummary.push(`特殊要求:${newBook.value.specialFeatures.join('、')}`);
|
|
|
- if (newBook.value.style) configSummary.push(`写作风格:${newBook.value.style}`);
|
|
|
-
|
|
|
- const confirmResult = await new Promise<boolean>((resolve) => {
|
|
|
- uni.showModal({
|
|
|
- title: '📋 生成配置确认',
|
|
|
- content: `书名:《${newBook.value.title}》\n\n${configSummary.join('\n')}\n\n确认开始生成?`,
|
|
|
- confirmText: '确认生成',
|
|
|
- cancelText: '返回修改',
|
|
|
- success: (res) => resolve(res.confirm),
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- if (!confirmResult) return;
|
|
|
-
|
|
|
- creating.value = true;
|
|
|
- try {
|
|
|
- // 将目标受众、知识难度、行业领域、特殊要求和写作风格合并到描述中,AI 生成时直接读取
|
|
|
- let mergedDesc = newBook.value.description.trim();
|
|
|
- if (newBook.value.knowledgeLevel && newBook.value.knowledgeLevel !== '不填') {
|
|
|
- mergedDesc += `\n知识难度:${newBook.value.knowledgeLevel}`;
|
|
|
- }
|
|
|
- if (newBook.value.targetAudience && newBook.value.targetAudience !== '不填') {
|
|
|
- mergedDesc += `\n面向人群:${newBook.value.targetAudience}`;
|
|
|
- }
|
|
|
- if (newBook.value.industry && newBook.value.industry !== '不填') {
|
|
|
- mergedDesc += `\n行业领域:${newBook.value.industry}`;
|
|
|
- }
|
|
|
- if (newBook.value.specialFeatures && newBook.value.specialFeatures.length > 0) {
|
|
|
- mergedDesc += `\n特殊要求:${newBook.value.specialFeatures.join('、')}`;
|
|
|
- }
|
|
|
- if (newBook.value.style && newBook.value.style !== '不填') {
|
|
|
- mergedDesc += `\n写作风格:${newBook.value.style}`;
|
|
|
- }
|
|
|
-
|
|
|
- await api.createBook({
|
|
|
- title: newBook.value.title,
|
|
|
- subtitle: newBook.value.subtitle,
|
|
|
- description: mergedDesc,
|
|
|
- bookScale: newBook.value.bookScale,
|
|
|
- });
|
|
|
-
|
|
|
- // 重置表单
|
|
|
- newBook.value = {
|
|
|
- title: '',
|
|
|
- subtitle: '',
|
|
|
- description: '',
|
|
|
- targetAudience: '',
|
|
|
- knowledgeLevel: '',
|
|
|
- industry: '',
|
|
|
- specialFeatures: [],
|
|
|
- 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;
|
|
|
+ isLoadingBooks.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 打开书籍
|
|
|
async function openBook(book: Book) {
|
|
|
- try {
|
|
|
- currentBook.value = await api.getBook(book.id);
|
|
|
- currentView.value = 'detail';
|
|
|
- // 更新 URL hash,带上书籍 ID
|
|
|
- updateUrlWithBookId(book.id);
|
|
|
- // 加载配额信息
|
|
|
- loadQuotaInfo();
|
|
|
- } catch (e) {
|
|
|
- uni.showToast({ title: '加载失败', icon: 'none' });
|
|
|
- }
|
|
|
+ uni.navigateTo({ url: `/pages/book-generator/detail?id=${book.id}` });
|
|
|
}
|
|
|
|
|
|
-// 更新 URL,带上书籍 ID
|
|
|
-function updateUrlWithBookId(bookId: string) {
|
|
|
- if (typeof window !== 'undefined') {
|
|
|
- const url = new URL(window.location.href);
|
|
|
- url.hash = `/pages/book-generator/index?id=${bookId}`;
|
|
|
- window.history.pushState({}, '', url.toString());
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 从 URL 获取书籍 ID
|
|
|
-function getBookIdFromUrl(): string | null {
|
|
|
- if (typeof window !== 'undefined') {
|
|
|
- const hash = window.location.hash;
|
|
|
- const match = hash.match(/id=([^&]+)/);
|
|
|
- return match ? match[1] : null;
|
|
|
- }
|
|
|
- return null;
|
|
|
-}
|
|
|
-
|
|
|
-// 加载指定 ID 的书籍
|
|
|
-async function loadBookById(bookId: string) {
|
|
|
+async function handleGenerateAllAudio(book: Book) {
|
|
|
+ generatingAudio.value[book.id] = true;
|
|
|
try {
|
|
|
- currentBook.value = await api.getBook(bookId);
|
|
|
- currentView.value = 'detail';
|
|
|
- loadQuotaInfo();
|
|
|
- } catch (e) {
|
|
|
- console.error('加载书籍失败:', e);
|
|
|
- uni.showToast({ title: '加载失败', icon: 'none' });
|
|
|
+ const result = await api.generateAllChaptersAudio(book.id, 'cherry');
|
|
|
+ uni.showToast({ title: `已启动 ${result.totalChapters} 个章节的音频生成`, icon: 'none', duration: 2500 });
|
|
|
+ setTimeout(async () => { await loadBooks(); generatingAudio.value[book.id] = false; }, 5000);
|
|
|
+ } catch (e: any) {
|
|
|
+ generatingAudio.value[book.id] = false;
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 返回列表
|
|
|
-function backToList() {
|
|
|
- currentView.value = 'list';
|
|
|
- loadBooks();
|
|
|
- // 清除 URL 参数
|
|
|
- if (typeof window !== 'undefined') {
|
|
|
- const url = new URL(window.location.href);
|
|
|
- url.hash = '/pages/book-generator/index';
|
|
|
- window.history.pushState({}, '', url.toString());
|
|
|
+async function handleMergeChapterAudio(book: Book) {
|
|
|
+ if (!canMergeAudio(book)) { uni.showToast({ title: '并非所有叶节点音频都已生成完成', icon: 'none' }); return; }
|
|
|
+ mergingAudio.value[book.id] = true;
|
|
|
+ try {
|
|
|
+ const result = await api.mergeChapterAudios(book.id);
|
|
|
+ uni.showToast({ title: `已启动音频合并,处理${result.processedParents}个上级章节`, icon: 'none', duration: 2500 });
|
|
|
+ setTimeout(async () => { await loadBooks(); mergingAudio.value[book.id] = false; }, 5000);
|
|
|
+ } catch (e: any) {
|
|
|
+ mergingAudio.value[book.id] = false;
|
|
|
+ uni.showToast({ title: e.message || '音频合并失败', icon: 'none' });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 页面显示时检查 URL 参数
|
|
|
-onShow(() => {
|
|
|
- const bookId = getBookIdFromUrl();
|
|
|
- if (bookId && currentView.value === 'list') {
|
|
|
- loadBookById(bookId);
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-// 加载用户配额信息(带缓存和防抖)
|
|
|
-async function loadQuotaInfo() {
|
|
|
- // 防止并发请求
|
|
|
- if (quotaLoading) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 检查缓存是否有效(30秒内不重复请求)
|
|
|
- const now = Date.now();
|
|
|
- if (quotaInfo.value && (now - quotaLastLoadTime) < QUOTA_CACHE_DURATION) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- quotaLoading = true;
|
|
|
+async function handleMergeChapterVideo(book: Book) {
|
|
|
+ if (!canMergeVideo(book)) { uni.showToast({ title: '并非所有叶节点视频都已生成完成', icon: 'none' }); return; }
|
|
|
+ mergingVideo.value[book.id] = true;
|
|
|
try {
|
|
|
- const response = await uni.request({
|
|
|
- url: '/api/subscription/audio-balance',
|
|
|
- method: 'GET'
|
|
|
- });
|
|
|
-
|
|
|
- const res = response.data as any;
|
|
|
- if (res.code === 0 && res.data) {
|
|
|
- quotaInfo.value = {
|
|
|
- totalMinutes: res.data.totalMinutes,
|
|
|
- usedMinutes: res.data.usedMinutes,
|
|
|
- remainingMinutes: res.data.remainingMinutes,
|
|
|
- overageEnabled: res.data.overageEnabled
|
|
|
- };
|
|
|
- quotaLastLoadTime = now;
|
|
|
- } else if (res.code === 429) {
|
|
|
- // 如果是 429 错误,等待更长时间再重试
|
|
|
- console.warn('配额接口请求过于频繁,稍后重试');
|
|
|
- quotaLastLoadTime = now; // 更新时间戳,避免立即重试
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- console.error('加载配额信息失败:', e);
|
|
|
- } finally {
|
|
|
- quotaLoading = false;
|
|
|
+ const result = await api.mergeChapterVideos(book.id);
|
|
|
+ uni.showToast({ title: `已启动视频合并,处理${result.processedParents}个上级章节`, icon: 'none', duration: 2500 });
|
|
|
+ setTimeout(async () => { await loadBooks(); mergingVideo.value[book.id] = false; }, 5000);
|
|
|
+ } catch (e: any) {
|
|
|
+ mergingVideo.value[book.id] = false;
|
|
|
+ uni.showToast({ title: e.message || '视频合并失败', icon: 'none' });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 生成大纲
|
|
|
-async function handleGenerateOutline() {
|
|
|
- if (!currentBook.value) return;
|
|
|
-
|
|
|
- generating.value = true;
|
|
|
+async function handleGenerateAllVideo(book: Book) {
|
|
|
+ generatingVideo.value[book.id] = 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' });
|
|
|
+ const result = await api.generateAllChaptersVideo(book.id);
|
|
|
+ uni.showToast({ title: `已启动 ${result.totalChapters} 个章节的视频生成`, icon: 'none', duration: 2500 });
|
|
|
+ setTimeout(async () => { await loadBooks(); generatingVideo.value[book.id] = false; }, 5000);
|
|
|
} catch (e: any) {
|
|
|
+ generatingVideo.value[book.id] = false;
|
|
|
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;
|
|
|
-
|
|
|
+async function handleTogglePublish(book: any) {
|
|
|
+ togglingPublish.value[book.id] = true;
|
|
|
try {
|
|
|
- // 启动异步生成任务(立即返回)
|
|
|
- await api.generateBook(currentBook.value.id, {
|
|
|
- generateForeword: true,
|
|
|
- generateAfterword: true,
|
|
|
- });
|
|
|
-
|
|
|
- // 显示提示
|
|
|
- uni.showToast({ title: '生成任务已启动,请稍候...', icon: 'none', duration: 2000 });
|
|
|
-
|
|
|
- // 开始轮询进度
|
|
|
- startPollingProgress(currentBook.value.id);
|
|
|
+ const res = await api.toggleBookPublish(book.id);
|
|
|
+ book.isPublished = res.isPublished;
|
|
|
+ uni.showToast({ title: book.isPublished ? '已公开到首页' : '已取消公开', icon: 'none' });
|
|
|
} catch (e: any) {
|
|
|
- generating.value = false;
|
|
|
- currentBook.value.status = 'failed';
|
|
|
- uni.showToast({ title: e.message || '启动失败', icon: 'none' });
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ togglingPublish.value[book.id] = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 轮询进度
|
|
|
-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);
|
|
|
+onMounted(() => {
|
|
|
+ loadBooks();
|
|
|
+});
|
|
|
|
|
|
- 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秒轮询一次
|
|
|
-}
|
|
|
+onShow(() => {
|
|
|
+ loadBooks();
|
|
|
+});
|
|
|
+</script>
|
|
|
|
|
|
-// 停止轮询
|
|
|
-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;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 跳转到章节详情页
|
|
|
-function goToChapterDetail(chapter: Chapter, sectionIndex?: number, subsectionIndex?: number) {
|
|
|
- if (!currentBook.value) return;
|
|
|
- // 跳转到独立的章节详情页面,使用数据库中的真实ID
|
|
|
- uni.navigateTo({
|
|
|
- url: `/pages/book-generator/chapter-detail?bookId=${encodeURIComponent(currentBook.value.id)}&chapter=${chapter.id}`
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 查看前言
|
|
|
-function viewForeword() {
|
|
|
- uni.showModal({
|
|
|
- title: '前言',
|
|
|
- content: currentBook.value?.metadata?.foreword || '',
|
|
|
- showCancel: true,
|
|
|
- cancelText: '关闭',
|
|
|
- confirmText: '复制',
|
|
|
- success: (res) => {
|
|
|
- if (res.confirm) {
|
|
|
- uni.setClipboardData({
|
|
|
- data: currentBook.value?.metadata?.foreword || '',
|
|
|
- success: () => {
|
|
|
- uni.showToast({ title: '已复制', icon: 'success' });
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 查看后记
|
|
|
-function viewAfterword() {
|
|
|
- uni.showModal({
|
|
|
- title: '后记',
|
|
|
- content: currentBook.value?.metadata?.afterword || '',
|
|
|
- showCancel: true,
|
|
|
- cancelText: '关闭',
|
|
|
- confirmText: '复制',
|
|
|
- success: (res) => {
|
|
|
- if (res.confirm) {
|
|
|
- uni.setClipboardData({
|
|
|
- data: currentBook.value?.metadata?.afterword || '',
|
|
|
- success: () => {
|
|
|
- uni.showToast({ title: '已复制', icon: 'success' });
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// ============ 音频生成 ============
|
|
|
-
|
|
|
-/**
|
|
|
- * 生成单个章节音频
|
|
|
- */
|
|
|
-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;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 切换书籍公开状态
|
|
|
-async function handleTogglePublish(book: any) {
|
|
|
- togglingPublish.value[book.id] = true;
|
|
|
- try {
|
|
|
- const res = await api.toggleBookPublish(book.id);
|
|
|
- book.isPublished = res.isPublished;
|
|
|
- uni.showToast({
|
|
|
- title: book.isPublished ? '已公开到首页' : '已取消公开',
|
|
|
- icon: 'none'
|
|
|
- });
|
|
|
- } catch (e: any) {
|
|
|
- uni.showToast({ title: e.message || '操作失败', icon: 'none' });
|
|
|
- } finally {
|
|
|
- togglingPublish.value[book.id] = false;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 跳转到发布页面
|
|
|
-function goToPublishBook(book: Book) {
|
|
|
- // 检查是否有音频或视频
|
|
|
- const hasAudio = book.chapters?.some(c => c.audioUrl);
|
|
|
- const hasVideo = book.chapters?.some(c => c.videoUrl);
|
|
|
-
|
|
|
- if (!hasAudio && !hasVideo) {
|
|
|
- uni.showToast({
|
|
|
- title: '请先生成音频或视频',
|
|
|
- icon: 'none',
|
|
|
- duration: 2000
|
|
|
- });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 打开书籍详情页,让用户选择要发布的章节
|
|
|
- openBook(book);
|
|
|
- uni.showToast({
|
|
|
- title: '请在章节详情中点击发布',
|
|
|
- icon: 'none',
|
|
|
- duration: 2000
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 页面加载
|
|
|
-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>
|
|
|
-
|
|
|
-<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;
|
|
|
-}
|
|
|
-
|
|
|
-/* 重要表单项 */
|
|
|
-.highlight-item {
|
|
|
- background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
|
- padding: 24rpx;
|
|
|
- border-radius: 16rpx;
|
|
|
- margin-bottom: 28rpx;
|
|
|
- border: 2rpx solid #bae6fd;
|
|
|
-}
|
|
|
-
|
|
|
-.label-with-badge {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12rpx;
|
|
|
- margin-bottom: 12rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.required-badge {
|
|
|
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
|
- color: white;
|
|
|
- font-size: 20rpx;
|
|
|
- padding: 4rpx 12rpx;
|
|
|
- border-radius: 8rpx;
|
|
|
- font-weight: bold;
|
|
|
-}
|
|
|
-
|
|
|
-.form-hint {
|
|
|
- display: block;
|
|
|
- font-size: 24rpx;
|
|
|
- color: #64748b;
|
|
|
- margin-bottom: 16rpx;
|
|
|
- line-height: 1.5;
|
|
|
-}
|
|
|
-
|
|
|
-.form-hint-small {
|
|
|
- display: block;
|
|
|
- font-size: 22rpx;
|
|
|
- color: #94a3b8;
|
|
|
- margin-bottom: 12rpx;
|
|
|
- line-height: 1.4;
|
|
|
-}
|
|
|
-
|
|
|
-/* 重要提示卡片 */
|
|
|
-.important-tip {
|
|
|
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
|
- padding: 24rpx;
|
|
|
- border-radius: 16rpx;
|
|
|
- margin-bottom: 28rpx;
|
|
|
- border: 2rpx solid #f59e0b;
|
|
|
-}
|
|
|
-
|
|
|
-.tip-header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12rpx;
|
|
|
- margin-bottom: 12rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.tip-icon {
|
|
|
- font-size: 32rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.tip-title {
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: bold;
|
|
|
- color: #92400e;
|
|
|
-}
|
|
|
-
|
|
|
-.tip-content {
|
|
|
- display: block;
|
|
|
- font-size: 24rpx;
|
|
|
- color: #78350f;
|
|
|
- line-height: 1.6;
|
|
|
- margin-bottom: 12rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.tip-examples {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.tip-example {
|
|
|
- display: block;
|
|
|
- font-size: 22rpx;
|
|
|
- color: #92400e;
|
|
|
- line-height: 1.5;
|
|
|
- padding-left: 16rpx;
|
|
|
-}
|
|
|
-
|
|
|
-/* 知识难度和面向人群的chip样式 */
|
|
|
-.level-chip,
|
|
|
-.audience-chip {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8rpx;
|
|
|
- padding: 12rpx 20rpx;
|
|
|
-}
|
|
|
-
|
|
|
-/* 特殊要求chip样式(多选) */
|
|
|
-.feature-chip.active {
|
|
|
- background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
|
|
- border-color: #2563eb;
|
|
|
- color: white;
|
|
|
-}
|
|
|
-
|
|
|
-.chip-icon {
|
|
|
- font-size: 28rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.chip-text {
|
|
|
- font-size: 26rpx;
|
|
|
-}
|
|
|
-
|
|
|
-/* 快速模板网格 */
|
|
|
-.template-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, 1fr);
|
|
|
- gap: 16rpx;
|
|
|
- margin-top: 16rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.template-card {
|
|
|
- background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
|
- border: 2rpx solid #e2e8f0;
|
|
|
- border-radius: 16rpx;
|
|
|
- padding: 20rpx;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- gap: 8rpx;
|
|
|
- transition: all 0.3s;
|
|
|
-}
|
|
|
-
|
|
|
-.template-card:active {
|
|
|
- transform: scale(0.95);
|
|
|
- background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
|
|
|
- border-color: #3b82f6;
|
|
|
-}
|
|
|
-
|
|
|
-.template-icon {
|
|
|
- font-size: 48rpx;
|
|
|
- margin-bottom: 4rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.template-name {
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: bold;
|
|
|
- color: #1e293b;
|
|
|
-}
|
|
|
-
|
|
|
-.template-desc {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #64748b;
|
|
|
- text-align: center;
|
|
|
- line-height: 1.4;
|
|
|
-}
|
|
|
-
|
|
|
-.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;
|
|
|
-}
|
|
|
-
|
|
|
-/* 预估信息卡片 */
|
|
|
-.estimate-card {
|
|
|
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
|
- border-radius: 16rpx;
|
|
|
- padding: 24rpx;
|
|
|
- margin-top: 20rpx;
|
|
|
- border: 1px solid #fcd34d;
|
|
|
-}
|
|
|
-
|
|
|
-.estimate-header {
|
|
|
- margin-bottom: 16rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.estimate-title {
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #92400e;
|
|
|
-}
|
|
|
-
|
|
|
-.estimate-row {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- padding: 8rpx 0;
|
|
|
-}
|
|
|
-
|
|
|
-.estimate-label {
|
|
|
- font-size: 26rpx;
|
|
|
- color: #78350f;
|
|
|
-}
|
|
|
-
|
|
|
-.estimate-value {
|
|
|
- font-size: 26rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #92400e;
|
|
|
-}
|
|
|
-
|
|
|
-/* 配额检查结果 */
|
|
|
-.quota-check {
|
|
|
- margin-top: 16rpx;
|
|
|
- padding-top: 16rpx;
|
|
|
- border-top: 1px dashed #fcd34d;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-ok, .quota-warning {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12rpx;
|
|
|
- padding: 12rpx 16rpx;
|
|
|
- border-radius: 12rpx;
|
|
|
- margin-bottom: 12rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-ok {
|
|
|
- background: rgba(16, 185, 129, 0.1);
|
|
|
-}
|
|
|
-
|
|
|
-.quota-warning {
|
|
|
- background: rgba(245, 158, 11, 0.1);
|
|
|
-}
|
|
|
-
|
|
|
-.quota-icon {
|
|
|
- font-size: 32rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-text {
|
|
|
- font-size: 26rpx;
|
|
|
- color: #374151;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-detail {
|
|
|
- padding: 8rpx 0;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-info {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #6b7280;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-overage {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- margin-top: 8rpx;
|
|
|
- padding: 8rpx 12rpx;
|
|
|
- background: rgba(239, 68, 68, 0.1);
|
|
|
- border-radius: 8rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-overage text {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #dc2626;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-price {
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-/* 标签组 */
|
|
|
-.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;
|
|
|
-}
|
|
|
-
|
|
|
-/* 实时生成状态面板 */
|
|
|
-.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;
|
|
|
- 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;
|
|
|
-}
|
|
|
-
|
|
|
-/* 三级大纲样式 */
|
|
|
-.outline-chapter {
|
|
|
- flex-direction: column;
|
|
|
- align-items: stretch;
|
|
|
- padding: 16rpx 0;
|
|
|
- border-bottom: 2rpx solid #e5e7eb;
|
|
|
-}
|
|
|
-
|
|
|
-.outline-row {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- gap: 16rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-row {
|
|
|
- width: 100%;
|
|
|
-}
|
|
|
-
|
|
|
-.sections-list {
|
|
|
- margin-left: 64rpx;
|
|
|
- margin-top: 8rpx;
|
|
|
- padding-left: 20rpx;
|
|
|
- border-left: 2rpx solid #e5e7eb;
|
|
|
-}
|
|
|
-
|
|
|
-.outline-section {
|
|
|
- flex-direction: column;
|
|
|
- align-items: stretch;
|
|
|
- padding: 12rpx 0;
|
|
|
- border-bottom: 1px dashed #f3f4f6;
|
|
|
-}
|
|
|
-
|
|
|
-.section-num {
|
|
|
- width: 40rpx;
|
|
|
- height: 40rpx;
|
|
|
- background: #8b5cf6;
|
|
|
- color: #ffffff;
|
|
|
- border-radius: 8rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- font-size: 22rpx;
|
|
|
- font-weight: 600;
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.section-info {
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.section-title {
|
|
|
- font-size: 26rpx;
|
|
|
- font-weight: 500;
|
|
|
- color: #374151;
|
|
|
- margin-bottom: 4rpx;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-.section-summary {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #9ca3af;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-.subsections-list {
|
|
|
- margin-left: 56rpx;
|
|
|
- margin-top: 6rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.outline-subsection {
|
|
|
- padding: 8rpx 0;
|
|
|
- border-bottom: none;
|
|
|
-}
|
|
|
-
|
|
|
-.subsection-num {
|
|
|
- width: 36rpx;
|
|
|
- height: 36rpx;
|
|
|
- background: #06b6d4;
|
|
|
- color: #ffffff;
|
|
|
- border-radius: 6rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- font-size: 20rpx;
|
|
|
- font-weight: 500;
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.subsection-info {
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.subsection-title {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #6b7280;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-/* 操作卡片 */
|
|
|
-.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;
|
|
|
-}
|
|
|
-
|
|
|
-/* ==================== 完整内容区样式 ==================== */
|
|
|
-.content-card {
|
|
|
- margin-top: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.content-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 24rpx;
|
|
|
- background: #f9fafb;
|
|
|
- border-radius: 16rpx;
|
|
|
- margin-top: 16rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.content-item.foreword,
|
|
|
-.content-item.afterword {
|
|
|
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-
|
|
|
-.content-item-header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- flex: 1;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-
|
|
|
-.content-item-left {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 16rpx;
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.content-item-right {
|
|
|
- flex-shrink: 0;
|
|
|
- margin-left: 16rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.content-label {
|
|
|
- font-size: 30rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #92400e;
|
|
|
-}
|
|
|
-
|
|
|
-.content-item.foreword .content-label,
|
|
|
-.content-item.afterword .content-label {
|
|
|
- color: #92400e;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-num-badge {
|
|
|
- font-size: 24rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #ffffff;
|
|
|
- background: #4f46e5;
|
|
|
- padding: 6rpx 12rpx;
|
|
|
- border-radius: 8rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-title-short {
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: 500;
|
|
|
- color: #1f2937;
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.audio-badge,
|
|
|
-.video-badge {
|
|
|
- font-size: 24rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.content-word-count {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #9ca3af;
|
|
|
-}
|
|
|
-
|
|
|
-.expand-icon {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #9ca3af;
|
|
|
-}
|
|
|
-
|
|
|
-/* 树形内容样式 */
|
|
|
-.content-tree-list {
|
|
|
- margin-top: 20rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.content-tree-item {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- padding: 16rpx 0;
|
|
|
- border-bottom: 1px solid #f3f4f6;
|
|
|
-}
|
|
|
-
|
|
|
-.content-tree-item:last-child {
|
|
|
- border-bottom: none;
|
|
|
-}
|
|
|
-
|
|
|
-.content-tree-row {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- gap: 16rpx;
|
|
|
-}
|
|
|
-
|
|
|
-/* 章样式 */
|
|
|
-.content-chapter {
|
|
|
- padding: 20rpx 0;
|
|
|
- border-bottom: 2rpx solid #e5e7eb;
|
|
|
-}
|
|
|
-
|
|
|
-.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 {
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-title {
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #1f2937;
|
|
|
- margin-bottom: 8rpx;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-outline-summary {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #6b7280;
|
|
|
- margin-bottom: 10rpx;
|
|
|
- display: block;
|
|
|
- line-height: 1.5;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-key-points {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 8rpx;
|
|
|
- margin-bottom: 10rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.point-tag {
|
|
|
- font-size: 20rpx;
|
|
|
- padding: 4rpx 12rpx;
|
|
|
- background: #f3f4f6;
|
|
|
- color: #6b7280;
|
|
|
- border-radius: 6rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-media-badges {
|
|
|
- display: flex;
|
|
|
- gap: 12rpx;
|
|
|
- margin-top: 8rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.media-badge {
|
|
|
- font-size: 20rpx;
|
|
|
- padding: 4rpx 12rpx;
|
|
|
- border-radius: 6rpx;
|
|
|
- background: #f3f4f6;
|
|
|
- color: #6b7280;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-status {
|
|
|
- font-size: 28rpx;
|
|
|
- color: #9ca3af;
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-status.done {
|
|
|
- color: #059669;
|
|
|
-}
|
|
|
-
|
|
|
-.chapter-status.pending {
|
|
|
- color: #d1d5db;
|
|
|
-}
|
|
|
-
|
|
|
-/* 节列表 */
|
|
|
-
|
|
|
-/* 小节媒体标识 */
|
|
|
-.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: 40rpx;
|
|
|
- margin-top: 12rpx;
|
|
|
- padding-left: 24rpx;
|
|
|
- border-left: 3rpx solid #e5e7eb;
|
|
|
- background: linear-gradient(to right, rgba(243, 244, 246, 0.3), transparent);
|
|
|
-}
|
|
|
-
|
|
|
-.content-section {
|
|
|
- padding: 12rpx 0;
|
|
|
- border-bottom: 1px dashed #f3f4f6;
|
|
|
-}
|
|
|
-
|
|
|
-.section-row {
|
|
|
- width: 100%;
|
|
|
- background: #fafafa;
|
|
|
- padding: 12rpx;
|
|
|
- border-radius: 8rpx;
|
|
|
- border-left: 3rpx solid #10b981;
|
|
|
-}
|
|
|
-
|
|
|
-.section-num-badge {
|
|
|
- width: 40rpx;
|
|
|
- height: 40rpx;
|
|
|
- background: #8b5cf6;
|
|
|
- color: #ffffff;
|
|
|
- border-radius: 8rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- font-size: 22rpx;
|
|
|
- font-weight: 600;
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.section-info {
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.section-title {
|
|
|
- font-size: 26rpx;
|
|
|
- font-weight: 500;
|
|
|
- color: #374151;
|
|
|
- margin-bottom: 4rpx;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-.section-summary {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #9ca3af;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-/* 小节列表 */
|
|
|
-.content-subsections-list {
|
|
|
- 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 {
|
|
|
- padding: 8rpx 0;
|
|
|
- border-bottom: none;
|
|
|
-}
|
|
|
-
|
|
|
-.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 {
|
|
|
- width: 36rpx;
|
|
|
- height: 36rpx;
|
|
|
- background: #06b6d4;
|
|
|
- color: #ffffff;
|
|
|
- border-radius: 6rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- font-size: 20rpx;
|
|
|
- font-weight: 500;
|
|
|
- flex-shrink: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.subsection-info {
|
|
|
- flex: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.subsection-title {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #6b7280;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-.content-body {
|
|
|
- padding: 24rpx;
|
|
|
- background: #ffffff;
|
|
|
- border-radius: 12rpx;
|
|
|
- margin-top: 12rpx;
|
|
|
- border: 1px solid #e5e7eb;
|
|
|
-}
|
|
|
-
|
|
|
-.content-meta {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- margin-bottom: 16rpx;
|
|
|
- padding-bottom: 16rpx;
|
|
|
- border-bottom: 1px solid #f3f4f6;
|
|
|
-}
|
|
|
-
|
|
|
-.content-actions {
|
|
|
- display: flex;
|
|
|
- gap: 12rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.mini-btn {
|
|
|
- padding: 8rpx 16rpx;
|
|
|
- border-radius: 8rpx;
|
|
|
- font-size: 22rpx;
|
|
|
- border: none;
|
|
|
-}
|
|
|
-
|
|
|
-.mini-btn.audio-btn {
|
|
|
- background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
|
- color: #ffffff;
|
|
|
-}
|
|
|
-
|
|
|
-.mini-btn.video-btn {
|
|
|
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
|
- color: #ffffff;
|
|
|
-}
|
|
|
-
|
|
|
-.content-text {
|
|
|
- font-size: 28rpx;
|
|
|
- line-height: 1.9;
|
|
|
- color: #374151;
|
|
|
- text-align: justify;
|
|
|
- white-space: pre-wrap;
|
|
|
-}
|
|
|
-
|
|
|
-/* ==================== 音频生成相关样式 ==================== */
|
|
|
-
|
|
|
-/* 书籍卡片操作区 */
|
|
|
-.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;
|
|
|
-}
|
|
|
-
|
|
|
-.publish-btn {
|
|
|
- flex: 1;
|
|
|
- height: 72rpx;
|
|
|
- background: linear-gradient(135deg, #059669 0%, #047857 100%);
|
|
|
- border-radius: 16rpx;
|
|
|
- font-size: 24rpx;
|
|
|
- color: #fff;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- border: none;
|
|
|
- padding: 0;
|
|
|
- margin-top: 8rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.publish-btn[disabled] {
|
|
|
- opacity: 0.6;
|
|
|
-}
|
|
|
-
|
|
|
-.publish-all-btn {
|
|
|
- width: 100%;
|
|
|
- height: 72rpx;
|
|
|
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
- border-radius: 12rpx;
|
|
|
- font-size: 26rpx;
|
|
|
- color: #ffffff;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- border: none;
|
|
|
- margin-top: 16rpx;
|
|
|
-}
|
|
|
-
|
|
|
-/* 目录项操作区 */
|
|
|
-.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;
|
|
|
-}
|
|
|
-
|
|
|
-/* 额度监控 */
|
|
|
-.quota-monitor {
|
|
|
- margin-top: 20rpx;
|
|
|
- padding: 16rpx;
|
|
|
- background: rgba(16, 185, 129, 0.1);
|
|
|
- border-radius: 12rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-monitor-title {
|
|
|
- font-size: 24rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #059669;
|
|
|
- margin-bottom: 8rpx;
|
|
|
- display: block;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-monitor-row {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- font-size: 24rpx;
|
|
|
- color: #374151;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-overage-warning {
|
|
|
- margin-top: 8rpx;
|
|
|
- padding: 8rpx 12rpx;
|
|
|
- background: rgba(239, 68, 68, 0.1);
|
|
|
- border-radius: 8rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.quota-overage-warning text {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #dc2626;
|
|
|
-}
|
|
|
-
|
|
|
-/* 中断提示 */
|
|
|
-.interrupted-tip {
|
|
|
- margin-top: 20rpx;
|
|
|
- padding: 20rpx;
|
|
|
- background: rgba(245, 158, 11, 0.1);
|
|
|
- border: 1px solid #fbbf24;
|
|
|
- border-radius: 12rpx;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.interrupted-icon {
|
|
|
- font-size: 36rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.interrupted-text {
|
|
|
- font-size: 26rpx;
|
|
|
- color: #92400e;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-.interrupted-hint {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #b45309;
|
|
|
-}
|
|
|
-
|
|
|
-/* 无章节警告 */
|
|
|
-.warning-tip {
|
|
|
- margin-top: 20rpx;
|
|
|
- padding: 24rpx;
|
|
|
- background: rgba(239, 68, 68, 0.1);
|
|
|
- border: 1px solid #fca5a5;
|
|
|
- border-radius: 12rpx;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- gap: 12rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-icon {
|
|
|
- font-size: 48rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-text {
|
|
|
- font-size: 26rpx;
|
|
|
- color: #dc2626;
|
|
|
- text-align: center;
|
|
|
- font-weight: 500;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-hint {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #991b1b;
|
|
|
- margin-bottom: 8rpx;
|
|
|
-}
|
|
|
-
|
|
|
-.warning-tip .action-btn {
|
|
|
- margin-top: 8rpx;
|
|
|
- width: 100%;
|
|
|
-}
|
|
|
+<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; height: 88rpx; display: flex; align-items: center; justify-content: center; }
|
|
|
+.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); }
|
|
|
+.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.published { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; font-weight: 600; }
|
|
|
+.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; }
|
|
|
+.book-actions { display: flex; flex-direction: column; gap: 12rpx; }
|
|
|
+.audio-btn, .merge-audio-btn, .merge-video-btn, .video-btn, .publish-btn { width: 100%; height: 72rpx; border-radius: 12rpx; font-size: 26rpx; display: flex; align-items: center; justify-content: center; border: none; }
|
|
|
+.audio-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: #ffffff; }
|
|
|
+.merge-audio-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; }
|
|
|
+.merge-video-btn { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: #ffffff; }
|
|
|
+.video-btn { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #ffffff; }
|
|
|
+.publish-btn { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: #ffffff; }
|
|
|
+.audio-btn[disabled], .merge-audio-btn[disabled], .merge-video-btn[disabled], .video-btn[disabled], .publish-btn[disabled] { opacity: 0.6; }
|
|
|
+.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; }
|
|
|
</style>
|
|
|
-
|
|
|
-
|