LAPTOP-1LHD2FP6\joeny 5 maanden geleden
bovenliggende
commit
1f36582284
3 gewijzigde bestanden met toevoegingen van 625 en 16 verwijderingen
  1. 321 6
      src/components/ModelConfig/index.vue
  2. 299 6
      src/components/SearchResults/index.vue
  3. 5 4
      src/views/report.vue

+ 321 - 6
src/components/ModelConfig/index.vue

@@ -1,16 +1,331 @@
 <template>
   <div class="model-config">
-    <h3>模型配置</h3>
-    <!-- 在这里添加模型配置的具体内容 -->
+    <div class="config-trigger" @click="showDrawer">
+      <SettingOutlined class="trigger-icon" />
+      <span>模型配置</span>
+    </div>
+
+    <a-drawer
+      title="AI模型配置"
+      placement="left"
+      :width="400"
+      :visible="visible"
+      @close="closeDrawer"
+      class="model-drawer"
+    >
+      <div class="model-list">
+        <div v-for="(model, index) in modelList" :key="index" class="model-item">
+          <div class="model-header">
+            <span class="model-title">模型 {{index + 1}}</span>
+            <a-button type="link" danger @click="removeModel(index)">删除</a-button>
+          </div>
+          
+          <a-form layout="vertical">
+            <a-form-item label="选择模型">
+              <a-select
+                v-model:value="model.type"
+                placeholder="请选择模型"
+                @change="(value) => handleModelChange(value, index)"
+              >
+                <a-select-opt-group v-for="group in modelGroups" :key="group.label" :label="group.label">
+                  <a-select-option 
+                    v-for="option in group.options" 
+                    :key="option.value" 
+                    :value="option.value"
+                  >
+                    {{ option.label }}
+                  </a-select-option>
+                </a-select-opt-group>
+              </a-select>
+            </a-form-item>
+
+            <template v-if="model.type">
+              <a-form-item label="API Key">
+                <a-input-password
+                  v-model:value="model.key"
+                  :placeholder="getKeyPlaceholder(model.type)"
+                />
+              </a-form-item>
+              
+              <template v-if="model.type === 'ollama'">
+                <a-form-item label="服务器地址">
+                  <a-input
+                    v-model:value="model.endpoint"
+                    placeholder="例如: http://localhost:11434"
+                  />
+                </a-form-item>
+              </template>
+
+              <template v-if="model.type === 'custom'">
+                <a-form-item label="模型名称">
+                  <a-input
+                    v-model:value="model.name"
+                    placeholder="请输入自定义模型名称"
+                  />
+                </a-form-item>
+                <a-form-item label="服务器地址">
+                  <a-input
+                    v-model:value="model.endpoint"
+                    placeholder="请输入API地址"
+                  />
+                </a-form-item>
+              </template>
+            </template>
+          </a-form>
+        </div>
+      </div>
+
+      <a-button 
+        type="dashed" 
+        block 
+        class="add-model-btn"
+        @click="addModel"
+      >
+        <PlusOutlined /> 添加模型
+      </a-button>
+
+      <div class="drawer-footer">
+        <a-button style="margin-right: 8px" @click="closeDrawer">取消</a-button>
+        <a-button type="primary" :loading="saving" @click="saveConfig">
+          保存配置
+        </a-button>
+      </div>
+    </a-drawer>
   </div>
 </template>
 
