一、技术栈与前置准备

1. 核心技术栈

  • 框架:UniApp + Vue3 (Setup 语法糖)
  • AI 接口:DeepSeek API (deepseek-chat 模型)
  • 样式:SCSS
  • 特性:响应式布局、跨端适配、异步请求

2. DeepSeek API 准备

  • 注册 DeepSeek 开发者账号,获取 API Key   https://platform.deepseek.com/
  • 了解 DeepSeek Chat Completions 接口文档
  • 注意:API Key 切勿前端硬编码(本文为演示方便暂时写死,生产环境需后端代理)

二、核心代码实现

1. DeepSeek API 封装(deepseek.js)

首先封装 DeepSeek 的请求方法,统一处理接口调用逻辑:

// deepseek.js - 稳健版本
export const DEEPSEEK_API_BASE = 'https://api.deepseek.com/v1'
export const DEEPSEEK_MODEL = 'deepseek-chat'
// 生产环境注意:API Key一定要通过后端代理,不要前端写死!
export const DEEPSEEK_API_KEY = '写你的KEY'

export const deepSeekChatStream = async ({
	messages,
	model = DEEPSEEK_MODEL,
	temperature = 0.2,
	baseUrl = DEEPSEEK_API_BASE,
	apiKey = DEEPSEEK_API_KEY,
	maxTokens = 800,
	onChunk,
	onError,
	onComplete
}) => {
	return new Promise((resolve, reject) => {
		let fullContent = ''
		let completed = false
		
		const requestTask = uni.request({
			url: `${baseUrl}/chat/completions`,
			method: 'POST',
			header: {
				'Content-Type': 'application/json',
				'Authorization': `Bearer ${apiKey}`
			},
			data: {
				model,
				messages,
				temperature,
				max_tokens: maxTokens,
				stream: true,
				stream_options: {
					include_usage: true
				}
			},
			timeout: 60000,
			enableChunked: true,
			success: (res) => {
				if (!completed) {
					completed = true
					onComplete?.(fullContent)
					resolve(fullContent)
				}
			},
			fail: (err) => {
				console.error('请求失败:', err)
				onError?.(err)
				reject(err)
			}
		})
		
		requestTask.onChunkReceived((res) => {
			try {
				// 获取 ArrayBuffer 并转换为字符串
				let chunk = ''
				const data = res.data
				
				// 兼容不同平台的解码方式
				if (typeof data === 'string') {
					chunk = data
				} else if (data instanceof ArrayBuffer) {
					// 使用 String.fromCharCode 直接转换
					const bytes = new Uint8Array(data)
					let str = ''
					for (let i = 0; i < bytes.length; i++) {
						str += String.fromCharCode(bytes[i])
					}
					// 处理 UTF-8 编码
					try {
						chunk = decodeURIComponent(escape(str))
					} catch (e) {
						chunk = str
					}
				}
				
				// 按行解析 SSE 数据
				const lines = chunk.split('\n')
				for (const line of lines) {
					const trimmedLine = line.trim()
					if (!trimmedLine || !trimmedLine.startsWith('data:')) continue
					
					const jsonStr = trimmedLine.substring(5).trim()
					if (jsonStr === '[DONE]') {
						completed = true
						onComplete?.(fullContent)
						resolve(fullContent)
						continue
					}
					
					try {
						const jsonData = JSON.parse(jsonStr)
						const content = jsonData.choices?.[0]?.delta?.content
						if (content) {
							fullContent += content
							onChunk?.(content, fullContent)
						}
					} catch (parseError) {
						console.warn('JSON 解析失败:', jsonStr)
					}
				}
			} catch (error) {
				console.error('处理数据块失败:', error)
				if (!completed) {
					onError?.(error)
				}
			}
		})
		
		return requestTask
	})
}

2. 聊天页面完整实现

