| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804 |
- <template>
- <view class="ai-qa-page">
- <!-- 页面内静态挂载的确认弹窗,统一跨端样式与交互 -->
- <AppConfirm ref="AppConfirm" />
- <!-- 自定义导航栏,透明覆盖在顶部 -->
- <view class="custom-navbar" :style="{ paddingTop: statusBarPx + 'px' }">
- <text class="nav-title">AI 智能助手</text>
- <view v-if="messages.length > 0" class="clear-btn" @click="clearChat">
- <text class="clear-text">清空</text>
- </view>
- </view>
- <!-- 顶部区域 -->
- <view class="top-section" v-show="messages.length === 0">
- <image class="ai-bg" src="/static/ai_bg.png" mode="aspectFill" />
- <image class="ai-banner" src="/static/ai_banner.png" mode="widthFix" />
- </view>
- <!-- 聊天消息列表 -->
- <view class="chat-list" v-show="messages.length > 0" :style="{ paddingTop: `${statusBarPx + 88}rpx`, bottom: chatBottom }">
- <scroll-view scroll-y class="chat-scroll" :scroll-top="scrollTop" :scroll-into-view="scrollIntoViewId" :scroll-with-animation="scrollWithAnimation" lower-threshold="60" @scrolltolower="onScrollToLower" @scroll="onScroll">
- <view v-for="(msg, index) in messages" :key="index" :class="['message', msg.role]">
- <!-- AI回复卡片 -->
- <view v-if="msg.role === 'assistant'" class="ai-card">
- <!-- AI头像和深度思考标识头部 -->
- <view class="ai-card-header">
- <view class="ai-avatar">
- <image src="/static/ai_avatar.png" mode="aspectFill" />
- </view>
- <view v-if="msg.isThinking" class="ai-loading-spinner" aria-label="loading"></view>
- <view class="thinking-header" @click="toggleThinking(index)">
-
- <text class="thinking-text" v-if="msg.isThinking || msg.hasThinking">{{ msg.isThinking ? '深度思考中…' : '已深度思考' }}</text>
- <view class="toggle-icon" :class="{ 'expanded': msg.showThinking }">
- <image class="caret-icon" src="/static/collapase_up.png" mode="widthFix" />
- </view>
- </view>
- </view>
- <!-- 回复内容 -->
- <view class="response-content" v-show="msg.showThinking">
- <!-- 思考内容(单独显示,支持Markdown) -->
- <view v-if="msg.hasThinking && msg.thinkContent" class="thinking-block"><rich-text :nodes="msg.thinkContentHtml"></rich-text></view>
- <!-- 正文内容(支持Markdown) -->
- <view class="answer-block"><rich-text :nodes="msg.contentHtml"></rich-text></view>
- </view>
- <!-- 复制按钮 -->
- <view class="copy-icon" v-show="msg.showThinking && !msg.isLoading" @click="copyContent(msg.content)">
- <image class="copy-icon-img" src="/static/ai_copy.png" mode="widthFix" />
- </view>
- </view>
- <!-- 用户消息 -->
- <view v-else class="user-bubble">
- <view class="user-content">
- {{msg.content}}
- </view>
- </view>
- </view>
- <!-- 底部锚点:用于自动滚动到最底部 -->
- <view :id="'bottom-anchor-' + anchorKey" style="height:1px;"></view>
- </scroll-view>
- </view>
- <!-- 底部输入框区域 -->
- <view class="input-bar">
- <view class="input-wrap" :class="{ 'chat-mode': messages.length > 0 }">
- <textarea
- class="input textarea"
- v-model="question"
- placeholder="输入你的问题"
- placeholder-style="color:#A3ADC2;"
- auto-height="true"
- confirm-type="send"
- show-confirm-bar="true"
- @confirm="onSend"
- @keyup.ctrl.enter="onSend"
- @keyup.meta.enter="onSend"
- @input="onInput"
- />
- <view class="send-btn" :class="{ 'chat-mode': messages.length > 0, 'disabled': (messages.length > 0 && !question.trim()) || isSending }" @click="onSend">
- <image class="send-icon" src="/static/aisend_btn_white.png" mode="widthFix" />
- </view>
- </view>
- </view>
- <tm-tabbar :permission="nowPermission" />
- </view>
- </template>
- <script>
- import { askAIStream, formatAIResponse, checkNetworkStatus } from '@/utils/ai-api.js';
- import { markdownToHtml } from '@/utils/markdown.js';
- import AppConfirm from '@/components/app-confirm/app-confirm.vue'
- export default {
- components: { AppConfirm },
- data() {
- return {
- nowPermission: '', // 当前权限,用于底部tab显示
- statusBarPx: 0, // 顶部状态栏高度
- question: '', // 输入的问题
- messages: [], // 聊天消息列表
- scrollTop: 0, // 滚动位置
- scrollIntoViewId: '', // 滚动锚点ID
- anchorKey: 0, // 锚点变更键,强制刷新
- scrollWithAnimation: false, // 默认先不开启动画,等待页面ready后再开启,避免初次抖动
- statusBarRpx: 0,
- screenWidth: 0,
- chatBottom: '105rpx',
- isSending: false, // 是否正在发送消息,防止重复发送
- isAutoScroll: true, // 是否自动滚动到底部
- lastAnchorUpdateTs: 0, // 上次锚点更新时间戳
- lastScrollTop: 0, // 上次滚动位置
- persistTimer: null // 本地持久化节流定时器
- }
- },
- created() {
- // 从缓存读取当前权限
- this.nowPermission = uni.getStorageSync('nowPermission');
- // 读取状态栏高度,保证顶部视觉延伸到状态栏
- const info = uni.getSystemInfoSync();
- this.statusBarPx = info.statusBarHeight || 0;
- this.screenWidth = info.screenWidth;
- this.statusBarRpx = Math.round(this.statusBarPx * 750 / this.screenWidth);
- },
- mounted() {
- this.updateChatBottom();
- this.$nextTick(() => { this.scrollWithAnimation = true; });
- // 页面挂载后尝试恢复本地缓存消息
- this.restoreMessagesFromStorage();
- },
- beforeDestroy() {
- // 页面销毁时无需特殊处理
- },
- onHide() {
- // 页面隐藏时立即保存一次
- this.saveMessagesImmediate();
- },
- methods: {
- onInput() {
- this.updateChatBottom();
- },
- updateChatBottom() {
- this.$nextTick(() => {
- const query = uni.createSelectorQuery().in(this);
- query.select('.input-bar').boundingClientRect(rect => {
- if (rect) {
- const heightRpx = Math.round(rect.height * 750 / this.screenWidth);
- const bottomOffset = 85; // 与 .input-bar 的 bottom 保持一致,避开底部tabbar
- this.chatBottom = (heightRpx + bottomOffset) + 'rpx';
- }
- }).exec();
- });
- },
- // 从本地恢复消息
- restoreMessagesFromStorage() {
- try {
- const key = 'aiQaMessagesV1';
- const cached = uni.getStorageSync(key);
- if (cached && Array.isArray(cached)) {
- this.messages = cached;
- // 恢复后滚动到底部
- this.$nextTick(() => { this.forceScrollToBottom(); });
- }
- } catch (e) {
- console.warn('恢复聊天缓存失败', e);
- }
- },
- // 立即保存消息
- saveMessagesImmediate() {
- try {
- const key = 'aiQaMessagesV1';
- uni.setStorageSync(key, this.messages);
- } catch (e) {
- console.warn('保存聊天缓存失败', e);
- }
- },
- // 节流保存
- saveMessagesDebounced() {
- if (this.persistTimer) { clearTimeout(this.persistTimer); }
- this.persistTimer = setTimeout(() => {
- this.saveMessagesImmediate();
- }, 300);
- },
- // 处理流式响应(HTTP chunked 实时追加),支持 <think> 思考区分和 Markdown
- async handleStreamingResponse(question, aiMessageIndex) {
- return new Promise((resolve, reject) => {
- // 流式期间关闭滚动动画,避免频繁触发导致抖动
- this.scrollWithAnimation = false;
- let fullText = '';
- let thinking = false;
- const onChunk = (chunkText, nowFull, rawPiece) => {
- // 识别思考标签:一旦检测到 <think> 立即进入思考态并标记 hasThinking
- if (typeof rawPiece === 'string') {
- if (rawPiece.indexOf('<think>') > -1) {
- thinking = true;
- this.$set(this.messages[aiMessageIndex], 'isThinking', true);
- if (!this.messages[aiMessageIndex].hasThinking) {
- this.$set(this.messages[aiMessageIndex], 'hasThinking', true);
- // 初始化思考内容(避免首块为空时不显示)
- if (!this.messages[aiMessageIndex].thinkContent) {
- this.$set(this.messages[aiMessageIndex], 'thinkContent', '');
- this.$set(this.messages[aiMessageIndex], 'thinkContentHtml', '');
- }
- }
- }
- if (rawPiece.indexOf('</think>') > -1) {
- thinking = false;
- this.$set(this.messages[aiMessageIndex], 'isThinking', false);
- this.$set(this.messages[aiMessageIndex], 'hasThinking', true);
- }
- }
- // 根据 thinking 累积到不同区域
- if (thinking) {
- const prev = this.messages[aiMessageIndex].thinkContent || '';
- const merged = prev + (chunkText || '');
- this.$set(this.messages[aiMessageIndex], 'thinkContent', merged);
- this.$set(this.messages[aiMessageIndex], 'thinkContentHtml', markdownToHtml(merged));
- } else {
- // 仅在非思考阶段累计正文
- fullText = nowFull || (fullText + (chunkText || ''));
- this.$set(this.messages[aiMessageIndex], 'content', fullText);
- this.$set(this.messages[aiMessageIndex], 'contentHtml', markdownToHtml(fullText));
- }
- // 统一调度自动滚动,内部带节流与 nextTick
- this.scheduleScrollToBottom();
- // 节流持久化,保证切换tab后还能恢复
- this.saveMessagesDebounced();
- };
- askAIStream(question, onChunk)
- .then((res) => {
- this.$set(this.messages[aiMessageIndex], 'isLoading', false);
- this.$set(this.messages[aiMessageIndex], 'isThinking', false);
- if (!res || res.success !== true) {
- reject(new Error(res && res.error ? res.error : '服务异常'));
- return;
- }
- // 流式结束,恢复滚动动画并强制滚动一次到底部
- this.scrollWithAnimation = true;
- this.forceScrollToBottom();
- this.saveMessagesImmediate();
- resolve();
- })
- .catch((err) => {
- console.error('AI流式请求失败:', err);
- this.$set(this.messages[aiMessageIndex], 'content', '网络连接失败,请检查网络后重试。');
- this.$set(this.messages[aiMessageIndex], 'isLoading', false);
- this.$set(this.messages[aiMessageIndex], 'isThinking', false);
- // 出错也恢复滚动动画并强制滚动
- this.scrollWithAnimation = true;
- this.forceScrollToBottom();
- this.saveMessagesImmediate();
- reject(err);
- });
- });
- },
- // 清空聊天记录
- async clearChat() {
- const ok = await this.$refs.AppConfirm.open({
- title: '确认清空',
- content: '确定要清空所有聊天记录吗?',
- confirmText: '清空',
- cancelText: '取消',
- type: 'danger'
- });
- if (ok) {
- this.messages = [];
- // 重置滚动位置
- this.scrollTop = 0;
- // 清除本地缓存
- try { uni.removeStorageSync('aiQaMessagesV1'); } catch (e) {}
- }
- },
- // 切换深度思考展开/收起
- toggleThinking(index) {
- this.$set(this.messages[index], 'showThinking', !this.messages[index].showThinking);
- },
- // 复制内容
- copyContent(content) {
- uni.setClipboardData({
- data: content,
- success: () => {
- uni.showToast({
- title: '复制成功',
- icon: 'success',
- duration: 1500
- });
- },
- fail: () => {
- uni.showToast({
- title: '复制失败',
- icon: 'none',
- duration: 1500
- });
- }
- });
- },
- // 发送按钮点击
- async onSend(){
- // 如果输入为空或正在发送中,直接返回
- if(!this.question.trim() || this.isSending){
- return;
- }
-
- // 设置发送状态,防止重复发送
- this.isSending = true;
-
- const userQuestion = this.question.trim();
-
- // 添加用户消息
- this.messages.push({role: 'user', content: userQuestion});
- this.saveMessagesDebounced();
-
- // 清空输入框
- this.question = '';
-
- // 滚动到底部
- this.scheduleScrollToBottom();
-
- // 添加AI回复占位消息
- const aiMessageIndex = this.messages.length;
- this.messages.push({
- role: 'assistant',
- content: '',
- showThinking: true,
- isLoading: true, // 标记为加载状态
- isThinking: true
- });
- this.saveMessagesDebounced();
-
- try {
- // 检查网络连接
- const isNetworkAvailable = await checkNetworkStatus();
- if (!isNetworkAvailable) {
- this.$set(this.messages[aiMessageIndex], 'content', '网络连接不可用,请检查网络设置后重试。');
- this.$set(this.messages[aiMessageIndex], 'isLoading', false);
- this.saveMessagesImmediate();
- return;
- }
- // 使用模拟流式响应处理
- await this.handleStreamingResponse(userQuestion, aiMessageIndex);
-
- } catch (error) {
- // 处理网络错误或其他异常
- let errorMessage = '网络连接失败,请检查网络后重试。';
-
- if (error && error.error) {
- errorMessage = error.error;
- } else if (error && error.message) {
- errorMessage = error.message;
- }
-
- this.$set(this.messages[aiMessageIndex], 'content', errorMessage);
- console.error('AI问答API调用异常:', error);
- } finally {
- // 无论成功还是失败,都要结束加载状态和发送状态
- this.$set(this.messages[aiMessageIndex], 'isLoading', false);
- this.isSending = false;
- }
-
- // 滚动到底部显示最新消息(强制一次)
- this.forceScrollToBottom();
- this.saveMessagesImmediate();
-
- this.updateChatBottom(); // 更新底部间距
- },
- scheduleScrollToBottom() {
- if (!this.isAutoScroll) return;
- const now = Date.now();
- if (now - this.lastAnchorUpdateTs < 80) return; // 80ms节流
- this.lastAnchorUpdateTs = now;
- this.anchorKey++;
- this.$nextTick(() => {
- this.scrollIntoViewId = 'bottom-anchor-' + this.anchorKey;
- });
- },
- // 强制滚动到底部:用于生成完成/异常时兜底
- forceScrollToBottom() {
- this.anchorKey++;
- this.$nextTick(() => {
- this.scrollIntoViewId = 'bottom-anchor-' + this.anchorKey;
- // 兼容部分端:给一个较大的 scrollTop 值
- this.scrollTop = 999999;
- });
- },
- onScroll(e) {
- const top = (e && e.detail && typeof e.detail.scrollTop === 'number') ? e.detail.scrollTop : 0;
- if (top + 40 < this.lastScrollTop) {
- this.isAutoScroll = false;
- }
- this.lastScrollTop = top;
- },
- onScrollToLower() {
- this.isAutoScroll = true;
- this.scheduleScrollToBottom();
- }
- }
- }
- </script>
- <style lang="less" scoped>
- .ai-qa-page {
- min-height: 100%;
- padding-bottom: 100rpx; // 预留底部tabbar高度
- .custom-navbar {
- position: fixed;
- top: 0;
- left: 0;
- z-index: 10;
- width: 100%;
- height: 88rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- background: transparent; // 透明,让背景延伸到顶部
- backdrop-filter: blur(10rpx); // 添加毛玻璃效果
- }
- .nav-title {
- font-size: 32rpx;
- color: #1f2333;
- font-weight: 600;
- letter-spacing: 1rpx; // 增加字间距
- }
- .clear-btn {
- position: absolute;
- right: 30rpx;
- top: 50%;
- transform: translateY(-50%);
- padding: 8rpx 16rpx;
- border-radius: 10rpx;
- background: rgba(255, 255, 255, 0.8);
- backdrop-filter: blur(10rpx);
- cursor: pointer;
- transition: all 0.3s ease;
- }
- .clear-btn:active {
- transform: translateY(-50%) scale(0.95);
- }
- .clear-text {
- font-size: 24rpx;
- color: #666;
- font-weight: 500;
- }
- .top-section {
- position: relative;
- width: 100%;
- padding-top: 88rpx;
- min-height: 800rpx; // 假设值,确保填充屏幕,实际根据banner调整
- overflow: hidden; // 确保背景图片不会溢出
- }
- .ai-bg {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 1;
- object-fit: cover; // 确保图片正确填充
- }
- .ai-banner {
- position: relative;
- z-index: 2;
- width: 100%;
- display: block; // 去除行内元素底部空隙
- object-fit: contain; // 确保图片保持比例
- }
- // 底部输入框容器
- .input-bar{
- position: fixed;
- left: 0;
- bottom: 85rpx; // 避开底部tabbar
- z-index: 5;
- width: 100%;
- padding: 0 25rpx 25rpx;
- box-sizing: border-box;
- }
- .input-wrap{
- position: relative;
- border-radius: 22rpx; // 微调圆角使条形更贴设计
- border: 1px solid transparent; // 初始状态:透明边框配合渐变背景
- background:
- linear-gradient(#fff, #fff) padding-box,
- linear-gradient(90deg, rgba(141, 102, 255, 1), rgba(52, 119, 255, 1)) border-box;
- overflow: hidden; // 裁剪内部使圆角生效
- padding-right: 96rpx; // 预留发送按钮空间
- transition: all 0.3s ease; // 添加过渡动画
- }
- .input-wrap.chat-mode {
- border: 3rpx solid #E7EDF2; // 对话状态:实线边框
- background: #fff; // 白色背景
- }
- .input{
- width: 100%;
- min-height: 56rpx; // 提高默认高度
- max-height: 220rpx;
- padding: 28rpx 28rpx 4rpx; // 按单行时的垂直居中计算(64-40)/2=12,28=16+12
- font-size: 28rpx;
- color: #292C33;
- box-sizing: border-box;
- background: transparent; // 确保背景透明
- border: none; // 移除边框
- outline: none; // 移除轮廓
- }
- // 多行输入样式
- .textarea{
- position: relative;
- top:-4rpx;
- line-height: 40rpx;
- overflow-y: auto;
- resize: none; // 禁用调整大小
- }
- .send-btn{
- position: absolute;
- right: 10rpx;
- bottom: 18rpx; // 固定在右下角
- width: 52rpx;
- height: 52rpx;
- border-radius: 16rpx;
- background: linear-gradient(90deg, #8C66FE 0%, #3377FF 100%); // 初始状态:渐变背景
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer; // 添加鼠标指针样式
- transition: all 0.3s ease; // 添加过渡动画
- }
- .send-btn.chat-mode {
- background: #3377FF; // 对话状态:纯色背景
- }
- .send-btn.disabled {
- background: #7A8599 !important; // 禁用状态下的灰色背景,优先级最高
- cursor: not-allowed; // 禁用状态下的鼠标样式
- }
- .send-btn:not(.disabled):active {
- transform: scale(0.95); // 只有在非禁用状态下才有点击缩小效果
- }
- .send-icon{
- width: 45rpx;
- height: 45rpx;
- object-fit: contain; // 确保图标正确显示
- }
- }
- // 添加聊天列表样式
- .chat-list {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- padding: 0 30rpx;
- background: #F5F6FA; // 对话背景色
- z-index: 3; // 低于input-bar的z5
- box-sizing: border-box;
- }
- .chat-scroll {
- height: 100%;
- }
- .message {
- display: flex;
- margin-bottom: 25rpx; // 缩小消息间距
- align-items: flex-start; // 确保头像和消息顶部对齐
- }
- .message:last-child {
- margin-bottom: 12rpx; // 最后一条消息底部更紧凑
- }
- .message.assistant {
- flex-direction: row;
- justify-content: flex-start; // 确保AI消息左对齐
- width: 100%; // 确保占满宽度
- }
- .message.user {
- flex-direction: row-reverse;
- text-align: right;
- justify-content: flex-end; // 确保用户消息右对齐
- width: 100%; // 确保占满宽度
- }
- .avatar {
- width: 40rpx;
- height: 40rpx;
- overflow: hidden;
- flex-shrink: 0; // 防止头像被压缩
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); // 添加头像阴影
- }
- .avatar image {
- width: 100%;
- height: 100%;
- object-fit: cover; // 确保头像图片正确填充
- }
- // AI卡片样式
- .ai-card {
- background: #fff; // AI回复卡片背景色为白色
- border-radius: 16rpx; // 调整圆角以匹配设计稿
- overflow: hidden; // 确保内部元素不会超出圆角
- box-shadow: none; // 去除卡片阴影
- width: 100%; // AI卡片宽度100%
- }
- .ai-card-header {
- display: flex;
- align-items: center;
- padding: 20rpx 30rpx;
- }
- .ai-loading-spinner {
- width: 28rpx;
- height: 28rpx;
- margin-right: 16rpx;
- border: 4rpx solid rgba(122, 133, 153, 0.15);
- border-top-color: #7A8599; // 指定颜色
- border-radius: 50%;
- animation: ai-rotate 0.9s linear infinite;
- }
- @keyframes ai-rotate {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- .ai-avatar {
- width: 40rpx;
- height: 40rpx;
- border-radius: 50%;
- overflow: hidden;
- margin-right: 20rpx;
- flex-shrink: 0; // 防止头像被压缩
- }
- .ai-avatar image {
- width: 100%;
- height: 100%;
- object-fit: cover; // 确保头像图片正确填充
- }
- // 用户消息气泡样式
- .user-bubble {
- background: #3377FF; // 蓝色背景
- color: #fff;
- box-shadow: 0 2rpx 10rpx rgba(51, 119, 255, 0.2);
- margin-left: auto; // 确保用户消息靠右
- max-width: 70%; // 用户消息稍微窄一点
- border-radius: 16rpx; // 添加圆角
- overflow: hidden; // 确保内容不超出圆角
- }
- .thinking-header {
- display: flex;
- align-items: center;
- padding: 0; // 移除内边距,因为现在在头部容器内
- border-bottom: none; // 移除边框,因为头部容器已经有边框
- cursor: pointer;
- background: transparent; // 透明背景,因为头部容器已经有背景
- transition: background-color 0.2s ease; // 添加过渡效果
- flex: 1; // 占据剩余空间
- }
- .thinking-header:hover {
- background: rgba(240, 242, 245, 0.5); // 悬停时的半透明背景色
- }
- .thinking-icon {
- width: 24rpx;
- height: 24rpx;
- margin-right: 12rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0; // 防止图标被压缩
- }
- .dot-line {
- width: 16rpx;
- height: 2rpx;
- background: #6B7280;
- position: relative;
- border-radius: 1rpx; // 添加圆角
- }
- .dot-line::before,
- .dot-line::after {
- content: '';
- position: absolute;
- width: 4rpx;
- height: 4rpx;
- background: #6B7280;
- border-radius: 50%;
- top: -1rpx;
- box-shadow: 0 0 2rpx rgba(107, 114, 128, 0.3); // 添加阴影效果
- }
- .dot-line::before {
- left: -2rpx;
- }
- .dot-line::after {
- right: -2rpx;
- }
- .thinking-text {
- font-size: 24rpx;
- color: #6B7280;
- flex: 1;
- font-weight: 500; // 添加字重
- }
- .toggle-icon {
- width: 40rpx;
- height: 40rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: transform 0.3s ease;
- flex-shrink: 0; // 防止图标被压缩
- margin-left: auto; // 推到右侧
- }
- .toggle-icon.expanded {
- transform: rotate(180deg);
- }
- .caret-icon {
- width: 30rpx;
- height: 30rpx;
- object-fit: contain; // 确保图标正确显示
- }
- .response-content {
- padding: 20rpx 30rpx;
- padding-top: 0;
- font-size: 28rpx;
- line-height: 42rpx;
- color: #333;
- word-break: break-all; // 确保长文本正确换行
- white-space: pre-wrap; // 保持换行格式
- }
- .thinking-block {
- font-size: 26rpx; // 比正文稍小
- line-height: 40rpx;
- color: #7A8599; // 指定灰色
- padding-bottom: 20rpx;
- margin-bottom: 20rpx;
- border-bottom: 2rpx dashed #E2E8F0; // 底部分割虚线
- }
- .answer-block {
- font-size: 28rpx;
- line-height: 42rpx;
- color: #333;
- }
- .copy-icon {
- padding: 0 30rpx 20rpx;
- display: flex;
- justify-content: flex-start; // 左对齐,显示在左下角
- cursor: pointer; // 添加鼠标指针样式
- }
- .message:last-child .copy-icon {
- padding-bottom: 8rpx; // 缩小最后一条的底部留白
- }
- .copy-icon-img {
- width: 32rpx;
- height: 32rpx;
- object-fit: contain; // 确保图标正确显示
- transition: transform 0.2s ease; // 添加过渡效果
- }
- .copy-icon:hover .copy-icon-img {
- transform: scale(1.1); // 悬停时稍微放大
- }
- // 加载状态样式
- .loading-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 40rpx 0;
- }
- .loading-dots {
- display: flex;
- align-items: center;
- margin-bottom: 20rpx;
- }
- .dot {
- width: 12rpx;
- height: 12rpx;
- border-radius: 50%;
- background: #3377FF;
- margin: 0 6rpx;
- animation: loading-dot 1.4s infinite ease-in-out;
- }
- .dot:nth-child(1) {
- animation-delay: -0.32s;
- }
- .dot:nth-child(2) {
- animation-delay: -0.16s;
- }
- @keyframes loading-dot {
- 0%, 80%, 100% {
- transform: scale(0.8);
- opacity: 0.5;
- }
- 40% {
- transform: scale(1);
- opacity: 1;
- }
- }
- .loading-text {
- font-size: 26rpx;
- color: #6B7280;
- font-weight: 500;
- }
- .user-content {
- padding: 20rpx 30rpx;
- font-size: 28rpx;
- line-height: 42rpx;
- color: #fff;
- word-break: break-all; // 确保长文本正确换行
- text-align: right; // 用户消息文本右对齐
- }
- </style>
|