-<script setup>
-// 添加必要的逻辑
+<script>
+import { defineComponent, ref, reactive } from 'vue';
+import { SettingOutlined, PlusOutlined } from '@ant-design/icons-vue';
+import { message } from 'ant-design-vue';
+
+const MODEL_GROUPS = [
+  {
+    label: '主流模型',
+    options: [
+      { label: 'ChatGPT', value: 'chatgpt', endpoint: 'https://api.openai.com/v1' },
+      { label: 'Claude', value: 'claude', endpoint: 'https://api.anthropic.com' },
+      { label: 'GPT-4', value: 'gpt4', endpoint: 'https://api.openai.com/v1' }
+    ]
+  },
+  {
+    label: '开源模型',
+    options: [
+      { label: 'DeepSeek', value: 'deepseek', endpoint: 'https://api.deepseek.com/v1' },
+      { label: 'Ollama', value: 'ollama', endpoint: 'http://localhost:11434' },
+      { label: 'ChatGLM', value: 'chatglm', endpoint: 'https://api.zhipuai.cn/v1' }
+    ]
+  },
+  {
+    label: '国内模型',
+    options: [
+      { label: '阿里通义千问', value: 'qianwen', endpoint: 'https://dashscope.aliyuncs.com/api/v1' },
+      { label: '百度文心一言', value: 'wenxin', endpoint: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1' },
+      { label: '讯飞星火', value: 'spark', endpoint: 'https://spark-api.xf-yun.com/v1' }
+    ]
+  },
+  {
+    label: '其他',
+    options: [
+      { label: '自定义模型', value: 'custom', endpoint: '' }
+    ]
+  }
+];
+
+export default defineComponent({
+  name: 'ModelConfig',
+  components: {
+    SettingOutlined,
+    PlusOutlined
+  },
+  setup() {
+    const visible = ref(false);
+    const saving = ref(false);
+    const modelList = ref([
+      { type: '', key: '', endpoint: '', name: '' }
+    ]);
+
+    const modelGroups = ref(MODEL_GROUPS);
+
+    const getKeyPlaceholder = (type) => {
+      const placeholders = {
+        'chatgpt': '请输入 OpenAI API Key',
+        'gpt4': '请输入 OpenAI API Key',
+        'claude': '请输入 Anthropic API Key',
+        'deepseek': '请输入 DeepSeek API Key',
+        'qianwen': '请输入阿里云 API Key',
+        'wenxin': '请输入百度 API Key',
+        'spark': '请输入讯飞 API Key',
+        'chatglm': '请输入智谱 API Key',
+        'ollama': '可选:请输入访问密钥',
+        'custom': '请输入 API Key'
+      };
+      return placeholders[type] || '请输入 API Key';
+    };
+
+    const handleModelChange = (value, index) => {
+      const model = modelList.value[index];
+      const selectedOption = MODEL_GROUPS.flatMap(group => group.options)
+        .find(option => option.value === value);
+      
+      if (selectedOption) {
+        model.endpoint = selectedOption.endpoint;
+        model.name = selectedOption.label;
+      }
+    };
+
+    const showDrawer = () => {
+      visible.value = true;
+    };
+
+    const closeDrawer = () => {
+      visible.value = false;
+    };
+
+    const addModel = () => {
+      modelList.value.push({ type: '', key: '', endpoint: '', name: '' });
+    };
+
+    const removeModel = (index) => {
+      if (modelList.value.length === 1) {
+        message.warning('至少保留一个模型配置');
+        return;
+      }
+      modelList.value.splice(index, 1);
+    };
+
+    const validateModel = (model) => {
+      if (!model.type) return false;
+      if (!model.key) return false;
+      if (model.type === 'ollama' && !model.endpoint) return false;
+      if (model.type === 'custom' && (!model.name || !model.endpoint)) return false;
+      return true;
+    };
+
+    const saveConfig = async () => {
+      // 验证所有字段都已填写
+      const hasInvalidModel = modelList.value.some(model => !validateModel(model));
+      if (hasInvalidModel) {
+        message.error('请填写完整的模型信息');
+        return;
+      }
+
+      saving.value = true;
+      try {
+        // 这里添加保存配置的逻辑
+        localStorage.setItem('modelConfig', JSON.stringify(modelList.value));
+        message.success('配置保存成功');
+        closeDrawer();
+      } catch (error) {
+        message.error('配置保存失败');
+      } finally {
+        saving.value = false;
+      }
+    };
+
+    // 初始化时从localStorage加载配置
+    const initConfig = () => {
+      const savedConfig = localStorage.getItem('modelConfig');
+      if (savedConfig) {
+        modelList.value = JSON.parse(savedConfig);
+      }
+    };
+
+    // 组件挂载时加载配置
+    initConfig();
+
+    return {
+      visible,
+      modelList,
+      modelGroups,
+      saving,
+      showDrawer,
+      closeDrawer,
+      addModel,
+      removeModel,
+      saveConfig,
+      handleModelChange,
+      getKeyPlaceholder
+    };
+  }
+});
 </script>
 
-<style scoped>
+<style lang="less" scoped>
 .model-config {
-  padding: 16px;
+  position: fixed;
+  left: 20px;
+  bottom: 20px;
+  z-index: 1000;
+
+  .config-trigger {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 8px 16px;
+    background: #fff;
+    border-radius: 20px;
+    cursor: pointer;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    transition: all 0.3s;
+
+    &:hover {
+      background: #f5f5f5;
+      transform: translateY(-2px);
+    }
+
+    .trigger-icon {
+      font-size: 16px;
+      color: #1677ff;
+    }
+
+    span {
+      font-size: 14px;
+      color: #1677ff;
+    }
+  }
+}
+
+.model-drawer {
+  .model-list {
+    .model-item {
+      margin-bottom: 24px;
+      padding: 16px;
+      background: #fafafa;
+      border-radius: 8px;
+
+      .model-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 16px;
+
+        .model-title {
+          font-weight: 500;
+          color: #1677ff;
+        }
+      }
+    }
+  }
+
+  .add-model-btn {
+    margin-bottom: 24px;
+  }
+
+  .drawer-footer {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+    border-top: 1px solid #f0f0f0;
+    padding: 16px 24px;
+    text-align: right;
+    left: 0;
+    background: #fff;
+  }
+}
+
+:deep(.ant-drawer-body) {
+  padding: 24px;
+  padding-bottom: 80px;
 }
 </style> 