<template>
	<view class="ai-consult-page">
		<!-- 聊天内容区域 -->
		<scroll-view
			class="chat-scroll"
			scroll-y
			:show-scrollbar="false"
			:scroll-with-animation="false"
			:scroll-into-view="scrollIntoView"
			@scroll="handleScroll"
		>
			<view class="chat-container">
				<!-- 时间显示 -->
				<view class="time-badge">{{ currentTimeText }}</view>
				<view class="expire-badge" v-if="expireSeconds > 0">剩余问诊时间:{{ expireText }}</view>

				<!-- 消息列表 -->
				<view v-for="(msg, index) in messageList" :key="msg.id" :id="'msg-' + index" class="message-item" :class="[msg.type === 'user' ? 'user-message' : 'ai-message']">
					<!-- AI头像 -->
					<view v-if="msg.type === 'ai'" class="avatar-wrapper">
						<image class="avatar" src="/static/images/yisheng.png" mode="aspectFill"></image>
					</view>

					<!-- 消息内容 -->
					<view class="message-content">
						<!-- 文字消息 -->
						<view v-if="!msg.image" class="message-bubble">
							<text>{{ msg.content }}</text>
						</view>
						<!-- 图片消息 -->
						<view v-if="msg.image" class="message-image" @tap="previewImage(msg.image)">
							<image :src="msg.image" mode="aspectFill" class="chat-image"></image>
						</view>
					</view>

					<!-- 用户头像 -->
					<view v-if="msg.type === 'user'" class="avatar-wrapper">
						<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
					</view>
				</view>

				<!-- AI正在输入提示 -->
				<view v-if="aiTyping" class="message-item ai-message">
					<view class="avatar-wrapper">
						<image class="avatar" src="/static/images/yisheng.png" mode="aspectFill"></image>
					</view>
					<view class="message-content">
						<view class="message-bubble typing-bubble">
							<view class="typing-indicator">
								<view class="dot"></view>
								<view class="dot"></view>
								<view class="dot"></view>
							</view>
						</view>
					</view>
				</view>

				<!-- 底部占位 -->
				<view class="bottom-placeholder"></view>
			</view>
		</scroll-view>

		<!-- 底部输入区域 -->
		<view class="input-section" :class="{ 'safe-bottom': safeBottom }">
			<view class="input-wrapper">
				<!-- 文本输入框 -->
				<view class="text-input-wrapper">
					<input
						class="text-input"
						type="text"
						v-model="inputMessage"
						placeholder="请输入您的问题..."
						placeholder-class="input-placeholder"
						:confirm-type="'send'"
						@confirm="sendMessage"
						@focus="handleInputFocus"
					/>
				</view>
				<!-- 表情按钮 -->
				<view class="action-btn" @tap="showEmoji">
					<image class="action-icon" src="/static/images/xiaolian.png" mode="aspectFit"></image>
				</view>

				<!-- 图片按钮 -->
				<view class="action-btn" @tap="chooseImage">
					<image class="action-icon" src="/static/images/tupian.png" mode="aspectFit"></image>
				</view>

				<!-- 发送按钮 -->
				<view v-if="inputMessage.trim()" class="send-btn" @tap="sendMessage">
					<image class="send-icon" src="/static/images/send.png" mode="aspectFit"></image>
				</view>
			</view>
		</view>

		<!-- 表情面板 -->
		<view class="emoji-panel" v-if="showEmojiPanel">
			<view class="emoji-list">
				<view class="emoji-item" v-for="(emoji, index) in emojiList" :key="index" @tap="selectEmoji(emoji)">
					<text>{{ emoji }}</text>
				</view>
			</view>
			<view class="emoji-close" @tap="showEmojiPanel = false">完成</view>
		</view>
	</view>
</template>

<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app'
import doctApi from '@/https/api/doct/index.js'
import { deepSeekChatStream  } from '@/https/api/ai/deepseek.js'

const currentTimeText = ref('')
const expireSeconds = ref(0)
const expireText = ref('00:00')
let currentTimeTimer = null
let expireTimer = null

const formatTime = (date = new Date()) => {
	const h = String(date.getHours()).padStart(2, '0')
	const m = String(date.getMinutes()).padStart(2, '0')
	return `${h}:${m}`
}

const formatExpire = (seconds = 0) => {
	const safe = Math.max(0, Number(seconds) || 0)
	const m = Math.floor(safe / 60)
	const s = safe % 60
	return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}

const startCurrentTimeTick = () => {
	currentTimeText.value = formatTime()
	if (currentTimeTimer) clearInterval(currentTimeTimer)
	currentTimeTimer = setInterval(() => {
		currentTimeText.value = formatTime()
	}, 1000)
}

