一切源于一个学习黑马程序员视频的突发奇想
在网页悬浮一个搜索按钮,点击可以实现调用deepseek文本模型回答你的问题
前端实现
前端使用vue实现的
首先是整体页面:AIWidget.vue
<template>
<div>
<!-- 悬浮 AI 按钮 -->
<el-button class="floating-button" @click="dialogVisible = true">
<el-icon><Search /></el-icon>
</el-button>
<!-- AI 搜索框 -->
<el-dialog v-model="dialogVisible" title="AI 搜索" width="400px">
<el-input
v-model="query"
placeholder="请输入搜索内容..."
@keyup.enter="handleSearch"
/>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-divider />
<el-scrollbar height="200px">
<ul v-if="results.length">
<li v-for="(item, index) in results" :key="index">{{ item }}</li>
</ul>
<p v-else class="no-result">暂无搜索结果</p>
</el-scrollbar>
</el-dialog>
</div>
</template>
<script>
import { ref, watch } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { aiSearch } from "@/api/aiSearch";
import { debounce } from "lodash-es";
export default {
components: { Search },
setup() {
const dialogVisible = ref(false);
const query = ref("");
const results = ref([]);
const loading = ref(false);
const handleSearch = debounce(async () => {
const cleanQuery = query.value.trim().replace(/<[^>]*>?/gm, "");
if (!cleanQuery) return;
loading.value = true;
try {
const response = await aiSearch(cleanQuery);
results.value = response.data || [];
} catch (error) {
ElMessage.error("搜索失败:" + error.message);
results.value = [];
} finally {
loading.value = false;
}
}, 500);
watch(dialogVisible, (val) => {
if (!val) {
query.value = "";
results.value = [];
}
});
return { dialogVisible, query, results, loading, handleSearch };
},
};
</script>
<style scoped>
.floating-button {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
font-size: 20px;
border-radius: 50%;
background-color: #409eff;
color: white;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: background-color 0.3s;
}
.floating-button:hover {
background-color: #66b1ff;
}
.search-btn {
margin-top: 10px;
width: 100%;
}
.no-result {
text-align: center;
color: gray;
}
</style>
直接放在src/commpoents下
接下来是JS文件,直接放在 src/api/ 下
aiSearch.js
import request from '@/utils/request';
// AI 搜索 API 请求
export function aiSearch(query) {
return request({
url: '/ai-search',
method: 'get',
params: { q: query }
});
}
为了在每个页面的右下角都显示这个搜索框,我们直接导入组件到App.vue
App.vue
<template>
<div>
<router-view />
<AIWidget /> <!-- 悬浮 AI 按钮 -->
</div>
</template>
<script setup>
import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme'
import AIWidget from "@/components/AIWidget.vue"; // 导入 AI 搜索组件
onMounted(() => {
nextTick(() => {
// 初始化主题样式
handleThemeStyle(useSettingsStore().theme)
})
})
</script>
后端实现
首先在 application.yml 添加配置:
siliconflow:
api:
url: https://api.siliconflow.cn/v1/chat/completions
token: your_token_here # 替换为实际token
model: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
创建请求/响应 DTO 对象:
AiSearchReq.java
package com.dkd.manage.controller.AI;
import com.dkd.common.core.domain.R;
import com.dkd.manage.service.AI.IAiSearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/ai-search")
public class AiSearchController {
@Autowired
private IAiSearchService aiSearchService;
@GetMapping("")
public R<List<String>> search(@RequestParam String q) {
return R.ok(aiSearchService.searchAI(q));
}
}
ChatCompletionReq.java
package com.dkd.manage.domain.dto.AI;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ChatCompletionReq {
private String model;
private List<Message> messages;
private boolean stream = false;
private int max_tokens = 512;
private double temperature = 0.7;
private double top_p = 0.7;
private int top_k = 50;
private double frequency_penalty = 0.5;
private int n = 1;
private ResponseFormat response_format = new ResponseFormat("text");
//private List<Tool> tools = new ArrayList<>();
@Data
@AllArgsConstructor
public static class Message {
private String role;
private String content;
}
@Data
@AllArgsConstructor
public static class ResponseFormat {
private String type;
}
@Data
public static class Tool {
private String type = "function";
private ToolFunction function = new ToolFunction();
}
@Data
public static class ToolFunction {
private String description = "";
private String name = "";
private Object parameters = new Object();
private boolean strict = false;
}
}
ChatCompletionResp.java
package com.dkd.manage.domain.dto.AI;
import lombok.Data;
import java.util.List;
@Data
public class ChatCompletionResp {
private List<Choice> choices;
@Data
public static class Choice {
private Message message;
}
@Data
public static class Message {
private String content;
}
}
Controller 层:
AiSearchController.java
package com.dkd.manage.controller.AI;
import com.dkd.common.core.domain.R;
import com.dkd.manage.service.AI.IAiSearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/ai-search")
public class AiSearchController {
@Autowired
private IAiSearchService aiSearchService;
@GetMapping("")
public R<List<String>> search(@RequestParam String q) {
return R.ok(aiSearchService.searchAI(q));
}
}
Service 接口:
IAiSearchService.java
package com.dkd.manage.service.AI;
import java.util.List;
public interface IAiSearchService {
List<String> searchAI(String query);
}
Service 实现:
AiSearchServiceImpl.java
package com.dkd.manage.service.impl;
import com.dkd.common.exception.ServiceException;
import com.dkd.manage.domain.dto.AI.ChatCompletionReq;
import com.dkd.manage.domain.dto.AI.ChatCompletionResp;
import com.dkd.manage.service.AI.IAiSearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Service
public class IAiSearchServiceImpl implements IAiSearchService {
@Value("${siliconflow.api.url}")
private String apiUrl;
@Value("${siliconflow.api.token}")
private String apiToken;
@Value("${siliconflow.api.model}")
private String model;
@Autowired
private RestTemplate restTemplate;
@Override
public List<String> searchAI(String query) {
// 1. 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(apiToken);
// 2. 构建请求体
ChatCompletionReq request = new ChatCompletionReq();
request.setModel(model);
request.setMessages(Collections.singletonList(
new ChatCompletionReq.Message("user", query)
));
// 3. 发送请求
HttpEntity<ChatCompletionReq> entity = new HttpEntity<>(request, headers);
ResponseEntity<ChatCompletionResp> response = restTemplate.exchange(
apiUrl,
HttpMethod.POST,
entity,
ChatCompletionResp.class
);
// 4. 处理响应
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
String content = response.getBody().getChoices().get(0).getMessage().getContent();
return Arrays.asList(content.split("\\n"));
}
throw new ServiceException("AI 服务调用失败");
}
}
配置 RestTemplate(如果尚未配置):
RestTemplateConfig.java
package com.dkd.framework.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}