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