|
@@ -15,23 +15,19 @@
|
|
|
|
|
|
|
|
<!-- ===== 区域 1:描述你想要的书籍 ===== -->
|
|
<!-- ===== 区域 1:描述你想要的书籍 ===== -->
|
|
|
<view class="card hero-card">
|
|
<view class="card hero-card">
|
|
|
- <view class="section-label">
|
|
|
|
|
- <text class="label-icon">📝</text>
|
|
|
|
|
- <text class="label-text">描述你想要的有声书</text>
|
|
|
|
|
- <text class="label-required">*</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <text class="section-hint">用自然语言描述主题、风格和读者,越具体效果越好</text>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 示例提示词 -->
|
|
|
|
|
|
|
+ <!-- 示例提示词(轮播展示) -->
|
|
|
<view class="examples-row">
|
|
<view class="examples-row">
|
|
|
<text class="examples-label">试试:</text>
|
|
<text class="examples-label">试试:</text>
|
|
|
<view class="example-chips">
|
|
<view class="example-chips">
|
|
|
- <text
|
|
|
|
|
- v-for="ex in examplePrompts"
|
|
|
|
|
- :key="ex"
|
|
|
|
|
- class="example-chip"
|
|
|
|
|
- @click="newBook.description = ex"
|
|
|
|
|
- >{{ ex }}</text>
|
|
|
|
|
|
|
+ <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>
|
|
</view>
|
|
|
|
|
|
|
@@ -59,17 +55,14 @@
|
|
|
:disabled="isRecommending || !newBook.description.trim()"
|
|
:disabled="isRecommending || !newBook.description.trim()"
|
|
|
@click="requestSmartRecommend"
|
|
@click="requestSmartRecommend"
|
|
|
>
|
|
>
|
|
|
- <text class="ai-btn-icon">✨</text>
|
|
|
|
|
- <text>{{ isRecommending ? 'AI 分析中...' : 'AI 智能推荐配置' }}</text>
|
|
|
|
|
|
|
+ <text>{{ isRecommending ? 'AI 分析中...' : 'AI 智能推荐配置' }}</text>
|
|
|
</button>
|
|
</button>
|
|
|
- <text class="ai-btn-hint">AI 会分析描述并自动填充下方所有配置</text>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- ===== 区域 2:AI 推荐结果(有配置时显示) ===== -->
|
|
<!-- ===== 区域 2:AI 推荐结果(有配置时显示) ===== -->
|
|
|
<view v-if="!isEditMode && hasAnyConfig" class="card config-summary-card">
|
|
<view v-if="!isEditMode && hasAnyConfig" class="card config-summary-card">
|
|
|
<view class="section-label">
|
|
<view class="section-label">
|
|
|
- <text class="label-icon">🎯</text>
|
|
|
|
|
<text class="label-text">当前配置</text>
|
|
<text class="label-text">当前配置</text>
|
|
|
<text v-if="recommendSource === 'ai'" class="config-source-tag">AI 推荐</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>
|
|
<text v-else-if="recommendSource === 'template'" class="config-source-tag template-tag">模板</text>
|
|
@@ -77,33 +70,27 @@
|
|
|
|
|
|
|
|
<view class="config-tags">
|
|
<view class="config-tags">
|
|
|
<view v-if="newBook.bookScale" class="config-tag scale-tag" @click="scrollToSection('scale')">
|
|
<view v-if="newBook.bookScale" class="config-tag scale-tag" @click="scrollToSection('scale')">
|
|
|
- <text class="config-tag-icon">📏</text>
|
|
|
|
|
<text class="config-tag-label">规模</text>
|
|
<text class="config-tag-label">规模</text>
|
|
|
<text class="config-tag-value">{{ getScaleShortLabel(newBook.bookScale) }}</text>
|
|
<text class="config-tag-value">{{ getScaleShortLabel(newBook.bookScale) }}</text>
|
|
|
<text class="config-tag-edit">修改</text>
|
|
<text class="config-tag-edit">修改</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view v-if="newBook.targetAudience" class="config-tag" @click="scrollToSection('audience')">
|
|
<view v-if="newBook.targetAudience" class="config-tag" @click="scrollToSection('audience')">
|
|
|
- <text class="config-tag-icon">{{ getAudienceIcon(newBook.targetAudience) }}</text>
|
|
|
|
|
<text class="config-tag-value">{{ getAudienceLabel(newBook.targetAudience) }}</text>
|
|
<text class="config-tag-value">{{ getAudienceLabel(newBook.targetAudience) }}</text>
|
|
|
<text class="config-tag-edit">修改</text>
|
|
<text class="config-tag-edit">修改</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view v-if="newBook.knowledgeLevel" class="config-tag" @click="scrollToSection('knowledge')">
|
|
<view v-if="newBook.knowledgeLevel" class="config-tag" @click="scrollToSection('knowledge')">
|
|
|
- <text class="config-tag-icon">{{ getKnowledgeIcon(newBook.knowledgeLevel) }}</text>
|
|
|
|
|
<text class="config-tag-value">{{ getKnowledgeLevelLabel(newBook.knowledgeLevel) }}</text>
|
|
<text class="config-tag-value">{{ getKnowledgeLevelLabel(newBook.knowledgeLevel) }}</text>
|
|
|
<text class="config-tag-edit">修改</text>
|
|
<text class="config-tag-edit">修改</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view v-if="newBook.style" class="config-tag" @click="scrollToSection('style')">
|
|
<view v-if="newBook.style" class="config-tag" @click="scrollToSection('style')">
|
|
|
- <text class="config-tag-icon">✍️</text>
|
|
|
|
|
<text class="config-tag-value">{{ newBook.style }}</text>
|
|
<text class="config-tag-value">{{ newBook.style }}</text>
|
|
|
<text class="config-tag-edit">修改</text>
|
|
<text class="config-tag-edit">修改</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view v-if="newBook.industry" class="config-tag" @click="scrollToSection('industry')">
|
|
<view v-if="newBook.industry" class="config-tag" @click="scrollToSection('industry')">
|
|
|
- <text class="config-tag-icon">🏭</text>
|
|
|
|
|
<text class="config-tag-value">{{ getIndustryLabel(newBook.industry) || newBook.industry }}</text>
|
|
<text class="config-tag-value">{{ getIndustryLabel(newBook.industry) || newBook.industry }}</text>
|
|
|
<text class="config-tag-edit">修改</text>
|
|
<text class="config-tag-edit">修改</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view v-if="newBook.specialFeatures.length > 0" class="config-tag" @click="scrollToSection('features')">
|
|
<view v-if="newBook.specialFeatures.length > 0" class="config-tag" @click="scrollToSection('features')">
|
|
|
- <text class="config-tag-icon">⭐</text>
|
|
|
|
|
<text class="config-tag-value">{{ newBook.specialFeatures.length }}项特殊要求</text>
|
|
<text class="config-tag-value">{{ newBook.specialFeatures.length }}项特殊要求</text>
|
|
|
<text class="config-tag-edit">修改</text>
|
|
<text class="config-tag-edit">修改</text>
|
|
|
</view>
|
|
</view>
|
|
@@ -112,19 +99,16 @@
|
|
|
<!-- 预估信息 -->
|
|
<!-- 预估信息 -->
|
|
|
<view v-if="bookEstimate && bookEstimate.words" class="estimate-bar">
|
|
<view v-if="bookEstimate && bookEstimate.words" class="estimate-bar">
|
|
|
<view class="estimate-item">
|
|
<view class="estimate-item">
|
|
|
- <text class="est-icon">📖</text>
|
|
|
|
|
<text class="est-num">{{ formatNumber(bookEstimate.words.avg) }}</text>
|
|
<text class="est-num">{{ formatNumber(bookEstimate.words.avg) }}</text>
|
|
|
<text class="est-unit">字</text>
|
|
<text class="est-unit">字</text>
|
|
|
</view>
|
|
</view>
|
|
|
<text class="est-divider">·</text>
|
|
<text class="est-divider">·</text>
|
|
|
<view class="estimate-item">
|
|
<view class="estimate-item">
|
|
|
- <text class="est-icon">📑</text>
|
|
|
|
|
<text class="est-num">{{ bookEstimate.estimatedChapters }}</text>
|
|
<text class="est-num">{{ bookEstimate.estimatedChapters }}</text>
|
|
|
<text class="est-unit">章</text>
|
|
<text class="est-unit">章</text>
|
|
|
</view>
|
|
</view>
|
|
|
<text class="est-divider">·</text>
|
|
<text class="est-divider">·</text>
|
|
|
<view class="estimate-item">
|
|
<view class="estimate-item">
|
|
|
- <text class="est-icon">🎧</text>
|
|
|
|
|
<text class="est-num">{{ bookEstimate.audioMinutes.avg }}</text>
|
|
<text class="est-num">{{ bookEstimate.audioMinutes.avg }}</text>
|
|
|
<text class="est-unit">分钟</text>
|
|
<text class="est-unit">分钟</text>
|
|
|
</view>
|
|
</view>
|
|
@@ -141,8 +125,6 @@
|
|
|
<text class="label-icon">📏</text>
|
|
<text class="label-icon">📏</text>
|
|
|
<text class="label-text">选择书籍规模</text>
|
|
<text class="label-text">选择书籍规模</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <text class="section-hint">规模决定总字数和章节数,AI 会根据规模规划大纲</text>
|
|
|
|
|
-
|
|
|
|
|
<view class="scale-grid">
|
|
<view class="scale-grid">
|
|
|
<view
|
|
<view
|
|
|
v-for="scale in displayScales"
|
|
v-for="scale in displayScales"
|
|
@@ -177,7 +159,6 @@
|
|
|
<!-- ===== 区域 4:内容定位 ===== -->
|
|
<!-- ===== 区域 4:内容定位 ===== -->
|
|
|
<view class="card" id="section-audience">
|
|
<view class="card" id="section-audience">
|
|
|
<view class="section-label">
|
|
<view class="section-label">
|
|
|
- <text class="label-icon">👥</text>
|
|
|
|
|
<text class="label-text">内容定位</text>
|
|
<text class="label-text">内容定位</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
@@ -191,7 +172,6 @@
|
|
|
:class="['chip', newBook.targetAudience === a.value ? 'chip-active' : '']"
|
|
:class="['chip', newBook.targetAudience === a.value ? 'chip-active' : '']"
|
|
|
@click="newBook.targetAudience = newBook.targetAudience === a.value ? '' : a.value"
|
|
@click="newBook.targetAudience = newBook.targetAudience === a.value ? '' : a.value"
|
|
|
>
|
|
>
|
|
|
- <text class="chip-icon">{{ a.icon }}</text>
|
|
|
|
|
<view class="chip-body">
|
|
<view class="chip-body">
|
|
|
<text class="chip-text">{{ a.label }}</text>
|
|
<text class="chip-text">{{ a.label }}</text>
|
|
|
<text class="chip-desc">{{ a.desc }}</text>
|
|
<text class="chip-desc">{{ a.desc }}</text>
|
|
@@ -210,7 +190,6 @@
|
|
|
:class="['chip chip-sm', newBook.knowledgeLevel === l.value ? 'chip-active' : '']"
|
|
:class="['chip chip-sm', newBook.knowledgeLevel === l.value ? 'chip-active' : '']"
|
|
|
@click="newBook.knowledgeLevel = newBook.knowledgeLevel === l.value ? '' : l.value"
|
|
@click="newBook.knowledgeLevel = newBook.knowledgeLevel === l.value ? '' : l.value"
|
|
|
>
|
|
>
|
|
|
- <text class="chip-icon">{{ l.icon }}</text>
|
|
|
|
|
<view class="chip-body">
|
|
<view class="chip-body">
|
|
|
<text class="chip-text">{{ l.label }}</text>
|
|
<text class="chip-text">{{ l.label }}</text>
|
|
|
</view>
|
|
</view>
|
|
@@ -228,7 +207,6 @@
|
|
|
:class="['chip chip-sm', newBook.bookType === bt.value ? 'chip-active' : '']"
|
|
:class="['chip chip-sm', newBook.bookType === bt.value ? 'chip-active' : '']"
|
|
|
@click="newBook.bookType = newBook.bookType === bt.value ? 'auto' : bt.value"
|
|
@click="newBook.bookType = newBook.bookType === bt.value ? 'auto' : bt.value"
|
|
|
>
|
|
>
|
|
|
- <text class="chip-icon">{{ bt.icon }}</text>
|
|
|
|
|
<view class="chip-body">
|
|
<view class="chip-body">
|
|
|
<text class="chip-text">{{ bt.label }}</text>
|
|
<text class="chip-text">{{ bt.label }}</text>
|
|
|
</view>
|
|
</view>
|
|
@@ -253,10 +231,29 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- ===== 区域 5:高级定制(可折叠) ===== -->
|
|
|
|
|
|
|
+ <!-- ===== 区域 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="card">
|
|
|
<view class="section-label collapsible" @click="showAdvancedOptions = !showAdvancedOptions">
|
|
<view class="section-label collapsible" @click="showAdvancedOptions = !showAdvancedOptions">
|
|
|
- <text class="label-icon">⚙️</text>
|
|
|
|
|
<text class="label-text">高级定制</text>
|
|
<text class="label-text">高级定制</text>
|
|
|
<view v-if="advancedBadgeCount > 0" class="adv-badge">{{ advancedBadgeCount }}</view>
|
|
<view v-if="advancedBadgeCount > 0" class="adv-badge">{{ advancedBadgeCount }}</view>
|
|
|
<text class="collapse-icon">{{ showAdvancedOptions ? '▲' : '▼' }}</text>
|
|
<text class="collapse-icon">{{ showAdvancedOptions ? '▲' : '▼' }}</text>
|
|
@@ -273,7 +270,6 @@
|
|
|
:class="['chip chip-sm chip-template', selectedTemplateName === tmpl.name ? 'chip-active' : '']"
|
|
:class="['chip chip-sm chip-template', selectedTemplateName === tmpl.name ? 'chip-active' : '']"
|
|
|
@click="applyTemplate(tmpl)"
|
|
@click="applyTemplate(tmpl)"
|
|
|
>
|
|
>
|
|
|
- <text class="chip-icon">{{ tmpl.icon }}</text>
|
|
|
|
|
<view class="chip-body">
|
|
<view class="chip-body">
|
|
|
<text class="chip-text">{{ tmpl.name }}</text>
|
|
<text class="chip-text">{{ tmpl.name }}</text>
|
|
|
<text class="chip-desc chip-desc-inline">{{ tmpl.desc }}</text>
|
|
<text class="chip-desc chip-desc-inline">{{ tmpl.desc }}</text>
|
|
@@ -307,7 +303,6 @@
|
|
|
:class="['chip chip-sm', newBook.industry === ind.value ? 'chip-active' : '']"
|
|
:class="['chip chip-sm', newBook.industry === ind.value ? 'chip-active' : '']"
|
|
|
@click="newBook.industry = newBook.industry === ind.value ? '' : ind.value"
|
|
@click="newBook.industry = newBook.industry === ind.value ? '' : ind.value"
|
|
|
>
|
|
>
|
|
|
- <text class="chip-icon">{{ ind.icon }}</text>
|
|
|
|
|
<text class="chip-text">{{ ind.label }}</text>
|
|
<text class="chip-text">{{ ind.label }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -323,7 +318,6 @@
|
|
|
:class="['chip chip-sm', newBook.specialFeatures.includes(sf.value) ? 'chip-active' : '']"
|
|
:class="['chip chip-sm', newBook.specialFeatures.includes(sf.value) ? 'chip-active' : '']"
|
|
|
@click="toggleSpecialFeature(sf.value)"
|
|
@click="toggleSpecialFeature(sf.value)"
|
|
|
>
|
|
>
|
|
|
- <text class="chip-icon">{{ sf.icon }}</text>
|
|
|
|
|
<text class="chip-text">{{ sf.label }}</text>
|
|
<text class="chip-text">{{ sf.label }}</text>
|
|
|
<text v-if="newBook.specialFeatures.includes(sf.value)" class="chip-check">✓</text>
|
|
<text v-if="newBook.specialFeatures.includes(sf.value)" class="chip-check">✓</text>
|
|
|
</view>
|
|
</view>
|
|
@@ -348,7 +342,6 @@
|
|
|
:disabled="!canCreateBook || creating"
|
|
:disabled="!canCreateBook || creating"
|
|
|
@click="createNewBook"
|
|
@click="createNewBook"
|
|
|
>
|
|
>
|
|
|
- <text class="btn-create-icon">🚀</text>
|
|
|
|
|
<text>{{ creating ? '创建中...' : '开始创建有声书' }}</text>
|
|
<text>{{ creating ? '创建中...' : '开始创建有声书' }}</text>
|
|
|
</button>
|
|
</button>
|
|
|
<text class="bottom-link" @click="switchToInteractive">需要更精细控制?试试<text class="bottom-link-highlight">交互模式</text></text>
|
|
<text class="bottom-link" @click="switchToInteractive">需要更精细控制?试试<text class="bottom-link-highlight">交互模式</text></text>
|
|
@@ -360,7 +353,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, onMounted } from 'vue';
|
|
|
|
|
|
|
+import { ref, computed, onMounted, onUnmounted, onBeforeUnmount } from 'vue';
|
|
|
import { onLoad } from '@dcloudio/uni-app';
|
|
import { onLoad } from '@dcloudio/uni-app';
|
|
|
import * as api from '../../utils/book-generator-api';
|
|
import * as api from '../../utils/book-generator-api';
|
|
|
import { getApiBaseUrl } from '../../utils/config';
|
|
import { getApiBaseUrl } from '../../utils/config';
|
|
@@ -388,6 +381,7 @@ const newBook = ref({
|
|
|
bookScale: '',
|
|
bookScale: '',
|
|
|
bookType: 'auto',
|
|
bookType: 'auto',
|
|
|
outlineLevel: 'auto',
|
|
outlineLevel: 'auto',
|
|
|
|
|
+ voiceSpeed: 1.0,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 示例提示词
|
|
// 示例提示词
|
|
@@ -398,6 +392,30 @@ const examplePrompts = [
|
|
|
'介绍中国历史的通识读物,按时间线讲述,通俗易懂适合大众读者',
|
|
'介绍中国历史的通识读物,按时间线讲述,通俗易懂适合大众读者',
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
|
|
+// 示例提示词轮播
|
|
|
|
|
+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<{
|
|
const bookEstimate = ref<{
|
|
|
scale: string;
|
|
scale: string;
|
|
@@ -645,30 +663,64 @@ async function requestSmartRecommend() {
|
|
|
let detectTimer: any = null;
|
|
let detectTimer: any = null;
|
|
|
async function detectScaleFromDesc() {
|
|
async function detectScaleFromDesc() {
|
|
|
const desc = newBook.value.description?.trim();
|
|
const desc = newBook.value.description?.trim();
|
|
|
- if (!desc || newBook.value.bookScale) return;
|
|
|
|
|
|
|
+ if (!desc) return;
|
|
|
clearTimeout(detectTimer);
|
|
clearTimeout(detectTimer);
|
|
|
detectTimer = setTimeout(async () => {
|
|
detectTimer = setTimeout(async () => {
|
|
|
- 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 */ }
|
|
|
|
|
|
|
+ // 检测是否为英文内容,自动调慢语速
|
|
|
|
|
+ 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);
|
|
}, 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() {
|
|
function switchToInteractive() {
|
|
|
uni.navigateTo({ url: '/pages/book-generator/interactive' });
|
|
uni.navigateTo({ url: '/pages/book-generator/interactive' });
|
|
|
}
|
|
}
|
|
@@ -750,6 +802,7 @@ async function createNewBook() {
|
|
|
bookScale: newBook.value.bookScale || undefined,
|
|
bookScale: newBook.value.bookScale || undefined,
|
|
|
bookType,
|
|
bookType,
|
|
|
genLevel,
|
|
genLevel,
|
|
|
|
|
+ voiceSpeed: newBook.value.voiceSpeed !== 1.0 ? newBook.value.voiceSpeed : undefined,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
uni.showToast({ title: '创建成功,正在跳转...', icon: 'success' });
|
|
uni.showToast({ title: '创建成功,正在跳转...', icon: 'success' });
|
|
@@ -763,7 +816,15 @@ async function createNewBook() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-onMounted(() => {});
|
|
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ startExampleRotation();
|
|
|
|
|
+});
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ stopExampleRotation();
|
|
|
|
|
+});
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ stopExampleRotation();
|
|
|
|
|
+});
|
|
|
onLoad(async (query: any) => {
|
|
onLoad(async (query: any) => {
|
|
|
if (query?.editId) {
|
|
if (query?.editId) {
|
|
|
isEditMode.value = true;
|
|
isEditMode.value = true;
|
|
@@ -828,29 +889,59 @@ onLoad(async (query: any) => {
|
|
|
.label-text { font-size: 30rpx; font-weight: 700; color: #1f2937; }
|
|
.label-text { font-size: 30rpx; font-weight: 700; color: #1f2937; }
|
|
|
.label-required { color: #ef4444; font-size: 28rpx; font-weight: 600; margin-left: 4rpx; }
|
|
.label-required { color: #ef4444; font-size: 28rpx; font-weight: 600; margin-left: 4rpx; }
|
|
|
.collapse-icon { font-size: 24rpx; color: #9ca3af; margin-left: auto; }
|
|
.collapse-icon { font-size: 24rpx; color: #9ca3af; margin-left: auto; }
|
|
|
-.section-hint { font-size: 24rpx; color: #9ca3af; display: block; margin-bottom: 24rpx; margin-top: 4rpx; }
|
|
|
|
|
|
|
|
|
|
/* ========== Hero 区域(描述) ========== */
|
|
/* ========== Hero 区域(描述) ========== */
|
|
|
.hero-card { background: linear-gradient(180deg, #ffffff 0%, #fefefe 100%); }
|
|
.hero-card { background: linear-gradient(180deg, #ffffff 0%, #fefefe 100%); }
|
|
|
|
|
|
|
|
-/* 示例提示词 */
|
|
|
|
|
|
|
+/* 示例提示词(轮播卡片) */
|
|
|
.examples-row { display: flex; align-items: flex-start; gap: 12rpx; margin-bottom: 20rpx; }
|
|
.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: 4rpx; }
|
|
|
|
|
-.example-chips { display: flex; flex-wrap: wrap; gap: 10rpx; flex: 1; }
|
|
|
|
|
|
|
+.examples-label { font-size: 22rpx; color: #9ca3af; flex-shrink: 0; padding-top: 12rpx; }
|
|
|
|
|
+.example-chips { display: flex; flex: 1; min-width: 0; }
|
|
|
|
|
+
|
|
|
.example-chip {
|
|
.example-chip {
|
|
|
- font-size: 22rpx; color: #4f46e5; background: #eef2ff;
|
|
|
|
|
- padding: 8rpx 16rpx; border-radius: 8rpx;
|
|
|
|
|
- line-height: 1.4; max-width: 340rpx;
|
|
|
|
|
|
|
+ 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;
|
|
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
}
|
|
}
|
|
|
-.example-chip:active { background: #e0e7ff; transform: scale(0.97); }
|
|
|
|
|
|
|
+.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; }
|
|
.textarea-wrapper { position: relative; margin-bottom: 16rpx; }
|
|
|
.form-textarea {
|
|
.form-textarea {
|
|
|
- width: 100%; min-height: 260rpx;
|
|
|
|
|
- padding: 24rpx; padding-bottom: 56rpx;
|
|
|
|
|
|
|
+ width: 100%; min-height: 420rpx;
|
|
|
|
|
+ padding: 28rpx; padding-bottom: 60rpx;
|
|
|
background: #f9fafb; border: 2rpx solid #e5e7eb;
|
|
background: #f9fafb; border: 2rpx solid #e5e7eb;
|
|
|
border-radius: 16rpx; font-size: 28rpx; line-height: 1.7;
|
|
border-radius: 16rpx; font-size: 28rpx; line-height: 1.7;
|
|
|
box-sizing: border-box; transition: border-color 0.2s, box-shadow 0.2s;
|
|
box-sizing: border-box; transition: border-color 0.2s, box-shadow 0.2s;
|
|
@@ -880,7 +971,6 @@ onLoad(async (query: any) => {
|
|
|
.btn-ai-recommend[disabled] { opacity: 0.5; }
|
|
.btn-ai-recommend[disabled] { opacity: 0.5; }
|
|
|
.btn-ai-loading { opacity: 0.8; }
|
|
.btn-ai-loading { opacity: 0.8; }
|
|
|
.ai-btn-icon { font-size: 32rpx; }
|
|
.ai-btn-icon { font-size: 32rpx; }
|
|
|
-.ai-btn-hint { font-size: 22rpx; color: #9ca3af; text-align: center; display: block; }
|
|
|
|
|
|
|
|
|
|
/* ========== 配置摘要卡片 ========== */
|
|
/* ========== 配置摘要卡片 ========== */
|
|
|
.config-summary-card {
|
|
.config-summary-card {
|
|
@@ -997,6 +1087,26 @@ onLoad(async (query: any) => {
|
|
|
}
|
|
}
|
|
|
.advanced-content { margin-top: 24rpx; padding-top: 24rpx; border-top: 1rpx solid #f3f4f6; }
|
|
.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 {
|
|
.bottom-bar {
|
|
|
position: fixed; bottom: 0; left: 0; right: 0;
|
|
position: fixed; bottom: 0; left: 0; right: 0;
|