| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138 |
- <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">{{ isEditMode ? '编辑书籍' : '创建有声书' }}</text>
- </view>
- </view>
- <!-- 主内容区 -->
- <view class="main-content">
- <!-- ===== 区域 1:描述你想要的书籍 ===== -->
- <view class="card hero-card">
- <!-- 示例提示词(轮播展示) -->
- <view class="examples-row">
- <text class="examples-label">试试:</text>
- <view class="example-chips">
- <view
- :key="currentExampleIndex"
- class="example-chip example-chip-fade"
- @click="useCurrentExample"
- >
- <text class="example-chip-icon">✨</text>
- <text class="example-chip-text">{{ examplePrompts[currentExampleIndex] }}</text>
- <text class="example-chip-hint">点击填入</text>
- </view>
- </view>
- </view>
- <!-- 文本域 -->
- <view class="textarea-wrapper">
- <textarea
- v-model="newBook.description"
- class="form-textarea"
- :class="{ 'textarea-error': attemptedCreate && !newBook.description.trim() }"
- placeholder="用一句话描述你想要的书籍,AI 会自动分析并推荐最佳配置..."
- :maxlength="2000"
- @blur="detectScaleFromDesc"
- />
- <view class="textarea-footer">
- <text class="char-count">{{ newBook.description.length }}/2000</text>
- <text v-if="newBook.description.trim()" class="clear-btn" @click="newBook.description = ''">清空</text>
- </view>
- </view>
- <!-- AI 智能推荐按钮 -->
- <view v-if="!isEditMode" class="ai-btn-row">
- <button
- class="btn-ai-recommend"
- :class="{ 'btn-ai-loading': isRecommending }"
- :disabled="isRecommending || !newBook.description.trim()"
- @click="requestSmartRecommend"
- >
- <text>{{ isRecommending ? 'AI 分析中...' : 'AI 智能推荐配置' }}</text>
- </button>
- </view>
- </view>
- <!-- ===== 区域 2:AI 推荐结果(有配置时显示) ===== -->
- <view v-if="!isEditMode && hasAnyConfig" class="card config-summary-card">
- <view class="section-label">
- <text class="label-text">当前配置</text>
- <text v-if="recommendSource === 'ai'" class="config-source-tag">AI 推荐</text>
- <text v-else-if="recommendSource === 'template'" class="config-source-tag template-tag">模板</text>
- </view>
- <view class="config-tags">
- <view v-if="newBook.bookScale" class="config-tag scale-tag" @click="scrollToSection('scale')">
- <text class="config-tag-label">规模</text>
- <text class="config-tag-value">{{ getScaleShortLabel(newBook.bookScale) }}</text>
- <text class="config-tag-edit">修改</text>
- </view>
- <view v-if="newBook.targetAudience" class="config-tag" @click="scrollToSection('audience')">
- <text class="config-tag-value">{{ getAudienceLabel(newBook.targetAudience) }}</text>
- <text class="config-tag-edit">修改</text>
- </view>
- <view v-if="newBook.knowledgeLevel" class="config-tag" @click="scrollToSection('knowledge')">
- <text class="config-tag-value">{{ getKnowledgeLevelLabel(newBook.knowledgeLevel) }}</text>
- <text class="config-tag-edit">修改</text>
- </view>
- <view v-if="newBook.style" class="config-tag" @click="scrollToSection('style')">
- <text class="config-tag-value">{{ newBook.style }}</text>
- <text class="config-tag-edit">修改</text>
- </view>
- <view v-if="newBook.industry" class="config-tag" @click="scrollToSection('industry')">
- <text class="config-tag-value">{{ getIndustryLabel(newBook.industry) || newBook.industry }}</text>
- <text class="config-tag-edit">修改</text>
- </view>
- <view v-if="newBook.specialFeatures.length > 0" class="config-tag" @click="scrollToSection('features')">
- <text class="config-tag-value">{{ newBook.specialFeatures.length }}项特殊要求</text>
- <text class="config-tag-edit">修改</text>
- </view>
- </view>
- <!-- 预估信息 -->
- <view v-if="bookEstimate && bookEstimate.words" class="estimate-bar">
- <view class="estimate-item">
- <text class="est-num">{{ formatNumber(bookEstimate.words.avg) }}</text>
- <text class="est-unit">字</text>
- </view>
- <text class="est-divider">·</text>
- <view class="estimate-item">
- <text class="est-num">{{ bookEstimate.estimatedChapters }}</text>
- <text class="est-unit">章</text>
- </view>
- <text class="est-divider">·</text>
- <view class="estimate-item">
- <text class="est-num">{{ bookEstimate.audioMinutes.avg }}</text>
- <text class="est-unit">分钟</text>
- </view>
- <view v-if="quotaCheck" class="estimate-item quota-badge">
- <text v-if="quotaCheck.allowed" class="quota-ok">✅ 额度充足</text>
- <text v-else class="quota-warn">⚠️ {{ quotaCheck.reason }}</text>
- </view>
- </view>
- </view>
- <!-- ===== 区域 3:规模选择 ===== -->
- <view v-if="!isEditMode" class="card" id="section-scale">
- <view class="section-label">
- <text class="label-icon">📏</text>
- <text class="label-text">选择书籍规模</text>
- </view>
- <view class="scale-grid">
- <view
- v-for="scale in displayScales"
- :key="scale.value"
- :class="['scale-card', newBook.bookScale === scale.value ? 'scale-active' : '']"
- @click="selectScale(scale.value)"
- >
- <text class="scale-name">{{ scale.label }}</text>
- <text class="scale-words">{{ scale.words }}</text>
- <text class="scale-audio">{{ scale.audio }}</text>
- </view>
- </view>
- <!-- 更多规模 -->
- <view v-if="!showAllScales" class="show-more-row">
- <text class="show-more-btn" @click="showAllScales = true">展开全部规模 ({{ bookScales.length }}档) ▼</text>
- </view>
- <view v-else class="scale-grid">
- <view
- v-for="scale in bookScales.slice(displayScales.length)"
- :key="scale.value"
- :class="['scale-card', newBook.bookScale === scale.value ? 'scale-active' : '']"
- @click="selectScale(scale.value)"
- >
- <text class="scale-name">{{ scale.name }}</text>
- <text class="scale-words">{{ scale.words }}</text>
- <text class="scale-audio">{{ scale.audio }}</text>
- </view>
- </view>
- </view>
- <!-- ===== 区域 4:内容定位 ===== -->
- <view class="card" id="section-audience">
- <view class="section-label">
- <text class="label-text">内容定位</text>
- </view>
- <!-- 面向人群 -->
- <view class="chip-section">
- <text class="chip-title">面向人群 <text class="chip-required">*</text></text>
- <view class="chip-row">
- <view
- v-for="a in audiences"
- :key="a.value"
- :class="['chip', newBook.targetAudience === a.value ? 'chip-active' : '']"
- @click="newBook.targetAudience = newBook.targetAudience === a.value ? '' : a.value"
- >
- <view class="chip-body">
- <text class="chip-text">{{ a.label }}</text>
- <text class="chip-desc">{{ a.desc }}</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 知识难度 -->
- <view class="chip-section" id="section-knowledge">
- <text class="chip-title">知识难度</text>
- <view class="chip-row">
- <view
- v-for="l in knowledgeLevels"
- :key="l.value"
- :class="['chip chip-sm', newBook.knowledgeLevel === l.value ? 'chip-active' : '']"
- @click="newBook.knowledgeLevel = newBook.knowledgeLevel === l.value ? '' : l.value"
- >
- <view class="chip-body">
- <text class="chip-text">{{ l.label }}</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 书籍类型 -->
- <view class="chip-section">
- <text class="chip-title">书籍类型</text>
- <view class="chip-row">
- <view
- v-for="bt in bookTypes"
- :key="bt.value"
- :class="['chip chip-sm', newBook.bookType === bt.value ? 'chip-active' : '']"
- @click="newBook.bookType = newBook.bookType === bt.value ? 'auto' : bt.value"
- >
- <view class="chip-body">
- <text class="chip-text">{{ bt.label }}</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 大纲层级 -->
- <view class="chip-section">
- <text class="chip-title">大纲层级</text>
- <view class="chip-row">
- <view
- v-for="opt in outlineLevelOptions"
- :key="opt.value"
- :class="['chip chip-xs', newBook.outlineLevel === opt.value ? 'chip-active' : '']"
- @click="newBook.outlineLevel = newBook.outlineLevel === opt.value ? 'auto' : opt.value"
- >
- <text class="chip-text">{{ opt.label }}</text>
- <text class="chip-desc chip-desc-inline">{{ opt.desc }}</text>
- </view>
- </view>
- </view>
- </view>
- <!-- ===== 区域 5:声音速度 ===== -->
- <view class="card" id="section-speed">
- <view class="section-label">
- <text class="label-text">语速</text>
- </view>
- <view class="speed-display">
- <text class="speed-value">{{ newBook.voiceSpeed.toFixed(1) }}x</text>
- </view>
- <slider
- :value="(newBook.voiceSpeed - 0.5) * 100 / 1.5"
- :min="0"
- :max="100"
- @changing="(e: any) => newBook.voiceSpeed = Number((0.5 + (e.detail.value / 100) * 1.5).toFixed(1))"
- activeColor="#4F46E5"
- backgroundColor="#e5e7eb"
- block-size="20"
- />
- <view class="param-range"><text>0.5x</text><text>2.0x</text></view>
- </view>
- <!-- ===== 区域 6:高级定制(可折叠) ===== -->
- <view class="card">
- <view class="section-label collapsible" @click="showAdvancedOptions = !showAdvancedOptions">
- <text class="label-text">高级定制</text>
- <view v-if="advancedBadgeCount > 0" class="adv-badge">{{ advancedBadgeCount }}</view>
- <text class="collapse-icon">{{ showAdvancedOptions ? '▲' : '▼' }}</text>
- </view>
- <view v-if="showAdvancedOptions" class="advanced-content">
- <!-- 快速模板 -->
- <view class="chip-section" id="section-template">
- <text class="chip-title">快速模板</text>
- <view class="chip-row">
- <view
- v-for="tmpl in quickTemplates"
- :key="tmpl.name"
- :class="['chip chip-sm chip-template', selectedTemplateName === tmpl.name ? 'chip-active' : '']"
- @click="applyTemplate(tmpl)"
- >
- <view class="chip-body">
- <text class="chip-text">{{ tmpl.name }}</text>
- <text class="chip-desc chip-desc-inline">{{ tmpl.desc }}</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 写作风格 -->
- <view class="chip-section" id="section-style">
- <text class="chip-title">写作风格</text>
- <view class="chip-row">
- <view
- v-for="s in styleOptions"
- :key="s"
- :class="['chip chip-xs', newBook.style === s ? 'chip-active' : '']"
- @click="newBook.style = newBook.style === s ? '' : s"
- >
- <text class="chip-text">{{ s }}</text>
- </view>
- </view>
- </view>
- <!-- 行业领域 -->
- <view class="chip-section" id="section-industry">
- <text class="chip-title">行业领域</text>
- <view class="chip-row">
- <view
- v-for="ind in industries"
- :key="ind.value"
- :class="['chip chip-sm', newBook.industry === ind.value ? 'chip-active' : '']"
- @click="newBook.industry = newBook.industry === ind.value ? '' : ind.value"
- >
- <text class="chip-text">{{ ind.label }}</text>
- </view>
- </view>
- </view>
- <!-- 特殊要求 -->
- <view class="chip-section" id="section-features">
- <text class="chip-title">特殊要求(多选)</text>
- <view class="chip-row">
- <view
- v-for="sf in specialFeatures"
- :key="sf.value"
- :class="['chip chip-sm', newBook.specialFeatures.includes(sf.value) ? 'chip-active' : '']"
- @click="toggleSpecialFeature(sf.value)"
- >
- <text class="chip-text">{{ sf.label }}</text>
- <text v-if="newBook.specialFeatures.includes(sf.value)" class="chip-check">✓</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- <!-- ===== 底部操作区 ===== -->
- <view class="bottom-bar">
- <button
- v-if="isEditMode"
- class="btn-create"
- :disabled="!canCreateBook || creating"
- @click="createNewBook"
- >
- <text>{{ creating ? '保存中...' : '保存修改' }}</text>
- </button>
- <template v-else>
- <button
- class="btn-create"
- :disabled="!canCreateBook || creating"
- @click="createNewBook"
- >
- <text>{{ creating ? '创建中...' : '开始创建有声书' }}</text>
- </button>
- <text class="bottom-link" @click="switchToInteractive">需要更精细控制?试试<text class="bottom-link-highlight">交互模式</text></text>
- </template>
- </view>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted, onUnmounted, onBeforeUnmount } from 'vue';
- import { onLoad } from '@dcloudio/uni-app';
- import * as api from '../../utils/book-generator-api';
- import { getApiBaseUrl } from '../../utils/config';
- const BASE_URL = getApiBaseUrl();
- // 创建表单
- const creating = ref(false);
- const showAdvancedOptions = ref(false);
- const showAllScales = ref(false);
- const isRecommending = ref(false);
- const attemptedCreate = ref(false);
- const isEditMode = ref(false);
- const editingBookId = ref<string | null>(null);
- const recommendSource = ref<'ai' | 'template' | 'manual' | ''>('');
- const newBook = ref({
- title: '',
- subtitle: '',
- description: '',
- targetAudience: '',
- knowledgeLevel: '',
- industry: '',
- specialFeatures: [] as string[],
- style: '',
- bookScale: '',
- bookType: 'auto',
- outlineLevel: 'auto',
- voiceSpeed: 1.0,
- });
- // 示例提示词
- const examplePrompts = [
- '教小学生学习编程的入门书,内容生动有趣,通过趣味案例引导孩子入门Python',
- '面向中学生的科幻科普读物,用故事化方式讲解物理学基础概念',
- '给职场新人的沟通技巧指南,结合实际案例,实用性强',
- '介绍中国历史的通识读物,按时间线讲述,通俗易懂适合大众读者',
- ];
- // 示例提示词轮播
- const currentExampleIndex = ref(0);
- let exampleTimer: ReturnType<typeof setInterval> | null = null;
- const EXAMPLE_INTERVAL_MS = 4000; // 每 4 秒切换一条
- function useCurrentExample() {
- newBook.value.description = examplePrompts[currentExampleIndex.value];
- }
- function startExampleRotation() {
- stopExampleRotation();
- if (examplePrompts.length <= 1) return;
- exampleTimer = setInterval(() => {
- currentExampleIndex.value = (currentExampleIndex.value + 1) % examplePrompts.length;
- }, EXAMPLE_INTERVAL_MS);
- }
- function stopExampleRotation() {
- if (exampleTimer) {
- clearInterval(exampleTimer);
- exampleTimer = null;
- }
- }
- // 书籍预估信息
- 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;
- } | null>(null);
- // 书籍规模配置(全新简洁标签)
- const bookScales = [
- { value: '340000', name: '超大', words: '34万字', audio: '约28h', label: '上2' },
- { value: '210000', name: '大型', words: '21万字', audio: '约17h', label: '上1' },
- { value: '130000', name: '标准', words: '13万字', audio: '约11h', label: '标准' },
- { value: '80000', name: '中型', words: '8万字', audio: '约7h', label: '下1' },
- { value: '50000', name: '小型', words: '5万字', audio: '约4h', label: '下2' },
- { value: '31000', name: '短篇', words: '3.1万字', audio: '约2.5h', label: '下3' },
- { value: '19000', name: '超短', words: '1.9万字', audio: '约1.5h', label: '下4' },
- { value: '12000', name: '微书', words: '1.2万字', audio: '约1h', label: '下5' },
- { value: '7000', name: '迷你', words: '7千字', audio: '约35m', label: '下6' },
- { value: '4000', name: '短文', words: '4千字', audio: '约20m', label: '下7' },
- { value: '3000', name: '速读', words: '3千字', audio: '约15m', label: '下8' },
- { value: '2000', name: '极短', words: '2千字', audio: '约10m', label: '下9' },
- { value: '1000', name: '试读', words: '1千字', audio: '约5m', label: '下10' },
- ];
- // 默认展示前6个规模
- const displayScales = computed(() => bookScales.slice(0, 6));
- // 选项配置
- const knowledgeLevels = [
- { value: '入门', label: '入门级', icon: '🌱', desc: '零基础,简单易懂' },
- { value: '基础', label: '基础级', icon: '📚', desc: '基本概念,循序渐进' },
- { value: '进阶', label: '进阶级', icon: '📈', desc: '深入讲解,系统全面' },
- { value: '高级', label: '高级', icon: '🎯', desc: '专业深入,前沿技术' },
- { value: '专家', label: '专家级', icon: '🔬', desc: '研究级别,底层原理' },
- ];
- const audiences = [
- { value: '儿童', label: '儿童', icon: '👶', desc: '6-12岁,形象生动' },
- { value: '青少年', label: '青少年', icon: '🧑', desc: '13-18岁,通俗易懂' },
- { value: '大学生', label: '大学生', icon: '🎓', desc: '系统专业,有理论' },
- { value: '专业人士', label: '专业人士', icon: '💼', desc: '从业者,实用深入' },
- { value: '研究生', label: '研究生', icon: '🔍', desc: '研究级别,前沿深入' },
- { value: '大众读者', label: '大众读者', icon: '👥', desc: '通俗普及,有趣味' },
- ];
- const industries = [
- { value: 'IT/计算机', label: 'IT/计算机', icon: '💻', desc: '编程、软件、互联网' },
- { value: '金融', label: '金融', icon: '💰', desc: '投资、银行、证券' },
- { value: '医学', label: '医学', icon: '🏥', desc: '医疗、健康、临床' },
- { value: '教育', label: '教育', icon: '📚', desc: '教学、学习、心理学' },
- { value: '文学', label: '文学', icon: '✍️', desc: '小说、散文、传记' },
- { value: '科普', label: '科普', icon: '🔬', desc: '科学、自然、探索' },
- { value: '商业', label: '商业', icon: '📊', desc: '管理、营销、创业' },
- { value: '艺术', label: '艺术', icon: '🎨', desc: '设计、音乐、影视' },
- ];
- const specialFeatures = [
- { value: '案例分析', label: '案例分析', icon: '📖' },
- { value: '代码示例', label: '代码示例', icon: '💻' },
- { value: '习题练习', label: '习题练习', icon: '✍️' },
- { value: '图表插图', label: '图表插图', icon: '📊' },
- { value: '项目实战', label: '项目实战', icon: '🎯' },
- { value: '思维导图', label: '思维导图', icon: '🧠' },
- { value: '总结回顾', label: '总结回顾', icon: '📝' },
- ];
- const quickTemplates = [
- { name: '大学教材', icon: '🎓', desc: '系统专业,有案例习题', config: { knowledgeLevel: '进阶', targetAudience: '大学生', industry: '教育', specialFeatures: ['案例分析', '习题练习', '总结回顾'], style: '专业严谨' } },
- { name: '技术教程', icon: '💻', desc: '有代码示例和实战项目', config: { knowledgeLevel: '入门', targetAudience: '专业人士', industry: 'IT/计算机', specialFeatures: ['代码示例', '项目实战', '案例分析'], style: '通俗易懂' } },
- { name: '科普读物', icon: '🔬', desc: '面向大众,通俗有趣', config: { knowledgeLevel: '入门', targetAudience: '大众读者', industry: '科普', specialFeatures: ['案例分析', '图表插图'], style: '轻松幽默' } },
- { name: '商业书籍', icon: '📊', desc: '有真实案例,实用性强', config: { knowledgeLevel: '进阶', targetAudience: '专业人士', industry: '商业', specialFeatures: ['案例分析', '总结回顾'], style: '专业严谨' } },
- { name: '儿童读物', icon: '👶', desc: '形象生动,有趣味性', config: { knowledgeLevel: '入门', targetAudience: '儿童', industry: '教育', specialFeatures: ['图表插图'], style: '轻松幽默' } },
- { name: '学术研究', icon: '🔍', desc: '前沿深入,有研究价值', config: { knowledgeLevel: '专家', targetAudience: '研究生', industry: 'IT/计算机', specialFeatures: ['案例分析', '总结回顾'], style: '专业严谨' } },
- ];
- const styleOptions = ['通俗易懂', '专业严谨', '轻松幽默', '诗意优美', '故事化'];
- const bookTypes = [
- { value: 'auto', label: '自动', icon: '🤖', desc: 'AI自行判断' },
- { value: '教材', label: '教材', icon: '📚', desc: '3层大纲' },
- { value: '技术教程', label: '技术教程', icon: '💻', desc: '3层大纲' },
- { value: '小说', label: '小说', icon: '📖', desc: '1层大纲' },
- { value: '商业', label: '商业', icon: '📊', desc: '2层大纲' },
- { value: '科普', label: '科普', icon: '🔬', desc: '2层大纲' },
- ];
- const outlineLevelOptions = [
- { value: 'auto', label: '自动', desc: '按规模适配' },
- { value: '1', label: '1层', desc: '仅章' },
- { value: '2', label: '2层', desc: '章→节' },
- { value: '3', label: '3层', desc: '章→节→小节' },
- ];
- // 计算属性
- const hasAnyConfig = computed(() => {
- return !!(newBook.value.bookScale || newBook.value.targetAudience ||
- newBook.value.knowledgeLevel || newBook.value.style ||
- newBook.value.industry || newBook.value.specialFeatures.length);
- });
- const advancedBadgeCount = computed(() => {
- let count = 0;
- if (selectedTemplateName.value) count++;
- if (newBook.value.style) count++;
- if (newBook.value.industry) count++;
- if (newBook.value.specialFeatures.length) count++;
- return count;
- });
- const canCreateBook = computed(() => {
- if (quotaCheck.value && !quotaCheck.value.allowed) return false;
- return !!newBook.value.description.trim();
- });
- const selectedTemplateName = ref('');
- // 辅助函数
- function getScaleShortLabel(value: string) {
- const scale = bookScales.find(s => s.value === value);
- return scale ? `${scale.name} (${scale.words})` : '';
- }
- function getAudienceLabel(val: string) { return audiences.find(a => a.value === val)?.label || ''; }
- function getAudienceIcon(val: string) { return audiences.find(a => a.value === val)?.icon || ''; }
- function getKnowledgeLevelLabel(val: string) { return knowledgeLevels.find(l => l.value === val)?.label || ''; }
- function getKnowledgeIcon(val: string) { return knowledgeLevels.find(l => l.value === val)?.icon || ''; }
- function getIndustryLabel(val: string) { return industries.find(i => i.value === val)?.label || ''; }
- function formatNumber(n: number): string {
- if (n >= 10000) return (n / 10000).toFixed(1) + '万';
- if (n >= 1000) return (n / 1000).toFixed(1) + '千';
- return String(n);
- }
- function scrollToSection(id: string) {
- // uniapp H5 环境下的 scrollTo
- // #ifdef H5
- const el = document.getElementById(`section-${id}`);
- if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
- // #endif
- }
- // 选择规模
- function selectScale(value: string) {
- newBook.value.bookScale = value;
- loadEstimate(value);
- }
- // 切换特殊要求
- function toggleSpecialFeature(feature: string) {
- const features = newBook.value.specialFeatures;
- const index = features.indexOf(feature);
- if (index > -1) {
- features.splice(index, 1);
- } else {
- features.push(feature);
- }
- }
- // 加载预估
- async function loadEstimate(scale: string) {
- try {
- // #ifdef H5
- const response = await uni.request({
- url: `${BASE_URL}/book-generator/langgraph/estimate?scale=${encodeURIComponent(scale)}&userId=1`,
- method: 'GET'
- });
- const res = response.data as any;
- if (res.code === 0 && res.data?.words) {
- 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;
- }
- // #endif
- } catch (e) { /* ignore */ }
- }
- // 应用模板
- function applyTemplate(template: typeof quickTemplates[0]) {
- if (selectedTemplateName.value === template.name) {
- // 再次点击取消模板
- selectedTemplateName.value = '';
- newBook.value.knowledgeLevel = '';
- newBook.value.targetAudience = '';
- newBook.value.industry = '';
- newBook.value.specialFeatures = [];
- newBook.value.style = '';
- recommendSource.value = '';
- return;
- }
- const config = template.config;
- newBook.value.knowledgeLevel = config.knowledgeLevel;
- newBook.value.targetAudience = config.targetAudience;
- newBook.value.industry = config.industry;
- newBook.value.specialFeatures = [...config.specialFeatures];
- newBook.value.style = config.style;
- selectedTemplateName.value = template.name;
- recommendSource.value = 'template';
- uni.showToast({ title: `已应用「${template.name}」模板`, icon: 'success' });
- }
- // AI 智能推荐
- async function requestSmartRecommend() {
- if (!newBook.value.description.trim() || isRecommending.value) return;
- isRecommending.value = true;
- try {
- const response = await uni.request({
- url: `${BASE_URL}/book-generator/langgraph/smart-recommend`,
- method: 'POST',
- data: { description: newBook.value.description.trim(), title: newBook.value.title.trim() },
- });
- const res = response.data as any;
- if (res.code === 0 && res.data) {
- const rec = res.data;
- if (rec.knowledgeLevel) newBook.value.knowledgeLevel = rec.knowledgeLevel;
- if (rec.targetAudience) newBook.value.targetAudience = rec.targetAudience;
- if (rec.style) newBook.value.style = rec.style;
- if (rec.industry) newBook.value.industry = rec.industry;
- recommendSource.value = 'ai';
- showAdvancedOptions.value = true;
- uni.showToast({ title: 'AI 推荐完成,可点击标签修改', icon: 'success' });
- } else {
- uni.showToast({ title: res.message || '推荐失败,请手动配置', icon: 'none' });
- }
- } catch (e: any) {
- uni.showToast({ title: '推荐服务暂不可用', icon: 'none' });
- } finally {
- isRecommending.value = false;
- }
- }
- // AI 检测规模
- let detectTimer: any = null;
- async function detectScaleFromDesc() {
- const desc = newBook.value.description?.trim();
- if (!desc) return;
- clearTimeout(detectTimer);
- detectTimer = setTimeout(async () => {
- // 检测是否为英文内容,自动调慢语速
- if (detectEnglishContent(desc)) {
- newBook.value.voiceSpeed = 0.5;
- }
- if (!newBook.value.bookScale) {
- try {
- const response = await uni.request({
- url: `${BASE_URL}/book-generator/langgraph/detect-scale`,
- method: 'POST',
- data: { description: desc }
- });
- const res = response.data as any;
- if (res.code === 0 && res.data) {
- newBook.value.bookScale = res.data.bookScale;
- bookEstimate.value = {
- scale: res.data.bookScale,
- words: { min: res.data.totalWords, max: res.data.totalWords, avg: res.data.totalWords },
- audioMinutes: { min: 0, max: 0, avg: 0 },
- estimatedChapters: res.data.chapters,
- };
- loadEstimate(res.data.bookScale);
- }
- } catch { /* ignore */ }
- }
- }, 800);
- }
- // 检测文本中英文相关内容的比例
- function detectEnglishRatio(text: string): number {
- // 检测纯英文单词的比例
- const words = text.match(/[a-zA-Z]+/g) || [];
- const totalWords = text.match(/[\u4e00-\u9fa5a-zA-Z]+/g) || [];
- if (totalWords.length === 0) return 0;
- return words.length / totalWords.length;
- }
- // 检测描述是否暗示内容为英文
- function detectEnglishContent(text: string): boolean {
- const lowerText = text.toLowerCase();
- // 检测英文相关关键词
- const englishKeywords = [
- 'english', '英文', '美文', 'toeic', 'toefl', 'ielts', 'gre', 'gmat',
- 'python', 'javascript', 'java', '编程', 'code', '编程语言',
- 'literature', '文学', 'novel', '小说', 'essay', '散文',
- 'programming', 'software', 'software', 'algorithm', '算法',
- 'technical', '技术', 'tutorial', '教程',
- ];
- // 检测英文单词占比
- const englishRatio = detectEnglishRatio(text);
- if (englishRatio > 0.4) return true;
- // 检测是否包含英文关键词
- return englishKeywords.some(kw => lowerText.includes(kw));
- }
- function switchToInteractive() {
- uni.navigateTo({ url: '/pages/book-generator/interactive' });
- }
- function goBack() {
- const pages = getCurrentPages();
- if (pages.length > 1) {
- uni.navigateBack();
- } else {
- uni.switchTab({ url: '/pages/index/index' });
- }
- }
- // 创建书籍
- async function createNewBook() {
- attemptedCreate.value = true;
- if (!canCreateBook.value) return;
- // 编辑模式
- if (isEditMode.value && editingBookId.value) {
- try {
- await api.updateBook(editingBookId.value, {
- title: newBook.value.title || undefined,
- description: newBook.value.description || undefined,
- targetAudience: newBook.value.targetAudience || undefined,
- style: newBook.value.style || undefined,
- bookScale: newBook.value.bookScale || undefined,
- });
- uni.showToast({ title: '更新成功', icon: 'success' });
- setTimeout(() => uni.redirectTo({ url: '/pages/book-generator/index' }), 800);
- } catch (e: any) {
- uni.showToast({ title: e.message || '更新失败', icon: 'none' });
- } finally {
- creating.value = false;
- }
- return;
- }
- // 创建模式
- const configSummary: string[] = [];
- if (newBook.value.knowledgeLevel) configSummary.push(`知识难度:${newBook.value.knowledgeLevel}`);
- if (newBook.value.targetAudience) configSummary.push(`面向人群:${newBook.value.targetAudience}`);
- if (newBook.value.industry) configSummary.push(`行业领域:${newBook.value.industry}`);
- if (newBook.value.specialFeatures.length > 0) configSummary.push(`特殊要求:${newBook.value.specialFeatures.join('、')}`);
- if (newBook.value.style) configSummary.push(`写作风格:${newBook.value.style}`);
- const confirmResult = await new Promise<boolean>((resolve) => {
- uni.showModal({
- title: '确认创建有声书',
- content: `${configSummary.join('\n')}\n\n书名将由AI根据描述自动生成\n\n确认开始生成?`,
- confirmText: '确认生成',
- cancelText: '返回修改',
- success: (res) => resolve(res.confirm),
- });
- });
- if (!confirmResult) return;
- creating.value = true;
- try {
- let mergedDesc = newBook.value.description.trim();
- if (newBook.value.knowledgeLevel && newBook.value.knowledgeLevel !== '不填')
- mergedDesc += `\n知识难度:${newBook.value.knowledgeLevel}`;
- if (newBook.value.targetAudience && newBook.value.targetAudience !== '不填')
- mergedDesc += `\n面向人群:${newBook.value.targetAudience}`;
- if (newBook.value.industry && newBook.value.industry !== '不填')
- mergedDesc += `\n行业领域:${newBook.value.industry}`;
- if (newBook.value.specialFeatures && newBook.value.specialFeatures.length > 0)
- mergedDesc += `\n特殊要求:${newBook.value.specialFeatures.join('、')}`;
- if (newBook.value.style && newBook.value.style !== '不填')
- mergedDesc += `\n写作风格:${newBook.value.style}`;
- const genLevel = newBook.value.outlineLevel === 'auto' ? undefined : parseInt(newBook.value.outlineLevel);
- const bookType = newBook.value.bookType === 'auto' ? undefined : newBook.value.bookType;
- const result = await api.createBook({
- title: newBook.value.title,
- subtitle: newBook.value.subtitle,
- description: mergedDesc,
- bookScale: newBook.value.bookScale || undefined,
- bookType,
- genLevel,
- voiceSpeed: newBook.value.voiceSpeed !== 1.0 ? newBook.value.voiceSpeed : undefined,
- });
- uni.showToast({ title: '创建成功,正在跳转...', icon: 'success' });
- setTimeout(() => {
- uni.navigateTo({ url: `/pages/book-generator/detail?id=${result.id}` });
- }, 1500);
- } catch (e: any) {
- uni.showToast({ title: e.message || '创建失败', icon: 'none' });
- } finally {
- creating.value = false;
- }
- }
- onMounted(() => {
- startExampleRotation();
- });
- onBeforeUnmount(() => {
- stopExampleRotation();
- });
- onUnmounted(() => {
- stopExampleRotation();
- });
- onLoad(async (query: any) => {
- if (query?.editId) {
- isEditMode.value = true;
- editingBookId.value = query.editId;
- try {
- const book = await api.getBookForEdit(query.editId);
- if (book) {
- newBook.value.title = book.title || '';
- newBook.value.description = book.description || '';
- newBook.value.targetAudience = book.targetAudience || '';
- newBook.value.style = book.style || '';
- newBook.value.bookScale = book.bookScale || '';
- }
- } catch (e) {
- console.error('加载书籍失败:', e);
- }
- }
- });
- </script>
- <style scoped>
- /* ========== 基础布局 ========== */
- .page { min-height: 100vh; background: #f3f4f6; }
- .nav-bar {
- position: fixed; top: 0; left: 0; right: 0; z-index: 100;
- background: rgba(255,255,255,0.92);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border-bottom: 0.5rpx solid rgba(0,0,0,0.06);
- }
- .nav-content {
- display: flex; align-items: center; height: 88rpx;
- padding: 0 32rpx; padding-top: env(safe-area-inset-top);
- }
- .nav-left { width: 60rpx; flex-shrink: 0; }
- .back-icon { font-size: 40rpx; color: #1f2937; }
- .page-title { font-size: 34rpx; font-weight: 700; color: #1f2937; flex: 1; text-align: center; margin-right: 60rpx; }
- .main-content {
- padding-top: calc(88rpx + 12rpx + env(safe-area-inset-top));
- padding-bottom: 200rpx;
- }
- /* ========== 卡片 ========== */
- .card {
- margin: 0 24rpx 20rpx;
- padding: 36rpx 28rpx;
- background: #ffffff;
- border-radius: 24rpx;
- box-shadow: 0 1rpx 3rpx rgba(0,0,0,0.04);
- overflow: hidden;
- }
- /* ========== 区域标题 ========== */
- .section-label {
- display: flex; align-items: center; gap: 10rpx;
- margin-bottom: 8rpx;
- }
- .section-label.collapsible { cursor: pointer; margin-bottom: 0; }
- .label-icon { font-size: 32rpx; }
- .label-text { font-size: 30rpx; font-weight: 700; color: #1f2937; }
- .label-required { color: #ef4444; font-size: 28rpx; font-weight: 600; margin-left: 4rpx; }
- .collapse-icon { font-size: 24rpx; color: #9ca3af; margin-left: auto; }
- /* ========== Hero 区域(描述) ========== */
- .hero-card { background: linear-gradient(180deg, #ffffff 0%, #fefefe 100%); }
- /* 示例提示词(轮播卡片) */
- .examples-row { display: flex; align-items: flex-start; gap: 12rpx; margin-bottom: 20rpx; }
- .examples-label { font-size: 22rpx; color: #9ca3af; flex-shrink: 0; padding-top: 12rpx; }
- .example-chips { display: flex; flex: 1; min-width: 0; }
- .example-chip {
- display: flex; align-items: center; gap: 12rpx;
- width: 100%;
- padding: 18rpx 20rpx;
- background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
- border: 1.5rpx solid #c7d2fe;
- border-radius: 14rpx;
- box-sizing: border-box;
- transition: transform 0.15s, box-shadow 0.15s;
- }
- .example-chip:active {
- transform: scale(0.98);
- box-shadow: 0 2rpx 8rpx rgba(99,102,241,0.18);
- }
- .example-chip-icon { font-size: 28rpx; flex-shrink: 0; }
- .example-chip-text {
- flex: 1; min-width: 0;
- font-size: 26rpx; color: #4338ca; line-height: 1.5;
- display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
- overflow: hidden;
- }
- .example-chip-hint {
- flex-shrink: 0;
- font-size: 20rpx; color: #6366f1;
- padding: 4rpx 10rpx;
- background: rgba(255,255,255,0.7);
- border-radius: 6rpx;
- font-weight: 500;
- }
- /* 切换时的淡入动画 */
- .example-chip-fade {
- animation: exampleFadeIn 0.45s ease both;
- }
- @keyframes exampleFadeIn {
- 0% { opacity: 0; transform: translateY(6rpx); }
- 100% { opacity: 1; transform: translateY(0); }
- }
- /* 文本域 */
- .textarea-wrapper { position: relative; margin-bottom: 16rpx; }
- .form-textarea {
- width: 100%; min-height: 420rpx;
- padding: 28rpx; padding-bottom: 60rpx;
- background: #f9fafb; border: 2rpx solid #e5e7eb;
- border-radius: 16rpx; font-size: 28rpx; line-height: 1.7;
- box-sizing: border-box; transition: border-color 0.2s, box-shadow 0.2s;
- }
- .form-textarea:focus { border-color: #6366f1; box-shadow: 0 0 0 6rpx rgba(99,102,241,0.08); }
- .textarea-error { border-color: #ef4444 !important; }
- .textarea-footer {
- position: absolute; bottom: 8rpx; right: 24rpx; left: 24rpx;
- display: flex; align-items: center; justify-content: flex-end; gap: 20rpx;
- height: 40rpx; pointer-events: none;
- }
- .char-count { font-size: 22rpx; color: #9ca3af; pointer-events: auto; }
- .clear-btn { font-size: 22rpx; color: #ef4444; pointer-events: auto; }
- /* AI 推荐按钮 */
- .ai-btn-row { display: flex; flex-direction: column; gap: 10rpx; }
- .btn-ai-recommend {
- display: flex; align-items: center; justify-content: center; gap: 10rpx;
- width: 100%; height: 84rpx;
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
- border-radius: 16rpx; border: none;
- font-size: 28rpx; font-weight: 600; color: white;
- transition: transform 0.15s;
- }
- .btn-ai-recommend:active { transform: scale(0.98); }
- .btn-ai-recommend[disabled] { opacity: 0.5; }
- .btn-ai-loading { opacity: 0.8; }
- .ai-btn-icon { font-size: 32rpx; }
- /* ========== 配置摘要卡片 ========== */
- .config-summary-card {
- background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%);
- border: 2rpx solid #bbf7d0;
- }
- .config-source-tag {
- font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 6rpx;
- background: #10b981; color: white; font-weight: 600; margin-left: auto;
- }
- .config-source-tag.template-tag { background: #6366f1; }
- .config-tags {
- display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 16rpx;
- }
- .config-tag {
- display: flex; align-items: center; gap: 6rpx;
- padding: 12rpx 18rpx; background: white;
- border-radius: 12rpx; border: 1.5rpx solid #d1d5db;
- font-size: 24rpx; transition: all 0.15s;
- }
- .config-tag:active { background: #f9fafb; transform: scale(0.97); }
- .config-tag.scale-tag { border-color: #6366f1; background: #eef2ff; }
- .config-tag-icon { font-size: 24rpx; }
- .config-tag-label { font-size: 20rpx; color: #9ca3af; }
- .config-tag-value { font-size: 24rpx; font-weight: 600; color: #1f2937; }
- .config-tag-edit { font-size: 20rpx; color: #6366f1; margin-left: 4rpx; }
- /* 预估信息 */
- .estimate-bar {
- display: flex; align-items: center; flex-wrap: wrap; gap: 8rpx;
- margin-top: 20rpx; padding: 20rpx;
- background: rgba(255,255,255,0.7); border-radius: 14rpx;
- }
- .estimate-item { display: flex; align-items: baseline; gap: 2rpx; }
- .est-icon { font-size: 22rpx; }
- .est-num { font-size: 28rpx; font-weight: 700; color: #065f46; }
- .est-unit { font-size: 22rpx; color: #047857; margin-left: 2rpx; }
- .est-divider { font-size: 22rpx; color: #6b7280; margin: 0 6rpx; }
- .quota-badge { margin-left: auto; flex-shrink: 0; }
- .quota-ok { font-size: 22rpx; color: #059669; font-weight: 600; }
- .quota-warn { font-size: 22rpx; color: #dc2626; font-weight: 600; }
- /* ========== 规模选择器 ========== */
- .scale-grid {
- display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx;
- margin-top: 16rpx;
- }
- .scale-card {
- display: flex; flex-direction: column; align-items: center; gap: 6rpx;
- padding: 20rpx 12rpx;
- background: #f9fafb; border: 2rpx solid #e5e7eb;
- border-radius: 14rpx; transition: all 0.2s;
- }
- .scale-card:active { transform: scale(0.96); }
- .scale-active {
- background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
- border-color: #6366f1; box-shadow: 0 0 0 3rpx rgba(99,102,241,0.1);
- }
- .scale-name { font-size: 26rpx; font-weight: 700; color: #1f2937; }
- .scale-active .scale-name { color: #4f46e5; }
- .scale-words { font-size: 22rpx; color: #6b7280; }
- .scale-active .scale-words { color: #6366f1; font-weight: 600; }
- .scale-audio { font-size: 20rpx; color: #9ca3af; }
- .scale-active .scale-audio { color: #6366f1; }
- .show-more-row { text-align: center; margin-top: 16rpx; }
- .show-more-btn {
- font-size: 24rpx; color: #6366f1; font-weight: 500;
- padding: 12rpx 32rpx; background: #eef2ff; border-radius: 12rpx;
- }
- /* ========== Chip 选择器组 ========== */
- .chip-section { margin-bottom: 28rpx; }
- .chip-section:last-child { margin-bottom: 0; }
- .chip-title {
- font-size: 26rpx; font-weight: 600; color: #374151;
- display: block; margin-bottom: 14rpx;
- }
- .chip-required { color: #ef4444; }
- .chip-row { display: flex; flex-wrap: wrap; gap: 12rpx; }
- .chip {
- display: flex; align-items: center; gap: 8rpx;
- padding: 14rpx 20rpx;
- background: #f9fafb; border: 2rpx solid #e5e7eb;
- border-radius: 14rpx; transition: all 0.15s;
- }
- .chip:active { transform: scale(0.97); }
- .chip-active {
- background: #eef2ff; border-color: #6366f1;
- box-shadow: 0 0 0 2rpx rgba(99,102,241,0.15);
- }
- .chip-sm { padding: 12rpx 18rpx; }
- .chip-xs { padding: 10rpx 16rpx; border-radius: 10rpx; }
- .chip-template { border-style: dashed; }
- .chip-icon { font-size: 26rpx; flex-shrink: 0; }
- .chip-sm .chip-icon { font-size: 24rpx; }
- .chip-body { display: flex; flex-direction: column; gap: 2rpx; }
- .chip-text { font-size: 26rpx; font-weight: 500; color: #374151; white-space: nowrap; }
- .chip-active .chip-text { color: #4f46e5; font-weight: 600; }
- .chip-sm .chip-text, .chip-xs .chip-text { font-size: 24rpx; }
- .chip-desc { font-size: 20rpx; color: #9ca3af; }
- .chip-active .chip-desc { color: #818cf8; }
- .chip-desc-inline { display: inline; margin-left: 6rpx; }
- .chip-check { font-size: 20rpx; color: #10b981; font-weight: 700; margin-left: 4rpx; }
- /* ========== 高级定制 ========== */
- .adv-badge {
- font-size: 20rpx; padding: 2rpx 10rpx; border-radius: 10rpx;
- background: #6366f1; color: white; font-weight: 600; margin-left: 8rpx;
- }
- .advanced-content { margin-top: 24rpx; padding-top: 24rpx; border-top: 1rpx solid #f3f4f6; }
- /* ========== 声音速度 ========== */
- .speed-display {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 8rpx;
- }
- .speed-value {
- font-size: 36rpx;
- font-weight: 700;
- color: #4f46e5;
- }
- .param-range {
- display: flex;
- justify-content: space-between;
- font-size: 22rpx;
- color: #9ca3af;
- margin-top: 4rpx;
- }
- /* ========== 底部操作区 ========== */
- .bottom-bar {
- position: fixed; bottom: 0; left: 0; right: 0;
- padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
- background: rgba(255,255,255,0.92);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border-top: 0.5rpx solid rgba(0,0,0,0.06);
- display: flex; flex-direction: column; align-items: center; gap: 12rpx;
- z-index: 100;
- }
- .btn-create {
- width: 100%; height: 96rpx;
- background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
- border-radius: 20rpx; border: none;
- font-size: 32rpx; font-weight: 700; color: white;
- display: flex; align-items: center; justify-content: center; gap: 10rpx;
- box-shadow: 0 8rpx 24rpx rgba(79,70,229,0.25);
- transition: transform 0.15s, box-shadow 0.15s;
- }
- .btn-create:active { transform: scale(0.98); box-shadow: 0 4rpx 12rpx rgba(79,70,229,0.2); }
- .btn-create[disabled] { opacity: 0.4; box-shadow: none; }
- .btn-create-icon { font-size: 36rpx; }
- .bottom-link {
- font-size: 24rpx; color: #9ca3af; text-align: center;
- }
- .bottom-link-highlight { color: #6366f1; font-weight: 600; }
- </style>
|