Ver Fonte

修改语音

yangg há 1 mês atrás
pai
commit
0020cc38fe

+ 12 - 10
src/components/ModelConfig/index.vue

@@ -22,6 +22,7 @@
             <div class="form-item">
               <label>选择模型</label>
               <a-select
+                style="width: 200px;"
                 v-model:value="model.type"
                 placeholder="请选择模型"
                 @change="(value) => handleModelChange(value, index)"
@@ -100,6 +101,7 @@
 import { ref, reactive, onMounted } from 'vue';
 import { PlusOutlined } from '@ant-design/icons-vue';
 import { message } from 'ant-design-vue';
+import axios from "axios";
 
 import upIcon from '@/assets/svg/ups.svg';
 import downIcon from '@/assets/svg/down.svg';
@@ -150,17 +152,16 @@ const modelGroups = ref([]);
 const fetchModels = async () => {
   loading.value = true;
   try {
-    const response = await fetch(`${import.meta.env.VITE_BASE_AI_API}/api/models`);
-    if (!response.ok) {
-      throw new Error('Failed to fetch models');
-    }
-    const data = await response.json();
-    
+    const response = await axios.get(`${import.meta.env.VITE_BASE_AI_API}/api/models`);
+    const data = response.data;
+    console.log(data);
+    if (data.code === 2000) {
+      
     // 将API返回的模型数据转换为组件需要的格式
-    const apiModels = data.data.map(model => ({
+    const apiModels = data.data.models.map(model => ({
       label: model.name,
-      value: model.id,
-      endpoint: model.endpoint || '',
+      value: model.type,
+      /* endpoint: model.endpoint || '', */
       description: model.description || ''
     }));
 
@@ -171,7 +172,8 @@ const fetchModels = async () => {
         options: apiModels
       },
       ...MODEL_GROUPS // 保留原有的模型组
-    ];
+      ];
+    }
   } catch (error) {
     console.error('获取模型列表失败:', error);
     message.error('获取模型列表失败');

+ 4 - 4
src/components/SearchResults/index.vue

@@ -156,11 +156,11 @@ export default defineComponent({
         console.log('API Response:', response.data);
         
         if (response.data && response.data.code === 200) {
-          displayResults.value = response.data.data.results || [];
+          displayResults.value = response.data.data.result || [];
           // 添加调试日志
-          console.log('Total Results:', response.data.data.total_results);
-          localTotal.value = response.data.data.total_results || 0;
-          emit('update:total', response.data.data.total_results || 0);
+          console.log('Total Results:', response.data.data.total);
+          localTotal.value = response.data.data.total || 0;
+          emit('update:total', response.data.data.total || 0);
         }
       } catch (error) {
         console.error('Web search failed:', error);

+ 41 - 10
src/components/VoiceConfig/index.vue

@@ -185,9 +185,15 @@ const handlePreviewVoice = async () => {
   try {
     isPreviewPlaying.value = true;
     
-    // 构建预览URL
-    const previewUrl = `${import.meta.env.VITE_BASE_AI_API}/get_voice_preview?voice_id=${voiceConfig.role}&text=你好`;
-    
+    // 获取预览音频数据
+    const response = await axios.get(`${import.meta.env.VITE_BASE_AI_API}/get_voice_preview?voice_id=${voiceConfig.role}`);
+    const audioData = response.data;
+
+    // 检查响应状态
+    if (audioData.code !== 200) {
+      throw new Error(audioData.msg || '获取音频失败');
+    }
+
     // 如果已存在音频元素,先停止并移除
     if (audioElement.value) {
       audioElement.value.pause();
@@ -195,7 +201,23 @@ const handlePreviewVoice = async () => {
     }
     
     // 创建新的音频元素
-    const audio = new Audio(previewUrl);
+    let audio;
+    
+    // 优先使用 base64 音频数据
+    if (audioData.audio && audioData.audio.startsWith('data:audio')) {
+      audio = new Audio(audioData.audio);
+    } 
+    // 如果没有 base64 数据,尝试使用音频 URL
+    else if (audioData.audio_url) {
+      const baseUrl = import.meta.env.VITE_BASE_AI_API;
+      const fullUrl = audioData.audio_url.startsWith('http') 
+        ? audioData.audio_url 
+        : `${baseUrl}${audioData.audio_url}`;
+      audio = new Audio(fullUrl);
+    } else {
+      throw new Error('无效的音频数据');
+    }
+
     audioElement.value = audio;
     
     // 监听播放结束事件
@@ -205,20 +227,29 @@ const handlePreviewVoice = async () => {
     };
     
     // 监听错误事件
-    audio.onerror = () => {
-      console.error('音频播放失败');
+    audio.onerror = (e) => {
+      console.error('音频播放失败:', e);
       isPreviewPlaying.value = false;
       audioElement.value = null;
       alert('试听失败');
     };
-    
-    // 开始播放
-    await audio.play();
+
+    // 确保音频已加载
+    audio.oncanplaythrough = async () => {
+      try {
+        await audio.play();
+      } catch (error) {
+        console.error('播放音频失败:', error);
+        isPreviewPlaying.value = false;
+        audioElement.value = null;
+        alert('播放失败');
+      }
+    };
     
   } catch (error) {
     console.error('试听失败:', error);
     isPreviewPlaying.value = false;
-    alert('试听失败');
+    alert(error.message || '试听失败');
   }
 };
 

+ 27 - 0
src/router/index.js

@@ -1,10 +1,37 @@
 import { createRouter, createWebHistory } from 'vue-router'
 import { ROUTE } from './menu.js'
 
+// 白名单路由,不需要登录就可以访问
+const whiteList = ['/login', '/register', '/forget-password', '/404', '/register/invitation']
+
 const route = createRouter({
     history: createWebHistory(import.meta.env.BASE_URL),
     scrollBehavior: () => ({ y: 0 }),
     routes: ROUTE
 })
 
+// 全局前置守卫
+route.beforeEach((to, from, next) => {
+    const token = localStorage.getItem('token')
+    
+    // 在白名单中的路由直接放行
+    if (whiteList.includes(to.path)) {
+        next()
+        return
+    }
+    
+    // 检查是否有token
+    if (!token) {
+        // 没有token,重定向到登录页面
+        next({
+            path: '/login',
+            query: { redirect: to.fullPath } // 保存原本要去的路由路径
+        })
+        return
+    }
+    
+    // 有token,正常放行
+    next()
+})
+
 export default route

+ 7 - 0
src/router/menu.js

@@ -21,6 +21,13 @@ export const ROUTE = [
     hidden: true,
     component: () => import('@views/login.vue')
   },
+  {
+    path: '/register/invitation',
+    name: 'register',
+    title: '注册',
+    hidden: true,
+    component: () => import('@views/register.vue')
+  },
   {
     path: '/forget-password',
     name: 'forget-password',

+ 330 - 0
src/views/register.vue

@@ -0,0 +1,330 @@
+<template>
+  <div class="register-page">
+    <div class="register-container">
+      <div class="register-content">
+        <div class="register-header">
+          <h2>用户注册</h2>
+          <p class="sub-title">请填写以下信息完成注册</p>
+        </div>
+        
+        <a-form 
+          ref="registerForm"
+          :model="formData"
+          :rules="rules"
+          layout="vertical"
+        >
+          <a-form-item label="邀请码" name="invitation_code">
+            <a-input
+              v-model:value="formData.invitation_code"
+              placeholder="请输入邀请码"
+              size="large"
+            >
+              <template #prefix>
+                <GiftOutlined style="color: rgba(0,0,0,.25)" />
+              </template>
+            </a-input>
+          </a-form-item>
+
+          <a-form-item label="用户名" name="username">
+            <a-input
+              v-model:value="formData.username"
+              placeholder="请输入用户名"
+              size="large"
+            >
+              <template #prefix>
+                <UserOutlined style="color: rgba(0,0,0,.25)" />
+              </template>
+            </a-input>
+          </a-form-item>
+
+          <a-form-item label="密码" name="password">
+            <a-input-password
+              v-model:value="formData.password"
+              placeholder="请输入密码"
+              size="large"
+            >
+              <template #prefix>
+                <LockOutlined style="color: rgba(0,0,0,.25)" />
+              </template>
+            </a-input-password>
+          </a-form-item>
+
+          <a-form-item label="确认密码" name="confirm_password">
+            <a-input-password
+              v-model:value="formData.confirm_password"
+              placeholder="请再次输入密码"
+              size="large"
+            >
+              <template #prefix>
+                <LockOutlined style="color: rgba(0,0,0,.25)" />
+              </template>
+            </a-input-password>
+          </a-form-item>
+
+          <a-form-item label="真实姓名" name="real_name">
+            <a-input
+              v-model:value="formData.real_name"
+              placeholder="请输入真实姓名"
+              size="large"
+            >
+              <template #prefix>
+                <IdcardOutlined style="color: rgba(0,0,0,.25)" />
+              </template>
+            </a-input>
+          </a-form-item>
+
+          <a-form-item label="手机号" name="mobile">
+            <a-input
+              v-model:value="formData.mobile"
+              placeholder="请输入手机号"
+              size="large"
+            >
+              <template #prefix>
+                <MobileOutlined style="color: rgba(0,0,0,.25)" />
+              </template>
+            </a-input>
+          </a-form-item>
+
+          <a-form-item label="邮箱" name="email">
+            <a-input
+              v-model:value="formData.email"
+              placeholder="请输入邮箱"
+              size="large"
+            >
+              <template #prefix>
+                <MailOutlined style="color: rgba(0,0,0,.25)" />
+              </template>
+            </a-input>
+          </a-form-item>
+
+          <a-form-item>
+            <a-button 
+              type="primary" 
+              :loading="loading"
+              size="large"
+              block
+              @click="handleRegister"
+            >
+              注册
+            </a-button>
+          </a-form-item>
+
+          <div class="form-footer">
+            <p class="login-tip">
+              已有账号?<router-link to="/login" class="login-link">立即登录</router-link>
+            </p>
+          </div>
+        </a-form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { defineComponent, reactive, ref, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import axios from 'axios';
+import { message } from 'ant-design-vue';
+import { 
+  UserOutlined,
+  LockOutlined,
+  MobileOutlined,
+  MailOutlined,
+  IdcardOutlined,
+  GiftOutlined
+} from '@ant-design/icons-vue';
+
+export default defineComponent({
+  name: 'register',
+  components: {
+    UserOutlined,
+    LockOutlined,
+    MobileOutlined,
+    MailOutlined,
+    IdcardOutlined,
+    GiftOutlined
+  },
+  setup() {
+    const router = useRouter();
+    const route = useRoute();
+    const registerForm = ref();
+    const loading = ref(false);
+    
+    const formData = reactive({
+      invitation_code: '',
+      username: '',
+      password: '',
+      confirm_password: '',
+      real_name: '',
+      mobile: '',
+      email: ''
+    });
+    
+    const rules = {
+      invitation_code: [
+        { required: true, message: '请输入邀请码' },
+        { len: 20, message: '邀请码长度必须为20位' }
+      ],
+      username: [
+        { required: true, message: '请输入用户名' },
+        { min: 4, message: '用户名长度不能小于4位' }
+      ],
+      password: [
+        { required: true, message: '请输入密码' },
+        { min: 6, message: '密码长度不能小于6位' }
+      ],
+      confirm_password: [
+        { required: true, message: '请确认密码' },
+        { 
+          validator: (rule, value) => {
+            if (value && value !== formData.password) {
+              return Promise.reject('两次输入的密码不一致');
+            }
+            return Promise.resolve();
+          }
+        }
+      ],
+      real_name: [
+        { required: true, message: '请输入真实姓名' }
+      ],
+      mobile: [
+        { required: true, message: '请输入手机号' },
+        { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
+      ],
+      email: [
+        { required: true, message: '请输入邮箱' },
+        { type: 'email', message: '请输入正确的邮箱格式' }
+      ]
+    };
+
+    // 从 URL 获取邀请码
+    onMounted(() => {
+      const code = route.query.code;
+      if (code) {
+        formData.invitation_code = code;
+      }
+    });
+
+    // 处理注册
+    const handleRegister = async () => {
+      try {
+        loading.value = true;
+        await registerForm.value.validate();
+        
+        const response = await axios.post(`${import.meta.env.VITE_BASE_AI_API}/user/register_by_invitation?code=${formData.invitation_code}`, {
+         /*  invitation_code: formData.invitation_code, */
+          username: formData.username,
+          password: formData.password,
+          confirm_password: formData.confirm_password,
+          real_name: formData.real_name,
+          mobile: formData.mobile,
+          email: formData.email
+        });
+
+        if (response.data.code === 200) {
+          message.success('注册成功');
+          router.push('/login');
+        } else {
+          message.error(response.data.message || '注册失败');
+        }
+      } catch (error) {
+        console.error('注册失败:', error);
+        message.error(error.response?.data?.message || '注册失败,请检查输入信息');
+      } finally {
+        loading.value = false;
+      }
+    };
+
+    return {
+      registerForm,
+      formData,
+      rules,
+      loading,
+      handleRegister
+    };
+  }
+});
+</script>
+
+<style lang="less" scoped>
+.register-page {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  
+  .register-container {
+    width: 100%;
+    max-width: 420px;
+    margin: 0 20px;
+  }
+  
+  .register-content {
+    background: rgba(255, 255, 255, 0.95);
+    padding: 40px;
+    border-radius: 16px;
+    box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
+    backdrop-filter: blur(8px);
+    animation: slideUp 0.6s ease-out;
+  }
+  
+  .register-header {
+    text-align: center;
+    margin-bottom: 32px;
+    
+    h2 {
+      font-size: 28px;
+      color: #1a1a1a;
+      margin-bottom: 8px;
+      font-weight: 600;
+    }
+    
+    .sub-title {
+      color: #666;
+      font-size: 14px;
+    }
+  }
+
+  .form-footer {
+    text-align: center;
+    margin-top: 24px;
+
+    .login-tip {
+      color: #666;
+      font-size: 14px;
+      
+      .login-link {
+        color: #667eea;
+        font-weight: 500;
+        
+        &:hover {
+          color: #764ba2;
+        }
+      }
+    }
+  }
+}
+
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+// 响应式适配
+@media (max-width: 480px) {
+  .register-content {
+    padding: 24px;
+  }
+  
+  .register-header h2 {
+    font-size: 24px;
+  }
+}
+</style>

+ 139 - 19
src/views/report.vue

@@ -21,9 +21,9 @@
             <i class="fas fa-key"></i>
             修改密码
           </div>
-          <div class="dropdown-item" @click="handleResetPassword">
-            <i class="fas fa-redo"></i>
-            重置密码
+          <div class="dropdown-item" @click="handleCreateInvitation">
+            <i class="fas fa-user-plus"></i>
+            创建邀请
           </div>
           <div class="dropdown-item" @click="handleLogout">
             <i class="fas fa-sign-out-alt"></i>
@@ -230,7 +230,7 @@
 
               <!-- 音频控制按钮 -->
               <div
-                v-if="message.role === 'assistant' && message.audioData"
+                v-if="message.role === 'assistant'"
                 class="audio-controls"
               >
                 <button class="audio-btn" @click="handleAudioClick(message)">
@@ -657,6 +657,7 @@ import DocumentPreview from "../components/DocumentPreview/index.vue";
 import KnowledgeResults from "../components/KnowledgeResults/index.vue";
 import ChangePasswordDialog from '../components/ChangePasswordDialog.vue'
 import { useRouter } from 'vue-router'
+import { message } from 'ant-design-vue';
 
 // 初始化 markdown-it
 const md = new MarkdownIt({
@@ -699,6 +700,7 @@ const isPlaying = ref(false);
 
 // 添加当前播放消息的 ID
 const currentPlayingId = ref(null);
+const currentAudioSource = ref(null);
 
 // 添加新的响应式变量
 const isUploading = ref(false);
@@ -1062,6 +1064,13 @@ const scrollToBottom = () => {
 const session_id = ref("");
 // 修改发送消息方法
 const sendMessage = async () => {
+  // 检查登录状态
+  const token = localStorage.getItem('token');
+  if (!token) {
+    router.push('/login');
+    return;
+  }
+
   if (
     (!inputContent.value.trim() && attachedFiles.value.length === 0) ||
     isTyping.value
@@ -1374,13 +1383,67 @@ const handleFileDrop = async (event) => {
 
 // 添加音频点击处理方法
 const handleAudioClick = (message) => {
-  if (currentPlayingId.value === message.id) {
-    stopAudio();
-  } else {
-    playAudioMessage(message.audioData, message.id);
+  if(message.audioData){
+    if (currentPlayingId.value === message.id) {
+      stopAudio();
+    } else {
+      playAudioMessage(message.audioData, message.id);
+    }  
+  }else{
+    // 用户消息的文本转语音处理
+    playUserMessage(message.content, message.id);
+  }
+};
+
+// 添加播放用户消息的方法
+const playUserMessage = async (text, messageId) => {
+  try {
+    // 如果当前有音频在播放,先停止
+    if (currentPlayingId.value) {
+      stopAudio();
+    }
+
+    const response = await axios.post(`${import.meta.env.VITE_BASE_AI_API}/api/voices/play_text`, {
+      text: text
+    }, {
+      headers: {
+        'Authorization': `JWT ${localStorage.getItem('token')}`
+      },
+    });
+    
+    if(response.data.code === 200){
+      // 创建音频元素
+      const audio = new Audio(`${import.meta.env.VITE_BASE_AI_API}${response.data.audio_url}`);
+      
+      // 保存当前播放的音频和ID
+      currentAudioSource.value = audio;
+      currentPlayingId.value = messageId;
+      
+      // 播放结束时清理
+      audio.onended = () => {
+        currentPlayingId.value = null;
+        currentAudioSource.value = null;
+      };
+      
+      // 开始播放
+      await audio.play();
+    }
+  } catch (error) {
+    console.error('播放文本失败:', error);
+    message.error('播放失败,请稍后重试');
   }
 };
 
+/* // 停止音频播放的函数
+const stopAudio = () => {
+  if (currentAudioSource.value) {
+    currentAudioSource.value.pause();
+    currentAudioSource.value.currentTime = 0;
+    currentAudioSource.value = null;
+    currentPlayingId.value = null;
+  }
+} */
+
 // 修改音频播放方法
 const playAudioMessage = async (audioBase64, messageId) => {
   try {
@@ -1417,6 +1480,11 @@ const stopAudio = () => {
     audioPlayer.value.pause();
     audioPlayer.value = null;
   }
+  if (currentAudioSource.value) {
+    currentAudioSource.value.pause();
+    currentAudioSource.value.currentTime = 0;
+    currentAudioSource.value = null;
+  }
   isPlaying.value = false;
   currentPlayingId.value = null;
 };
@@ -1433,7 +1501,7 @@ const renderMarkdown = (content) => {
 };
 
 // 添加新的响应式变量
-const rightMenuWidth = ref(600); // 默认宽度
+const rightMenuWidth = ref(450); // 默认宽度
 const minWidth = 0; // 最小宽度
 const maxWidth = 800; // 最大宽度
 
@@ -1772,6 +1840,54 @@ const updateCollapsedState = () => {
     }
   };
 
+  const handleCreateInvitation = async () => {
+    try {
+      const response = await axios.post(`${import.meta.env.VITE_BASE_AI_API}/user/create_invitation`, {}, {
+        headers: {
+          'Authorization': `JWT ${localStorage.getItem('token')}`
+        }
+      });
+      console.log(response);
+      if (response.data.code === 200) {
+        message.success('创建邀请成功');
+        // 从当前窗口location获取基础URL
+        const baseUrl = `${window.location.protocol}//${window.location.host}`;
+        // 跳转到完整URL
+        window.location.href = `${baseUrl}${response.data.data.invitation_link}`;
+      } else {
+        message.error(response.data.message || '创建邀请失败');
+      }
+    } catch (error) {
+      console.error('创建邀请失败:', error);
+      message.error(error.response?.data?.message || '创建邀请失败,请稍后重试');
+    }
+    showDropdown.value = false; // 关闭下拉菜单
+  };
+
+/*   return {
+    loginForm,
+    formData,
+    rules,
+    loading,
+    rememberMe,
+    loginType,
+    countdown,
+    handleLogin,
+    sendVerificationCode,
+    switchToMobileLogin,
+    switchToAccountLogin,
+    switchToWechatLogin,
+    wechatLoginModal,
+    wechatQrCode,
+    getWechatQrCode,
+    handleWechatLoginCancel,
+    handleCreateInvitation
+  }; */
+
+  // These variables are already declared above
+  /* const currentAudioSource = ref(null);
+  const currentPlayingId = ref(null); */
+
  </script>
 
 <style scoped>
@@ -1871,12 +1987,13 @@ const updateCollapsedState = () => {
   border-right: 1px solid rgba(0, 0, 0, 0.06);
   position: absolute;
   left: 0;
-  transform: translateX(0);
+  /* transform: translateX(0); */
   transition: all 0.3s ease;
   z-index: 2;
 }
 
 .menu.collapsed {
+  display: none;
   transform: translateX(-260px);
   width: 0;
 }
@@ -1903,6 +2020,7 @@ const updateCollapsedState = () => {
   right: 0;
   top: 0;
   bottom: 0;
+  justify-content: space-between;
 }
 
 .menu:not(.collapsed) + .chat {
@@ -1915,11 +2033,11 @@ const updateCollapsedState = () => {
 }
 
 .layout:has(.right_menu:not(.collapsed)) .chat {
-  right: 600px;
+  right: 450px;
 }
 
 .right_menu:not(.collapsed) ~ .chat {
-  right: 600px;
+  right: 450px;
 }
 
 .messages {
@@ -2539,7 +2657,7 @@ const updateCollapsedState = () => {
   display: flex;
   flex-direction: column;
   gap: 16px;
-  margin: 0 auto;
+  margin: 30px auto 0;
 }
 
 .message-item {
@@ -2601,7 +2719,7 @@ const updateCollapsedState = () => {
 
 /* 当右侧菜单展开时,给聊天区域添加右边距 */
 .right_menu:not(.collapsed) + .chat {
-  margin-right: 600px;
+  margin-right: 450px;
 }
 
 .collapse-left-button{
@@ -3113,7 +3231,7 @@ const updateCollapsedState = () => {
     /* 移动端菜单调整 */
     position: fixed;
     width: 100%;
-    height: 100%;
+    height: 93%;
     z-index: 1000;
     transform: translateX(-100%);
   }
@@ -3123,8 +3241,9 @@ const updateCollapsedState = () => {
   }
 
   .collapse-left-button {
+    z-index: 999;
     /* 移动端收缩按钮调整 */
-    display: none;
+   /*  display: none; */
   }
 
   .chat {
@@ -3642,6 +3761,7 @@ button,
     font-size: 14px;
     transition: all 0.3s;
     white-space: nowrap;
+    color: #000000a6;
   }
 }
 
@@ -3810,7 +3930,7 @@ button,
   right: 0;
   top: 0;
   bottom: 0;
-  width: 600px;
+  width: 450px;
   background: #fff;
   border-left: 1px solid rgba(0, 0, 0, 0.06);
   display: flex;
@@ -3903,13 +4023,13 @@ button,
 }
 
 .right_menu:not(.collapsed) ~ .chat {
-  right: 600px;
+  right: 450px;
 }
 
 /* 移动端适配 */
 @media screen and (max-width: 768px) {
   .right_menu {
-    width: 100%;
+    width: 93%;
   }
 
   .right_menu:not(.collapsed) ~ .chat {