Browse Source

refactor(ai-content): 简化流程为3步骤

- 去掉大纲预览和角色设定步骤
- 简化为:基础设置 → 生成 → 质量检测
- 新增统一generate接口,根据内容类型自动选择生成策略
- 支持:小说、营销文案、教育内容、商务话术、媒体内容等

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MyFramework User 5 months ago
parent
commit
bb2bbaacd1

+ 205 - 498
my-uniapp-vue3/src/pages/ai-content/index.vue

@@ -15,19 +15,6 @@
 
     <!-- 主内容区 -->
     <view class="main-content">
-      <!-- 步骤指示器 -->
-      <view class="steps-indicator">
-        <view
-          v-for="(step, index) in steps"
-          :key="index"
-          class="step-item"
-          :class="{ active: currentStep >= index, current: currentStep === index }"
-        >
-          <view class="step-circle">{{ index + 1 }}</view>
-          <text class="step-text">{{ step }}</text>
-        </view>
-      </view>
-
       <!-- 步骤1:基础设置 -->
       <view v-if="currentStep === 0" class="card">
         <view class="card-header">
@@ -37,26 +24,19 @@
         <!-- 内容类型 -->
         <view class="form-item">
           <text class="form-label">内容类型</text>
-          <view class="type-grid">
-            <view
-              v-for="cat in contentCategories"
-              :key="cat.name"
-              class="type-category"
-            >
-              <view class="category-header">{{ cat.name }}</view>
-              <view class="type-items">
-                <view
-                  v-for="type in cat.types"
-                  :key="type"
-                  class="type-item"
-                  :class="{ active: selectedType === type }"
-                  @click="selectedType = type"
-                >
-                  {{ type }}
-                </view>
+          <scroll-view scroll-x class="type-scroll">
+            <view class="type-list">
+              <view
+                v-for="type in allTypes"
+                :key="type"
+                class="type-item"
+                :class="{ active: selectedType === type }"
+                @click="selectedType = type"
+              >
+                {{ type }}
               </view>
             </view>
-          </view>
+          </scroll-view>
         </view>
 
         <!-- 行业选择 -->
@@ -77,14 +57,18 @@
           </scroll-view>
         </view>
 
-        <!-- 主题输入 -->
+        <!-- 主题/需求输入 -->
         <view class="form-item">
-          <text class="form-label">创作主题</text>
-          <input
-            v-model="theme"
-            class="theme-input"
-            placeholder="输入创作主题,如:都市爱情、励志故事"
+          <text class="form-label">创作需求</text>
+          <textarea
+            v-model="prompt"
+            class="prompt-input"
+            :placeholder="getPromptPlaceholder()"
+            :maxlength="500"
           />
+          <view class="input-hint">
+            <text>{{ prompt.length }}/500 字</text>
+          </view>
         </view>
 
         <!-- 目标字数 -->
@@ -92,112 +76,48 @@
           <text class="form-label">目标字数:{{ targetLength }} 字</text>
           <slider
             :value="targetLength"
-            :min="500"
+            :min="100"
             :max="10000"
-            :step="500"
+            :step="100"
             @change="(e: any) => targetLength = e.detail.value"
             activeColor="#4F46E5"
             backgroundColor="#e5e7eb"
             block-size="20"
           />
           <view class="slider-range">
-            <text>500字</text>
+            <text>100字</text>
             <text>10000字</text>
           </view>
         </view>
 
-        <button class="next-btn" :disabled="generatingOutline" @click="goToStep2">
-          <text v-if="generatingOutline">生成大纲中...</text>
-          <text v-else>下一步:生成大纲 →</text>
+        <button
+          class="generate-btn"
+          :disabled="!canGenerate || generating"
+          @click="startGenerate"
+        >
+          <text v-if="generating">{{ generatingProgress || '生成中...' }}</text>
+          <text v-else>🚀 开始生成</text>
         </button>
       </view>
 
-      <!-- 步骤2:大纲预览 -->
+      <!-- 步骤2:生成结果 -->
       <view v-if="currentStep === 1" class="card">
         <view class="card-header">