const startExpireTick = (seconds = 0) => {
	expireSeconds.value = Math.max(0, Number(seconds) || 0)
	expireText.value = formatExpire(expireSeconds.value)
	if (expireTimer) clearInterval(expireTimer)
	if (expireSeconds.value <= 0) return

	expireTimer = setInterval(() => {
		if (expireSeconds.value > 0) {
			expireSeconds.value -= 1
			expireText.value = formatExpire(expireSeconds.value)
		}
		if (expireSeconds.value <= 0) {
			clearInterval(expireTimer)
			expireTimer = null
			uni.showToast({
				title: '问诊时效已结束',
				icon: 'none'
			})
		}
	}, 1000)
}
const userAvatar=ref()
// 问诊订单检测
onLoad(async () => {
	// startCurrentTimeTick()
	// try {
	// 	uni.showLoading({
	// 		title: '校验问诊资格...'
	// 	})
	// 	const res = await doctApi.checkAskOrder()
	// 	uni.hideLoading()

	// 	const hasValidOrder = !!res?.data?.has_ask_order
	// 	const seconds = Number(res?.data?.expire_seconds || 0)
	// 	startExpireTick(seconds)

	// 	if (!hasValidOrder) {
	// 		uni.showModal({
	// 			title: '提示',
	// 			content: '当前无有效问诊订单,请先创建问诊订单',
	// 			showCancel: false,
	// 			success: () => {
	// 				uni.navigateBack()
	// 			}
	// 		})
	// 	}
	// } catch (error) {
	// 	uni.hideLoading()
	// 	console.error('检测问诊订单失败:', error)
	// 	uni.showToast({
	// 		title: '检测失败,请稍后重试',
	// 		icon: 'none'
	// 	})
	// 	setTimeout(() => {
	// 		uni.navigateBack()
	// 	}, 1000)
	// }
	userAvatar.value=uni.getStorageSync('userInfo').avatar
	console.log(userAvatar.value)
})

// 状态栏高度
const statusBarHeight = ref(0);
// 滚动到指定元素
const scrollIntoView = ref('');
// 是否开启底部安全区
const safeBottom = ref(true);

let msgSeq = 0
const makeMsg = (payload) => ({
	id: `${Date.now()}-${msgSeq++}`,
	...payload
})

// 消息列表
const messageList = ref([
	{
		id: `${Date.now()}-${msgSeq++}`,
		type: 'ai',
		content: '您好,请您描述下您的问题。'
	}
]);

// 输入框内容
const inputMessage = ref('');
// AI正在输入
const aiTyping = ref(false);
// 显示表情面板
const showEmojiPanel = ref(false);
const sending = ref(false)

// 表情列表
const emojiList = ref([
	'😊',
	'😂',
	'😍',
	'🥰',
	'😘',
	'😗',
	'😙',
	'😚',
	'😋',
	'😛',
	'😝',
	'😜',
	'🤪',
	'🤨',
	'🧐',
	'🤓',
	'😎',
	'🥳',
	'🤩',
	'😏',
	'😒',
	'😞',
	'😔',
	'😟',
	'😕',
	'🙁',
	'☹️',
	'😣',
	'😖',
	'😫',
	'😩',
	'🥺',
	'😢',
	'😭',
	'😤',
	'😠',
	'😡',
	'🤬',
	'🤯',
	'😳',
	'🥵',
	'🥶',
	'😱',
	'😨',
	'😰',
	'😥',
	'😓',
	'🤗',
	'🤔',
	'🤭',
	'🤫',
	'🤥',
	'😶',
	'😐',
	'😑',
	'😬',
	'🙄',
	'😯',
	'😦',
	'😧',
	'😮',
	'😲',
	'🥱',
	'😴',
	'🤤',
	'😪',
	'😵',
	'🤐',
	'🥴',
	'🤢',
	'🤮',
	'🤧',
	'😷',
	'🤒',
	'🤕'
]);

// 获取状态栏高度
onMounted(() => {
	const systemInfo = uni.getSystemInfoSync();
	statusBarHeight.value = systemInfo.statusBarHeight || 40;

	// 滚动到底部
	scrollToBottom();
});

onUnmounted(() => {
	if (currentTimeTimer) {
		clearInterval(currentTimeTimer)
		currentTimeTimer = null
	}
	if (expireTimer) {
		clearInterval(expireTimer)
		expireTimer = null
	}
})

// 滚动到底部
const scrollToBottom = () => {
	nextTick(() => {
		const lastIndex = messageList.value.length - 1;
		if (lastIndex >= 0) {
			scrollIntoView.value = `msg-${lastIndex}`;
		}
	});
};

// 处理滚动(保留回调位,避免后续需要时再加)
const handleScroll = (_e) => {};