+ 299 - 6
src/components/SearchResults/index.vue

@@ -1,16 +1,309 @@
 <template>
   <div class="search-results">
-    <h3>搜索结果</h3>
-    <!-- 在这里添加搜索结果的具体内容 -->
+    <div class="results-header">
+      <h2>Related Search Results</h2>
+      <div class="result-stats">
+        Found {{ total }} results
+      </div>
+    </div>
+
+    <div class="results-container" ref="resultsContainer" @scroll="handleScroll">
+      <!-- 搜索结果列表 -->
+      <div v-if="displayResults.length > 0" class="results-list">
+        <div v-for="(item, index) in displayResults" :key="index" class="result-item">
+          <h3 class="result-title">
+            <a :href="item.url" target="_blank" rel="noopener noreferrer">{{ item.title }}</a>
+          </h3>
+          <p class="result-url">{{ item.url }}</p>
+          <p class="result-snippet">{{ item.snippet }}</p>
+          <div class="result-meta">
+            <span class="result-date">{{ formatDate(item.date) }}</span>
+            <span class="result-source">{{ item.source }}</span>
+          </div>
+        </div>
+
+        <!-- 加载更多指示器 -->
+        <div v-if="hasMore" class="loading-more" ref="loadingTrigger">
+          <div v-if="loading" class="loading-spinner"></div>
+          <span v-else>Loading more results...</span>
+        </div>
+      </div>
+
+      <!-- 无结果显示 -->
+      <div v-else class="no-results">
+        <FileSearchOutlined class="no-results-icon" />
+        <p>No related results found</p>
+      </div>
+    </div>
   </div>
 </template>
 
