Преглед изворни кода

feat: 添加 AI 文本生成功能

- 后端新增 /api/ai/generate 接口,调用阿里云 Qwen 大模型
- 前端生成页面添加 AI 生成按钮和弹窗
- 用户输入问题后,AI 生成答案并填充到文本框
MyFramework User пре 5 месеци
родитељ
комит
c39e6504ea
3 измењених фајлова са 265 додато и 0 уклоњено
  1. 186 0
      client/src/pages/history/index.vue
  2. 2 0
      server/src/app.ts
  3. 77 0
      server/src/modules/ai/ai.controller.ts

+ 186 - 0
client/src/pages/history/index.vue

@@ -23,6 +23,9 @@
           auto-height
         />
         <view class="input-actions">
+          <button class="action-btn ai-btn" @click="showAIModal">
+            <text>🤖 AI 生成</text>
+          </button>
           <button class="action-btn" @click="clearText">清空</button>
           <button class="action-btn" @click="pasteText">粘贴</button>
         </view>
@@ -101,6 +104,37 @@
         <text class="btn-text">{{ generating ? '生成中...' : '生成音频' }}</text>
       </button>
     </view>
+
+    <!-- AI 生成弹窗 -->
+    <view v-if="showAiModal" class="ai-modal-mask" @click="closeAiModal">
+      <view class="ai-modal" @click.stop>
+        <view class="ai-modal-header">
+          <text class="ai-modal-title">🤖 AI 智能生成</text>
+          <text class="ai-modal-close" @click="closeAiModal">×</text>
+        </view>
+        <view class="ai-modal-content">
+          <textarea
+            v-model="aiPrompt"
+            class="ai-prompt-input"
+            placeholder="输入你想了解的内容,例如:计算机原理是什么?什么是人工智能?"
+            :maxlength="500"
+          />
+          <view class="ai-prompt-hint">
+            <text>AI 将根据你的输入生成相关文本,可直接用于生成音频</text>
+          </view>
+        </view>
+        <view class="ai-modal-footer">
+          <button class="ai-cancel-btn" @click="closeAiModal">取消</button>
+          <button
+            class="ai-confirm-btn"
+            :disabled="!aiPrompt.trim() || aiGenerating"
+            @click="handleAIGenerate"
+          >
+            <text>{{ aiGenerating ? '生成中...' : '生成文本' }}</text>
+          </button>
+        </view>
+      </view>
+    </view>
   </view>
 </template>
 
@@ -108,6 +142,7 @@
 import { ref, computed, onMounted } from 'vue';
 import { useUserStore } from '../../store/user';
 import { useAudioStore } from '../../store/audio';
+import { post } from '../../utils/request';
 import type { VoiceParams } from '../../types';
 
 const userStore = useUserStore();
@@ -122,6 +157,9 @@ const voiceParams = ref<VoiceParams>({
   volume: 50,
 });
 const generating = ref(false);