// 返回上一页
const goBack = () => {
	uni.navigateBack();
};

// 显示更多菜单
const showMoreMenu = () => {
	uni.showActionSheet({
		itemList: ['清空对话', '问题反馈'],
		success: (res) => {
			if (res.tapIndex === 0) {
				clearChat();
			} else if (res.tapIndex === 1) {
				feedback();
			}
		}
	});
};

// 清空对话
const clearChat = () => {
	uni.showModal({
		title: '提示',
		content: '确定清空所有对话记录吗?',
		success: (res) => {
			if (res.confirm) {
				messageList.value = [makeMsg({ type: 'ai', content: '您好,我是AI助手,有什么可以帮您的吗?' })];
			}
		}
	});
};

// 问题反馈
const feedback = () => {
	uni.navigateTo({
		url: '/pages/feedback/feedback'
	});
};

// 发送消息
const sendMessage = async () => {
	const content = inputMessage.value.trim();
	if (!content) return;
	if (sending.value) return;

	// 添加用户消息
	messageList.value.push(makeMsg({ type: 'user', content }));
	inputMessage.value = '';

	// 滚动到底部
	scrollToBottom();

	// 显示AI正在输入
	aiTyping.value = true;
	scrollToBottom();

	await sendAiReply()
};

// 修改 buildDeepSeekMessages 方法
const buildDeepSeekMessages = () => {
  const systemPrompt = '你是一名专业的畜牧兽医AI问诊助手。请用简洁的语言回答,先追问关键症状(如日龄、精神、食欲、体温、粪便等),再给出可能原因和建议。避免长篇大论。'
  
  // 只保留最近8轮对话,减少token消耗
  const MAX_HISTORY = 8
  
  const history = messageList.value
    .filter((m) => !!m?.content && (m.type === 'user' || m.type === 'ai'))
    .slice(-MAX_HISTORY) // 限制历史长度
    .map((m) => ({
      role: m.type === 'user' ? 'user' : 'assistant',
      content: String(m.content || '').substring(0, 500) // 限制单条消息长度
    }))

  // 如果历史消息太多,保留最近的,但确保至少有一轮对话
  if (history.length > MAX_HISTORY) {
    history.splice(0, history.length - MAX_HISTORY)
  }

  return [{ role: 'system', content: systemPrompt }, ...history]
}
const sendAiReply = async () => {
  sending.value = true
  
  // 先显示"正在输入"状态
  aiTyping.value = true
  scrollToBottom()
  
  let currentAiMsgId = null
  let fullContent = ''
  let hasReceivedContent = false
  
  try {
    await deepSeekChatStream({
      messages: buildDeepSeekMessages(),
      temperature: 0.2,
      maxTokens: 800, // 限制输出长度
      onChunk: (chunk, currentFullContent) => {
        // 首次接收到内容时,关闭输入动画并创建消息
        if (!hasReceivedContent) {
          aiTyping.value = false
          hasReceivedContent = true
          
          // 创建新的AI消息
          const newMsg = makeMsg({ 
            type: 'ai', 
            content: currentFullContent 
          })
          messageList.value.push(newMsg)
          currentAiMsgId = newMsg.id
        } else {
          // 更新现有消息内容
          const aiMsg = messageList.value.find(m => m.id === currentAiMsgId)
          if (aiMsg) {
            aiMsg.content = currentFullContent
          }
        }
        fullContent = currentFullContent
        scrollToBottom()
      },
      onError: (error) => {
        console.error('DeepSeek 流式请求失败:', error)
        aiTyping.value = false
        
        if (!hasReceivedContent) {
          // 如果没有收到任何内容,显示错误消息
          messageList.value.push(makeMsg({ 
            type: 'ai', 
            content: '网络繁忙,请稍后重试。您可以重新描述问题,我会尽力帮您解答。' 
          }))
        } else {
          // 如果已经收到部分内容,在末尾追加错误提示
          const aiMsg = messageList.value.find(m => m.id === currentAiMsgId)
          if (aiMsg) {
            aiMsg.content += '\n\n⚠️ 网络异常,回复可能不完整,请稍后重试。'
          }
        }
        scrollToBottom()
      },
      onComplete: (finalContent) => {
        console.log('流式响应完成', finalContent)
        sending.value = false
        
        // 确保最终内容完整
        if (currentAiMsgId && finalContent) {
          const aiMsg = messageList.value.find(m => m.id === currentAiMsgId)
          if (aiMsg) {
            aiMsg.content = finalContent
          }
        }
      }
    })
  } catch (error) {
    console.error('发送消息失败:', error)
    aiTyping.value = false
    messageList.value.push(makeMsg({ 
      type: 'ai', 
      content: '服务异常,请稍后再试。' 
    }))
    sending.value = false
    scrollToBottom()
  }
}

