Browse Source

feat: 优化有声书创建流程界面交互

- 优化 create.vue 创建页面交互
- 优化 interactive.vue 交互页面体验

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyFramework User 3 months ago
parent
commit
2f5755fea5

+ 2 - 6
my-uniapp-vue3/src/App.vue

@@ -175,12 +175,8 @@ async function triggerWechatOAuth() {
   try {
     const { get } = await import('./utils/request');
 
-    // 获取当前页面路径,用于授权后返回
-    const pages = getCurrentPages();
-    const currentPage = pages[pages.length - 1];
-    const route = currentPage?.route || 'pages/index_index';
-    const currentPath = `/${route}${window.location.hash}`;
-    const redirectUrl = `${window.location.origin}${currentPath}`;
+    // 直接使用当前完整 URL 作为回调地址
+    const redirectUrl = window.location.href;
     console.log('[App] 授权回调地址:', redirectUrl);
 
     // 调用后端获取微信授权链接

+ 189 - 168
my-uniapp-vue3/src/pages/book-generator/create.vue

@@ -8,29 +8,22 @@
         </view>
         <text class="page-title">{{ isEditMode ? '编辑书籍' : '创建书籍' }}</text>
       </view>
-      <!-- 模式切换独立行 -->
-      <view v-if="!isEditMode" class="mode-bar">
-        <text class="mode-switch" @click="switchToInteractive">切换到交互模式</text>
-      </view>
     </view>
 
     <!-- 主内容区 -->
     <view class="main-content">
       <view class="card">
 
-        <!-- ========== 基础信息区 ========== -->
         <view class="section">
-          <text class="section-title">基础信息</text>
 
           <!-- 内容描述 + 字数计数 -->
           <view class="form-item">
-            <text class="form-label">内容描述 *</text>
             <view class="textarea-wrapper">
               <textarea
                 v-model="newBook.description"
                 class="form-textarea"
                 :class="{ 'error': attemptedCreate && !newBook.description.trim() }"
-                placeholder="描述这本书的内容、主题、写作目的..."
+                placeholder="例如:教小学生学习编程的入门书,内容生动有趣,通过趣味案例引导孩子入门Python..."
                 :maxlength="500"
                 @blur="detectScaleFromDesc"
               />
@@ -45,7 +38,6 @@
               :disabled="isRecommending || !newBook.description.trim()"
               @click="requestSmartRecommend"
             >
-              <text class="recommend-icon">{{ isRecommending ? '⏳' : '✨' }}</text>
               <text class="recommend-text">{{ isRecommending ? '推荐中...' : 'AI自动优化描述' }}</text>
             </button>
           </view>
@@ -57,38 +49,22 @@
               :disabled="!canCreateBook || creating"
               @click="createNewBook"
             >
-              {{ creating ? '创建中...' : '🚀 创建书籍' }}
+              {{ creating ? '创建中...' : '创建书籍' }}
             </button>
           </view>
 
-          <!-- 书籍规模 chip 选择 -->
+          <!-- 书籍规模 -->
           <view class="form-item">
-            <text class="form-label">书籍规模 *</text>
-            <view class="scale-chips">
-              <view
-                v-for="scale in commonScales"
-                :key="scale.value"
-                :class="['chip', newBook.bookScale === scale.value ? 'active' : '']"
-                @click="onScaleChipClick(scale.value)"
-              >
-                {{ scale.label }} {{ scale.audio }}
-              </view>
+            <view class="selector-field" @click="showScalePicker">
+              <text class="selector-value">{{ getScaleDisplay(newBook.bookScale) || '请选择书籍规模' }}</text>
+              <text class="selector-arrow">▼</text>
             </view>
-            <picker
-              mode="selector"
-              :range="bookScales"
-              range-key="label"
-              :value="bookScales.findIndex(s => s.value === newBook.bookScale)"
-              @change="onBookScaleChange"
-            >
-              <text class="more-scales">更多规模 ▼</text>
-            </picker>
           </view>
 
           <!-- 预估信息(选择规模后显示在最上方) -->
           <view v-if="!isEditMode && bookEstimate && bookEstimate.words && bookEstimate.audioMinutes" class="estimate-card">
             <view class="estimate-row">
-              <text class="estimate-label">📊 预估:</text>
+              <text class="estimate-label">预估:</text>
               <text class="estimate-value">{{ bookEstimate.words.min }}~{{ bookEstimate.words.max }}字</text>
               <text class="estimate-sep">|</text>
               <text class="estimate-value">{{ bookEstimate.audioMinutes.min }}~{{ bookEstimate.audioMinutes.max }}分钟</text>
@@ -96,81 +72,51 @@
               <text class="estimate-value">约{{ bookEstimate.estimatedChapters }}章</text>
             </view>
             <view v-if="quotaCheck" class="quota-row">
-              <text v-if="quotaCheck.allowed" class="quota-ok">额度充足</text>
-              <text v-else class="quota-warn">⚠️ {{ quotaCheck.reason }}</text>
+              <text v-if="quotaCheck.allowed" class="quota-ok">额度充足</text>
+              <text v-else class="quota-warn">{{ quotaCheck.reason }}</text>
             </view>
           </view>
         </view>
 
-        <!-- ========== 读者配置区 ========== -->
         <view class="section">
-          <text class="section-title">读者配置</text>
-
           <!-- 面向人群 -->
           <view class="form-item">
-            <text class="form-label">面向人群</text>
-            <view class="chip-group">
-              <view
-                v-for="audience in audiences"
-                :key="audience.value"
-                :class="['chip', newBook.targetAudience === audience.value ? 'active' : '']"
-                @click="newBook.targetAudience = audience.value; syncToDescription('面向人群', audience.value)"
-              >
-                <text class="chip-icon">{{ audience.icon }}</text>
-                <text class="chip-text">{{ audience.label }}</text>
-              </view>
+            <view class="selector-field" @click="showAudiencePicker">
+              <text class="selector-label">面向人群</text>
+              <text class="selector-value">{{ getAudienceLabel(newBook.targetAudience) || '请选择' }}</text>
+              <text class="selector-arrow">▼</text>
             </view>
           </view>
 
           <!-- 知识难度 -->
           <view class="form-item">
-            <text class="form-label">知识难度</text>
-            <view class="chip-group">
-              <view
-                v-for="level in knowledgeLevels"
-                :key="level.value"
-                :class="['chip', newBook.knowledgeLevel === level.value ? 'active' : '']"
-                @click="newBook.knowledgeLevel = level.value; syncToDescription('知识难度', level.value)"
-              >
-                <text class="chip-icon">{{ level.icon }}</text>
-                <text class="chip-text">{{ level.label }}</text>
-              </view>
+            <view class="selector-field" @click="showKnowledgeLevelPicker">
+              <text class="selector-label">知识难度</text>
+              <text class="selector-value">{{ getKnowledgeLevelLabel(newBook.knowledgeLevel) || '请选择' }}</text>
+              <text class="selector-arrow">▼</text>
             </view>
           </view>
 
           <!-- 书籍类型 -->
           <view class="form-item">
-            <text class="form-label">书籍类型</text>
-            <view class="chip-group">
-              <view
-                v-for="bt in bookTypes"
-                :key="bt.value"
-                :class="['chip', newBook.bookType === bt.value ? 'active' : '']"
-                @click="newBook.bookType = bt.value; syncToDescription('书籍类型', bt.value)"
-              >
-                <text class="chip-icon">{{ bt.icon }}</text>
-                <text class="chip-text">{{ bt.label }}</text>
-              </view>
+            <view class="selector-field" @click="showBookTypePicker">
+              <text class="selector-label">书籍类型</text>
+              <text class="selector-value">{{ getBookTypeLabel(newBook.bookType) || '请选择' }}</text>
+              <text class="selector-arrow">▼</text>
             </view>
           </view>
 
           <!-- 大纲层级 -->
           <view class="form-item">
-            <text class="form-label">大纲层级</text>
-            <view class="chip-group">
-              <view
-                v-for="opt in outlineLevelOptions"
-                :key="opt.value"
-                :class="['chip', newBook.outlineLevel === opt.value ? 'active' : '']"
-                @click="newBook.outlineLevel = opt.value; syncToDescription('大纲层级', opt.value)"
-              >
-                <text class="chip-text">{{ opt.label }}</text>
-              </view>
+            <view class="selector-field" @click="showOutlineLevelPicker">
+              <text class="selector-label">大纲层级</text>
+              <text class="selector-value">{{ getOutlineLevelLabel(newBook.outlineLevel) || '请选择' }}</text>
+              <text class="selector-arrow">▼</text>
             </view>
           </view>
         </view>
 
-        <!-- ========== 高级定制区(默认展开) ========== -->
+        <!-- ========== 高级定制区(默认折叠) ========== -->
         <view class="section">
           <view class="section-header" @click="showAdvancedOptions = !showAdvancedOptions">
             <text class="section-title">高级定制</text>
@@ -180,65 +126,37 @@
           <view v-if="showAdvancedOptions" class="section-content">
             <!-- 快速模板 -->
             <view class="form-item">
-              <text class="form-label">📋 快速模板</text>
-              <view class="template-grid">
-                <view
-                  v-for="template in quickTemplates"
-                  :key="template.name"
-                  class="template-card"
-                  @click="applyTemplate(template)"
-                >
-                  <text class="template-icon">{{ template.icon }}</text>
-                  <text class="template-name">{{ template.name }}</text>
-                  <text class="template-desc">{{ template.desc }}</text>
-                </view>
+              <view class="selector-field" @click="showTemplatePicker">
+                <text class="selector-label">快速模板</text>
+                <text class="selector-value">{{ selectedTemplateName || '请选择模板(可选)' }}</text>
+                <text class="selector-arrow">▼</text>
               </view>
             </view>
 
             <!-- 写作风格 -->
             <view class="form-item">
-              <text class="form-label">写作风格</text>
-              <view class="chip-group">
-                <view
-                  v-for="style in styles"
-                  :key="style"
-                  :class="['chip', newBook.style === style ? 'active' : '']"
-                  @click="newBook.style = style; syncToDescription('写作风格', style)"
-                >
-                  {{ style }}
-                </view>
+              <view class="selector-field" @click="showStylePicker">
+                <text class="selector-label">写作风格</text>
+                <text class="selector-value">{{ newBook.style || '请选择' }}</text>
+                <text class="selector-arrow">▼</text>
               </view>
             </view>
 
             <!-- 行业领域 -->
             <view class="form-item">
-              <text class="form-label">行业领域</text>
-              <view class="chip-group">
-                <view
-                  v-for="industry in industries"
-                  :key="industry.value"
-                  :class="['chip', newBook.industry === industry.value ? 'active' : '']"
-                  @click="newBook.industry = industry.value; syncToDescription('行业领域', industry.label)"
-                >
-                  <text class="chip-icon">{{ industry.icon }}</text>
-                  <text class="chip-text">{{ industry.label }}</text>
-                </view>
+              <view class="selector-field" @click="showIndustryPicker">
+                <text class="selector-label">行业领域</text>
+                <text class="selector-value">{{ getIndustryLabel(newBook.industry) || '请选择' }}</text>
+                <text class="selector-arrow">▼</text>
               </view>
             </view>
 
             <!-- 特殊要求 (多选) -->
             <view class="form-item">
-              <text class="form-label">特殊要求</text>
-              <view class="chip-group">
-                <view
-                  v-for="feature in specialFeatures"
-                  :key="feature.value"
-                  :class="['chip', newBook.specialFeatures.includes(feature.value) ? 'active' : '']"
-                  @click="toggleSpecialFeature(feature.value)"
-                >
-                  <text class="chip-icon">{{ feature.icon }}</text>
-                  <text class="chip-text">{{ feature.label }}</text>
-                </view>
+              <view class="selector-field" @click="showSpecialFeaturesPicker">
+                <text class="selector-label">特殊要求</text>
+                <text class="selector-value">{{ getSpecialFeaturesLabel() || '请选择' }}</text>
+                <text class="selector-arrow">▼</text>
               </view>
             </view>
           </view>
@@ -250,7 +168,7 @@
             {{ creating ? '保存中...' : '保存修改' }}
           </button>
           <template v-else>
-            <button class="btn-cancel" @click="goBack">取消</button>
+            <button class="btn-primary" @click="switchToInteractive">切换到交互模式</button>
             <button
               class="btn-primary"
               :disabled="!canCreateBook || creating"
@@ -275,7 +193,7 @@ const BASE_URL = getApiBaseUrl();
 
 // 创建表单
 const creating = ref(false);
-const showAdvancedOptions = ref(true); // 默认展开高级选项
+const showAdvancedOptions = ref(false); // 默认隐藏高级选项
 const isRecommending = ref(false);
 const attemptedCreate = ref(false);
 const isEditMode = ref(false);
@@ -490,6 +408,132 @@ const canCreateBook = computed(() => {
   return !!newBook.value.description.trim();
 });
 
+// 选中模板名称
+const selectedTemplateName = ref('');
+
+// 辅助函数:获取标签
+function getAudienceLabel(val: string) {
+  return audiences.find(a => a.value === val)?.label || '';
+}
+function getKnowledgeLevelLabel(val: string) {
+  return knowledgeLevels.find(l => l.value === val)?.label || '';
+}
+function getBookTypeLabel(val: string) {
+  return bookTypes.find(b => b.value === val)?.label || '';
+}
+function getOutlineLevelLabel(val: string) {
+  return outlineLevelOptions.find(o => o.value === val)?.label || '';
+}
+function getIndustryLabel(val: string) {
+  return industries.find(i => i.value === val)?.label || '';
+}
+function getSpecialFeaturesLabel() {
+  if (!newBook.value.specialFeatures.length) return '';
+  return newBook.value.specialFeatures.join('、');
+}
+
+// 弹出选择器
+function showAudiencePicker() {
+  uni.showActionSheet({
+    itemList: audiences.map(a => a.label),
+    success: (res) => {
+      const selected = audiences[res.tapIndex];
+      newBook.value.targetAudience = selected.value;
+      syncToDescription('面向人群', selected.value);
+    }
+  });
+}
+
+function showKnowledgeLevelPicker() {
+  uni.showActionSheet({
+    itemList: knowledgeLevels.map(l => l.label),
+    success: (res) => {
+      const selected = knowledgeLevels[res.tapIndex];
+      newBook.value.knowledgeLevel = selected.value;
+      syncToDescription('知识难度', selected.value);
+    }
+  });
+}
+
+function showBookTypePicker() {
+  uni.showActionSheet({
+    itemList: bookTypes.map(b => b.label),
+    success: (res) => {
+      const selected = bookTypes[res.tapIndex];
+      newBook.value.bookType = selected.value;
+      syncToDescription('书籍类型', selected.value);
+    }
+  });
+}
+
+function showOutlineLevelPicker() {
+  uni.showActionSheet({
+    itemList: outlineLevelOptions.map(o => o.label),
+    success: (res) => {
+      const selected = outlineLevelOptions[res.tapIndex];
+      newBook.value.outlineLevel = selected.value;
+      syncToDescription('大纲层级', selected.value);
+    }
+  });
+}
+
+function showStylePicker() {
+  uni.showActionSheet({
+    itemList: styles,
+    success: (res) => {
+      const selected = styles[res.tapIndex];
+      newBook.value.style = selected;
+      syncToDescription('写作风格', selected);
+    }
+  });
+}
+
+function showIndustryPicker() {
+  uni.showActionSheet({
+    itemList: industries.map(i => i.label),
+    success: (res) => {
+      const selected = industries[res.tapIndex];
+      newBook.value.industry = selected.value;
+      syncToDescription('行业领域', selected.label);
+    }
+  });
+}
+
+function showSpecialFeaturesPicker() {
+  const selected = newBook.value.specialFeatures;
+  const items = specialFeatures.map(f => {
+    const checked = selected.includes(f.value);
+    return checked ? `${f.label} ✓` : f.label;
+  });
+  uni.showActionSheet({
+    itemList: items,
+    success: (res) => {
+      toggleSpecialFeature(specialFeatures[res.tapIndex].value);
+    }
+  });
+}
+
+function showScalePicker() {
+  uni.showActionSheet({
+    itemList: bookScales.map(s => s.label),
+    success: (res) => {
+      const selected = bookScales[res.tapIndex];
+      onScaleChipClick(selected.value);
+    }
+  });
+}
+
+function showTemplatePicker() {
+  const templateNames = quickTemplates.map(t => t.name);
+  uni.showActionSheet({
+    itemList: templateNames,
+    success: (res) => {
+      applyTemplate(quickTemplates[res.tapIndex]);
+      selectedTemplateName.value = quickTemplates[res.tapIndex].name;
+    }
+  });
+}
+
 // 切换特殊要求
 function toggleSpecialFeature(feature: string) {
   const features = newBook.value.specialFeatures;
@@ -513,6 +557,7 @@ function applyTemplate(template: typeof quickTemplates[0]) {
 
   // 自动展开高级选项
   showAdvancedOptions.value = true;
+  selectedTemplateName.value = template.name;
 
   uni.showToast({
     title: `已应用${template.name}模板`,
@@ -649,10 +694,6 @@ async function onBookScaleChange(e: any) {
   }
 }
 
-function switchToInteractive() {
-  uni.redirectTo({ url: '/pages/book-generator/interactive' });
-}
-
 // AI 预览检测规模(用户未手动选时,描述失焦自动触发)
 let detectTimer: any = null;
 async function detectScaleFromDesc() {
@@ -680,6 +721,10 @@ async function detectScaleFromDesc() {
   }, 800);
 }
 
+function switchToInteractive() {
+  uni.navigateTo({ url: '/pages/book-generator/interactive' });
+}
+
 function goBack() {
   const pages = getCurrentPages();
   if (pages.length > 1) {
@@ -840,12 +885,8 @@ onLoad(async (query: any) => {
   text-align: center;
 }
 
-/* 模式切换独立行 */
-.mode-bar { padding: 0 32rpx 16rpx 32rpx; }
-.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; }
-
 .main-content {
-  padding-top: calc(88rpx + 16rpx + 72rpx + 16rpx + env(safe-area-inset-top));
+  padding-top: calc(88rpx + 16rpx + env(safe-area-inset-top));
 }
 
 .card {
@@ -864,6 +905,19 @@ onLoad(async (query: any) => {
 .form-hint { font-size: 24rpx; color: #9ca3af; margin-bottom: 16rpx; display: block; }
 .form-hint-small { font-size: 24rpx; color: #9ca3af; margin-bottom: 16rpx; display: block; }
 
+.selector-field {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 24rpx;
+  background: #f3f4f6;
+  border-radius: 12rpx;
+  border: 2rpx solid #e5e7eb;
+}
+.selector-label { font-size: 28rpx; color: #374151; font-weight: 600; }
+.selector-value { font-size: 28rpx; color: #9ca3af; flex: 1; text-align: right; margin-right: 12rpx; }
+.selector-arrow { font-size: 22rpx; color: #9ca3af; }
+
 .form-input {
   width: 100%;
   height: 88rpx;
@@ -896,38 +950,6 @@ onLoad(async (query: any) => {
   color: #9ca3af;
 }
 
-.chip-group { display: flex; flex-wrap: wrap; gap: 12rpx; }
-.chip {
-  display: flex;
-  align-items: center;
-  gap: 8rpx;
-  padding: 12rpx 20rpx;
-  background: #f3f4f6;
-  border-radius: 24rpx;
-  font-size: 24rpx;
-  color: #4b5563;
-  transition: all 0.2s;
-}
-.chip.active { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; }
-.chip-icon { font-size: 24rpx; }
-.chip-text { font-size: 24rpx; }
-
-.template-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; margin-top: 16rpx; }
-.template-card {
-  padding: 20rpx;
-  background: #f9fafb;
-  border: 2rpx solid #e5e7eb;
-  border-radius: 12rpx;
-  text-align: center;
-  transition: all 0.2s;
-}
-.template-card:active { transform: scale(1.02); box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.1); }
-.template-icon { font-size: 40rpx; display: block; margin-bottom: 8rpx; }
-.template-name { font-size: 26rpx; font-weight: 600; color: #1f2937; display: block; }
-.template-desc { font-size: 22rpx; color: #9ca3af; display: block; margin-top: 4rpx; }
-
-.scale-chips { display: flex; flex-wrap: wrap; gap: 12rpx; margin-bottom: 16rpx; }
-.more-scales { font-size: 24rpx; color: #6366f1; padding: 12rpx 0; }
 
 /* 预估卡片 */
 .estimate-card {
@@ -957,7 +979,6 @@ onLoad(async (query: any) => {
   border: none;
 }
 .btn-smart-recommend[disabled] { opacity: 0.5; }
-.recommend-icon { font-size: 32rpx; }
 .recommend-text { font-size: 28rpx; font-weight: 600; color: white; }
 
 /* 快速创建按钮 */

+ 2 - 7
my-uniapp-vue3/src/pages/book-generator/interactive.vue

@@ -578,19 +578,14 @@ let progressTimer: any = null;
 
 function goBack() {
   if (currentStep.value === 0) {
-    const pages = getCurrentPages();
-    if (pages.length > 1) {
-      uni.navigateBack();
-    } else {
-      uni.switchTab({ url: '/pages/index/index' });
-    }
+    uni.navigateBack({ delta: 1 });
     return;
   }
   currentStep.value--;
 }
 
 function switchToSimple() {
-  uni.redirectTo({ url: '/pages/book-generator/create' });
+  uni.navigateTo({ url: '/pages/book-generator/create' });
 }
 
 async function fetchEstimate() {