| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301 |
- <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.chapters && book.chapters.length > 0" 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 === 'failed' && currentBook?.error?.includes('额度')" 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 v-if="currentBook?.chapters && currentBook.chapters.length > 0" class="card content-card">
- <text class="card-title">📖 完整内容</text>
- <!-- 前言 -->
- <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
- v-for="chapter in currentBook.chapters"
- :key="chapter.id"
- class="content-item chapter-item"
- @click="goToChapterDetail(chapter)"
- >
- <view class="content-item-header">
- <view class="content-item-left">
- <text class="chapter-num-badge">第{{ chapter.number }}章</text>
- <text class="chapter-title-short">{{ chapter.title }}</text>
- <text v-if="chapter.audioUrl" class="audio-badge">🎵</text>
- <text v-if="chapter.videoUrl" class="video-badge">🎬</text>
- </view>
- <view class="content-item-right">
- <text class="expand-icon">→</text>
- </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 { onShow } from '@dcloudio/uni-app';
- 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 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';
- // 更新 URL hash,带上书籍 ID
- updateUrlWithBookId(book.id);
- // 加载配额信息
- loadQuotaInfo();
- } catch (e) {
- uni.showToast({ title: '加载失败', icon: 'none' });
- }
- }
- // 更新 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) {
- try {
- currentBook.value = await api.getBook(bookId);
- currentView.value = 'detail';
- loadQuotaInfo();
- } catch (e) {
- console.error('加载书籍失败:', e);
- uni.showToast({ title: '加载失败', 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());
- }
- }
- // 页面显示时检查 URL 参数
- onShow(() => {
- const bookId = getBookIdFromUrl();
- if (bookId && currentView.value === 'list') {
- loadBookById(bookId);
- }
- });
- // 加载用户配额信息
- 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;
- }
- }
- // 跳转到章节详情页
- function goToChapterDetail(chapter: Chapter) {
- if (!currentBook.value) return;
- // 跳转到独立的章节详情页面
- uni.navigateTo({
- url: `/pages/book-generator/chapter-detail?bookId=${encodeURIComponent(currentBook.value.id)}&chapter=${chapter.number}`
- });
- }
- // 查看前言
- 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;
- }
- }
- // 页面加载
- 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;
- }
- /* ==================== 完整内容区样式 ==================== */
- .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-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;
- }
- /* 目录项操作区 */
- .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>
|