-          <text class="card-title">📑 内容大纲</text>
-          <text class="header-action" @click="regenerateOutline">🔄 重新生成</text>
-        </view>
-
-        <view v-if="generatingOutline" class="loading-state">
-          <text>正在生成大纲...</text>
-        </view>
-
-        <view v-else class="outline-content">
-          <view class="outline-stats">
-            <text class="stat-item">总章节:{{ outline.length }}</text>
-            <text class="stat-item">预计字数:{{ targetLength }}</text>
-          </view>
-
-          <view
-            v-for="(chapter, index) in outline"
-            :key="index"
-            class="chapter-item"
-          >
-            <view class="chapter-header">
-              <text class="chapter-title">{{ chapter.title }}</text>
-              <text class="chapter-words">{{ chapter.wordCount }}字</text>
-            </view>
-            <text class="chapter-desc">{{ chapter.description }}</text>
-          </view>
-        </view>
-
-        <view class="btn-row">
-          <button class="back-btn" @click="currentStep = 0">← 上一步</button>
-          <button class="next-btn" :disabled="generating" @click="goToStep3">
-          <text v-if="generatingCharacters">生成角色中...</text>
-          <text v-else>下一步:生成内容 →</text>
-        </button>
-        </view>
-      </view>
-
-      <!-- 步骤3:角色设定 -->
-      <view v-if="currentStep === 2" class="card">
-        <view class="card-header">
-          <text class="card-title">👥 角色设定</text>
-          <text class="header-action" @click="generateCharacters">🔄 重新生成</text>
-        </view>
-
-        <view class="characters-list">
-          <view
-            v-for="char in characters"
-            :key="char.id"
-            class="character-card"
-          >
-            <view class="char-avatar">{{ char.gender === '男' ? '👨' : '👩' }}</view>
-            <view class="char-info">
-              <text class="char-name">{{ char.name }}</text>
-              <text class="char-detail">年龄:{{ char.age }}</text>
-              <text class="char-detail">性格:{{ char.personality }}</text>
-              <text class="char-detail">角色:{{ char.role }}</text>
-            </view>
-          </view>
-        </view>
-
-        <view class="btn-row">
-          <button class="back-btn" @click="currentStep = 1">← 上一步</button>
-          <button class="next-btn" @click="currentStep = 3">下一步:生成内容 →</button>
-        </view>
-      </view>
-
-      <!-- 步骤4:内容生成 -->
-      <view v-if="currentStep === 3" class="card">
-        <view class="card-header">
-          <text class="card-title">✍️ 内容生成</text>
+          <text class="card-title">📝 生成结果</text>
           <view class="header-actions">
+            <text class="header-action" @click="copyContent">📋 复制</text>
             <text class="header-action" @click="saveDraft">💾 保存</text>
-            <text class="header-action" @click="showQualityScore">📊 评分</text>
           </view>
         </view>
 
-        <!-- 生成进度 -->
+        <!-- 进度显示 -->
         <view v-if="generating" class="progress-section">
           <view class="progress-header">
             <text>{{ generatingProgress }}</text>
             <text v-if="currentCharCount > 0">{{ currentCharCount }} 字</text>
           </view>
           <progress
-            :percent="Math.round((currentChapter + 1) / outline.length * 100)"
+            :percent="Math.round(progressPercent)"
             activeColor="#4F46E5"
             backgroundColor="#e5e7eb"
             stroke-width="8"
@@ -205,31 +125,22 @@
         </view>
 
         <!-- 生成的内容 -->
-        <view class="generated-content">
-          <view
-            v-for="(chapter, index) in generatedChapters"
-            :key="index"
-            class="content-chapter"
-          >
-            <view class="chapter-badge">{{ index + 1 }}</view>
-            <text class="chapter-text">{{ chapter }}</text>
-          </view>
+        <view v-if="generatedContent" class="result-content">
+          <text class="content-text">{{ generatedContent }}</text>
         </view>
 
-        <view v-if="!generating && generatedChapters.length === 0" class="empty-state">
-          <text>点击下方按钮开始生成内容</text>
+        <view class="result-footer">
+          <text class="word-count">{{ generatedContent?.length || 0 }} 字</text>
         </view>
 
         <view class="btn-row">
-          <button class="back-btn" @click="currentStep = 2">← 上一步</button>
-          <button class="generate-btn" :disabled="generating" @click="startGeneration">
-            {{ generating ? '生成中...' : '🚀 开始生成' }}
-          </button>
+          <button class="back-btn" @click="reset">← 重新生成</button>
+          <button class="primary-btn" @click="useContent">📝 使用此内容</button>
         </view>
       </view>
 
-      <!-- 步骤5:质量检测 -->
-      <view v-if="currentStep === 4" class="card">
+      <!-- 步骤3:质量检测 -->
+      <view v-if="currentStep === 2" class="card">
         <view class="card-header">
           <text class="card-title">✅ 质量检测</text>
         </view>
@@ -238,7 +149,9 @@
         <view class="quality-item">
           <view class="quality-header">
             <text class="quality-title">🔍 敏感词检测</text>
-            <text class="quality-status pass">通过</text>
+            <text class="quality-status" :class="sensitiveResult.isClean ? 'pass' : 'fail'">
+              {{ sensitiveResult.isClean ? '通过' : '有问题' }}
+            </text>
           </view>
           <view v-if="sensitiveResult.foundWords.length > 0" class="sensitive-words">
             <text v-for="word in sensitiveResult.foundWords" :key="word" class="sensitive-tag">
@@ -251,7 +164,9 @@
         <view class="quality-item">
           <view class="quality-header">
             <text class="quality-title">📋 合规检查</text>
-            <text class="quality-status pass">通过</text>
+            <text class="quality-status" :class="complianceResult.passed ? 'pass' : 'fail'">
+              {{ complianceResult.passed ? '通过' : '有问题' }}
+            </text>
           </view>
           <view v-if="complianceResult.warnings.length > 0" class="compliance-warnings">
             <text v-for="warn in complianceResult.warnings" :key="warn" class="warning-tag">
