📄 先整体看一下这个文件长啥样(结构)
<script setup>
// 这里写逻辑(大脑)
</script>
<template>
<!-- 这里写界面(脸) -->
</template>
<style scoped>
/* 这里写样式(衣服) */
</style>
就像一个人:有脑子、有脸、有衣服。我们一个一个来!
1️⃣ <script setup> —— “大脑”部分
💡
<script setup>是 Vue 3 的一种写法,意思是:“我这个组件的逻辑从这里开始”。
🔹 第一行:引入工具
import { ref } from 'vue'
- 白话:我要用 Vue 提供的一个叫
ref的小工具。 - 作用:用来创建“会自动通知页面更新的小本本”。后面你会看到。
🔹 声明三个“小本本”(响应式变量)
// 用户输入的问题,默认已经写好一句话
const question = ref('讲一个喜洋洋和灰太狼的故事,20字')
// 控制是否“慢慢显示答案”,默认打开(true = 打开)
const stream = ref(true)
// AI 回答的内容,一开始是空的
const content = ref("")
-
ref()是什么?
想象你有一张便利贴:- 普通变量:你写了字,别人看不见你改了没。
ref变量:你一改字,旁边的人(比如网页)立刻知道:“哇!他改了!快更新画面!”
-
所以这三个变量,一变,页面就会自动刷新对应内容。
🔹 核心功能:askLLM() —— “去问AI”
const askLLM = async () => {
- 白话:定义一个叫
askLLM的动作,意思是“去问大聪明(AI)”。 async表示:这个动作要花时间(比如打电话、等回信),别卡住其他事。
✅ 步骤1:检查问题是不是空的
if (!question.value) {
console.log('question 不能为空');
return
}
-
解释:
question.value:因为question是ref,所以要用.value才能拿到真实内容。- 如果你啥也没写就点“提交”,它会说“问题不能为空”,然后不干了。
✅ 步骤2:先显示“思考中...”
content.value = '思考中...';
- 用户体验:让你知道它没卡死,正在努力干活!
✅ 步骤3:准备打电话给 AI
const endpoint = 'https://api.deepseek.com/chat/completions';
const headers = {
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`,
'Content-Type': 'application/json'
}
-
endpoint:AI 的电话号码(API 地址)。 -
headers:打电话时要说的“暗号”:-
Authorization:你的身份证明(API 密钥),藏在环境变量里。-
VITE_DEEPSEEK_API_KEY:这是你在项目根目录.env文件里写的密钥,比如:VITE_DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx -
加
VITE_前缀,是 Vite 框架的安全规则——只有这样前端才能读到。
-
-
Content-Type:告诉对方:“我说的是 JSON 格式的话”。
-
✅ 步骤4:拨打电话(发送请求)
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
model: 'deepseek-chat',
stream: stream.value,
messages: [
{ role: 'user', content: question.value }
]
})
})
-
白话翻译:
“喂,DeepSeek 吗?我是小盒子。
我想用deepseek-chat这个模型,
用户问的是:‘xxx’,
要不要流式回答?(看stream开关)” -
fetch:浏览器自带的“打电话”函数。 -
await:等电话打通、对方回完再继续。
🔹 步骤5:处理 AI 的回答(分两种情况)
🟢 情况A:流式回答(Streaming 打勾 ✅)
if (stream.value) {
// 先清空之前的答案
content.value = "";
// 准备“收音机”:用来一段一段听 AI 说话
const reader = response.body?.getReader(); // 获取数据流读取器
const decoder = new TextDecoder(); // 把二进制转成文字
let done = false; // 标记是否说完
let buffer = ''; // 临时拼图板(防断包)
⚠️ 网络传输可能把一句话拆成几段,比如:
- 第一段收到:
data: {"delta": "灰太"- 第二段才收到:
狼追羊"}所以需要buffer先存着不完整的部分。
🔁 循环收听 AI 说话:
while(!done) {
// 读一段数据(可能是几个字,也可能是一整句)
const { value, done: doneReading } = await reader?.read()
done = doneReading;
// 把二进制转成字符串,并和上一次没拼完的接起来
const chunkValue = buffer + decoder.decode(value);
buffer = ''; // 清空临时板
// 按换行符拆开,只保留以 "data: " 开头的行(SSE 格式)
const lines = chunkValue.split('\n').filter(line => line.startsWith('data: '))
// 逐行处理
for (const line of lines) {
const incoming = line.slice(6); // 去掉开头的 "data: "
// 如果看到 [DONE],说明说完了
if (incoming === '[DONE]') {
done = true;
break;
}
try {
// 尝试把这一行变成“真正的数据”
const data = JSON.parse(incoming);
const delta = data.choices[0].delta.content; // AI 这次说了啥
if (delta) {
content.value += delta; // 把新字加到答案板上(页面自动更新!)
}
} catch(err) {
// 如果这行不完整(比如只收到一半),先存回 buffer,等下一段
buffer += `data: ${incoming}`;
}
}
}
}
✅ 关键点:
content.value += delta:每收到一个字,就贴到答案板上 → 页面实时显示!buffer防止网络断包导致乱码。
🔴 情况B:非流式(Streaming 不打勾 ❌)
else {
const data = await response.json();
console.log(data);
content.value = data.choices[0].message.content;
}
- 白话:等 AI 想好整句话,一次性拿回来,直接贴到答案板。
- 简单粗暴,适合短回答。
2️⃣ <template> —— “脸”部分(你看到的界面)
<template>
<div class="container">
<!-- 输入区域 -->
<div>
<label>输入:</label>
<input class="input" v-model="question"/>
<button @click="askLLM">提交</button>
</div>
<!-- 输出区域 -->
<div class="output">
<div>
<label>Streaming</label>
<input type="checkbox" v-model="stream" />
<div>{{content}}</div>
</div>
</div>
</div>
</template>
🔍 关键指令解释:
| 代码 | 白话解释 |
|---|---|
v-model="question" | 输入框和 question 小本本双向绑定——你打字,它自动记;它改值,输入框也变 |
@click="askLLM" | 点按钮就执行 askLLM 这个动作(去问AI) |
v-model="stream" | checkbox 和 stream 开关同步——打勾 = true,不打 = false |
{{content}} | 把 content 小本本的内容显示在这里(自动更新!) |
3️⃣ <style scoped> —— “衣服”部分(样式)
<style scoped>
* { margin: 0; padding: 0; }
.container {
display: flex;
flex-direction: column;
align-items: start;
justify-content: start;
height: 100vh;
font-size: 0.85rem;
}
.input { width: 200px; }
button {
padding: 0 10px;
margin-left: 6px;
}
.output {
margin-top: 10px;
min-height: 300px;
width: 100%;
text-align: left;
}
</style>
-
scoped:这些样式只在这个组件生效,不会影响别的页面。 -
简单布局:
- 整体垂直排列(
flex-direction: column) - 输入框宽 200 像素
- 按钮左边留点空隙
- 输出区至少高 300 像素,方便看长答案
- 整体垂直排列(
✅ 最后总结:整个流程串起来
-
你打开页面 →
question默认有字,stream打开,content为空 -
你改输入框 →
question自动更新(v-model) -
你点“提交” → 执行
askLLM() -
它检查问题 → 显示“思考中...” → 给 DeepSeek 打电话
-
根据
stream开关:- ✅ 打勾:AI 一个字一个字回,网页实时拼出来
- ❌ 不打勾:AI 想好后一次性回,网页直接显示
-
content一变 → 页面自动更新(Vue 响应式魔法)
🎯 你现在可以做到:
- 看懂每一行代码是干啥的
- 知道怎么改默认问题
- 知道怎么开关流式模式
- 知道 API 密钥放哪安全
不惧机器聪明,只怕人心懈怠~~~~