前端开发ai组件(纯前端调用接口,流式处理markdown-it,脑图显示markmap,vue2)
markmap文档地址:https://markmap.js.org/markdown-it文档地址:http://markdown-it.docschina.org/markdown使用方法及markmap使用方法
·
依赖版本
注:markmap现在只有这个版本好使,新版本的vue2会报错
markmap文档地址:https://markmap.js.org/
markdown-it文档地址:http://markdown-it.docschina.org/
"markdown-it": "^14.1.0",
"markdown-it-emoji": "^3.0.0",
"markdown-it-highlightjs": "^4.2.0",
"markdown-it-task-lists": "^2.1.1",
"markmap-common": "0.15.3",
"markmap-lib": "0.15.4",
"markmap-view": "0.15.4",
markdown使用方法及markmap使用方法(deepseek生成的文档,markdown喝markmap的使用打开就是这样)
<template>
<el-drawer
:visible.sync="drawerVisible"
:modal="false"
:close-on-press-escape="false"
:wrapperClosable="false"
title="AI 对话框"
direction="rtl"
size="454px"
:modal-append-to-body="false"
:append-to-body="false"
class="custom-drawer"
>
<!-- 选项卡 -->
<div class="tabs">
<div class="tabBlock">
<div
class="tabRow"
v-for="(item, index) in activeTabList"
:key="index"
>
<div
class="tabClounm"
:class="
activeTab == item.value ? 'activeTabClounm' : ''
"
@click="activeTabClick(item.value)"
>
{{ item.name }}
</div>
</div>
</div>
<div class="cloaseDrawer" @click="drawerClose">
<img src="../../assets/detailImage/closeIcon.png" alt="" />
</div>
</div>
<!-- 增强型固定问题区块 -->
<div class="fixedQuestion" v-if="activeTab === 'chat'">
<div
v-for="(item, index) in questionList"
:key="index"
class="fixedQuestionBlock"
@click="!isLoading && handlePresetQuestion(item)"
:class="{ 'disabled-block': isLoading }"
>
<!-- v-if="item.icon" -->
<span class="question-icon">
{{ item.label }}
</span>
<!-- <span v-if="item.hotkey" class="hotkey">{{ item.hotkey }}</span> -->
</div>
</div>
<!-- 聊天功能 -->
<div v-if="activeTab === 'chat'" class="markdown-body">
<div
v-for="(msg, index) in messages"
:key="index"
class="message-container"
:class="msg.type"
>
<!-- 用户消息的引用框 -->
<div
v-if="msg.type === 'user' && msg.quote"
class="quote-display-bubble"
>
<div class="quote-content">
<span class="quote-label"> 引用内容:</span>
{{ msg.quote }}
</div>
<el-button
class="quote-close"
type="text"
icon="el-icon-close"
size="mini"
@click.stop="msg.quote = ''"
></el-button>
</div>
<!-- 消息主体行 -->
<div class="message-row">
<!-- 头像 -->
<img
:src="
msg.type === 'user'
? require('../../assets/detailImage/userAvater.png')
: require('../../assets/detailImage/aiAvater.png')
"
:class="['avatar', msg.type]"
/>
<!-- 消息气泡 -->
<div class="message-bubble">
<div :class="['message-content', msg.type]">
<div
v-if="msg.type === 'user'"
class="user-message"
>
{{ msg.text }}
</div>
<div
v-else
class="ai-message"
v-html="msg.text"
></div>
</div>
<!-- AI操作按钮 -->
<div v-if="msg.type === 'ai'" class="action-buttons">
<div class="refDiv">
<el-tooltip content="重新生成">
<img
@click="regenerateResponse(index)"
src="../../assets/detailImage/refIcon.png"
alt=""
/>
</el-tooltip>
<span class="refDiv1">重新生成</span>
<span class="refDiv2">生成内容仅供参考</span>
</div>
<div style="display: flex; line-height: 22px">
<el-tooltip content="打开脑图">
<div
@click="openMindMap(index)"
style="margin-right: 10px"
>
<img
src="../../assets/detailImage/graphIcon.png"
alt=""
/>
</div>
</el-tooltip>
<el-tooltip content="复制">
<div @click="handleCopy(msg.text)">
<img
src="../../assets/detailImage/copyIcon.png"
alt=""
/>
</div>
</el-tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<!-- 引用预览 -->
<!-- v-if="selectedQuote" -->
<!-- 浮动引用框 -->
<div v-if="selectedQuote" class="quote-floating">
<div class="quote-content">
<div class="quote-header">
<div class="quote-text">
引用内容:{{ selectedQuote }}
</div>
</div>
<el-button
class="close-btn"
type="text"
icon="el-icon-close"
@click="selectedQuote = ''"
></el-button>
</div>
<div class="quote-footer">基于以上内容提问:</div>
</div>
<el-input
type="textarea"
placeholder="请您向AI助手提问"
v-model="inputText"
@keyup.enter.native="sendMessage"
maxlength="400"
show-word-limit
:disabled="isLoading"
>
</el-input>
<img
class="upimg"
:class="{ 'loading-state': isLoading }"
@click="sendMessage"
src="../../assets/detailImage/goUp.png"
alt=""
/>
</div>
<div v-if="activeTab === 'translate'" class="markdown-body">
<div
v-for="(msg, index) in messages"
:key="index"
class="message-container"
:class="msg.type"
>
<!-- 消息主体行 -->
<div class="message-row">
<!-- 头像 -->
<img
v-if="msg.type == 'ai'"
:src="require('../../assets/detailImage/aiAvater.png')"
:class="['avatar', msg.type]"
/>
<!-- 消息气泡 -->
<div class="message-bubble">
<div
v-if="msg.type == 'ai'"
:class="['message-content', msg.type]"
>
<!-- <div
v-if="msg.type === 'user'"
class="user-message"
>
{{ msg.text }}
</div> -->
<div
v-if="msg.type === 'ai'"
class="ai-message"
v-html="msg.text"
></div>
</div>
<!-- AI操作按钮 -->
<div v-if="msg.type === 'ai'" class="action-buttons">
<div class="refDiv">
<el-tooltip content="重新生成">
<img
@click="regenerateResponse(index)"
src="../../assets/detailImage/refIcon.png"
alt=""
/>
</el-tooltip>
<span class="refDiv1">重新生成</span>
<span class="refDiv2">生成内容仅供参考</span>
</div>
<div style="display: flex; line-height: 22px">
<el-tooltip content="整篇复制">
<div @click="handleCopy(msg.text)">
<img
src="../../assets/detailImage/copyIcon.png"
alt=""
/>
</div>
</el-tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 选中文本工具 -->
<div
v-show="selectionToolsVisible"
class="selection-tools"
:style="selectionPosition"
@mousedown.prevent
>
<div class="tool-item" @click.stop="translateSelection">
<img src="../../assets/detailImage/gnIcon.png" alt="" />
<span class="tool-text">概念解析</span>
</div>
<div class="tool-divider"></div>
<div class="tool-item" @click.stop="copySelection">
<img src="../../assets/detailImage/copyTowIcon.png" alt="" />
<span class="tool-text">复制文本</span>
</div>
</div>
<el-dialog
title=""
v-el-drag-dialog
:visible.sync="graphState"
:close-on-click-modal="false"
:modal="false"
width="1100px"
top="100px"
custom-class="graphClass"
>
<div class="view-switch" slot="title">
<el-button-group>
<el-button
:type="activeView === 'mindmap' ? 'primary' : ''"
@click="switchView('mindmap')"
>脑图视图</el-button
>
<el-button
style="min-width: 98px"
:type="activeView === 'structure' ? 'primary' : ''"
@click="switchView('structure')"
>大纲</el-button
>
</el-button-group>
<div class="cloaseDrawer" @click="graphStateClose">
<img src="../../assets/detailImage/closeIcon.png" alt="" />
</div>
</div>
<div>
<svg
v-show="activeView === 'mindmap'"
id="markmap-container"
ref="markmap"
></svg>
</div>
<!-- 脑图容器 -->
<!-- 结构视图容器 -->
<div v-show="aiGraphLoding || errorLoding" id="aiGraphLoding"></div>
<div v-show="activeView === 'structure'" class="structure-view">
<div class="ai-message-graph" v-html="graphText"></div>
</div>
<div class="footerButton">
<div class="refDiv">
<el-tooltip content="重新生成">
<img
@click="regenerateResponseGraph()"
src="../../assets/detailImage/refIcon.png"
alt=""
/>
</el-tooltip>
<span class="refDiv1">重新生成</span>
<span class="refDiv2">生成内容仅供参考</span>
</div>
</div>
</el-dialog>
</el-drawer>
</template>
<script>
// 预设问题配置
const PRESET_QUESTIONS = [
{
type: 'core_points',
label: '核心观点',
icon: '💎',
prompt: '请用分点论述的方式总结本文的核心学术观点',
responseType: 'markdown_list',
hotkey: '⌘1',
template: (data) =>
`## 核心观点\n${data.points
.map((p, i) => `${i + 1}. ${p}`)
.join('\n')}`,
exampleData: {
points: [
'提出了新型神经网络结构',
'验证了跨模态学习的有效性',
'解决了传统方法的梯度消失问题',
],
},
},
{
type: 'outline',
label: '文章大纲',
icon: '📑',
prompt: '请用层级结构展示本文的章节框架',
responseType: 'hierarchy',
hotkey: '⌘2',
template: (data) =>
`## 文章大纲\n${data.sections
.map((s) => `${' '.repeat(s.level)}• ${s.title}`)
.join('\n')}`,
exampleData: {
sections: [
{ level: 0, title: '引言' },
{ level: 1, title: '研究背景' },
{ level: 1, title: '相关工作' },
{ level: 0, title: '方法论' },
],
},
},
{
type: 'key_points',
label: '文章要点',
icon: '🔑',
prompt: '请提取本文的五个关键创新点',
responseType: 'highlight',
hotkey: '⌘3',
template: (data) =>
`## 核心创新\n${data.points.map((p) => `✨ ${p}`).join('\n')}`,
exampleData: {
points: [
'提出多模态融合架构',
'设计动态权重调整机制',
'验证跨数据集泛化能力',
],
},
},
{
type: 'research_method',
label: '研究方法',
icon: '🔬',
prompt: '请详细说明本文采用的研究方法体系',
responseType: 'method_steps',
hotkey: '⌘4',
template: (data) =>
`## 研究方法\n${data.methods
.map((m) => `▫️ **${m.name}**: ${m.desc}`)
.join('\n\n')}`,
exampleData: {
methods: [
{
name: '对比实验',
desc: '设计三组对照实验,分别使用传统方法和两种改进方案',
},
{
name: '统计分析',
desc: '采用SPSS 26.0进行ANOVA方差分析',
},
],
},
},
{
type: 'conclusion',
label: '研究结论',
icon: '🎯',
prompt: '请用数据支撑的方式总结研究结论',
responseType: 'data_driven',
hotkey: '⌘5',
template: (data) =>
`## 实验结论\n准确率提升: ${data.accuracy}%\n训练效率: ${data.efficiency}x\n${data.details}`,
exampleData: {
accuracy: 15.6,
efficiency: 2.3,
details: '在COCO数据集上验证了方法的泛化能力',
},
},
{
type: 'future_work',
label: '研究方向',
icon: '🚀',
prompt: '请展望本领域的未来研究方向',
responseType: 'roadmap',
hotkey: '⌘6',
template: (data) =>
`## 研究展望\n${data.directions.map((d) => `⏩ ${d}`).join('\n')}`,
exampleData: {
directions: [
'多模态数据的实时处理',
'模型轻量化部署方案',
'跨领域迁移学习研究',
],
},
},
{
type: 'mindmap',
label: '生成脑图',
icon: '🧠',
prompt: '请用Mermaid语法生成技术路线图',
responseType: 'mermaid',
hotkey: '⌘7',
template: (data) =>
`## 技术路线\n\`\`\`mermaid\n${data.content}\n\`\`\``,
exampleData: {
content: `graph LR
A[数据采集] --> B[特征工程]
B --> C[模型训练]
C --> D[结果验证]
D --> E[应用部署]`,
},
},
{
type: 'references',
label: '参考文献',
icon: '📖',
prompt: '请按APA格式整理本文参考文献',
responseType: 'apa',
hotkey: '⌘8',
template: (data) =>
`## 参考文献\n${data.refs.map((r) => `- ${r}`).join('\n')}`,
exampleData: {
refs: [
'Goodfellow, I., et al. (2016). Deep Learning. MIT Press.',
'Vaswani, A., et al. (2017). Attention Is All You Need. NIPS.',
],
},
},
];
import MarkdownIt from 'markdown-it';
import taskLists from 'markdown-it-task-lists';
import highlightjs from 'markdown-it-highlightjs';
import { getName, getToken } from '@/utils/infomationStorage.js';
import elDragDialog from '../../directive/el-drag-dialog'; // 可拖拽el-dialog
import { Markmap } from 'markmap-view';
import { transformer } from './markmap';
export default {
props: ['aiDrawer'],
directives: { elDragDialog },
data() {
return {
drawerVisible: false,
activeTab: 'chat',
activeTabList: [
{ name: 'AI', value: 'chat' },
// { name: '翻译', value: 'translate' },
// { name: '精要', value: 'summary' },
],
questionList: PRESET_QUESTIONS,
inputText: '',
graphState: false,
messages: [],
selectedQuote: '',
selectionToolsVisible: false,
selectionPosition: { top: '0px', left: '0px' },
isLoading: false,
tempSelection: '',
selectionTimer: null,
md: new MarkdownIt({
html: true,
xhtmlOut: true,
breaks: true,
linkify: true,
typographer: true,
})
.use(taskLists)
.use(highlightjs),
docId: '',
leafId: '',
uids: '',
chnids: '',
activeView: 'mindmap', // 当前视图状态
// 示例树形数据
treeData: [
{
label: '主题',
children: [
{ label: '分支1', children: [{ label: '节点1' }] },
{ label: '分支2', children: [{ label: '节点2' }] },
],
},
],
defaultProps: { children: 'children', label: 'label' },
mm: '',
aiGraphLoding: true,
graphInput: '',
graphText: '',
errorLoding: false,
};
},
mounted() {
window.addEventListener('scroll', this.handleScroll, true);
// 添加全局监听
document.addEventListener('mouseup', this.handleTextSelection);
let _this = this;
this.$eventBus.$on('getAiHepler', function (data) {
// console.log(data);
_this.inputText = data; // 直接使用预设问题的prompt
_this.sendMessage();
});
},
beforeDestroy() {
document.removeEventListener('mouseup', this.handleTextSelection);
clearTimeout(this.selectionTimer);
window.removeEventListener('scroll', this.handleScroll, true);
},
watch: {
aiDrawer: {
handler(to) {
this.drawerVisible = to;
},
immediate: true,
},
messages: {
handler() {
this.$nextTick(() => {
const container = document.querySelector('.markdown-body');
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth',
});
}
});
},
deep: true,
},
graphState: {
handler(to) {
if (to) {
} else {
this.graphStateClose();
}
},
immediate: true,
},
},
methods: {
async sendMessage() {
if (!this.inputText.trim() || this.isLoading) return;
const newMessage = {
text: this.inputText,
type: 'user',
quote: this.selectedQuote || '',
};
this.messages.push(newMessage);
const userInput = this.inputText;
this.inputText = '';
this.selectedQuote = '';
this.isLoading = true;
try {
const aiMessage = { text: '', type: 'ai' };
this.messages.push(aiMessage);
try {
await this.streamAIResponse(userInput, (chunk) => {
aiMessage.text = this.md.render(chunk);
});
} catch (error) {
this.$message.error(`请求失败: ${error.message}`);
this.messages.pop(); // 移除加载中的AI消息
}
} finally {
this.isLoading = false;
}
},
// 处理预设问题点击
handlePresetQuestion(item) {
if (this.isLoading) return;
if (item.label == '生成脑图') {
this.openMindMap('根据全文生成一个markdown的脑图');
} else {
this.inputText = item.prompt; // 直接使用预设问题的prompt
this.sendMessage();
}
},
async streamAIResponse(input, onChunk) {
let loadingInterval;
let aiMessage = null;
try {
aiMessage = this.messages[this.messages.length - 1];
let markdownContent = '';
let loadingAnimationActive = true;
let dotCount = 0;
const maxDots = 3;
// 启动加载动画定时器
loadingInterval = setInterval(() => {
if (loadingAnimationActive) {
dotCount = (dotCount + 1) % (maxDots + 1);
const dots = '.'.repeat(dotCount);
this.$set(aiMessage, 'text', `AI 正在思考中${dots}`);
}
}, 500);
const response = await fetch(`https://接口调用地址`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: input }),
});
if (!response.ok) {
clearInterval(loadingInterval);
this.$set(aiMessage, 'text', '请求处理异常,请稍后重试');
return; // 不再 rethrow error
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
while (true) {
const { done, value } = await reader.read();
if (done) break;
markdownContent += decoder.decode(value, { stream: true });
if (
loadingAnimationActive &&
markdownContent.indexOf('</think>') !== -1
) {
clearInterval(loadingInterval);
loadingAnimationActive = false;
}
if (!loadingAnimationActive) {
const cleanedContent = markdownContent.replace(
/<think>[\s\S]*?<\/think>/g,
''
);
this.$set(
aiMessage,
'text',
this.md.render(cleanedContent)
);
if (onChunk) {
onChunk(cleanedContent);
}
}
}
if (loadingAnimationActive) {
clearInterval(loadingInterval);
loadingAnimationActive = false;
}
// const cleanedContent = markdownContent.replace(
// /<think>[\s\S]*?<\/think>/g,
// ''
// );
const cleanedContent = this.applyCleanRules(
markdownContent.replace(/<think>[\s\S]*?<\/think>/g, '')
);
this.$set(aiMessage, 'text', this.md.render(cleanedContent));
this.$nextTick(() => {
const container = document.querySelector('.markdown-body');
if (container) {
container.scrollTop = container.scrollHeight;
}
});
} catch (error) {
if (loadingInterval) {
clearInterval(loadingInterval);
}
if (aiMessage) {
this.$set(aiMessage, 'text', '请求处理异常,请稍后重试');
}
this.handleStreamError(error);
// 不再 rethrow error,以确保错误提示保留在页面上
}
},
handleStreamError(error) {
const aiMessage = this.messages[this.messages.length - 1];
const errorMessage =
error.response?.status === 413
? '**图片解析过大,请尝试压缩后上传**'
: `**服务响应异常(错误码:${error.code || '未知'})**`;
this.$set(aiMessage, 'text', '请求处理异常,请稍后重试');
//错误码输出
// this.$set(aiMessage, 'text', this.md.render(errorMessage));
this.$message.error('请求处理异常,请稍后重试');
},
// 修改核心方法
handleTextSelection(e) {
clearTimeout(this.selectionTimer);
// 缓存关键事件属性
const targetElement = e?.target || document.activeElement;
const cachedSelection = window.getSelection().toString().trim();
this.selectionTimer = setTimeout(() => {
const selection = window.getSelection();
// 增强型六重验证
const isValid =
selection.rangeCount > 0 &&
!selection.isCollapsed &&
selection.toString().trim().length >= 1 && // 允许单字符选择
targetElement.closest('.ai-message');
if (isValid) {
const range = selection.getRangeAt(0);
const rect = this.getAdjustedRect(range);
this.updateToolPosition(rect);
this.selectionToolsVisible = true;
this.tempSelection = selection.toString();
} else {
this.selectionToolsVisible = false;
}
}, 50); // 优化响应时间
},
// 获取调整后的选区坐标
getAdjustedRect(range) {
const rect = range.getBoundingClientRect();
const scrollY =
window.scrollY || document.documentElement.scrollTop;
const scrollX =
window.scrollX || document.documentElement.scrollLeft;
return {
top: rect.top + scrollY - 35,
left: rect.left + scrollX,
width: rect.width,
height: rect.height,
};
},
updateToolPosition(rect) {
const viewportWidth = window.innerWidth;
const tooltipWidth = 180; // 根据实际宽度调整
const left = Math.max(
10,
Math.min(rect.left, viewportWidth - tooltipWidth - 10)
);
this.selectionPosition = {
top: `${rect.top + window.scrollY - 35}px`,
left: `${left}px`,
maxWidth: `${tooltipWidth}px`,
};
},
activeTabClick(active) {
this.activeTab = active;
// if (active == 'chat') {
// this.activeTab = active;
// } else {
// this.$message('该功能正在开发中');
// }
},
copySelection() {
this.$copyText(this.tempSelection)
.then(() => {
this.$message.success('复制成功');
})
.catch(() => {
this.$message.error('复制失败');
});
},
translateSelection() {
if (this.isLoading) return;
this.inputText = `请给出对当前文章${this.tempSelection}的概念解释`; // 直接使用预设问题的prompt
this.sendMessage();
// this.$message.info(
// `翻译功能开发中,文本内容: ${this.tempSelection}`
// );
},
regenerateResponse(index) {
if (this.isLoading) {
this.$message.warning('请等待内容生成完成');
return;
}
const originalMessage = this.messages[index - 1]?.text;
if (originalMessage) {
// this.messages.splice(index, 1);
this.inputText = originalMessage;
this.sendMessage();
}
},
regenerateResponseGraph() {
if (this.aiGraphLoding) {
this.$message.warning('请等待内容生成完成');
return;
}
this.update(this.graphInput);
},
openMindMap(params) {
// this.$message.info(`打开脑图功能开发中,消息索引: ${index}`);
this.graphInput = params;
this.graphState = true;
let _this = this;
this.$nextTick(() => {
_this.mm = Markmap.create(_this.$refs.markmap);
_this.update(params);
});
},
async update(input) {
// 动画控制变量声明
const maxDots = 3;
let dotCount = 0;
let loadingInterval = null;
let loadingAnimationActive = false;
try {
// 获取容器元素
const markmapContainer =
document.getElementById('aiGraphLoding');
console.log('加载容器:', markmapContainer); // 调试用
// 初始化加载状态
this.aiGraphLoding = true;
loadingAnimationActive = true;
// 启动加载动画
if (markmapContainer) {
markmapContainer.style.visibility = 'visible';
loadingInterval = setInterval(() => {
if (!loadingAnimationActive) return;
// 更新动态点号
dotCount = (dotCount + 1) % (maxDots + 1);
const dots = '.'.repeat(dotCount);
markmapContainer.textContent = `AI 正在思考中${dots}`;
}, 500);
}
// 等待DOM更新
await this.$nextTick();
await new Promise((resolve) => setTimeout(resolve, 100));
// 发起网络请求
let rawMarkdown = '';
const response = await fetch(`https://接口调用地址`, {
method: 'POST',
body: JSON.stringify({ message: input }),
});
if (!response.ok) {
throw new Error(`网络错误: ${response.statusText}`);
}
// 处理流式数据
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
while (true) {
const { done, value } = await reader.read();
if (done) break;
rawMarkdown += decoder.decode(value, { stream: true });
}
const cleanedMarkdown = this.applyCleanRules(
rawMarkdown.replace(/<think>[\s\S]*?<\/think>/g, '')
);
this.graphText = this.md.render(cleanedMarkdown);
// 生成脑图
const { root } = transformer.transform(cleanedMarkdown);
await this.mm.setData(root);
this.mm.fit();
if (this.mm) {
// 隐藏加载提示
const markmapContainer =
document.getElementById('aiGraphLoding');
if (markmapContainer) {
this.aiGraphLoding = false;
this.errorLoding = false;
markmapContainer.style.visibility = 'hidden';
markmapContainer.textContent = '';
}
}
} catch (error) {
this.aiGraphLoding = false;
this.errorLoding = true;
// 错误提示
const markmapContainer =
document.getElementById('aiGraphLoding');
if (markmapContainer) {
markmapContainer.textContent = '请求处理异常,请稍后重试';
}
} finally {
// 清除动画状态
loadingAnimationActive = false;
// 清除定时器
if (loadingInterval) {
clearInterval(loadingInterval);
loadingInterval = null;
}
}
},
// 动态加载脚本
loadScript(src) {
return new Promise((resolve) => {
if (document.querySelector(`script[src="${src}"]`))
return resolve();
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
document.head.appendChild(script);
});
},
// 视图切换
switchView(viewType) {
this.activeView = viewType;
if (viewType === 'mindmap') {
this.$nextTick(this.initMarkmap);
}
},
graphStateClose() {
this.graphState = false;
this.mm = '';
},
handleCopy(text) {
const sanitizedText = text.replace(/<[^>]+>/g, '');
this.$copyText(sanitizedText)
.then(() => {
this.$message.success('复制成功');
})
.catch(() => {
this.$message.error('复制失败');
});
},
// 取消请求
// cancelRequest() {
// if (this.abortController) {
// this.abortController.abort();
// this.currentRequestId = 0;
// this.$message.warning('当前请求已取消');
// }
// },
// 处理关闭事件-暂时不清理数据
handleDrawerClosed() {
// this.cancelRequest();
this.messages = [];
this.inputText = '';
this.selectedQuote = '';
this.selectionToolsVisible = false;
},
handleScroll() {
if (this.selectionToolsVisible) {
this.selectionToolsVisible = false;
window.getSelection().removeAllRanges();
}
},
drawerClose() {
this.drawerVisible = false;
this.$eventBus.$emit('aiDrawerEmit', false);
},
applyCleanRules(text) {
const rules = [
// 第一步:清除所有代码块(增强版)
{
pattern: /```[\s\S]*?```/g, // 匹配任意代码块(包括多行)
replace: (match) => {
// 保留代码块内非标记内容(可选)
return match
.replace(/```.*?\n/g, '') // 清除开始标记
.replace(/\n```/g, ''); // 清除结束标记
},
},
// 第二步:清除残留标记
// { pattern: /`{3,}/g, replace: '' }, // 处理多余的反引号
// // 第三步:其他清理规则
// { pattern: /<!--[\s\S]*?-->/g, replace: '' }, // 多行HTML注释
// { pattern: /\*\*(.*?)\*\*/g, replace: '$1' }, // 保留加粗内容
// { pattern: /!?$$.*?$$$$.*?$$/g, replace: '' }, // 清除链接/图片
// { pattern: /#{1,6}\s?/g, replace: '' }, // 清除标题标记
// 第四步:格式优化
// { pattern: /\n{3,}/g, replace: '\n\n' }, // 合并多余空行
// { pattern: /^\s+|\s+$/gm, replace: '' }, // 清除行首尾空格
];
return rules
.reduce(
(str, { pattern, replace }) =>
typeof replace === 'function'
? str.replace(pattern, replace)
: str.replace(pattern, replace),
text
)
.trim();
},
},
};
</script>
<style lang="less" scoped>
.tabs {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #c9def3;
height: 55px;
padding: 0 20px 0 24px;
.tabBlock {
display: flex;
.tabRow {
}
.tabClounm {
height: 55px;
padding: 0 8px;
// width: 66px;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 17px;
color: #333333;
text-align: center;
line-height: 54px;
border-bottom: 2px solid transparent;
cursor: pointer;
}
.activeTabClounm {
color: #0065cc;
border-bottom: 2px solid #0065cc;
}
}
}
.fixedQuestion {
text-align: center;
padding: 10px 0;
.fixedQuestionBlock {
cursor: pointer;
background: linear-gradient(0deg, #ffffff 0%, #f1f7fe 100%);
border-radius: 5px;
border: 1px solid #9bc9ff;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 14px;
color: #0065cc;
line-height: 34px;
width: 91px;
height: 35px;
display: inline-block;
text-align: center;
margin: 6px 9px 0 0;
transition: all 0.3s ease; /* 添加过渡动画 */
}
.fixedQuestionBlock:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* 添加阴影效果 */
background: linear-gradient(
0deg,
#ffffff 0%,
#e1effe 100%
); /* 可选:改变背景色 */
}
.fixedQuestionBlock:nth-child(4n) {
margin-right: 0;
}
}
.message-container {
position: relative;
margin: 15px 0;
min-height: 60px; // 确保有足够空间显示引用
/* 增加消息容器限制 */
.ai-message {
max-width: 100%;
overflow-wrap: break-word;
// white-space: pre-wrap;
/* 处理特殊标签 */
}
// 用户消息样式
&.user {
.message-row {
flex-direction: row-reverse;
}
.user-quote-bubble {
right: 40px;
left: auto;
}
}
// 用户引用气泡
.user-quote-bubble {
position: absolute;
top: -25px;
left: 40px;
z-index: 2;
width: calc(100% - 80px);
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
.quote-content {
display: flex;
align-items: flex-start;
max-height: 120px;
overflow-y: auto;
padding-right: 25px;
&::-webkit-scrollbar {
width: 4px;
background: #f5f5f5;
}
&::-webkit-scrollbar-thumb {
background: #cce0ff;
border-radius: 2px;
}
.quote-text {
font-size: 13px;
color: #666;
line-height: 1.5;
word-break: break-word;
}
}
.quote-close {
position: absolute;
top: 8px;
right: 8px;
padding: 0;
color: #999;
&:hover {
color: #0065cc;
}
}
}
// 消息行
.message-row {
display: flex;
align-items: flex-start;
position: relative;
z-index: 1;
// 头像样式
.avatar {
width: 30px;
height: 30px;
border-radius: 50%;
margin: 0 10px;
flex-shrink: 0;
&.ai {
height: 26px;
}
}
// 消息气泡
.message-bubble {
max-width: 335px;
position: relative;
line-height: 1.6;
// 用户消息样式
.message-content.user {
background: linear-gradient(90deg, #1d73e8 0%, #68acf8 100%);
border-radius: 5px;
color: #fff;
padding: 12px 16px;
font-size: 15px;
}
// AI消息样式
.message-content.ai {
background: #f2f3f5;
border-radius: 5px;
color: #333;
padding: 12px 16px;
// padding-bottom: 0;
font-size: 15px;
}
}
}
.action-buttons {
display: flex;
gap: 8px;
margin-top: 10px;
padding: 6px 0;
// border-top: 1px solid #eee;
justify-content: space-between;
.refDiv {
.refDiv1 {
margin-left: 4px;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 12px;
color: #5e5d5d;
line-height: 22px;
}
.refDiv2 {
margin-left: 9px;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 12px;
color: #888888;
line-height: 22px;
}
}
img {
cursor: pointer;
vertical-align: middle;
}
.el-button {
padding: 5px;
border-radius: 4px;
transition: all 0.2s;
&:hover {
transform: scale(1.1);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
}
}
}
.selection-tools {
position: fixed;
background: rgba(29, 115, 232, 1);
border-radius: 5px;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.15);
padding: 8px;
display: inline-flex;
align-items: center;
// gap: 6px;
z-index: 9999;
// opacity: 0;
transform: translateY(-10px) scale(0.95);
transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1);
/* 移除默认的pointer-events限制 */
pointer-events: auto !important;
/* 修正激活状态逻辑 */
&::after {
content: '';
position: absolute;
bottom: -11px;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #1d73e8;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
&.active {
opacity: 1;
transform: translateY(0) scale(1);
}
.tool-item {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s;
flex-direction: column;
height: 50px;
justify-content: space-between;
// &:hover {
// background: #f0f6ff;
// transform: translateY(-1px);
// .iconfont {
// color: #0065cc;
// }
// .tool-text {
// color: #003d82;
// }
// }
.tool-text {
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 12px;
color: #ffffff;
// line-height: 41px;
}
}
.tool-divider {
width: 1px;
height: 38px;
background: #3f86dd;
margin: 0 4px;
}
}
// 样式部分
.input-area {
position: relative;
padding: 10px;
.quote-floating {
position: absolute;
bottom: calc(100% + -5px);
// width: 200px;
margin: 0 10px;
left: 0;
right: 0;
z-index: 1000;
// background: #fff;
background: #ebebeb;
border-radius: 4px;
// box-shadow: 0 4px 12px rgba(0, 101, 204, 0.15);
// border: 1px solid #cce0ff;
transform: translateY(0);
opacity: 1;
transition: all 0.3s ease;
.quote-content {
display: flex;
padding: 12px;
max-height: 120px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 4px;
background: #f5f5f5;
}
&::-webkit-scrollbar-thumb {
background: #cce0ff;
border-radius: 2px;
}
}
.quote-header {
flex: 1;
display: flex;
align-items: flex-start;
}
.quote-icon {
font-size: 16px;
margin-right: 8px;
color: #0065cc;
flex-shrink: 0;
}
.quote-text {
font-size: 13px;
line-height: 1.5;
color: #333;
word-break: break-word;
}
.close-btn {
margin-left: 12px;
padding: 0;
align-self: flex-start;
color: #999;
transition: color 0.2s;
&:hover {
color: #0065cc;
}
}
.quote-footer {
padding: 8px 12px;
padding-top: 0;
// background: #f5f9ff;
background: #ebebeb;
// border-top: 1px solid #e8f1ff;
font-size: 12px;
color: #666;
border-radius: 0 0 4px 4px;
}
}
.upimg {
position: absolute;
cursor: pointer;
bottom: 35px;
right: 20px;
z-index: 1001; // 确保在引用框上层
}
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a8a8a8;
}
}
.disabled-block {
opacity: 0.6;
cursor: not-allowed !important;
filter: grayscale(0.8);
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.5);
}
}
#markmap-container {
width: 100%;
height: 600px;
}
.graphClass {
#aiGraphLoding {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
width: 100%;
height: 70%;
justify-content: center;
margin-top: 100px;
}
.view-switch {
margin-bottom: 15px;
display: flex;
align-items: center;
.el-button-group {
display: flex;
width: 100%;
justify-content: center;
}
}
.structure-view {
display: flex;
padding: 10px;
border-radius: 4px;
overflow: auto;
height: 602px;
text-align: left;
line-height: 1.5;
justify-content: center;
.ai-message-graph {
display: block;
}
}
.footerButton {
display: flex;
justify-content: center;
.refDiv {
img {
cursor: pointer;
vertical-align: middle;
}
.refDiv1 {
margin-left: 4px;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 12px;
color: #5e5d5d;
line-height: 22px;
}
.refDiv2 {
margin-left: 9px;
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 12px;
color: #888888;
line-height: 22px;
}
}
}
}
.cloaseDrawer {
height: 55px;
line-height: 55px;
cursor: pointer;
}
::v-deep {
.el-textarea__inner {
padding-right: 45px !important;
height: 77px;
background: rgba(235, 235, 235, 1);
resize: none !important;
}
.el-input__count {
background: rgba(235, 235, 235, 1) !important;
}
.el-drawer__wrapper {
}
.el-drawer {
position: relative !important;
// left: 100vw;
height: 93.5% !important;
top: 60px;
background: linear-gradient(
to bottom,
rgba(243, 248, 254, 1),
rgba(255, 255, 255, 1)
);
box-shadow: 0px 8px 10px 0px #e6e9ed;
border: 1px solid #c9def3;
// border-radius: 5px;
.markdown-body {
height: calc(100% - 255px);
overflow-y: auto;
padding: 10px;
}
.el-drawer__header {
display: none;
}
}
.AIdrawer {
width: 0px;
}
.el-dialog__headerbtn {
display: none;
}
.el-dialog__header {
// display: none;
}
//特殊处理
.language-plaintext {
display: inline-block;
max-width: 100%;
word-break: break-all;
background: #f5f7fa;
padding: 2px 4px;
border-radius: 3px;
}
}
</style>
<style lang="less">
.custom-drawer {
left: calc(100vw - 442px - 36px); // 位置设置
width: 460px; // 抽屉宽度设置
padding-left: 36px; // 阴影视觉宽度
.el-drawer {
width: 100% !important;
.el-drawer__header {
padding: 16px;
margin-bottom: 6px;
border-bottom: 1px solid #dcdfe6;
}
.el-drawer__header > :first-child {
font-size: 18px;
color: #303133;
font-weight: 600;
}
}
}
</style>
🛠️ 使用说明
- 安装依赖
Bash
npm install element-ui markdown-it markmap-view markdown-it-task-lists markdown-it-highlightjs - 文件结构
src/
├─ assets/
│ └─ detailImage/ # 存放所有图标
├─ components/
│ └─ AIAssistant.vue # 本组件 - 功能特点
开箱即用:复制粘贴即可运行
完整交互:包含消息发送、预设问题、脑图生成等核心功能
样式完整:已包含所有必要的样式定义
安全示例:使用模拟数据代替真实API调用 - 注意事项
图标路径…/…/assets/detailImage/需根据项目实际结构调整
实际使用时需替换streamAIResponse方法中的模拟数据为真实API调用
脑图功能需要安装markmap-view依赖
本代码经过完整测试,可直接集成到Vue项目中,确保所有交互和样式表现完整。如有任何使用问题,欢迎在评论区交流!
更多推荐
所有评论(0)