@@ -282,7 +197,7 @@
         </view>
 
         <view class="btn-row">
-          <button class="back-btn" @click="currentStep = 3">← 上一步</button>
+          <button class="back-btn" @click="currentStep = 1">← 返回结果</button>
           <button class="primary-btn" @click="useContent">📝 使用此内容</button>
         </view>
       </view>
@@ -296,12 +211,17 @@
           <text class="modal-close" @click="showHelp = false">✕</text>
         </view>
         <view class="modal-body">
-          <text class="help-text">1. 选择内容类型和行业</text>
-          <text class="help-text">2. 输入创作主题</text>
-          <text class="help-text">3. 生成并预览大纲</text>
-          <text class="help-text">4. 查看角色设定</text>
-          <text class="help-text">5. 一键生成完整内容</text>
-          <text class="help-text">6. 质量检测后使用</text>
+          <text class="help-title">支持的类型:</text>
+          <text class="help-text">• 创作类:小说、故事、剧本、诗歌、散文</text>
+          <text class="help-text">• 营销类:产品介绍、广告文案、朋友圈、小红书、抖音脚本</text>
+          <text class="help-text">• 教育类:课件、培训、教程、知识科普</text>
+          <text class="help-text">• 商务类:销售话术、客服话术、商务邮件</text>
+          <text class="help-text">• 媒体类:新闻播报、天气预报、体育解说</text>
+          <text class="help-title">使用流程:</text>
+          <text class="help-text">1. 选择内容类型</text>
+          <text class="help-text">2. 选择所属行业</text>
+          <text class="help-text">3. 描述您的需求</text>
+          <text class="help-text">4. 一键生成</text>
         </view>
       </view>
     </view>
@@ -310,40 +230,29 @@
 
 <script setup lang="ts">
 import { ref, computed } from 'vue';