// 选择图片
const chooseImage = () => {
	uni.chooseImage({
		count: 1,
		success: (res) => {
			const tempFilePaths = res.tempFilePaths;

			// 添加图片消息
			messageList.value.push(makeMsg({ type: 'user', image: tempFilePaths[0] }));

			// 滚动到底部
			scrollToBottom();

			// AI回复
			aiTyping.value = true;
			setTimeout(() => {
				aiTyping.value = false;
				messageList.value.push(makeMsg({ type: 'ai', content: '图片已收到,正在分析中,请稍候...' }));
				scrollToBottom();
			}, 1000);
		}
	});
};

// 预览图片
const previewImage = (url) => {
	uni.previewImage({
		urls: [url]
	});
};

// 显示表情面板
const showEmoji = () => {
	showEmojiPanel.value = !showEmojiPanel.value;
};

// 选择表情
const selectEmoji = (emoji) => {
	inputMessage.value += emoji;
};

// 输入框获得焦点
const handleInputFocus = () => {
	showEmojiPanel.value = false;
};
</script>

<style lang="scss" scoped>
.ai-consult-page {
	background-color: #f5f5f5;
	min-height: 100vh;
	position: relative;
}

// 自定义导航栏
.custom-navbar {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	background-color: #ffffff;
	z-index: 100;
	padding-bottom: 20rpx;

	.nav-content {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 0 30rpx;
		height: 88rpx;

		.nav-left {
			width: 48rpx;
			height: 48rpx;

			.back-icon {
				width: 100%;
				height: 100%;
			}
		}

		.nav-title {
			font-size: 36rpx;
			font-weight: 600;
			color: #27ae60;
		}

		.nav-right {
			width: 48rpx;
			height: 48rpx;

			.more-icon {
				width: 100%;
				height: 100%;
			}
		}
	}
}

// 聊天滚动区域
.chat-scroll {
	background-color: #f5f5f5;
	padding: 0 30rpx;
	box-sizing: border-box;
	height: calc(100vh - 150rpx);
	/* 兜底隐藏滚动条(部分端不认 show-scrollbar) */
	::-webkit-scrollbar {
		width: 0;
		height: 0;
		color: transparent;
	}
}

.chat-container {
	padding: 30rpx 0;
}

// 时间标签
.time-badge {
	text-align: center;
	font-size: 24rpx;
	color: #999999;
	margin: 20rpx 0 10rpx;
}

.expire-badge {
	text-align: center;
	font-size: 26rpx;
	color: #27ae60;
	font-weight: 600;
	margin-bottom: 16rpx;
}

// 消息项
.message-item {
	display: flex;
	margin-bottom: 30rpx;

	&.user-message {
		flex-direction: row-reverse;
	}
}

// 头像
.avatar-wrapper {
	width: 80rpx;
	height: 80rpx;
	flex-shrink: 0;

	.avatar {
		width: 100%;
		height: 100%;
		border-radius: 50%;
		background-color: #e0e0e0;
	}
}

// 消息内容区域
.message-content {
	max-width: 70%;
	margin: 0 20rpx;
}

// 消息气泡
.message-bubble {
	padding: 20rpx 24rpx;
	background-color: #ffffff;
	border-radius: 20rpx;
	font-size: 28rpx;
	color: #333333;
	line-height: 1.5;
	word-break: break-word;

	&.typing-bubble {
		padding: 20rpx 30rpx;
	}
}

// 用户消息样式
.user-message .message-bubble {
	background-color: #27ae60;
	color: #ffffff;
}

// 图片消息
.message-image {
	width: 200rpx;
	height: 200rpx;
	border-radius: 12rpx;
	overflow: hidden;

	.chat-image {
		width: 100%;
		height: 100%;
	}
}

// 用户图片消息
.user-message .message-image {
	margin-left: auto;
}

// 正在输入动画
.typing-indicator {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 8rpx;

	.dot {
		width: 12rpx;
		height: 12rpx;
		background-color: #999999;
		border-radius: 50%;
		animation: typing 1.4s infinite ease-in-out;

		&:nth-child(1) {
			animation-delay: 0s;
		}

		&:nth-child(2) {
			animation-delay: 0.2s;
		}

		&:nth-child(3) {
			animation-delay: 0.4s;
		}
	}
}

