create.vue 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. <template>
  2. <view class="page">
  3. <!-- 顶部导航栏 -->
  4. <view class="nav-bar">
  5. <view class="nav-content">
  6. <view class="nav-left" @click="goBack">
  7. <text class="back-icon">←</text>
  8. </view>
  9. <text class="page-title">创建书籍</text>
  10. </view>
  11. <!-- 模式切换独立行 -->
  12. <view class="mode-bar">
  13. <text class="mode-switch" @click="switchToInteractive">切换到交互模式</text>
  14. </view>
  15. </view>
  16. <!-- 主内容区 -->
  17. <view class="main-content">
  18. <view class="card">
  19. <view class="card-header">
  20. <text class="card-title">📖 创建新书籍</text>
  21. </view>
  22. <!-- 简洁模式提示 -->
  23. <view class="simple-mode-tip">
  24. <view class="tip-header">
  25. <text class="tip-icon">💡</text>
  26. <text class="tip-title">简洁模式</text>
  27. </view>
  28. <text class="tip-content">
  29. 先填写基本信息创建书籍,高级选项可在创建后修改。
  30. </text>
  31. </view>
  32. <!-- 书名:由AI根据描述自动生成,无需手动输入 -->
  33. <!-- 描述/主题 -->
  34. <view class="form-item">
  35. <text class="form-label">内容描述 *</text>
  36. <textarea
  37. v-model="newBook.description"
  38. class="form-textarea"
  39. placeholder="描述这本书的内容、主题、写作目的..."
  40. :maxlength="500"
  41. @blur="detectScaleFromDesc"
  42. />
  43. </view>
  44. <!-- 面向人群 -->
  45. <view class="form-item highlight-item">
  46. <view class="label-with-badge">
  47. <text class="form-label">面向人群</text>
  48. <view class="required-badge">重要</view>
  49. </view>
  50. <text class="form-hint">选择目标读者,AI会根据人群调整语言风格和表达方式</text>
  51. <view class="chip-group">
  52. <view
  53. v-for="audience in audiences"
  54. :key="audience.value"
  55. :class="['chip', 'audience-chip', newBook.targetAudience === audience.value ? 'active' : '']"
  56. @click="newBook.targetAudience = audience.value"
  57. >
  58. <text class="chip-icon">{{ audience.icon }}</text>
  59. <text class="chip-text">{{ audience.label }}</text>
  60. </view>
  61. </view>
  62. </view>
  63. <!-- 书籍类型 -->
  64. <view class="form-item">
  65. <view class="label-with-badge">
  66. <text class="form-label">书籍类型</text>
  67. <view class="required-badge" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);">影响大纲结构</view>
  68. </view>
  69. <text class="form-hint-small">选择类型决定大纲层级。不选则由AI自动判断</text>
  70. <view class="chip-group" style="margin-top: 8rpx;">
  71. <view
  72. v-for="bt in bookTypes"
  73. :key="bt.value"
  74. :class="['chip', newBook.bookType === bt.value ? 'active' : '']"
  75. @click="newBook.bookType = bt.value"
  76. >
  77. <text class="chip-icon">{{ bt.icon }}</text>
  78. <text class="chip-text">{{ bt.label }}</text>
  79. <text class="chip-desc" style="font-size: 20rpx; margin-left: 4rpx;">{{ bt.desc }}</text>
  80. </view>
  81. </view>
  82. </view>
  83. <!-- 书籍规模 -->
  84. <view class="form-item">
  85. <text class="form-label">书籍规模 *</text>
  86. <view class="scale-picker">
  87. <view
  88. v-for="scale in bookScales"
  89. :key="scale.value"
  90. :class="['scale-option', { active: newBook.bookScale === scale.value }]"
  91. @click="selectBookScale(scale.value)"
  92. >
  93. <text class="scale-words">{{ scale.words }}</text>
  94. <text class="scale-label">{{ scale.label }} {{ scale.chapters }}</text>
  95. <text class="scale-audio">{{ scale.audio }}</text>
  96. </view>
  97. </view>
  98. </view>
  99. <!-- 知识难度 -->
  100. <view class="form-item highlight-item">
  101. <view class="label-with-badge">
  102. <text class="form-label">知识难度 *</text>
  103. </view>
  104. <view class="chip-group">
  105. <view
  106. v-for="level in knowledgeLevels"
  107. :key="level.value"
  108. :class="['chip', 'level-chip', newBook.knowledgeLevel === level.value ? 'active' : '']"
  109. @click="newBook.knowledgeLevel = level.value"
  110. >
  111. <text class="chip-icon">{{ level.icon }}</text>
  112. <text class="chip-text">{{ level.label }}</text>
  113. </view>
  114. </view>
  115. </view>
  116. <!-- 智能推荐按钮 -->
  117. <view class="form-item smart-recommend-section">
  118. <button
  119. class="btn-smart-recommend"
  120. :disabled="isRecommending || !newBook.description.trim()"
  121. @click="requestSmartRecommend"
  122. >
  123. <text class="recommend-icon">{{ isRecommending ? '⏳' : '✨' }}</text>
  124. <text class="recommend-text">{{ isRecommending ? '推荐中...' : '智能推荐' }}</text>
  125. </button>
  126. <text class="form-hint-small">根据内容描述,AI推荐难度/人群/风格/领域</text>
  127. </view>
  128. <!-- 高级选项折叠区 -->
  129. <view class="advanced-section">
  130. <view class="advanced-header" @click="showAdvancedOptions = !showAdvancedOptions">
  131. <text class="advanced-title">高级选项</text>
  132. <text class="advanced-arrow">{{ showAdvancedOptions ? '▲' : '▼' }}</text>
  133. </view>
  134. <!-- 折叠内容 -->
  135. <view v-if="showAdvancedOptions" class="advanced-content">
  136. <!-- 副标题 -->
  137. <view class="form-item">
  138. <text class="form-label">副标题</text>
  139. <input
  140. v-model="newBook.subtitle"
  141. class="form-input"
  142. placeholder="例如:一本写给青少年的科普书"
  143. />
  144. </view>
  145. <!-- 快速模板 -->
  146. <view class="form-item">
  147. <text class="form-label">📋 快速模板</text>
  148. <text class="form-hint-small">点击模板自动填充所有配置,也可手动修改</text>
  149. <view class="template-grid">
  150. <view
  151. v-for="template in quickTemplates"
  152. :key="template.name"
  153. class="template-card"
  154. @click="applyTemplate(template)"
  155. >
  156. <text class="template-icon">{{ template.icon }}</text>
  157. <text class="template-name">{{ template.name }}</text>
  158. <text class="template-desc">{{ template.desc }}</text>
  159. </view>
  160. </view>
  161. </view>
  162. <!-- 重要提示 -->
  163. <view class="important-tip">
  164. <view class="tip-header">
  165. <text class="tip-icon">💡</text>
  166. <text class="tip-title">重要提示</text>
  167. </view>
  168. <text class="tip-content">
  169. 知识难度和面向人群会显著影响生成内容的风格和深度。同样的知识,面向不同人群,表达方式完全不同:
  170. </text>
  171. <view class="tip-examples">
  172. <text class="tip-example">• 面向小学生:形象、简单、通俗易懂</text>
  173. <text class="tip-example">• 面向大学生:系统、专业、有理论深度</text>
  174. <text class="tip-example">• 面向研究生:前沿、深入、有研究价值</text>
  175. </view>
  176. </view>
  177. <!-- 写作风格 -->
  178. <view class="form-item">
  179. <text class="form-label">写作风格</text>
  180. <view class="chip-group">
  181. <view
  182. v-for="style in styles"
  183. :key="style"
  184. :class="['chip', newBook.style === style ? 'active' : '']"
  185. @click="newBook.style = style"
  186. >
  187. {{ style }}
  188. </view>
  189. </view>
  190. </view>
  191. <!-- 行业领域 -->
  192. <view class="form-item">
  193. <text class="form-label">行业领域</text>
  194. <text class="form-hint-small">选择行业领域,AI会使用相关的案例和术语</text>
  195. <view class="chip-group">
  196. <view
  197. v-for="industry in industries"
  198. :key="industry.value"
  199. :class="['chip', newBook.industry === industry.value ? 'active' : '']"
  200. @click="newBook.industry = industry.value"
  201. >
  202. <text class="chip-icon">{{ industry.icon }}</text>
  203. <text class="chip-text">{{ industry.label }}</text>
  204. </view>
  205. </view>
  206. </view>
  207. <!-- 特殊要求 (多选) -->
  208. <view class="form-item">
  209. <text class="form-label">特殊要求</text>
  210. <text class="form-hint-small">可多选,AI会在生成时包含这些元素</text>
  211. <view class="chip-group">
  212. <view
  213. v-for="feature in specialFeatures"
  214. :key="feature.value"
  215. :class="['chip', 'feature-chip', newBook.specialFeatures.includes(feature.value) ? 'active' : '']"
  216. @click="toggleSpecialFeature(feature.value)"
  217. >
  218. <text class="chip-icon">{{ feature.icon }}</text>
  219. <text class="chip-text">{{ feature.label }}</text>
  220. </view>
  221. </view>
  222. </view>
  223. </view>
  224. </view>
  225. <!-- 大纲层级 -->
  226. <view class="form-item">
  227. <view class="label-with-badge">
  228. <text class="form-label">大纲层级</text>
  229. <view class="required-badge" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);">高级</view>
  230. </view>
  231. <text class="form-hint-small">控制章节结构的详细程度,默认"自动"会根据书籍规模智能选择</text>
  232. <view class="chip-group" style="margin-top: 8rpx;">
  233. <view
  234. v-for="opt in outlineLevelOptions"
  235. :key="opt.value"
  236. :class="['chip', 'level-chip', newBook.outlineLevel === opt.value ? 'active' : '']"
  237. @click="newBook.outlineLevel = opt.value"
  238. >
  239. <text class="chip-text">{{ opt.label }}</text>
  240. <text class="chip-desc" style="font-size: 20rpx; color: #9ca3af; margin-left: 4rpx;">{{ opt.desc }}</text>
  241. </view>
  242. </view>
  243. </view>
  244. <!-- 预估信息(选择规模后显示) -->
  245. <view v-if="bookEstimate && bookEstimate.words && bookEstimate.audioMinutes" class="estimate-card">
  246. <view class="estimate-header">
  247. <text class="estimate-title">📊 生成预估</text>
  248. </view>
  249. <view class="estimate-row">
  250. <text class="estimate-label">预估字数:</text>
  251. <text class="estimate-value">{{ bookEstimate.words.min }}~{{ bookEstimate.words.max }}字</text>
  252. </view>
  253. <view class="estimate-row">
  254. <text class="estimate-label">预估时长:</text>
  255. <text class="estimate-value">{{ bookEstimate.audioMinutes.min }}~{{ bookEstimate.audioMinutes.max }}分钟</text>
  256. </view>
  257. <view class="estimate-row">
  258. <text class="estimate-label">预估章节:</text>
  259. <text class="estimate-value">约{{ bookEstimate.estimatedChapters }}章</text>
  260. </view>
  261. <view v-if="bookEstimate.genLevelInfo" class="estimate-row genlevel-row">
  262. <text class="estimate-label">大纲层级:</text>
  263. <view class="genlevel-tag" :class="'level-' + bookEstimate.defaultGenLevel">
  264. <text class="genlevel-label">{{ bookEstimate.genLevelInfo.label }}</text>
  265. <text class="genlevel-desc">{{ bookEstimate.genLevelInfo.desc }}</text>
  266. </view>
  267. </view>
  268. <!-- 配额检查结果 -->
  269. <view v-if="quotaCheck" class="quota-check">
  270. <view v-if="quotaCheck.allowed" class="quota-ok">
  271. <text class="quota-icon">✅</text>
  272. <text class="quota-text">额度充足,可生成</text>
  273. </view>
  274. <view v-else class="quota-warning">
  275. <text class="quota-icon">⚠️</text>
  276. <text class="quota-text">{{ quotaCheck.reason }}</text>
  277. </view>
  278. <!-- 详细配额信息 -->
  279. <view class="quota-detail">
  280. <text class="quota-info">您当前额度:{{ quotaCheck.quota.remainingMinutes }}分钟 / {{ quotaCheck.quota.totalMinutes }}分钟</text>
  281. <view v-if="quotaCheck.costEstimate.overageMinutes > 0" class="quota-overage">
  282. <text>预计超出:{{ quotaCheck.costEstimate.overageMinutes }}分钟</text>
  283. <text class="quota-price">额外费用:¥{{ quotaCheck.costEstimate.estimatedPrice }}</text>
  284. </view>
  285. </view>
  286. </view>
  287. </view>
  288. <view class="btn-group">
  289. <button class="btn-cancel" @click="goBack">取消</button>
  290. <button
  291. class="btn-primary"
  292. :disabled="!canCreateBook || creating"
  293. @click="createNewBook"
  294. >
  295. {{ creating ? '创建中...' : '创建书籍' }}
  296. </button>
  297. </view>
  298. </view>
  299. </view>
  300. </view>
  301. </template>
  302. <script setup lang="ts">
  303. import { ref, computed, onMounted } from 'vue';
  304. import { onLoad } from '@dcloudio/uni-app';
  305. import * as api from '../../utils/book-generator-api';
  306. import { getApiBaseUrl } from '../../utils/config';
  307. const BASE_URL = getApiBaseUrl();
  308. // 创建表单
  309. const creating = ref(false);
  310. const showAdvancedOptions = ref(false);
  311. const isRecommending = ref(false);
  312. const newBook = ref({
  313. title: '',
  314. subtitle: '',
  315. description: '',
  316. targetAudience: '',
  317. knowledgeLevel: '',
  318. industry: '',
  319. specialFeatures: [] as string[],
  320. style: '',
  321. bookScale: '',
  322. bookType: 'auto',
  323. outlineLevel: 'auto',
  324. });
  325. // 书籍预估信息
  326. const bookEstimate = ref<{
  327. scale: string;
  328. words: { min: number; max: number; avg: number };
  329. audioMinutes: { min: number; max: number; avg: number };
  330. estimatedChapters: number;
  331. defaultGenLevel?: number;
  332. genLevelInfo?: { label: string; desc: string };
  333. } | null>(null);
  334. // 配额检查结果
  335. const quotaCheck = ref<{
  336. allowed: boolean;
  337. reason: string | null;
  338. estimatedWords: { min: number; max: number; avg: number };
  339. estimatedAudioMinutes: { min: number; max: number; avg: number };
  340. quota: {
  341. totalMinutes: number;
  342. usedMinutes: number;
  343. remainingMinutes: number;
  344. overageEnabled: boolean;
  345. overagePrice: number;
  346. };
  347. costEstimate: {
  348. inQuotaMinutes: number;
  349. overageMinutes: number;
  350. estimatedPrice: number;
  351. displayText: string;
  352. };
  353. } | null>(null);
  354. // 选项配置
  355. const knowledgeLevels = [
  356. { value: '入门', label: '入门级', icon: '🌱', desc: '零基础,简单易懂' },
  357. { value: '基础', label: '基础级', icon: '📚', desc: '基本概念,循序渐进' },
  358. { value: '进阶', label: '进阶级', icon: '📈', desc: '深入讲解,系统全面' },
  359. { value: '高级', label: '高级级', icon: '🎯', desc: '专业深入,前沿技术' },
  360. { value: '专家', label: '专家级', icon: '🔬', desc: '研究级别,底层原理' },
  361. ];
  362. const audiences = [
  363. { value: '儿童', label: '儿童', icon: '👶', desc: '6-12岁,形象生动' },
  364. { value: '青少年', label: '青少年', icon: '🧑', desc: '13-18岁,通俗易懂' },
  365. { value: '大学生', label: '大学生', icon: '🎓', desc: '系统专业,有理论' },
  366. { value: '专业人士', label: '专业人士', icon: '💼', desc: '从业者,实用深入' },
  367. { value: '研究生', label: '研究生', icon: '🔍', desc: '研究级别,前沿深入' },
  368. { value: '大众读者', label: '大众读者', icon: '👥', desc: '通俗普及,有趣味' },
  369. ];
  370. const industries = [
  371. { value: '', label: '不选', icon: '➖', desc: '不限定行业领域' },
  372. { value: 'IT/计算机', label: 'IT/计算机', icon: '💻', desc: '编程、软件、互联网' },
  373. { value: '金融', label: '金融', icon: '💰', desc: '投资、银行、证券' },
  374. { value: '医学', label: '医学', icon: '🏥', desc: '医疗、健康、临床' },
  375. { value: '教育', label: '教育', icon: '📚', desc: '教学、学习、心理学' },
  376. { value: '文学', label: '文学', icon: '✍️', desc: '小说、散文、传记' },
  377. { value: '科普', label: '科普', icon: '🔬', desc: '科学、自然、探索' },
  378. { value: '商业', label: '商业', icon: '📊', desc: '管理、营销、创业' },
  379. { value: '艺术', label: '艺术', icon: '🎨', desc: '设计、音乐、影视' },
  380. ];
  381. const specialFeatures = [
  382. { value: '案例分析', label: '案例分析', icon: '📖' },
  383. { value: '代码示例', label: '代码示例', icon: '💻' },
  384. { value: '习题练习', label: '习题练习', icon: '✍️' },
  385. { value: '图表插图', label: '图表插图', icon: '📊' },
  386. { value: '项目实战', label: '项目实战', icon: '🎯' },
  387. { value: '思维导图', label: '思维导图', icon: '🧠' },
  388. { value: '总结回顾', label: '总结回顾', icon: '📝' },
  389. ];
  390. const quickTemplates = [
  391. {
  392. name: '大学教材',
  393. icon: '🎓',
  394. desc: '面向大学生,系统专业,有案例习题',
  395. config: {
  396. knowledgeLevel: '进阶',
  397. targetAudience: '大学生',
  398. industry: '教育',
  399. specialFeatures: ['案例分析', '习题练习', '总结回顾'],
  400. style: '专业严谨',
  401. },
  402. },
  403. {
  404. name: '技术教程',
  405. icon: '💻',
  406. desc: '面向初学者,有代码示例和实战项目',
  407. config: {
  408. knowledgeLevel: '入门',
  409. targetAudience: '初学者',
  410. industry: 'IT/计算机',
  411. specialFeatures: ['代码示例', '项目实战', '案例分析'],
  412. style: '通俗易懂',
  413. },
  414. },
  415. {
  416. name: '科普读物',
  417. icon: '🔬',
  418. desc: '面向大众,通俗易懂,有趣味性',
  419. config: {
  420. knowledgeLevel: '入门',
  421. targetAudience: '大众读者',
  422. industry: '科普',
  423. specialFeatures: ['案例分析', '图表插图'],
  424. style: '轻松幽默',
  425. },
  426. },
  427. {
  428. name: '商业书籍',
  429. icon: '📊',
  430. desc: '面向从业者,有真实案例,实用性强',
  431. config: {
  432. knowledgeLevel: '进阶',
  433. targetAudience: '专业人士',
  434. industry: '商业',
  435. specialFeatures: ['案例分析', '总结回顾'],
  436. style: '专业严谨',
  437. },
  438. },
  439. {
  440. name: '儿童读物',
  441. icon: '👶',
  442. desc: '面向儿童,形象生动,有趣味性',
  443. config: {
  444. knowledgeLevel: '入门',
  445. targetAudience: '儿童',
  446. industry: '教育',
  447. specialFeatures: ['图表插图'],
  448. style: '轻松幽默',
  449. },
  450. },
  451. {
  452. name: '学术研究',
  453. icon: '🔍',
  454. desc: '面向研究生,前沿深入,有研究价值',
  455. config: {
  456. knowledgeLevel: '专家',
  457. targetAudience: '研究生',
  458. industry: 'IT/计算机',
  459. specialFeatures: ['案例分析', '总结回顾'],
  460. style: '专业严谨',
  461. },
  462. },
  463. ];
  464. const styles = ['不填', '通俗易懂', '专业严谨', '轻松幽默', '诗意优美', '故事化'];
  465. const bookScales = [
  466. { value: '340000', label: '上2', words: '约34万字', chapters: '170章', audio: '约28小时' },
  467. { value: '210000', label: '上1', words: '约21万字', chapters: '105章', audio: '约17小时' },
  468. { value: '130000', label: '标准', words: '约13万字', chapters: '65章', audio: '约11小时' },
  469. { value: '80000', label: '下1', words: '约8万字', chapters: '40章', audio: '约7小时' },
  470. { value: '50000', label: '下2', words: '约5万字', chapters: '25章', audio: '约4小时' },
  471. { value: '31000', label: '下3', words: '约3.1万字', chapters: '16章', audio: '约2.5小时' },
  472. { value: '19000', label: '下4', words: '约1.9万字', chapters: '10章', audio: '约1.5小时' },
  473. { value: '12000', label: '下5', words: '约1.2万字', chapters: '6章', audio: '约1小时' },
  474. { value: '7000', label: '下6', words: '约7千字', chapters: '4章', audio: '约35分钟' },
  475. { value: '4000', label: '下7', words: '约4千字', chapters: '2章', audio: '约20分钟' },
  476. { value: '3000', label: '下8', words: '约3千字', chapters: '2章', audio: '约15分钟' },
  477. { value: '2000', label: '下9', words: '约2千字', chapters: '1章', audio: '约10分钟' },
  478. { value: '1000', label: '下10', words: '约1千字', chapters: '1章', audio: '约5分钟' },
  479. ];
  480. // 书籍类型选项
  481. const bookTypes = [
  482. { value: 'auto', label: '自动检测', icon: '🤖', desc: 'AI判断类型决定大纲' },
  483. { value: '教材', label: '教材/学术', icon: '📚', desc: '3层大纲' },
  484. { value: '技术教程', label: '技术教程', icon: '💻', desc: '3层大纲' },
  485. { value: '小说', label: '小说/文学', icon: '📖', desc: '1层大纲' },
  486. { value: '商业', label: '商业/经管', icon: '📊', desc: '2层大纲' },
  487. { value: '科普', label: '科普/大众', icon: '🔬', desc: '2层大纲' },
  488. ];
  489. // 大纲层级选项
  490. const outlineLevelOptions = [
  491. { value: 'auto', label: '自动', desc: '根据规模自动选择' },
  492. { value: '1', label: '1层', desc: '仅章,无节和小节' },
  493. { value: '2', label: '2层', desc: '章→节,有节无小节' },
  494. { value: '3', label: '3层', desc: '章→节→小节,完整三层' },
  495. ];
  496. // 计算属性
  497. const canCreateBook = computed(() => {
  498. if (quotaCheck.value && !quotaCheck.value.allowed) {
  499. return false;
  500. }
  501. return !!newBook.value.description.trim();
  502. });
  503. // 切换特殊要求
  504. function toggleSpecialFeature(feature: string) {
  505. const features = newBook.value.specialFeatures;
  506. const index = features.indexOf(feature);
  507. if (index > -1) {
  508. features.splice(index, 1);
  509. } else {
  510. features.push(feature);
  511. }
  512. }
  513. // 应用模板
  514. function applyTemplate(template: typeof quickTemplates[0]) {
  515. const config = template.config;
  516. newBook.value.knowledgeLevel = config.knowledgeLevel;
  517. newBook.value.targetAudience = config.targetAudience;
  518. newBook.value.industry = config.industry;
  519. newBook.value.specialFeatures = [...config.specialFeatures];
  520. newBook.value.style = config.style;
  521. // 自动展开高级选项
  522. showAdvancedOptions.value = true;
  523. uni.showToast({
  524. title: `已应用${template.name}模板`,
  525. icon: 'success',
  526. });
  527. }
  528. // 智能推荐
  529. async function requestSmartRecommend() {
  530. if (!newBook.value.description.trim() || isRecommending.value) return;
  531. isRecommending.value = true;
  532. try {
  533. const response = await uni.request({
  534. url: `${BASE_URL}/book-generator/langgraph/smart-recommend`,
  535. method: 'POST',
  536. data: {
  537. description: newBook.value.description.trim(),
  538. title: newBook.value.title.trim(),
  539. },
  540. });
  541. const res = response.data as any;
  542. if (res.code === 0 && res.data) {
  543. const rec = res.data;
  544. if (rec.knowledgeLevel) {
  545. newBook.value.knowledgeLevel = rec.knowledgeLevel;
  546. }
  547. if (rec.targetAudience) {
  548. newBook.value.targetAudience = rec.targetAudience;
  549. }
  550. if (rec.style) {
  551. newBook.value.style = rec.style;
  552. }
  553. if (rec.industry) {
  554. newBook.value.industry = rec.industry;
  555. }
  556. // 自动展开高级选项显示推荐结果
  557. showAdvancedOptions.value = true;
  558. uni.showToast({
  559. title: '推荐成功',
  560. icon: 'success',
  561. });
  562. } else {
  563. uni.showToast({
  564. title: res.message || '推荐失败',
  565. icon: 'none',
  566. });
  567. }
  568. } catch (e: any) {
  569. console.error('智能推荐失败:', e);
  570. uni.showToast({
  571. title: '推荐服务暂不可用',
  572. icon: 'none',
  573. });
  574. } finally {
  575. isRecommending.value = false;
  576. }
  577. }
  578. // 选择书籍规模时加载预估
  579. async function selectBookScale(scale: string) {
  580. newBook.value.bookScale = scale;
  581. try {
  582. const bt = newBook.value.bookType;
  583. const bookTypeParam = bt && bt !== 'auto' ? `&bookType=${encodeURIComponent(bt)}` : '';
  584. const response = await uni.request({
  585. url: `${BASE_URL}/book-generator/langgraph/estimate?scale=${encodeURIComponent(scale)}&userId=1${bookTypeParam}`,
  586. method: 'GET'
  587. });
  588. const res = response.data as any;
  589. if (res.code === 0 && res.data && res.data.words && res.data.audioMinutes) {
  590. bookEstimate.value = {
  591. scale: res.data.scale,
  592. words: res.data.words,
  593. audioMinutes: res.data.audioMinutes,
  594. estimatedChapters: res.data.estimatedChapters
  595. };
  596. if (res.data.quotaCheck) {
  597. quotaCheck.value = res.data.quotaCheck;
  598. }
  599. } else {
  600. console.warn('预估数据不完整:', res);
  601. bookEstimate.value = null;
  602. }
  603. } catch (e) {
  604. console.error('加载预估失败:', e);
  605. bookEstimate.value = null;
  606. quotaCheck.value = null;
  607. }
  608. }
  609. function switchToInteractive() {
  610. uni.redirectTo({ url: '/pages/book-generator/interactive' });
  611. }
  612. // AI 预览检测规模(用户未手动选时,描述失焦自动触发)
  613. let detectTimer: any = null;
  614. async function detectScaleFromDesc() {
  615. const desc = newBook.value.description?.trim();
  616. if (!desc || newBook.value.bookScale) return; // 已手动选过,不覆盖
  617. clearTimeout(detectTimer);
  618. detectTimer = setTimeout(async () => {
  619. try {
  620. const response = await uni.request({
  621. url: `${BASE_URL}/book-generator/langgraph/detect-scale`,
  622. method: 'POST',
  623. data: { description: desc }
  624. });
  625. const res = response.data as any;
  626. if (res.code === 0 && res.data) {
  627. newBook.value.bookScale = res.data.bookScale;
  628. bookEstimate.value = {
  629. scale: res.data.bookScale,
  630. words: { min: res.data.totalWords, max: res.data.totalWords, avg: res.data.totalWords },
  631. audioMinutes: { min: 0, max: 0, avg: 0 },
  632. estimatedChapters: res.data.chapters,
  633. };
  634. }
  635. } catch { /* AI 调用失败不影响用户操作 */ }
  636. }, 800);
  637. }
  638. function goBack() {
  639. const pages = getCurrentPages();
  640. if (pages.length > 1) {
  641. uni.navigateBack();
  642. } else {
  643. uni.switchTab({ url: '/pages/index/index' });
  644. }
  645. }
  646. // 创建新书籍
  647. async function createNewBook() {
  648. if (!canCreateBook.value) return;
  649. const configSummary: string[] = [];
  650. if (newBook.value.knowledgeLevel) configSummary.push(`知识难度:${newBook.value.knowledgeLevel}`);
  651. if (newBook.value.targetAudience) configSummary.push(`面向人群:${newBook.value.targetAudience}`);
  652. if (newBook.value.industry) configSummary.push(`行业领域:${newBook.value.industry}`);
  653. if (newBook.value.specialFeatures.length > 0) configSummary.push(`特殊要求:${newBook.value.specialFeatures.join('、')}`);
  654. if (newBook.value.style) configSummary.push(`写作风格:${newBook.value.style}`);
  655. const confirmResult = await new Promise<boolean>((resolve) => {
  656. uni.showModal({
  657. title: '📋 生成配置确认',
  658. content: `${configSummary.join('\n')}\n\n书名将由AI根据描述自动生成\n\n确认开始生成?`,
  659. confirmText: '确认生成',
  660. cancelText: '返回修改',
  661. success: (res) => resolve(res.confirm),
  662. });
  663. });
  664. if (!confirmResult) return;
  665. creating.value = true;
  666. try {
  667. let mergedDesc = newBook.value.description.trim();
  668. if (newBook.value.knowledgeLevel && newBook.value.knowledgeLevel !== '不填') {
  669. mergedDesc += `\n知识难度:${newBook.value.knowledgeLevel}`;
  670. }
  671. if (newBook.value.targetAudience && newBook.value.targetAudience !== '不填') {
  672. mergedDesc += `\n面向人群:${newBook.value.targetAudience}`;
  673. }
  674. if (newBook.value.industry && newBook.value.industry !== '不填') {
  675. mergedDesc += `\n行业领域:${newBook.value.industry}`;
  676. }
  677. if (newBook.value.specialFeatures && newBook.value.specialFeatures.length > 0) {
  678. mergedDesc += `\n特殊要求:${newBook.value.specialFeatures.join('、')}`;
  679. }
  680. if (newBook.value.style && newBook.value.style !== '不填') {
  681. mergedDesc += `\n写作风格:${newBook.value.style}`;
  682. }
  683. const genLevel = newBook.value.outlineLevel === 'auto' ? undefined : parseInt(newBook.value.outlineLevel);
  684. const bookType = newBook.value.bookType === 'auto' ? undefined : newBook.value.bookType;
  685. const result = await api.createBook({
  686. title: newBook.value.title,
  687. subtitle: newBook.value.subtitle,
  688. description: mergedDesc,
  689. bookScale: newBook.value.bookScale || undefined,
  690. bookType,
  691. genLevel,
  692. });
  693. uni.showToast({ title: '创建成功', icon: 'success' });
  694. // 跳转到详情页
  695. setTimeout(() => {
  696. uni.navigateTo({
  697. url: `/pages/book-generator/detail?id=${result.id}`
  698. });
  699. }, 1500);
  700. } catch (e: any) {
  701. uni.showToast({ title: e.message || '创建失败', icon: 'none' });
  702. } finally {
  703. creating.value = false;
  704. }
  705. }
  706. onMounted(() => {
  707. // 不再默认选中规模,让 AI 根据描述自动判断
  708. });
  709. </script>
  710. <style scoped>
  711. .page {
  712. min-height: 100vh;
  713. background: #f9fafb;
  714. }
  715. .nav-bar {
  716. position: fixed;
  717. top: 0;
  718. left: 0;
  719. right: 0;
  720. z-index: 100;
  721. background: #ffffff;
  722. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  723. }
  724. .nav-content {
  725. display: flex;
  726. align-items: center;
  727. height: 88rpx;
  728. padding: 0 32rpx;
  729. padding-top: env(safe-area-inset-top);
  730. }
  731. .nav-left { width: 60rpx; flex-shrink: 0; }
  732. .back-icon { font-size: 40rpx; color: #1f2937; }
  733. .page-title {
  734. font-size: 32rpx;
  735. font-weight: 600;
  736. color: #1f2937;
  737. flex: 1;
  738. text-align: center;
  739. }
  740. /* 模式切换独立行 */
  741. .mode-bar { padding: 0 32rpx 16rpx 32rpx; }
  742. .mode-switch { display: flex; align-items: center; justify-content: center; width: 100%; height: 72rpx; background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: #fff; border-radius: 12rpx; font-size: 26rpx; font-weight: 600; }
  743. .main-content {
  744. padding-top: calc(88rpx + 16rpx + 72rpx + 16rpx + env(safe-area-inset-top));
  745. }
  746. .card {
  747. margin: 24rpx;
  748. padding: 32rpx;
  749. background: #ffffff;
  750. border-radius: 16rpx;
  751. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  752. }
  753. .card-header { margin-bottom: 32rpx; }
  754. .card-title { font-size: 36rpx; font-weight: 700; color: #1f2937; }
  755. .form-item { margin-bottom: 32rpx; }
  756. .form-label { font-size: 28rpx; font-weight: 600; color: #374151; margin-bottom: 12rpx; display: block; }
  757. .form-hint { font-size: 24rpx; color: #9ca3af; margin-bottom: 16rpx; display: block; }
  758. .form-hint-small { font-size: 24rpx; color: #9ca3af; margin-bottom: 16rpx; display: block; }
  759. .form-input {
  760. width: 100%;
  761. height: 88rpx;
  762. padding: 0 24rpx;
  763. background: #f9fafb;
  764. border: 2rpx solid #e5e7eb;
  765. border-radius: 12rpx;
  766. font-size: 28rpx;
  767. }
  768. .form-textarea {
  769. width: 100%;
  770. min-height: 200rpx;
  771. padding: 24rpx;
  772. background: #f9fafb;
  773. border: 2rpx solid #e5e7eb;
  774. border-radius: 12rpx;
  775. font-size: 28rpx;
  776. line-height: 1.6;
  777. }
  778. .label-with-badge { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
  779. .required-badge {
  780. padding: 4rpx 12rpx;
  781. background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
  782. color: white;
  783. border-radius: 8rpx;
  784. font-size: 20rpx;
  785. font-weight: 500;
  786. }
  787. .optional-badge {
  788. padding: 4rpx 12rpx;
  789. background: linear-gradient(135deg, #10b981 0%, #059669 100%);
  790. color: white;
  791. border-radius: 8rpx;
  792. font-size: 20rpx;
  793. font-weight: 500;
  794. }
  795. .highlight-item { padding: 24rpx; background: rgba(79, 70, 229, 0.03); border-radius: 12rpx; border: 1rpx solid rgba(79, 70, 229, 0.1); }
  796. .chip-group { display: flex; flex-wrap: wrap; gap: 12rpx; }
  797. .chip {
  798. display: flex;
  799. align-items: center;
  800. gap: 8rpx;
  801. padding: 12rpx 20rpx;
  802. background: #f3f4f6;
  803. border-radius: 24rpx;
  804. font-size: 24rpx;
  805. color: #4b5563;
  806. transition: all 0.2s;
  807. }
  808. .chip.active { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; }
  809. .chip-icon { font-size: 24rpx; }
  810. .chip-text { font-size: 24rpx; }
  811. .template-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; margin-top: 16rpx; }
  812. .template-card {
  813. padding: 20rpx;
  814. background: #f9fafb;
  815. border: 2rpx solid #e5e7eb;
  816. border-radius: 12rpx;
  817. text-align: center;
  818. }
  819. .template-icon { font-size: 40rpx; display: block; margin-bottom: 8rpx; }
  820. .template-name { font-size: 26rpx; font-weight: 600; color: #1f2937; display: block; }
  821. .template-desc { font-size: 22rpx; color: #9ca3af; display: block; margin-top: 4rpx; }
  822. .important-tip { padding: 24rpx; background: rgba(79, 70, 229, 0.05); border-radius: 12rpx; margin: 24rpx 0; }
  823. .tip-header { display: flex; align-items: center; gap: 8rpx; margin-bottom: 12rpx; }
  824. .tip-icon { font-size: 28rpx; }
  825. .tip-title { font-size: 28rpx; font-weight: 600; color: #4f46e5; }
  826. .tip-content { font-size: 24rpx; color: #6b7280; line-height: 1.6; display: block; margin-bottom: 12rpx; }
  827. .tip-examples { display: flex; flex-direction: column; gap: 8rpx; }
  828. .tip-example { font-size: 24rpx; color: #9ca3af; }
  829. .scale-picker { display: flex; flex-wrap: wrap; gap: 12rpx; }
  830. .scale-option {
  831. flex: 1;
  832. min-width: 200rpx;
  833. padding: 16rpx;
  834. background: #f9fafb;
  835. border: 2rpx solid #e5e7eb;
  836. border-radius: 12rpx;
  837. text-align: center;
  838. }
  839. .scale-option.active { border-color: #4f46e5; background: rgba(79, 70, 229, 0.05); }
  840. .scale-words { font-size: 28rpx; font-weight: 600; color: #1f2937; display: block; }
  841. .scale-label { font-size: 24rpx; color: #6b7280; display: block; margin-top: 4rpx; }
  842. .scale-audio { font-size: 22rpx; color: #9ca3af; display: block; margin-top: 4rpx; }
  843. .estimate-card { padding: 24rpx; background: rgba(16, 185, 129, 0.05); border-radius: 12rpx; margin-top: 24rpx; }
  844. .estimate-header { margin-bottom: 16rpx; }
  845. .estimate-title { font-size: 28rpx; font-weight: 600; color: #059669; }
  846. .estimate-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8rpx; }
  847. .estimate-label { font-size: 24rpx; color: #6b7280; }
  848. .estimate-value { font-size: 24rpx; font-weight: 500; color: #1f2937; }
  849. .genlevel-row { margin-top: 8rpx; padding-top: 8rpx; border-top: 1rpx solid rgba(16, 185, 129, 0.15); }
  850. .genlevel-tag { display: flex; align-items: center; gap: 6rpx; padding: 4rpx 12rpx; border-radius: 8rpx; font-size: 22rpx; }
  851. .genlevel-tag.level-1 { background: rgba(239, 68, 68, 0.1); }
  852. .genlevel-tag.level-2 { background: rgba(245, 158, 11, 0.1); }
  853. .genlevel-tag.level-3 { background: rgba(79, 70, 229, 0.1); }
  854. .genlevel-label { font-weight: 600; color: #1f2937; font-size: 22rpx; }
  855. .genlevel-desc { color: #9ca3af; font-size: 20rpx; }
  856. .quota-check { margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid rgba(16, 185, 129, 0.2); }
  857. .quota-ok { display: flex; align-items: center; gap: 8rpx; margin-bottom: 12rpx; }
  858. .quota-icon { font-size: 24rpx; }
  859. .quota-text { font-size: 24rpx; color: #059669; }
  860. .quota-warning { display: flex; align-items: center; gap: 8rpx; margin-bottom: 12rpx; }
  861. .quota-detail { margin-top: 8rpx; }
  862. .quota-info { font-size: 22rpx; color: #9ca3af; display: block; }
  863. .quota-overage { margin-top: 8rpx; display: flex; justify-content: space-between; }
  864. .quota-price { font-size: 22rpx; color: #ef4444; }
  865. .btn-group { display: flex; gap: 16rpx; margin-top: 32rpx; }
  866. .btn-cancel, .btn-primary {
  867. flex: 1;
  868. height: 88rpx;
  869. border-radius: 16rpx;
  870. font-size: 28rpx;
  871. display: flex;
  872. align-items: center;
  873. justify-content: center;
  874. border: none;
  875. }
  876. .btn-cancel { background: #f3f4f6; color: #6b7280; }
  877. .btn-primary { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; }
  878. .btn-primary[disabled] { opacity: 0.5; }
  879. /* 简洁模式提示 */
  880. .simple-mode-tip { padding: 24rpx; background: rgba(16, 185, 129, 0.08); border-radius: 12rpx; margin-bottom: 32rpx; }
  881. .simple-mode-tip .tip-header { display: flex; align-items: center; gap: 8rpx; margin-bottom: 8rpx; }
  882. .simple-mode-tip .tip-icon { font-size: 28rpx; }
  883. .simple-mode-tip .tip-title { font-size: 28rpx; font-weight: 600; color: #059669; }
  884. .simple-mode-tip .tip-content { font-size: 24rpx; color: #6b7280; line-height: 1.5; }
  885. /* 智能推荐按钮 */
  886. .smart-recommend-section { margin-top: 24rpx; }
  887. .btn-smart-recommend {
  888. display: flex;
  889. align-items: center;
  890. justify-content: center;
  891. gap: 8rpx;
  892. width: 100%;
  893. height: 80rpx;
  894. background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
  895. border-radius: 12rpx;
  896. border: none;
  897. margin-bottom: 12rpx;
  898. }
  899. .btn-smart-recommend[disabled] { opacity: 0.5; }
  900. .recommend-icon { font-size: 32rpx; }
  901. .recommend-text { font-size: 28rpx; font-weight: 600; color: white; }
  902. /* 高级选项折叠区 */
  903. .advanced-section { margin-top: 32rpx; border-top: 1rpx solid #e5e7eb; padding-top: 24rpx; }
  904. .advanced-header {
  905. display: flex;
  906. align-items: center;
  907. justify-content: space-between;
  908. padding: 16rpx 0;
  909. }
  910. .advanced-title { font-size: 28rpx; font-weight: 600; color: #4b5563; }
  911. .advanced-arrow { font-size: 24rpx; color: #9ca3af; }
  912. .advanced-content { margin-top: 16rpx; }
  913. </style>