-import { get, post } from '../../utils/request';
-
-const steps = ['基础设置', '大纲预览', '角色设定', '内容生成', '质量检测'];
-const currentStep = ref(0);
-
-// 基础设置
-const contentCategories = [
-  { name: '创作类', types: ['小说', '故事', '剧本', '诗歌', '散文'] },
-  { name: '营销类', types: ['产品介绍', '广告文案', '朋友圈', '小红书', '抖音脚本'] },
-  { name: '教育类', types: ['课件', '培训', '教程', '知识科普'] },
-  { name: '商务类', types: ['销售话术', '客服话术', '商务邮件'] },
-  { name: '媒体类', types: ['新闻播报', '天气预报', '体育解说'] },
+import { post } from '../../utils/request';
+
+const allTypes = [
+  '小说', '故事', '剧本', '诗歌', '散文',
+  '产品介绍', '广告文案', '朋友圈', '小红书', '抖音脚本',
+  '课件', '培训', '教程', '知识科普',
+  '销售话术', '客服话术', '商务邮件',
+  '新闻播报', '天气预报', '体育解说',
 ];
 
 const industries = ['通用', '医疗健康', '教育培训', '金融服务', '电子商务', '餐饮美食'];
+
+const currentStep = ref(0);
 const selectedType = ref('小说');
 const selectedIndustry = ref('通用');
-const theme = ref('');
+const prompt = ref('');
 const targetLength = ref(2000);
 
-// 大纲
-const generatingOutline = ref(false);
-const outline = ref<{ title: string; description: string; wordCount: number }[]>([]);
-
-// 角色
-const characters = ref<any[]>([]);
-const generatingCharacters = ref(false);
-
-// 内容生成
+// 生成状态
 const generating = ref(false);
-const currentChapter = ref(0);
-const generatedChapters = ref<string[]>([]);
-const generatingProgress = ref(''); // 当前进度
-const currentCharCount = ref(0); // 当前已生成字数
+const generatingProgress = ref('');
+const currentCharCount = ref(0);
+const generatedContent = ref('');
 
 // 质量检测
 const sensitiveResult = ref<{ isClean: boolean; foundWords: string[] }>({ isClean: true, foundWords: [] });
@@ -353,7 +262,14 @@ const qualityScore = ref<any>(null);
 // UI状态
 const showHelp = ref(false);
 
-const fullContent = computed(() => generatedChapters.value.join('\n\n'));
+const canGenerate = computed(() => {
+  return prompt.value.trim().length > 0 && !generating.value;
+});
+
+const progressPercent = computed(() => {
+  if (!generating.value) return 100;
+  return 50; // 生成中固定50%
+});
 
 const dimensionNames: Record<string, string> = {
   fluency: '流畅度',
@@ -367,101 +283,47 @@ function getDimensionName(key: string) {
   return dimensionNames[key] || key;
 }
 
+function getPromptPlaceholder() {
+  const placeholders: Record<string, string> = {
+    '小说': '描述你想要的故事类型,如:都市爱情、玄幻穿越、悬疑推理...',
+    '故事': '描述故事的主题和背景...',
+    '产品介绍': '介绍产品的名称、特点、优势...',
+    '广告文案': '描述产品卖点、活动内容...',
+    '朋友圈': '描述想表达的心情或内容...',
+    '培训': '描述培训主题和目标受众...',
+    '销售话术': '描述产品类型和目标客户...',
+    '新闻播报': '描述新闻事件内容...',
+  };
+  return placeholders[selectedType.value] || '描述您的创作需求...';
+}
+
 function goBack() {
   uni.navigateBack();
 }
 
-async function goToStep2() {
-  if (!theme.value.trim()) {
-    uni.showToast({ title: '请输入创作主题', icon: 'none' });
-    return;
-  }
-  uni.showLoading({ title: '正在生成大纲...' });
-  await generateOutline();
-  uni.hideLoading();
-  if (outline.value.length > 0) {
-    currentStep.value = 1;
-  }
-}
+async function startGenerate() {
+  if (!canGenerate.value) return;
 
-async function goToStep3() {
-  uni.showLoading({ title: '正在生成角色...' });
-  await generateCharacters();
-  uni.hideLoading();
-  currentStep.value = 2;
-}
+  generating.value = true;
+  generatingProgress.value = '正在生成内容...';
+  currentCharCount.value = 0;
+  generatedContent.value = '';
 
-async function generateOutline() {
-  generatingOutline.value = true;
   try {
-    const result = await post<any>('/ai-content/outline/generate', {
+    const result = await post<any>('/ai-content/generate', {
       type: selectedType.value,
-      theme: theme.value,
-      chapters: Math.ceil(targetLength.value / 1500),
+      industry: selectedIndustry.value,
+      prompt: prompt.value,
+      targetLength: targetLength.value,
     });
-    outline.value = result.outline || [];
-  } catch (e: any) {
-    uni.showToast({ title: '生成大纲失败', icon: 'none' });
-  } finally {
-    generatingOutline.value = false;
-  }
-}
 
-async function regenerateOutline() {
-  await generateOutline();
-}
-
-async function generateCharacters() {
-  generatingCharacters.value = true;
-  try {
-    const result = await post<any>('/ai-content/characters/generate', {
-      type: selectedType.value,
-      genre: theme.value,
-    });
-    characters.value = result.characters || [];
-  } catch (e) {
-    characters.value = [
-      { id: 'char-1', name: '张三', age: 30, gender: '男', personality: '正直勇敢', role: '主角' },
-      { id: 'char-2', name: '李四', age: 28, gender: '女', personality: '聪明机智', role: '女主' },
-    ];
-  } finally {
-    generatingCharacters.value = false;
-  }
-}
-
-async function startGeneration() {
-  generating.value = true;
-  currentChapter.value = 0;
-  generatedChapters.value = [];
-  generatingProgress.value = '准备开始...';
-  currentCharCount.value = 0;
-
-  try {
-    for (let i = 0; i < outline.value.length; i++) {
-      currentChapter.value = i;
-      generatingProgress.value = `正在生成第 ${i + 1}/${outline.value.length} 章...`;
-      currentCharCount.value = 0;
-
-      const chapterTitle = outline.value[i]?.title || `第${i + 1}章`;
-      const previousContent = i > 0 ? generatedChapters.value[i - 1] : '';
-
-      const result = await post<any>('/ai-content/chunk/generate', {
-        outlineId: `outline-${Date.now()}`,
-        chapterIndex: i,
-        chapterTitle,
-        previousContent,
-      });
-
-      generatedChapters.value.push(result.content || `这是第${i + 1}章的内容...`);
-      currentCharCount.value = result.wordCount || result.content?.length || 0;
-      generatingProgress.value = `第 ${i + 1}/${outline.value.length} 章完成 (${currentCharCount.value}字)`;
-    }
+    generatedContent.value = result.content || result.text || '生成内容为空';
+    currentCharCount.value = generatedContent.value.length;
+    generatingProgress.value = '生成完成';
 
     // 自动进行质量检测
-    generatingProgress.value = '进行质量检测...';
     await checkQuality();
-    currentStep.value = 4;
-    generatingProgress.value = '';
+    currentStep.value = 1;
   } catch (e: any) {
     uni.showToast({ title: '生成失败: ' + (e.message || '未知错误'), icon: 'none' });
   } finally {
@@ -470,18 +332,18 @@ async function startGeneration() {
 }
 
 async function checkQuality() {
-  const content = fullContent.value;
+  if (!generatedContent.value) return;
 
   // 敏感词检测
   try {
-    const sensitive = await post<any>('/ai-content/sensitive/check', { text: content });
+    const sensitive = await post<any>('/ai-content/sensitive/check', { text: generatedContent.value });
     sensitiveResult.value = sensitive;
   } catch (e) {}
 
   // 合规检查
   try {
     const compliance = await post<any>('/ai-content/compliance/check', {
-      text: content,
+      text: generatedContent.value,
       industry: selectedIndustry.value,
     });
     complianceResult.value = compliance;
@@ -489,12 +351,17 @@ async function checkQuality() {
 
   // 质量评分
   try {
-    qualityScore.value = await post<any>('/ai-content/quality/score', { text: content });
+    qualityScore.value = await post<any>('/ai-content/quality/score', { text: generatedContent.value });
   } catch (e) {}
 }
 
-function showQualityScore() {
-  checkQuality();
+function copyContent() {
+  uni.setClipboardData({
+    data: generatedContent.value,
+    success: () => {
+      uni.showToast({ title: '已复制', icon: 'success' });
+    },
+  });
 }
 
 function saveDraft() {
@@ -502,9 +369,20 @@ function saveDraft() {
 }
 
 function useContent() {
-  uni.setStorageSync('ai_generated_text', fullContent.value);
+  uni.setStorageSync('ai_generated_text', generatedContent.value);
   uni.navigateBack();
 }
+
+function reset() {
+  currentStep.value = 0;
+  generatedContent.value = '';
+  generatingProgress.value = '';
+  currentCharCount.value = 0;
+}
+
+function showQualityScore() {
+  currentStep.value = 2;
+}
 </script>
 
 <style scoped>
@@ -556,52 +434,6 @@ function useContent() {
   padding: 140rpx 32rpx 32rpx;
 }
 
-/* 步骤指示器 */
-.steps-indicator {
-  display: flex;
-  justify-content: space-between;
-  margin-bottom: 32rpx;
-  padding: 24rpx;
-  background: #fff;
-  border-radius: 16rpx;
-}
-
-.step-item {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  flex: 1;
-  opacity: 0.4;
-}
-
-.step-item.active {
-  opacity: 1;
-}
-
-.step-item.current .step-circle {
-  background: #4F46E5;
-  color: #fff;
-}
-
-.step-circle {
-  width: 48rpx;
-  height: 48rpx;
-  border-radius: 50%;
-  background: #e5e7eb;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 24rpx;
-  font-weight: 600;
-  color: #6b7280;
-  margin-bottom: 8rpx;
-}
-
-.step-text {
-  font-size: 20rpx;
-  color: #6b7280;
-}
-
 .card {
   background: #fff;
   border-radius: 24rpx;
@@ -622,16 +454,16 @@ function useContent() {
   color: #1f2937;
 }
 
-.header-action {
-  font-size: 26rpx;
-  color: #4F46E5;
-}
-
 .header-actions {
   display: flex;
   gap: 24rpx;
 }
 
+.header-action {
+  font-size: 26rpx;
+  color: #4F46E5;
+}
+
 /* 表单 */
 .form-item {
   margin-bottom: 32rpx;
@@ -644,176 +476,67 @@ function useContent() {
   display: block;
 }
 
-/* 内容类型网格 */
-.type-grid {
-  display: flex;
-  flex-direction: column;
-  gap: 16rpx;
-}
-
-.type-category {
-  background: #f9fafb;
-  border-radius: 12rpx;
-  padding: 16rpx;
-}
-
-.category-header {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #4F46E5;
-  margin-bottom: 12rpx;
-}
-
-.type-items {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 12rpx;
-}
-
-.type-item {
-  font-size: 24rpx;
-  padding: 8rpx 16rpx;
-  background: #fff;
-  border-radius: 8rpx;
-  color: #6b7280;
-  border: 2rpx solid transparent;
-}
-
-.type-item.active {
-  background: #4F46E5;
-  color: #fff;
-}
-
-/* 行业选择 */
-.industry-scroll {
+/* 类型选择 */
+.type-scroll, .industry-scroll {
   white-space: nowrap;
 }
 
-.industry-list {
-  display: flex;
+.type-list, .industry-list {
+  display: inline-flex;
   gap: 16rpx;
   padding: 8rpx 0;
 }
 
-.industry-item {
+.type-item, .industry-item {
   font-size: 26rpx;
   padding: 12rpx 24rpx;
   background: #f3f4f6;
   border-radius: 32rpx;
   color: #6b7280;
   white-space: nowrap;
+  border: 2rpx solid transparent;
 }
 
-.industry-item.active {
+.type-item.active, .industry-item.active {
   background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
   color: #fff;
+  border-color: #4f46e5;
 }
 
-/* 主题输入 */
-.theme-input {
+/* 输入框 */
+.prompt-input {
   width: 100%;
-  height: 80rpx;
+  min-height: 200rpx;
   background: #f9fafb;
-  border-radius: 12rpx;
-  padding: 0 24rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
   font-size: 28rpx;
+  line-height: 1.6;
 }
 
-/* 滑块 */
-.slider-range {
-  display: flex;
-  justify-content: space-between;
+.input-hint {
+  text-align: right;
   margin-top: 8rpx;
 }
 
-.slider-range text {
+.input-hint text {
   font-size: 22rpx;
   color: #9ca3af;
 }
 
-/* 大纲内容 */
-.outline-stats {
-  display: flex;
-  gap: 24rpx;
-  margin-bottom: 24rpx;
-}
-
-.stat-item {
-  font-size: 24rpx;
-  color: #6b7280;
-  background: #f3f4f6;
-  padding: 8rpx 16rpx;
-  border-radius: 8rpx;
-}
-
-.chapter-item {
-  background: #f9fafb;
-  border-radius: 12rpx;
-  padding: 20rpx;
-  margin-bottom: 16rpx;
-}
-
-.chapter-header {
+/* 滑块 */
+.slider-range {
   display: flex;
   justify-content: space-between;
-  align-items: center;
-  margin-bottom: 8rpx;
-}
-
-.chapter-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #1f2937;
+  margin-top: 8rpx;
 }
 
-.chapter-words {
+.slider-range text {
   font-size: 22rpx;
   color: #9ca3af;
 }
 
-.chapter-desc {
-  font-size: 24rpx;
-  color: #6b7280;
-}
-
-/* 角色列表 */
-.characters-list {
-  display: flex;
-  flex-direction: column;
-  gap: 16rpx;
-}
-
-.character-card {
-  display: flex;
-  background: #f9fafb;
-  border-radius: 16rpx;
-  padding: 24rpx;
-}
-
-.char-avatar {
-  font-size: 64rpx;
-  margin-right: 24rpx;
-}
-
-.char-info {
-  flex: 1;
-}
-
-.char-name {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #1f2937;
-  margin-bottom: 8rpx;
-}
-
-.char-detail {
-  font-size: 24rpx;
-  color: #6b7280;
-  display: block;
-  margin-bottom: 4rpx;
-}
-
-/* 生成进度 */
+/* 进度 */
 .progress-section {
   background: #f9fafb;
   border-radius: 12rpx;
@@ -832,40 +555,30 @@ function useContent() {
   color: #6b7280;
 }
 
-/* 生成的内容 */
-.generated-content {
-  max-height: 500rpx;
+/* 结果 */
+.result-content {
+  background: #f9fafb;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  max-height: 600rpx;
   overflow-y: auto;
-  margin-bottom: 24rpx;
 }
 
-.content-chapter {
-  display: flex;
-  background: #f9fafb;
-  border-radius: 12rpx;
-  padding: 20rpx;
-  margin-bottom: 16rpx;
+.content-text {
+  font-size: 28rpx;
+  color: #374151;
+  line-height: 1.8;
+  white-space: pre-wrap;
 }
 
-.chapter-badge {
-  width: 40rpx;
-  height: 40rpx;
-  background: #4F46E5;
-  color: #fff;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 22rpx;
-  margin-right: 16rpx;
-  flex-shrink: 0;
+.result-footer {
+  margin-top: 16rpx;
+  text-align: right;
 }
 
-.chapter-text {
-  font-size: 26rpx;
-  color: #374151;
-  line-height: 1.6;
-  white-space: pre-wrap;
+.word-count {
+  font-size: 24rpx;
+  color: #9ca3af;
 }
 
 /* 质量检测 */
@@ -900,6 +613,11 @@ function useContent() {
   color: #059669;
 }
 
+.quality-status.fail {
+  background: #fee2e2;
+  color: #dc2626;
+}
+
 .quality-score {
   font-size: 32rpx;
   font-weight: 700;
@@ -966,7 +684,7 @@ function useContent() {
   margin-top: 24rpx;
 }
 
-.next-btn, .primary-btn {
+.generate-btn, .primary-btn {
   flex: 1;
   height: 88rpx;
   background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
@@ -980,6 +698,11 @@ function useContent() {
   color: #fff;
 }
 
+.generate-btn[disabled] {
+  background: #e5e7eb;
+  color: #9ca3af;
+}
+
 .back-btn {
   flex: 1;
   height: 88rpx;
@@ -993,34 +716,8 @@ function useContent() {
   color: #6b7280;
 }
 
-.generate-btn {
-  flex: 1;
-  height: 88rpx;
-  background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
-  border-radius: 16rpx;
+.generate-btn::after, .back-btn::after, .primary-btn::after {
   border: none;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #fff;
-}
-
-.generate-btn[disabled] {
-  background: #e5e7eb;
-  color: #9ca3af;
-}
-
-.next-btn::after, .back-btn::after, .generate-btn::after, .primary-btn::after {
-  border: none;
-}
-
-.empty-state, .loading-state {
-  text-align: center;
-  padding: 48rpx;
-  color: #9ca3af;
-  font-size: 28rpx;
 }
 
 /* 弹窗 */
@@ -1039,9 +736,11 @@ function useContent() {
 
 .modal-content {
   width: 600rpx;
+  max-height: 80vh;
   background: #fff;
   border-radius: 24rpx;
   padding: 32rpx;
+  overflow-y: auto;
 }
 
 .modal-header {
@@ -1064,12 +763,20 @@ function useContent() {
 .modal-body {
   display: flex;
   flex-direction: column;
-  gap: 16rpx;
+  gap: 8rpx;
 }
 
-.help-text {
+.help-title {
   font-size: 28rpx;
-  color: #374151;
+  font-weight: 600;
+  color: #1f2937;
+  margin-top: 16rpx;
+  margin-bottom: 8rpx;
+}
+
+.help-text {
+  font-size: 26rpx;
+  color: #6b7280;
   line-height: 1.6;
 }
 </style>

+ 12 - 0
server/src/modules/ai-content/ai-content.controller.ts

@@ -26,6 +26,18 @@ router.get('/content-types/all', async (ctx) => {
   ctx.body = { code: 0, message: 'success', data: result };
 });
 
+// 统一内容生成接口
+router.post('/generate', async (ctx) => {
+  const { type, industry, prompt, targetLength } = ctx.request.body as {
+    type: string;
+    industry: string;
+    prompt: string;
+    targetLength: number;
+  };
+  const result = await aiContentService.generateContent(type, industry, prompt, targetLength);
+  ctx.body = { code: 0, message: 'success', data: result };
+});
+
 // 获取行业列表
 router.get('/industries', async (ctx) => {
   const result = aiContentService.getIndustries();

+ 306 - 0
server/src/modules/ai-content/ai-content.service.ts

@@ -231,6 +231,312 @@ export class AIContentService {
     return [...new Set(all)];
   }
 
+  /**
+   * 统一内容生成 - 根据类型自动选择生成策略
+   */
+  async generateContent(type: string, industry: string, prompt: string, targetLength: number = 2000) {
+    // 根据内容类型选择不同的生成策略
+    const novelTypes = ['小说', '故事', '剧本'];
+    const marketingTypes = ['产品介绍', '广告文案', '朋友圈', '小红书', '抖音脚本'];
+    const educationTypes = ['课件', '培训', '教程', '知识科普'];
+    const businessTypes = ['销售话术', '客服话术', '商务邮件'];
+    const mediaTypes = ['新闻播报', '天气预报', '体育解说'];
+    const otherTypes = ['诗歌', '散文'];
+
+    let content = '';
+
+    if (novelTypes.includes(type)) {
+      // 小说类:生成分章节的长篇内容
+      content = await this.generateNovel(prompt, targetLength);
+    } else if (marketingTypes.includes(type)) {
+      // 营销类:生成营销文案
+      content = await this.generateMarketing(type, prompt, industry, targetLength);
+    } else if (educationTypes.includes(type)) {
+      // 教育类:生成教学内容
+      content = await this.generateEducation(type, prompt, industry, targetLength);
+    } else if (businessTypes.includes(type)) {
+      // 商务类:生成话术或邮件
+      content = await this.generateBusiness(type, prompt, industry, targetLength);
+    } else if (mediaTypes.includes(type)) {
+      // 媒体类:生成播报内容
+      content = await this.generateMedia(type, prompt, industry);
+    } else {
+      // 其他类型:通用生成
+      content = await this.generateGeneric(type, prompt, targetLength);
+    }
+
+    return { content, type, industry, wordCount: content.length };
+  }
+
+  /**
+   * 生成小说
+   */
+  private async generateNovel(prompt: string, targetLength: number): Promise<string> {
+    let content = '';
+    const chapterCount = Math.ceil(targetLength / 2000);
+    const chapters: string[] = [];
+
+    const outlinePrompt = `根据以下需求,为小说生成大纲:
+
+需求:${prompt}
+章节数:${chapterCount}章
+
+请以JSON格式返回:
+{"chapters": [{"title": "第X章标题", "description": "章节概要"}]}`;
+
+    try {
+      const outlineResponse = await this.callLLM(outlinePrompt);
+      const jsonMatch = outlineResponse.match(/\{[\s\S]*\}/);
+      if (jsonMatch) {
+        const parsed = JSON.parse(jsonMatch[0]);
+        const chaptersOutline = parsed.chapters || [];
+
+        for (let i = 0; i < chaptersOutline.length; i++) {
+          const chapter = chaptersOutline[i];
+          const chapterPrompt = `续写小说章节:
+
+章节标题:${chapter.title}
+章节概要:${chapter.description}
+
+要求:
+1. 内容丰富、生动,不少于1500字
+2. 包含人物对话、心理描写、场景描写
+3. 情节紧凑,有吸引力
+4. 直接返回正文内容`;
+
+          try {
+            const chapterContent = await this.callLLM(chapterPrompt);
+            chapters.push(`【${chapter.title}】\n\n${chapterContent}`);
+          } catch {
+            chapters.push(`【${chapter.title}】\n\n[内容生成失败]`);
+          }
+        }
+      }
+    } catch {
+      // 如果大纲生成失败,直接根据主题生成
+    }
+
+    if (chapters.length === 0) {
+      // 降级:直接生成单段内容
+      const fallbackPrompt = `根据以下主题,写一篇${targetLength}字的小说:
+
+主题:${prompt}
+
+要求:
+1. 内容丰富、生动
+2. 包含人物对话、心理描写、场景描写
+3. 直接返回正文内容`;
+      content = await this.callLLM(fallbackPrompt);
+    } else {
+      content = chapters.join('\n\n');
+    }
+
+    return content;
+  }
+
+  /**
+   * 生成营销文案
+   */
+  private async generateMarketing(type: string, prompt: string, industry: string, targetLength: number): Promise<string> {
+    const templates: Record<string, string> = {
+      '产品介绍': `为以下产品写一篇详细介绍文案:
+
+产品信息:${prompt}
+行业:${industry}
+
+要求:
+1. 结构清晰,包含产品特点、优势、适用场景
+2. 语言生动,有感染力
+3. 字数:${targetLength}字左右`,
+      '广告文案': `写一条吸引人的广告文案:
+
+产品/活动:${prompt}
+行业:${industry}
+
+要求:
+1. 简短有力,突出卖点
+2. 有吸引力,让人想购买
+3. 直接返回文案内容`,
+      '朋友圈': `写一条朋友圈文案:
+
+内容主题:${prompt}
+
+要求:
+1. 轻松自然,有生活气息
+2. 符合朋友圈风格
+3. 可以带emoji`,
+      '小红书': `写一篇小红书种草文案:
+
+产品/体验:${prompt}
+
+要求:
+1. 生动有趣,吸引人
+2. 包含真实体验感
+3. 带话题标签
+4. 字数:${targetLength}字左右`,
+      '抖音脚本': `写一个抖音短视频脚本:
+
+内容:${prompt}
+
+要求:
+1. 结构:开头-内容-结尾
+2. 节奏快,有爆点
+3. 适合短视频节奏`,
+    };
+
+    const templatePrompt = templates[type] || `根据以下内容,生成合适的文案:
+
+${prompt}
+
+要求:字数${targetLength}字左右,直接返回内容`;
+
+    return await this.callLLM(templatePrompt);
+  }
+
+  /**
+   * 生成教育培训内容
+   */
+  private async generateEducation(type: string, prompt: string, industry: string, targetLength: number): Promise<string> {
+    const templates: Record<string, string> = {
+      '课件': `为以下主题制作课件内容:
+
+主题:${prompt}
+行业:${industry}
+
+要求:
+1. 结构清晰:导入-讲解-总结
+2. 重点突出
+3. 字数:${targetLength}字左右`,
+      '培训': `编写培训内容:
+
+主题:${prompt}
+对象:培训学员
+
+要求:
+1. 实用性强,易于理解
+2. 包含案例和练习
+3. 字数:${targetLength}字左右`,
+      '教程': `写一篇教程:
+
+主题:${prompt}
+
+要求:
+1. 步骤清晰
+2. 易于跟着操作
+3. 字数:${targetLength}字左右`,
+      '知识科普': `写一篇科普文章:
+
+主题:${prompt}
+
+要求:
+1. 语言通俗易懂
+2. 有趣味性
+3. 字数:${targetLength}字左右`,
+    };
+
+    const templatePrompt = templates[type] || `根据以下主题生成教学内容:
+
+${prompt}
+
+要求:字数${targetLength}字左右`;
+    return await this.callLLM(templatePrompt);
+  }
+
+  /**
+   * 生成商务内容
+   */
+  private async generateBusiness(type: string, prompt: string, industry: string, targetLength: number): Promise<string> {
+    const templates: Record<string, string> = {
+      '销售话术': `写一段销售话术:
+
+产品:${prompt}
+行业:${industry}
+
+要求:
+1. 开场白-产品介绍-处理异议-促成
+2. 专业且有说服力
+3. 字数:${targetLength}字左右`,
+      '客服话术': `写一段客服话术:
+
+场景:${prompt}
+行业:${industry}
+
+要求:
+1. 礼貌耐心
+2. 解决问题导向
+3. 字数:${targetLength}字左右`,
+      '商务邮件': `写一封商务邮件:
+
+背景:${prompt}
+行业:${industry}
+
+要求:
+1. 格式规范
+2. 语言专业得体
+3. 目的明确`,
+    };
+
+    const templatePrompt = templates[type] || `生成商务内容:
+
+${prompt}
+
+要求:字数${targetLength}字左右`;
+    return await this.callLLM(templatePrompt);
+  }
+
+  /**
+   * 生成媒体内容
+   */
+  private async generateMedia(type: string, prompt: string, industry: string): Promise<string> {
+    const templates: Record<string, string> = {
+      '新闻播报': `写一段新闻播报稿:
+
+新闻内容:${prompt}
+
+要求:
+1. 语言正式、简洁
+2. 客观准确
+3. 适合朗读`,
+      '天气预报': `写一段天气预报稿:
+
+地区和天气:${prompt}
+
+要求:
+1. 语言清晰
+2. 包含温度、天气状况、建议
+3. 适合播报`,
+      '体育解说': `写一段体育解说稿:
+
+比赛内容:${prompt}
+
+要求:
+1. 生动有趣
+2. 专业术语准确
+3. 节奏感强`,
+    };
+
+    const templatePrompt = templates[type] || `生成媒体内容:
+
+${prompt}`;
+    return await this.callLLM(templatePrompt);
+  }
+
+  /**
+   * 通用生成
+   */
+  private async generateGeneric(type: string, prompt: string, targetLength: number): Promise<string> {
+    const templatePrompt = `以"${type}"的格式,生成内容:
+
+需求:${prompt}
+
+要求:
+1. 符合${type}的特点
+2. 字数:${targetLength}字左右
+3. 直接返回正文内容`;
+
+    return await this.callLLM(templatePrompt);
+  }
+
   /**
    * 获取行业列表
    */