@keyframes typing {
	0%,
	60%,
	100% {
		transform: translateY(0);
	}

	30% {
		transform: translateY(-10px);
	}
}

// 底部输入区域
.input-section {
	position: fixed;
	bottom: 0;
	left: 0;
	right: 0;
	background-color: #ffffff;
	padding: 20rpx 30rpx;
	border-top: 1rpx solid #f0f0f0;

	&.safe-bottom {
		padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
	}
}

.input-wrapper {
	display: flex;
	align-items: center;
	gap: 20rpx;
}

// 操作按钮
.action-btn {
	width: 48rpx;
	height: 48rpx;
	flex-shrink: 0;

	.action-icon {
		width: 100%;
		height: 100%;
	}
}

// 文本输入框
.text-input-wrapper {
	flex: 1;
	height: 72rpx;
	background-color: #f5f5f5;
	border-radius: 36rpx;
	padding: 0 30rpx;

	.text-input {
		width: 100%;
		height: 100%;
		font-size: 28rpx;
	}

	.input-placeholder {
		color: #999999;
		font-size: 28rpx;
	}
}

// 发送按钮
.send-btn {
	width: 48rpx;
	height: 48rpx;
	flex-shrink: 0;

	.send-icon {
		width: 100%;
		height: 100%;
	}
}

// 表情面板
.emoji-panel {
	position: fixed;
	bottom: 120rpx;
	left: 0;
	right: 0;
	background-color: #ffffff;
	border-top: 1rpx solid #f0f0f0;
	padding: 20rpx;
	z-index: 101;

	.emoji-list {
		display: flex;
		flex-wrap: wrap;
		gap: 16rpx;
		max-height: 400rpx;
		overflow-y: auto;
		padding: 10rpx;

		.emoji-item {
			width: 80rpx;
			height: 80rpx;
			display: flex;
			align-items: center;
			justify-content: center;
			font-size: 40rpx;
			background-color: #f5f5f5;
			border-radius: 10rpx;

			&:active {
				background-color: #e0e0e0;
			}
		}
	}

	.emoji-close {
		text-align: center;
		padding: 20rpx;
		color: #27ae60;
		font-size: 28rpx;
		border-top: 1rpx solid #f0f0f0;
		margin-top: 10rpx;
	}
}

// 底部占位
.bottom-placeholder {
	height: 20rpx;
}
</style>
 

三、核心功能解析

1. 消息交互流程

  1. 用户输入问题并发送 → 添加到消息列表
  2. 显示 AI 正在输入状态 → 调用 DeepSeek API
  3. 接收 AI 回复 → 添加到消息列表 → 滚动到底部
  4. 异常处理:网络错误 / API 报错 → 友好提示

2. 关键技术点

(1)DeepSeek 消息格式构建
const buildDeepSeekMessages = () => {
	const systemPrompt = '你是一名专业的畜牧兽医AI问诊助手...'
	const history = messageList.value
		.filter((m) => !!m?.content && (m.type === 'user' || m.type === 'ai'))
		.map((m) => ({
			role: m.type === 'user' ? 'user' : 'assistant',
			content: String(m.content || '')
		}))
	return [{ role: 'system', content: systemPrompt }, ...history]
}
  • system:系统提示词,定义 AI 角色和回复规则
  • user:用户消息
  • assistant:AI 回复
(2)倒计时功能
  • 实时显示当前时间
  • 问诊时长倒计时,到期自动提示
  • 页面销毁时清除定时器,避免内存泄漏
(3)UI 交互优化
  • AI 正在输入动画效果
  • 消息气泡区分用户 / AI 样式
  • 适配底部安全区(iPhone 刘海屏)
  • 表情面板选择与插入
  • 图片选择与预览

四、总结

本文基于 UniApp+Vue3 实现了完整的 DeepSeek AI 问诊聊天功能,核心包括:

  1. DeepSeek API 封装与调用
  2. 聊天界面 UI 实现与交互
  3. 消息管理与状态控制
  4. 异常处理与用户体验优化

该方案可快速适配各类 AI 聊天场景,只需修改系统提示词即可切换不同的 AI 角色(如客服、助手、教育等)。生产环境中需重点关注 API Key 的安全管理和性能优化,提升用户体验。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