+const aiGenerating = ref(false);
+const aiPrompt = ref('');
+const showAiModal = ref(false);
 
 // 计算属性
 const canGenerate = computed(() => {
@@ -146,6 +184,46 @@ async function pasteText() {
   }
 }
 
+// AI 生成文本
+async function handleAIGenerate() {
+  if (!aiPrompt.value.trim()) {
+    uni.showToast({ title: '请输入想了解的内容', icon: 'none' });
+    return;
+  }
+
+  aiGenerating.value = true;
+  try {
+    const result = await post<{ text: string }>('/ai/generate', {
+      prompt: aiPrompt.value,
+    });
+
+    // 将生成的文本填充到输入框
+    text.value = result.text;
+    aiPrompt.value = '';
+    
+    // 关闭弹窗
+    showAiModal.value = false;
+    
+    uni.showToast({ title: '已生成文本', icon: 'success' });
+  } catch (error: any) {
+    console.error('AI 生成失败:', error);
+    uni.showToast({ title: error.message || 'AI 生成失败', icon: 'none' });
+  } finally {
+    aiGenerating.value = false;
+  }
+}
+
+// 显示 AI 生成弹窗
+function showAIModal() {
+  aiPrompt.value = '';
+  showAiModal.value = true;
+}
+
+// 关闭 AI 生成弹窗
+function closeAiModal() {
+  showAiModal.value = false;
+}
+
 // 生成音频
 async function handleGenerate() {
   if (!canGenerate.value) return;
@@ -365,4 +443,112 @@ async function handleGenerate() {
   font-weight: 600;
   color: #ffffff;
 }
+
+/* AI 弹窗样式 */
+.ai-modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 1000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 32rpx;
+}
+
+.ai-modal {
+  width: 100%;
+  background: #ffffff;
+  border-radius: 24rpx;
+  overflow: hidden;
+}
+
+.ai-modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 32rpx;
+  border-bottom: 1rpx solid #f3f4f6;
+}
+
+.ai-modal-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.ai-modal-close {
+  font-size: 48rpx;
+  color: #9ca3af;
+  line-height: 1;
+}
+
+.ai-modal-content {
+  padding: 32rpx;
+}
+
+.ai-prompt-input {
+  width: 100%;
+  height: 200rpx;
+  padding: 24rpx;
+  background: #f9fafb;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  color: #1f2937;
+  line-height: 1.6;
+  box-sizing: border-box;
+}
+
+.ai-prompt-hint {
+  margin-top: 16rpx;
+}
+
+.ai-prompt-hint text {
+  font-size: 22rpx;
+  color: #9ca3af;
+}
+
+.ai-modal-footer {
+  display: flex;
+  gap: 24rpx;
+  padding: 32rpx;
+  border-top: 1rpx solid #f3f4f6;
+}
+
+.ai-cancel-btn,
+.ai-confirm-btn {
+  flex: 1;
+  height: 80rpx;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.ai-cancel-btn {
+  background: #f3f4f6;
+  color: #6b7280;
+  border: none;
+}
+
+.ai-confirm-btn {
+  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
+  color: #ffffff;
+  border: none;
+}
+
+.ai-confirm-btn[disabled] {
+  background: #e5e7eb;
+}
+
+.ai-btn {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+}
 </style>

+ 2 - 0
server/src/app.ts

@@ -13,6 +13,7 @@ import ttsRoutes from './modules/tts/tts.controller';
 import audioRoutes from './modules/audio/audio.controller';
 import memberRoutes from './modules/member/member.controller';
 import shareRoutes from './modules/share/share.controller';
+import aiRoutes from './modules/ai/ai.controller';
 
 const app = new Koa();
 const router = new Router();
@@ -40,6 +41,7 @@ router.use('/api/tts', ttsRoutes.routes());
 router.use('/api/audio', audioRoutes.routes());
 router.use('/api/member', memberRoutes.routes());
 router.use('/api/share', shareRoutes.routes());
+router.use('/api/ai', aiRoutes.routes());
 
 app.use(router.routes()).use(router.allowedMethods());
 

+ 77 - 0
server/src/modules/ai/ai.controller.ts

@@ -0,0 +1,77 @@
+import Router from '@koa/router';
+import { Context } from 'koa';
+import axios from 'axios';
+import { config } from '../../config';
+
+const router = new Router();
+
+// AI 生成文本
+router.post('/generate', async (ctx: Context) => {
+  const { prompt } = ctx.request.body as {
+    prompt: string;
+  };
+
+  if (!prompt || prompt.trim().length === 0) {
+    ctx.status = 400;
+    ctx.body = { code: 400, message: '请输入提示词' };
+    return;
+  }
+
+  try {
+    // 使用阿里云 DashScope API 调用 Qwen 大模型
+    const apiKey = config.dashscope.apiKey;
+    
+    if (!apiKey) {
+      ctx.status = 500;
+      ctx.body = { code: 500, message: '未配置 AI API Key' };
+      return;
+    }
+
+    // 调用阿里云百炼文本生成 API
+    const response = await axios.post(
+      'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
+      {
+        model: 'qwen3-8b-flash',
+        input: {
+          prompt: prompt,
+        },
+        parameters: {
+          result_format: 'message',
+        },
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${apiKey}`,
+          'Content-Type': 'application/json',
+        },
+        timeout: 60000,
+      }
+    );
+
+    const data = response.data;
+    
+    if (data.code) {
+      throw new Error(data.message || 'AI 调用失败');
+    }
+
+    // 提取生成的文本
+    const generatedText = data.output?.choices?.[0]?.message?.content || '';
+    
+    ctx.body = {
+      code: 0,
+      message: 'success',
+      data: {
+        text: generatedText,
+      },
+    };
+  } catch (error: any) {
+    console.error('❌ AI 生成失败:', error.response?.data || error.message);
+    ctx.status = 500;
+    ctx.body = {
+      code: 500,
+      message: error.message || 'AI 生成失败,请稍后重试',
+    };
+  }
+});
+
+export default router;