-<script setup>
-// 添加必要的逻辑
+<script>
+import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
+import { FileSearchOutlined } from '@ant-design/icons-vue';
+import dayjs from 'dayjs';
+
+export default defineComponent({
+  name: 'SearchResults',
+  components: {
+    FileSearchOutlined
+  },
+  setup() {
+    const allResults = ref([
+      {
+        title: 'Ant Design - The world\'s second most popular React UI framework',
+        url: 'https://ant.design',
+        snippet: 'Ant Design is a comprehensive design system for enterprise-level products. Create efficient and enjoyable work experiences with high-quality components and best practices.',
+        date: '2024-03-20',
+        source: 'Official Documentation'
+      },
+      {
+        title: 'Getting Started with Ant Design Vue',
+        url: 'https://antdv.com/docs/vue/introduce',
+        snippet: 'Ant Design Vue is an enterprise-class UI components based on Ant Design and Vue 3. Provides high-quality Vue components out of the box with excellent development experience.',
+        date: '2024-03-19',
+        source: 'Ant Design Vue'
+      },
+      {
+        title: 'Ant Design System Principles and Guidelines',
+        url: 'https://ant.design/docs/spec/introduce',
+        snippet: 'Learn about the design principles behind Ant Design. Discover how to create consistent and beautiful user interfaces following our comprehensive guidelines.',
+        date: '2024-03-18',
+        source: 'Design Guidelines'
+      },
+      {
+        title: 'Understanding Component Architecture in Ant Design',
+        url: 'https://medium.com/antdesign/component-architecture',
+        snippet: 'Deep dive into how Ant Design components are structured and how they work together to create a cohesive design system. Learn best practices for component implementation.',
+        date: '2024-03-17',
+        source: 'Medium Article'
+      },
+      {
+        title: 'Customizing Ant Design Themes',
+        url: 'https://ant.design/docs/react/customize-theme',
+        snippet: 'Learn how to customize Ant Design themes to match your brand identity. Complete guide to variable customization, component styling, and creating consistent design tokens.',
+        date: '2024-03-16',
+        source: 'Technical Guide'
+      }
+    ]);
+    
+    const displayResults = ref([]);
+    const loading = ref(false);
+    const currentPage = ref(1);
+    const pageSize = ref(10);
+    const total = ref(28);
+    const hasMore = ref(true);
+    const loadingTrigger = ref(null);
+    const resultsContainer = ref(null);
+
+    // 模拟获取更多结果
+    const fetchMoreResults = async () => {
+      if (loading.value || !hasMore.value) return;
+      
+      loading.value = true;
+      try {
+        // 模拟API延迟
+        await new Promise(resolve => setTimeout(resolve, 800));
+        
+        const startIndex = (currentPage.value - 1) * pageSize.value;
+        const endIndex = startIndex + pageSize.value;
+        const newResults = allResults.value.slice(startIndex, endIndex);
+        
+        // 添加新结果到显示列表
+        displayResults.value = [...displayResults.value, ...newResults];
+        
+        // 更新页码和是否还有更多
+        currentPage.value += 1;
+        hasMore.value = displayResults.value.length < total.value;
+      } catch (error) {
+        console.error('Failed to fetch more results:', error);
+      } finally {
+        loading.value = false;
+      }
+    };
+
+    // 使用 Intersection Observer 监听滚动
+    let observer = null;
+    
+    const setupIntersectionObserver = () => {
+      observer = new IntersectionObserver(
+        (entries) => {
+          const target = entries[0];
+          if (target.isIntersecting && !loading.value) {
+            fetchMoreResults();
+          }
+        },
+        {
+          root: resultsContainer.value,
+          threshold: 0.1
+        }
+      );
+
+      if (loadingTrigger.value) {
+        observer.observe(loadingTrigger.value);
+      }
+    };
+
+    onMounted(() => {
+      // 初始加载第一页
+      fetchMoreResults();
+      // 设置观察者
+      setupIntersectionObserver();
+    });
+
+    onUnmounted(() => {
+      if (observer) {
+        observer.disconnect();
+      }
+    });
+
+    // 格式化日期
+    const formatDate = (date) => {
+      return dayjs(date).format('YYYY-MM-DD');
+    };
+
+    return {
+      displayResults,
+      loading,
+      total,
+      hasMore,
+      formatDate,
+      loadingTrigger,
+      resultsContainer
+    };
+  }
+});
 </script>
 
