| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125 |
- <template>
- <div>
- <!-- 试题弹框 -->
- <transition name="fade-scale">
- <div
- v-show="questionDialogVisible"
- class="child-dialog-wrapper"
- @click.self="handleCloseQuestionDialog"
- >
- <div class="child-dialog">
- <div class="question-title">
- <span class="question-icon">?</span>
- <span v-html="currentQuestion.ccQuestContent"></span>
- </div>
- <!-- 选项区域 -->
- <div
- v-if="currentQuestion.ccQuestOption && currentQuestion.ccQuestOption.length > 0"
- class="options-container"
- >
- <div
- v-for="(option, index) in currentQuestion.ccQuestOption.split(',')"
- :key="index"
- class="question-option"
- >
- <el-radio
- v-model="selectedOption"
- :label="option"
- :value="option"
- >
- <span>{{ option }}</span>
- </el-radio>
- </div>
- </div>
- <div v-else class="no-options">
- <!-- 暂无选项 -->
- </div>
- <!-- 底部按钮 -->
- <div class="dialog-footer">
- <el-button
- class="child-button confirm"
- @click="handleSubmitAnswer"
- >确定</el-button
- >
- </div>
- <!-- 右侧小图标 -->
- <div
- v-if="currentQuestion.ccAiAnswer !== null"
- class="ai-icon-container"
- @click="handleAIClick"
- >
- <img
- src="@/assets/images/xiaozhi.png"
- alt="AI对话"
- class="ai-icon"
- />
- <span class="ai-text">小智智能助手</span>
- </div>
- </div>
- </div>
- </transition>
- <!-- AI对话弹框 -->
- <div
- v-show="showAIDialog"
- class="ai-dialog-wrapper"
- @click.self="showAIDialog = false"
- >
- <div class="ai-dialog">
- <div class="ai-dialog-header">
- <h3>
- <img :src="auto" alt="" />
- 小智智能助手
- </h3>
- <el-button @click="showAIDialog = false" class="close-btn"
- >×</el-button
- >
- </div>
- <div class="ai-dialog-content">
- <div class="ai-message-history" ref="messageContainer" @scroll="handleScroll">
- <div
- v-for="(message, index) in messageList"
- :key="index"
- :class="['message', message.type]"
- >
- <img
- v-if="message.type === 'user'"
- src="@/assets/images/user.png"
- class="avatar user"
- />
- <img v-else src="@/assets/images/xiaozhi.png" class="avatar" />
- <div
- class="message-content"
- v-if="message.type === 'user'"
- v-html="message.content"
- ></div>
- <div class="message-content" v-else>
- <MarkdownView class="left-text" :content="message.content" />
- </div>
- </div>
- </div>
- <!-- 弹框默认消息 -->
- <DefaultMessage
- class="default-messages"
- :category="'ai_develop'"
- :questTip="currentQuestion.ccAiQuestTip || ''"
- @select-message="handleSelectMessage"
- />
- <!-- 消息输入框 -->
- <el-input
- v-model="prompt"
- placeholder="输入问题..."
- class="user-input"
- @keyup.enter="handleSendByKeydown"
- >
- <!-- 语音输入 -->
- <template #prepend>
- <el-button
- @click="toggleSpeechInput"
- size="small"
- :class="{ 'recording': isRecording }"
- circle
- >
- <el-icon v-if="!isRecording"><Microphone /></el-icon>
- <el-icon v-else><Mute /></el-icon>
- <!-- 显示倒计时(仅录音时显示) -->
- <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
- </el-button>
- </template>
-
- <!-- 终止按钮和发送按钮条件渲染 -->
- <template #append>
- <!-- 终止问答按钮 -->
- <div
- v-if="conversationInProgress"
- @click="stopStream"
- class="stop-btn"
- title="终止问答"
- >
- <img :src="stopicon" alt="停止" />
- </div>
- <!-- 发送按钮 -->
- <el-button v-if="!conversationInProgress" @click="handleSendByButton" size="large" round
- >发送</el-button
- >
- </template>
- </el-input>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import {ref,onUnmounted, defineProps, defineEmits, onMounted, watch, nextTick} from 'vue'
- import { ElMessage } from 'element-plus'
- import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
- import { teacherList } from '@/api/teachers.js'
- import DefaultMessage from '@/components/DefaultMessage/index.vue'
- import MarkdownView from '@/components/MarkdownView/index.vue'
- import { saveRecord } from '@/api/personalized/index.js'
- // 语音图标导入
- import { Microphone, Mute } from '@element-plus/icons-vue'
- // 终止
- import stopicon from '@/assets/icon/stopicon.png'
- // 导入图标
- import auto from '@/assets/icon/auto_awesome.png'
- // 定义props
- const props = defineProps({
- questionDialogVisible: { type: Boolean, default: false },
- currentQuestion: { type: Object, default: () => ({}) },
- gradeId: { type: String, default: '' },
- typeId: { type: String, default: '' },
- courseId: { type: String, default: '' }
- })
- // 定义emits
- const emits = defineEmits(['closeQuestionDialog', 'submitAnswer'])
- // 内部状态
- const showAIDialog = ref(false)
- const selectedOption = ref(null)
- const messageList = ref([])
- const prompt = ref('')
- const messageContainer = ref(null)
- const aiQuestionCount = ref(0)
- const userScrolled = ref(false) //是否用户手动滚动
- const xZAiData = ref({})
- const activeConversationId = ref(null)
- const conversationInProgress = ref(false)
- const conversationInAbortController = ref()
- const receiveMessageFullText = ref('')
- const isComposing = ref(false)
- const inputTimeout = ref()
- const enableContext = ref(true)
- // tts
- import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
- const { playAudioChunk , stopPlayback } = useAudioPlayer();
- // 语音输入响应式变量
- const isRecording = ref(false) // 录音状态
- const recognition = ref(null) // 语音识别实例
- const countdown = ref(0) // 倒计时剩余秒数
- const countdownTimer = ref(null) // 倒计时定时器
- // 处理选择的默认消息
- const handleSelectMessage = message => {
- prompt.value = message
- }
- // 关闭试题弹框
- const handleCloseQuestionDialog = () => {
- stopPlayback(); // 销毁语音读取
- emits('closeQuestionDialog')
- }
- // 提交答案
- const handleSubmitAnswer = () => {
- if (props.currentQuestion.ccQuestOption && props.currentQuestion.ccQuestOption.length > 0 && !selectedOption.value) {
- ElMessage.warning('请选择一个选项')
- return
- }
- emits('submitAnswer', { selectedOption: selectedOption.value })
- selectedOption.value = null
- }
- // 处理 AI 助手点击事件
- const handleAIClick = async () => {
- // 清空输入框
- messageList.value = []
- showAIDialog.value = true
- // 创建对话
- await createAiChart()
- if (props.currentQuestion.ccQuestContent) {
- // prompt.value = props.currentQuestion.ccQuestContent
- sendMessage()
- prompt.value = ''
- // 执行发送
- await doSendMessageStream({
- conversationId: activeConversationId.value,
- content: props.currentQuestion.ccQuestContent,
- contentAnswer: props.currentQuestion.ccAiAnswer,
- })
- }
- }
- // 数字人接口
- const getXzAi = async () => {
- try {
- const grade = localStorage.getItem('selectedGrade') || ''
- // 获取AI数据
- const juniorAIRes = await teacherList({ category: grade + 'AI' })
- const aiPerson = juniorAIRes.data.list.find(
- person => person.name === '小智'
- )
- if (aiPerson) {
- xZAiData.value = {
- id: aiPerson.id,
- name: aiPerson.name,
- image: aiPerson.model2dPath,
- message: aiPerson.systemMessage,
- default: aiPerson.questTip
- }
- } else {
- console.warn('未找到名为小智的数据')
- }
- } catch (error) {
- console.error('获取年级AI数据失败:', error)
- }
- }
- //创建对话
- const createAiChart = async () => {
- // 先获取数字人接口
- await getXzAi()
- // 智能问答
- await CreateDialogue({ roleId: xZAiData.value.id })
- .then(res => {
- console.log("创建会话:", res);
- activeConversationId.value = res.data
- })
- .catch(error => {
- console.error('请求出错:', error)
- })
- }
-
- // 发送消息
- const sendMessage = async () => {
- if (prompt.value.trim()) {
- // 添加用户消息到历史记录
- messageList.value.push({
- type: 'user',
- content: prompt.value
- })
- // 增加问答次数
- aiQuestionCount.value++
- // 保存AI问答次数
- try {
- await saveRecord({
- brpNjId: props.gradeId,
- brpType: 'aiCount',
- brpProgress: aiQuestionCount.value
- })
- } catch (error) {
- console.error('保存AI问答次数失败:', error)
- }
- // // 模拟 AI 回复
- // const aiResponse = await simulateAIResponse(prompt.value)
- // messageList.value.push({
- // type: 'ai',
- // content: aiResponse
- // })
- // 清空输入框
- prompt.value = ''
- }
- }
- // =========== 【语音录入】相关 ===========
- // 初始化语音识别
- const initSpeechRecognition = () => {
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
- if (!SpeechRecognition) {
- ElMessage.warning('当前浏览器不支持语音输入功能')
- return null
- }
- const instance = new SpeechRecognition()
- instance.lang = "zh-CN"
- instance.interimResults = false
- instance.onresult = (event) => {
- if (event.results?.[0]?.[0]) {
- prompt.value += event.results[0][0].transcript
- }
- }
- // 识别器结束时清除定时器
- instance.onend = () => {
- clearInterval(countdownTimer.value)
- isRecording.value = false
- countdown.value = 0
- }
- instance.onerror = (event) => {
- console.error("语音识别错误:", event.error)
- clearInterval(countdownTimer.value) // 出错时清除定时器
- isRecording.value = false
- ElMessage.error('语音输入失败,请重试')
- countdown.value = 0
- }
- return instance
- }
- // 切换录音状态
- const toggleSpeechInput = () => {
- // 无论当前状态如何,先清除可能存在的旧定时器
- clearInterval(countdownTimer.value)
- countdownTimer.value = null
- if (isRecording.value) {
- // 手动停止时重置状态
- countdown.value = 0
- recognition.value?.stop()
- isRecording.value = false
- } else {
- // 初始化倒计时前再次清除定时器(防止快速点击)
- clearInterval(countdownTimer.value)
- countdown.value = 10 // 重置为10秒
- recognition.value = initSpeechRecognition()
- if (!recognition.value) return
- navigator.mediaDevices.getUserMedia({ audio: true })
- .then(() => {
- recognition.value.start()
- isRecording.value = true
- // 启动新的倒计时定时器
- countdownTimer.value = setInterval(() => {
- countdown.value--
- if (countdown.value <= 0) {
- clearInterval(countdownTimer.value) // 倒计时结束清除
- recognition.value.stop()
- isRecording.value = false
- countdown.value = 0
- }
- }, 1000)
- })
- .catch((err) => {
- console.error("麦克风权限获取失败:", err)
- ElMessage.warning('请允许麦克风权限以使用语音输入')
- // 出错时重置状态
- isRecording.value = false
- countdown.value = 0
- })
- }
- }
- // 模拟 AI 回复
- const simulateAIResponse = question => {
- return new Promise(resolve => {
- setTimeout(() => {
- if (props.currentQuestion.ccAiAnswer) {
- resolve(props.currentQuestion.ccAiAnswer)
- return
- }
- // 若未匹配到自定义回复,给出默认回复
- resolve(`您的问题是:${question},这是 AI 的回复示例。`)
- }, 1000)
- })
- }
- /** 处理来自 keydown 的发送消息 */
- const handleSendByKeydown = async event => {
- // 判断用户是否在输入
- if (isComposing.value) {
- return
- }
- // 进行中不允许发送
- if (conversationInProgress.value) {
- return
- }
- const content = prompt.value?.trim()
- if (event.key === 'Enter') {
- if (event.shiftKey) {
- // 插入换行
- prompt.value += '\r\n'
- event.preventDefault() // 防止默认的换行行为
- } else {
- // 发送消息
- await doSendMessage(content)
- event.preventDefault() // 防止默认的提交行为
- }
- }
- }
- /** 处理来自【发送】按钮的发送消息 */
- const handleSendByButton = () => {
- doSendMessage(prompt.value?.trim())
- }
- /** 真正执行【发送】消息操作 */
- const doSendMessage = async content => {
- // 校验
- if (content.length < 1) {
- console.error('发送失败,原因:内容为空!')
- return
- }
- if (activeConversationId.value == null) {
- console.error('还没创建对话,不能发送!')
- return
- }
- // 清空输入框
- prompt.value = ''
- // 执行发送
- await doSendMessageStream({
- conversationId: activeConversationId.value,
- content: content,
- contentAnswer: null,
- })
- }
- /** 真正执行【发送】消息操作 */
- const doSendMessageStream = async userMessage => {
- // 创建 AbortController 实例,以便中止请求
- conversationInAbortController.value = new AbortController()
- // 标记对话进行中
- conversationInProgress.value = true
- // 设置为空
- receiveMessageFullText.value = ''
- try {
- // 1.1 先添加两个假数据,等 stream 返回再替换
- messageList.value.push({
- id: -1,
- conversationId: activeConversationId.value,
- type: 'user',
- content: userMessage.content,
- createTime: new Date()
- })
- messageList.value.push({
- id: -2,
- conversationId: activeConversationId.value,
- type: 'assistant',
- content: '思考中...',
- createTime: new Date()
- })
- // 2. 发送 event stream
- let isFirstChunk = true // 是否是第一个 chunk 消息段
- console.log("userMessage", userMessage)
- await sendChatMessageStream(
- userMessage.conversationId,
- userMessage.content,
- userMessage.contentAnswer,
- conversationInAbortController.value,
- enableContext.value,
- async res => {
- const { code, data, msg } = JSON.parse(res.data)
- if (code !== 0) {
- console.log(`对话异常! ${msg}`)
- return
- }
- if (data.eventType === 'TEXT') {
- // 如果内容为空,就不处理。
- if (data.receive?.content === '') {
- return
- }
- receiveMessageFullText.value += data.receive.content
- // 首次返回需要添加一个 message 到页面,后面的都是更新
- if (isFirstChunk) {
- isFirstChunk = false
- // 弹出两个假数据
- messageList.value.pop()
- messageList.value.pop()
- // 更新返回的数据
- messageList.value.push(data.send)
- messageList.value.push(data.receive)
- } else {
- //更新最后一条消息
- if (messageList.value.length > 0) {
- const lastMessage = messageList.value[messageList.value.length - 1]
- if (lastMessage.id === data.receive.id) {
- lastMessage.content = receiveMessageFullText.value
- }
- }
- }
- } else if (data.eventType === 'AUDIO') {
- // 处理音频消息
- await playAudioChunk(data.audioData);
- }
- // 添加此行确保触发滚动
- scrollToBottom()
- },
- error => {
- console.log(`对话异常! ${error}`)
- stopStream()
- // 需要抛出异常,禁止重试
- throw error
- },
- () => {
- stopStream()
- }
- )
- } catch (error) {
- console.error('发送消息失败:', error)
- stopStream()
- }
- }
- /** 停止 stream 流式调用 */
- const stopStream = async () => {
- // 如果 stream 进行中的 message,就需要调用 controller 结束
- if (conversationInAbortController.value) {
- conversationInAbortController.value.abort()
- }
- // 销毁语音读取
- stopPlayback();
- // 设置为 false
- conversationInProgress.value = false
- }
- /** 处理 prompt 输入变化 */
- const handlePromptInput = event => {
- // 非输入法 输入设置为 true
- if (!isComposing.value) {
- // 回车 event data 是 null
- if (event.data == null) {
- return
- }
- isComposing.value = true
- }
- // 清理定时器
- if (inputTimeout.value) {
- clearTimeout(inputTimeout.value)
- }
- // 重置定时器
- inputTimeout.value = setTimeout(() => {
- isComposing.value = false
- }, 400)
- }
- const onCompositionstart = () => {
- isComposing.value = true
- }
- const onCompositionend = () => {
- setTimeout(() => {
- isComposing.value = false
- }, 200)
- }
- // 监听props变化
- watch(() => props.questionDialogVisible, (newVal) => {
- if (newVal && props.currentQuestion) {
- // 重置选项
- selectedOption.value = null
- }
- })
- // 监听showAIDialog变化,在关闭时销毁语音读取
- watch(() => showAIDialog.value, (newVal) => {
- if (!newVal) {
- stopPlayback();
- }
- })
- // 监听消息列表变化,自动滚动到底部
- watch(messageList, () => {
- scrollToBottom()
- }, { deep: true })
- //处理滚动事件,判断用户是否手动滚动
- const handleScroll = () => {
- if (messageContainer.value) {
- const { scrollTop, scrollHeight, clientHeight } = messageContainer.value
- // 当用户滚动距离底部超过50px时,认为是手动滚动
- userScrolled.value = scrollTop + clientHeight < scrollHeight - 50
- }
- }
- // 单独的滚动到底部函数
- const scrollToBottom = () => {
- // 如果用户手动滚动过,不自动滚动
- if (userScrolled.value) return
- nextTick(() => {
- if (messageContainer.value) {
- // 强制重排以确保获取最新高度
- messageContainer.value.scrollTop = messageContainer.value.scrollHeight
- // 双重保险:使用requestAnimationFrame确保在浏览器重绘后执行
- requestAnimationFrame(() => {
- messageContainer.value.scrollTop = messageContainer.value.scrollHeight
- })
- }
- })
- }
- onMounted(() => {
- // 初始化
- })
- // 组件卸载时清理语音资源
- onUnmounted(() => {
- stopPlayback();
- });
- </script>
- <style scoped lang="scss">
- @use 'sass:math';
- @use 'sass:color'; // 引入 color 模块
- // 定义rpx转换函数
- @function rpx($px) {
- @return math.div($px, 750) * 100vw;
- }
- // 定义儿童风格的蓝紫色调
- $primary-color: rgba(106, 90, 205, 0.52); // 主色调:蓝紫色
- $secondary-color: rgba(147, 112, 219, 0.66); // 辅助色:亮蓝紫色
- $accent-color: rgb(133, 89, 220); // 强调色:暗蓝紫色
- $light-color: #ffffff; // 浅色背景:淡紫色
- $text-color: #483d8b; // 文本颜色:靛蓝色
- // 儿童风格试题弹框样式
- .child-dialog {
- .el-dialog__header {
- display: none; // 隐藏原有的标题栏
- }
- .el-dialog__body {
- padding: rpx(20);
- position: relative;
- }
- .el-dialog__footer {
- border-top: none;
- padding: rpx(10) rpx(20);
- text-align: center;
- margin-top: auto; // 使底部按钮位于底部
- }
- .el-dialog__wrapper {
- // 修改半透明背景色
- background-color: rgba(0, 0, 0, 0.6);
- }
- .el-dialog {
- border: none;
- border-radius: rpx(20);
- background: linear-gradient(
- 135deg,
- $light-color,
- #d8bfd8
- ); // 柔和的蓝紫色渐变
- overflow: hidden;
- display: flex; // 添加 flex 布局
- flex-direction: column; // 设置垂直布局
- min-height: 0; // 防止子元素溢出
- // 添加装饰元素
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: rpx(10);
- background: linear-gradient(90deg, $secondary-color, $accent-color);
- }
- }
- }
- // 问题标题样式
- .question-title {
- padding: rpx(15);
- border-radius: rpx(12);
- margin-bottom: rpx(20);
- color: #483d8b;
- font-weight: bold;
- font-size: rpx(12);
- position: relative;
- display: flex;
- text-align: left;
- .question-icon {
- background-color: $accent-color;
- color: white;
- width: rpx(24);
- height: rpx(24);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: rpx(10);
- font-weight: bold;
- box-shadow: 0 rpx(2) rpx(5) rgba($accent-color, 0.3);
- flex-shrink: 0; // 防止图标被压缩
- }
- }
- // 选项容器样式
- .options-container {
- margin-bottom: rpx(20);
- }
- // 问题选项样式
- .question-option {
- margin: rpx(8) 0;
- padding: rpx(10) rpx(15);
- border-radius: rpx(12);
- border: rpx(1) solid rgba($primary-color, 0.3);
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- background-color: white;
- box-shadow: 0 rpx(2) rpx(5) rgba($primary-color, 0.05);
- ::v-deep(.el-radio__label) {
- color: $text-color;
- margin-left: rpx(8);
- flex: 1;
- text-align: left;
- // 增大字体大小
- font-size: rpx(12);
- }
- // 选中时的样式变化
- .el-radio__input.is-checked + .el-radio__label {
- font-weight: bold;
- color: $accent-color;
- }
- &:hover {
- background-color: rgba($primary-color, 0.05);
- border-color: rgba($primary-color, 0.5);
- transform: translateY(-rpx(1));
- }
- }
- // 暂无选项样式
- .no-options {
- color: rgba($text-color, 0.7);
- text-align: center;
- padding: rpx(20);
- font-size: rpx(12);
- }
- // 底部按钮样式
- .child-button {
- min-width: rpx(70);
- height: rpx(25);
- border-radius: rpx(8);
- font-size: rpx(12);
- font-weight: 500;
- transition: all 0.3s ease;
- box-shadow: 0 rpx(2) rpx(8) rgba(0, 0, 0, 0.1);
- &.confirm {
- background: linear-gradient(to bottom, #ab81ff, #8559dc);
- border: none;
- border-right: 15px;
- color: white;
- &:hover {
- background: linear-gradient(
- to bottom,
- color.adjust(#ab81ff, $lightness: -5%),
- color.adjust(#8559dc, $lightness: -5%)
- );
- transform: translateY(-rpx(1));
- color: white;
- }
- }
- }
- // AI对话图标样式
- .ai-icon-container {
- position: absolute;
- bottom: rpx(10);
- right: rpx(20);
- display: flex;
- flex-direction: column;
- align-items: center;
- cursor: pointer;
- transition: all 0.3s ease;
- &:hover {
- transform: translateY(-rpx(2));
- }
- .ai-icon {
- width: rpx(30);
- height: rpx(30);
- margin-bottom: rpx(0);
- // filter: drop-shadow(0 rpx(2) rpx(4) rgba($primary-color, 0.3));
- // 添加过渡动画
- transition: transform 0.3s ease;
- }
- // 悬浮时放大效果
- .ai-icon:hover {
- transform: scale(1.5);
- }
- .ai-text {
- color: $text-color;
- font-size: rpx(8);
- background-color: rgba(255, 255, 255, 0.7);
- padding: rpx(2) rpx(5);
- border-radius: rpx(5);
- }
- }
- // AI对话弹框样式
- .ai-dialog-wrapper {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- display: flex;
- justify-content: flex-end;
- align-items: center;
- z-index: 1001;
- pointer-events: none;
- }
- .ai-dialog {
- border: none;
- border-radius: rpx(15);
- background: rgb(255, 255, 255, 0.8);
- overflow: hidden;
- padding: rpx(20);
- width: 30%;
- // 增加高度
- height: 80%;
- margin-right: rpx(50);
- pointer-events: auto;
- position: relative;
- display: flex;
- flex-direction: column;
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: rpx(10);
- }
- }
- .ai-dialog-header {
- position: relative;
- display: flex;
- justify-content: center;
- align-items: center;
- margin-bottom: rpx(15);
- margin-top: rpx(-10);
- img {
- width: rpx(15);
- }
- h3 {
- color: black;
- font-size: rpx(12);
- }
- .close-btn {
- padding: 0;
- width: rpx(20);
- height: rpx(20);
- font-size: rpx(16);
- line-height: 1;
- position: absolute;
- background-color: transparent;
- border: none;
- margin-left: rpx(200);
- }
- }
- .ai-dialog-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- }
- .ai-message-history {
- flex: 1;
- // 当内容超出容器高度时,显示垂直滚动条
- overflow-y: auto;
- margin-bottom: rpx(15);
- // 可以根据实际情况调整最大高度
- font-size: rpx(5);
- max-height: 50vh;
- .message {
- display: flex;
- align-items: flex-start;
- margin-bottom: rpx(10);
- &.user {
- flex-direction: row-reverse;
- }
- .avatar {
- width: rpx(30);
- height: rpx(30);
- border-radius: 50%;
- margin: 0 rpx(10);
- }
- .user {
- width: 15px;
- height: 15px;
- }
- .message-content {
- background-color: #ffffff;
- font-size: rpx(5);
- max-width: 80%;
- color: black;
- box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
- }
- }
- // 滚动条整体样式
- &::-webkit-scrollbar {
- width: rpx(4); // 滚动条宽度
- }
- // 滚动条滑块样式
- &::-webkit-scrollbar-thumb {
- background-color: $primary-color; // 滑块颜色
- border-radius: rpx(4); // 滑块圆角
- }
- // 滚动条轨道样式
- &::-webkit-scrollbar-track {
- background-color: rgba($primary-color, 0.2); // 轨道颜色
- border-radius: rpx(4); // 轨道圆角
- }
- .message {
- display: flex;
- align-items: flex-start;
- margin-bottom: rpx(10);
- &.user {
- flex-direction: row-reverse;
- }
- .avatar {
- width: rpx(30);
- height: rpx(30);
- border-radius: 50%;
- margin: 0 rpx(10);
- }
- .message-content {
- background-color: white;
- padding: rpx(8) rpx(12);
- border-radius: rpx(5);
- font-size: rpx(8);
- color: black;
- max-width: 80%;
- box-shadow: 0 rpx(1) rpx(3) rgba(0, 0, 0, 0.05);
- }
- }
- }
- // 终止按钮
- .stop-btn {
- cursor: pointer;
- display: flex;
- align-items: center;
- padding: rpx(5);
- img {
- width: rpx(20);
- height: rpx(20);
- }
- }
- // 用户输入框样式
- .user-input {
- gap: rpx(5); // 间距
- ::v-deep(.el-input__wrapper) {
- height: rpx(23);
- border-radius: rpx(5);
- border-color: rgba($primary-color, 0.3);
- &:focus-within {
- box-shadow: 0 0 0 rpx(1) rgba($primary-color, 0.5);
- }
- }
- ::v-deep(.el-input__inner) {
- font-size: rpx(10);
- text-indent: 1em;
- }
-
- // 语音按钮样式
- ::v-deep(.el-input-group__prepend) {
- width: rpx(15);
- background: white;
- border-radius: rpx(5);
- text-align: center;
- }
-
- ::v-deep(.el-input-group__prepend .el-button.recording) {
- padding: rpx(5) rpx(10);
- background: #fff;
- border: none;
- border-radius: rpx(5);
- cursor: pointer;
- display: flex;
- align-items: center;
- color: #dc3545;
- }
-
- ::v-deep(.el-input-group__append) {
- border: none;
- background: linear-gradient(to bottom, #ab81ff, #8559dc);
- border-radius: rpx(5);
- color: white;
- font-size: rpx(9);
- border-left: none;
- }
- }
- /* 定义淡入和缩放动画 */
- .fade-scale-enter-active,
- .fade-scale-leave-active {
- transition: all 0.5s ease;
- }
- .fade-scale-enter-from,
- .fade-scale-leave-to {
- opacity: 0.1;
- transform: scale(0.9);
- }
- // 自定义试题弹框背景
- .child-dialog-wrapper {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.6); // 半透明背景
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 1000;
- }
- .child-dialog {
- border: none;
- max-width: 45%;
- border-radius: rpx(15);
- background: rgb(255, 255, 255, 0.8); // 柔和的蓝紫色渐变
- overflow: hidden;
- padding: rpx(10);
- position: relative;
- }
- .default-messages {
- margin-top: rpx(-10);
- margin-bottom: rpx(5);
- }
- </style>
|