-<style scoped>
+<style lang="less" scoped>
 .search-results {
-  padding: 16px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  width: 100%;
+  overflow: hidden;
+
+  .results-header {
+    padding: 16px 24px;
+    border-bottom: 1px solid #f0f0f0;
+    
+    h2 {
+      margin: 0;
+      font-size: 18px;
+      color: rgba(0, 0, 0, 0.85);
+    }
+
+    .result-stats {
+      margin-top: 4px;
+      font-size: 14px;
+      color: rgba(0, 0, 0, 0.45);
+    }
+  }
+
+  .results-container {
+    flex: 1;
+    overflow-y: auto;
+    padding: 0 24px;
+    scroll-behavior: smooth;
+  }
+
+  .results-list {
+    .result-item {
+      padding: 20px 0;
+      border-bottom: 1px solid #f0f0f0;
+
+      &:last-child {
+        border-bottom: none;
+      }
+
+      .result-title {
+        margin: 0 0 8px;
+        font-size: 16px;
+        
+        a {
+          color: #1677ff;
+          text-decoration: none;
+
+          &:hover {
+            text-decoration: underline;
+          }
+        }
+      }
+
+      .result-url {
+        color: #0ca678;
+        font-size: 13px;
+        margin-bottom: 8px;
+      }
+
+      .result-snippet {
+        color: rgba(0, 0, 0, 0.65);
+        font-size: 14px;
+        margin-bottom: 8px;
+        line-height: 1.5;
+      }
+
+      .result-meta {
+        font-size: 12px;
+        color: rgba(0, 0, 0, 0.45);
+
+        .result-date {
+          margin-right: 16px;
+        }
+      }
+    }
+  }
+
+  .no-results {
+    text-align: center;
+    padding: 48px 0;
+    color: rgba(0, 0, 0, 0.45);
+
+    .no-results-icon {
+      font-size: 48px;
+      margin-bottom: 16px;
+    }
+
+    p {
+      font-size: 14px;
+    }
+  }
+
+  .loading-more {
+    text-align: center;
+    padding: 20px 0;
+    color: rgba(0, 0, 0, 0.45);
+    font-size: 14px;
+  }
+
+  .loading-spinner {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    border: 2px solid #f3f3f3;
+    border-top: 2px solid #1677ff;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+  }
+
+  @keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+  }
+
+  // 添加滚动条样式
+  .results-container::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  .results-container::-webkit-scrollbar-thumb {
+    background-color: rgba(0, 0, 0, 0.2);
+    border-radius: 3px;
+  }
+
+  .results-container::-webkit-scrollbar-track {
+    background-color: transparent;
+  }
 }
 </style> 

+ 5 - 4
src/views/report.vue

@@ -12,12 +12,8 @@
         <span>Ant Design X</span>
       </div>
 
-      <!-- 添加模型配置组件 -->
-      <ModelConfig />
-
       <!-- New Conversation 按钮 -->
       <button class="new-conversation-btn" @click="addNewConversation">
-        <!-- <PlusOutlined /> -->
         <img class="tab-icon" src="../assets/svg/plus.svg" alt="" />
         <span>New Conversation</span>
       </button>
@@ -36,6 +32,9 @@
           {{ conversation.title }}
         </div>
       </div>
+
+      <!-- 添加模型配置组件到底部 -->
+      <ModelConfig />
     </div>
 
     <div class="chat">
@@ -994,6 +993,8 @@ const renderMarkdown = (content) => {
   display: flex;
   flex-direction: column;
   border-right: 1px solid rgba(0, 0, 0, 0.06);
+  position: relative;
+  z-index: 1;
 }
 
 .conversations {