提交 377f30c9 authored 作者: coderBryanFu's avatar coderBryanFu

update

上级 13b5b9ff
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -19,10 +19,15 @@
"license": "MIT",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@kangc/v-md-editor": "^2.3.18",
"@microsoft/fetch-event-source": "^2.0.1",
"@traptitech/markdown-it-katex": "^3.6.0",
"axios": "^1.12.2",
"echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0",
"element-plus": "^2.4.4",
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.0",
"vue": "^3.4.0",
"vue-router": "^4.2.5"
},
......
<template>
<div id="app">
<el-container>
<el-container class="wrapper">
<el-header>
<nav class="navbar">
<div class="nav-brand">
......@@ -66,17 +66,38 @@
<el-main class="main-container">
<router-view />
</el-main>
<div class="ai-btn" @click="openAiBox">
<div class="icon">
<img src="@/assets/icons/ai-icon.png" alt="" />
</div>
<div class="text">智能问答</div>
</div>
<div class="ai-dialog" v-if="isShowAiBox">
<AiBox @close="closeAiBox" />
</div>
</el-container>
</div>
</template>
<script setup>
import { ref } from "vue";
import { Monitor, House, User, Location, Document, Bell, Message, ArrowDown } from "@element-plus/icons-vue";
import { useRouter } from "vue-router";
import Breadcrumb from "@/components/BreadCrumb/index.vue";
import AiBox from "./components/AiBox.vue";
const router = useRouter();
const isShowAiBox = ref(false);
const closeAiBox = () => {
isShowAiBox.value = false;
};
const openAiBox = () => {
isShowAiBox.value = true;
};
const handleHomeCommand = command => {
router.push(command);
};
......@@ -134,6 +155,7 @@ body {
/* 移除宽度限制,让子页面自己控制布局 */
width: 100%;
height: 984px;
position: relative;
}
.nav-brand {
display: flex;
......@@ -251,6 +273,39 @@ body {
}
}
.wrapper {
position: relative;
.ai-btn {
position: absolute;
top: 50%;
right: 46px;
cursor: pointer;
.icon {
width: 96px;
height: 96px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-align: center;
}
}
.ai-dialog {
position: absolute;
right: 100px;
top: 50px;
z-index: 9999;
}
}
.el-header {
padding: 0;
height: 96px;
......
<template>
<div class="ai-wrapper">
<div class="header">
<div class="icon">
<img src="@/assets/icons/aiBox/ai-bg.png" alt="" />
<div class="inner-icon">
<img src="@/assets/icons/aiBox/ai-logo.png" alt="" />
</div>
</div>
<div class="title">智能问答</div>
<div class="close" @click="handleClose">
<img src="@/assets/icons/aiBox/close.png" alt="" />
</div>
</div>
<div class="main">
<div class="chat-content" ref="contentContainer">
<!-- 消息列表 -->
<div class="message-list">
<div v-for="(message, index) in messages" :key="index" :class="['message-item', message.type]">
<!-- AI 消息 -->
<div v-if="message.type === 'ai'" class="message ai-message">
<div class="avatar">AI</div>
<div class="bubble">
<div class="content markdown-content" v-html="renderMarkdown(message.content)"></div>
</div>
</div>
<!-- 用户消息 -->
<div v-else class="message user-message">
<div class="bubble">
<div class="content">{{ message.content }}</div>
</div>
<div class="avatar"></div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="message-item ai">
<div class="message ai-message">
<div class="avatar">AI</div>
<div class="bubble">
<div class="loading-indicator">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
<el-input type="textarea" :rows="3" v-model="question" placeholder="请输入问题开启智能问答" />
<div class="btn">
<div class="icon">
<img src="@/assets/icons/aiBox/idea.png" alt="" />
</div>
<div class="text">概括页面内容</div>
</div>
<div class="submit">
<img src="@/assets/icons/aiBox/submit.png" alt="" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, onMounted, onUnmounted } from "vue";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import MarkdownIt from "markdown-it";
import { ElMessage } from "element-plus";
const contentContainer = ref(null);
const userInput = ref("");
const isLoading = ref(false);
const abortController = ref(null);
// 消息数据
const messages = ref([
{
type: "user",
content: "你好"
},
{
type: "ai",
content: "您好!我是AI助手,有什么可以帮助您的吗?"
}
]);
// Markdown 渲染器
const md = new MarkdownIt();
// 渲染 markdown
const renderMarkdown = content => {
return md.render(content);
};
// 自动滚动到底部
const scrollToBottom = async () => {
await nextTick();
if (contentContainer.value) {
const container = contentContainer.value;
container.scrollTop = container.scrollHeight;
}
};
// 添加消息
const addMessage = (type, content) => {
messages.value.push({
type,
content,
timestamp: new Date().getTime()
});
scrollToBottom();
};
// 更新最后一条 AI 消息(用于流式输出)
const updateLastAIMessage = content => {
const lastMessage = messages.value[messages.value.length - 1];
if (lastMessage && lastMessage.type === "ai") {
lastMessage.content = content;
scrollToBottom();
}
};
// 使用 fetchEventSource 连接
// const connectSSE = async question => {
// // 添加用户消息
// addMessage("user", question);
// // 添加空的 AI 消息用于流式更新
// addMessage("ai", "");
// isLoading.value = true;
// // 创建 AbortController 用于取消请求
// abortController.value = new AbortController();
// try {
// await fetchEventSource("/api/chat-stream", {
// method: "POST",
// headers: {
// "Content-Type": "application/json"
// },
// body: JSON.stringify({
// question: question
// // 其他参数...
// }),
// signal: abortController.value.signal,
// onopen: async response => {
// console.log("SSE 连接已建立", response.status);
// if (response.status !== 200) {
// throw new Error(`请求失败: ${response.status}`);
// }
// },
// onmessage: event => {
// try {
// if (event.data === "[DONE]") {
// // 流式输出结束
// isLoading.value = false;
// return;
// }
// const data = JSON.parse(event.data);
// if (data.type === "content" && data.content) {
// // 流式更新内容
// updateLastAIMessage(prev => prev + data.content);
// } else if (data.type === "error") {
// throw new Error(data.message || "请求失败");
// }
// } catch (error) {
// console.error("解析 SSE 数据错误:", error);
// }
// },
// onclose: () => {
// console.log("SSE 连接已关闭");
// isLoading.value = false;
// },
// onerror: error => {
// console.error("SSE 连接错误:", error);
// ElMessage.error("连接失败,请重试");
// isLoading.value = false;
// // 不要抛出错误,否则会重试
// }
// });
// } catch (error) {
// console.error("SSE 请求失败:", error);
// if (error.name !== "AbortError") {
// ElMessage.error(error.message || "请求失败");
// }
// isLoading.value = false;
// }
// };
// 发送消息
const sendMessage = async () => {
const question = userInput.value.trim();
if (!question) {
ElMessage.warning("请输入问题");
return;
}
if (isLoading.value) {
ElMessage.warning("请等待上一条消息回复完成");
return;
}
userInput.value = "";
// await connectSSE(question);
};
// 停止生成
const stopGenerate = () => {
if (abortController.value) {
abortController.value.abort();
abortController.value = null;
isLoading.value = false;
ElMessage.info("已停止生成");
}
};
const emit = defineEmits(["close"]);
const handleClose = () => {
emit("close");
};
onMounted(() => {
scrollToBottom();
});
onUnmounted(() => {
if (abortController.value) {
abortController.value.abort();
}
});
</script>
<style lang="scss" scoped>
.ai-wrapper {
width: 548px;
height: 1048px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
background: rgba(255, 255, 255, 1);
.header {
position: relative;
display: flex;
height: 50px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
.icon {
width: 30px;
height: 28px;
margin-top: 11px;
margin-left: 14px;
position: relative;
img {
width: 100%;
height: 100%;
}
.inner-icon {
position: absolute;
top: 3px;
left: 6px;
width: 15px;
height: 11px;
img {
width: 100%;
height: 100%;
}
}
}
.title {
margin-top: 10px;
margin-left: 12px;
height: 30px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 30px;
}
.close {
position: absolute;
right: 22px;
top: 21px;
width: 12px;
height: 12px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
.main {
height: 830px;
// background: rgba(225, 225, 225, 0.5);
width: 520px;
margin: 10px auto;
overflow-x: hidden;
overflow-y: auto;
}
.footer {
position: absolute;
left: 22px;
bottom: 20px;
width: 504px;
height: 120px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
overflow: hidden;
.btn {
position: absolute;
left: 20px;
bottom: 15px;
width: 160px;
height: 36px;
box-sizing: border-box;
border: 1px solid rgba(5, 95, 194, 1);
border-radius: 18px;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
.icon {
width: 14px;
height: 14px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 30px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 30px;
}
}
.submit {
position: absolute;
right: 19px;
bottom: 15px;
width: 36px;
height: 36px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
}
:deep(.el-textarea__inner) {
border: none;
/* 去掉右下角斜线(调整 resize) */
resize: none;
/* 去掉焦点边框 */
outline: none;
box-shadow: none;
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 30px;
}
:deep(.el-textarea__inner::placeholder) {
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 30px;
}
/* 确保获得焦点时也没有边框 */
:deep(.el-textarea__inner:focus) {
border: none;
outline: none;
box-shadow: none;
}
/* 确保获得焦点时也没有边框 */
:deep(.el-textarea__inner:hover) {
border: none;
outline: none;
box-shadow: none;
}
/* 对话内容区域 */
.chat-content {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #fff;
}
.message-list {
max-width: 900px;
margin: 0 auto;
}
.message-item {
margin-bottom: 24px;
}
.message {
display: flex;
align-items: flex-start;
gap: 12px;
max-width: 100%;
}
.user-message {
justify-content: flex-end;
}
.ai-message {
justify-content: flex-start;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.user-message .avatar {
background: #409eff;
color: white;
order: 2;
}
.ai-message .avatar {
background: #8ac4ff;
color: white;
}
.bubble {
max-width: 100%;
padding: 10px 12px;
border-radius: 12px;
position: relative;
}
.user-message .bubble {
background: rgba(231, 243, 255, 1);
color: white;
border-radius: 20px 0 20px 20px;
}
.ai-message .bubble {
background: #f0f2f5;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
border-radius: 0 20px 20px 20px;
}
.content {
line-height: 1.6;
word-wrap: break-word;
}
.user-message .content {
color: rgba(5, 95, 194, 1);
}
/* Markdown 内容样式 */
.markdown-content :deep(h1) {
font-size: 1.4em;
margin: 0.8em 0 0.4em 0;
font-weight: 600;
}
.markdown-content :deep(h2) {
font-size: 1.2em;
margin: 0.8em 0 0.4em 0;
font-weight: 600;
}
.markdown-content :deep(h3) {
font-size: 1.1em;
margin: 0.8em 0 0.4em 0;
font-weight: 600;
}
.markdown-content :deep(p) {
margin: 0.4em 0;
}
.markdown-content :deep(code) {
background: rgba(175, 184, 193, 0.2);
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
font-family: "SFMono-Regular", Consolas, monospace;
}
.markdown-content :deep(pre) {
background: #f6f8fa;
padding: 12px;
border-radius: 6px;
overflow: auto;
margin: 1em 0;
}
.markdown-content :deep(pre code) {
background: none;
padding: 0;
}
.markdown-content :deep(blockquote) {
border-left: 4px solid #dfe2e5;
padding-left: 16px;
margin: 1em 0;
color: #6a737d;
}
.markdown-content :deep(ul),
.markdown-content :deep(ol) {
padding-left: 2em;
margin: 1em 0;
}
/* 加载指示器 */
.loading-indicator {
display: flex;
gap: 4px;
padding: 8px 0;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #999;
animation: bounce 1.4s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* 滚动条样式 */
.chat-content::-webkit-scrollbar {
width: 6px;
}
.chat-content::-webkit-scrollbar-track {
background: #f1f1f1;
}
.chat-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.chat-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
\ No newline at end of file
// composables/useMarkdownStream.js
import { ref, computed, nextTick } from 'vue'
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
// 引入 highlight.js 样式
import 'highlight.js/styles/github.css'
import mdKatex from '@traptitech/markdown-it-katex'
export function useMarkdownStream() {
const rawContent = ref('')
const createMd = () => {
const md = new MarkdownIt(
{
html: true,
breaks: true, // 将换行符转换为 <br>
linkify: true, // 自动链接 URL
typographer: true, // 启用排版扩展
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs" style="fontSize:14px"><code class="language-' + lang + '">' +
hljs.highlight(str, {
language: lang,
ignoreIllegals: true
}).value +
'</code></pre>'
} catch (err) {
console.warn(`代码高亮错误 (${lang}):`, err)
}
}
// 默认处理
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
}
}
)
// 自定义代码块渲染规则
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const lang = token.info.trim()
const code = token.content
// 使用 highlight.js 进行高亮
if (lang && hljs.getLanguage(lang)) {
try {
const highlighted = hljs.highlight(code, {
language: lang,
ignoreIllegals: true
})
return `
<div class="code-block-wrapper">
<div class="code-header">
<span class="code-lang">${lang}</span>
</div>
<pre class="hljs"><code class="language-${lang}">${highlighted.value}</code></pre>
</div>
`
} catch (err) {
console.warn(`代码高亮错误 (${lang}):`, err)
}
}
// 默认代码块
return `
<div class="code-block-wrapper">
<div class="code-header">
<span class="code-lang">${lang || 'text'}</span>
<button class="copy-btn" onclick="copyCode(this)">复制</button>
</div>
<pre class="hljs"><code>${md.utils.escapeHtml(code)}</code></pre>
</div>
`
}
// md.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
// md.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
// // 自定义渲染规则 - 修复中文排版
// md.renderer.rules.paragraph_open = (tokens, idx, options, env, self) => {
// return '<p style="margin: 1em 0; line-height: 1.6;">'
// }
// md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
// const token = tokens[idx]
// const level = token.tag.slice(1)
// const styles = {
// '1': 'font-size: 1.8em; margin: 1.2em 0 0.6em 0; font-weight: 700; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em;',
// '2': 'font-size: 1.5em; margin: 1.2em 0 0.6em 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em;',
// '3': 'font-size: 1.3em; margin: 1.2em 0 0.6em 0; font-weight: 600;',
// '4': 'font-size: 1.2em; margin: 1.2em 0 0.6em 0; font-weight: 600;',
// '5': 'font-size: 1.1em; margin: 1.2em 0 0.6em 0; font-weight: 600;',
// '6': 'font-size: 1em; margin: 1.2em 0 0.6em 0; font-weight: 600; color: #666;'
// }
// return `<${token.tag} style="${styles[level] || ''}">`
// }
// md.renderer.rules.list_item_open = (tokens, idx, options, env, self) => {
// return '<li style="margin: 0.5em 0; line-height: 1.6;">'
// }
// md.renderer.rules.table_open = (tokens, idx, options, env, self) => {
// return '<table style="border-collapse: collapse; width: 100%; margin: 1em 0; border: 1px solid #dfe2e5;">'
// }
// md.renderer.rules.th_open = (tokens, idx, options, env, self) => {
// return '<th style="border: 1px solid #dfe2e5; padding: 8px 12px; background: #f6f8fa; font-weight: 600; text-align: left;">'
// }
// md.renderer.rules.td_open = (tokens, idx, options, env, self) => {
// return '<td style="border: 1px solid #dfe2e5; padding: 8px 12px;">'
// }
// md.renderer.rules.blockquote_open = (tokens, idx, options, env, self) => {
// return '<blockquote style="border-left: 4px solid #dfe2e5; margin: 1em 0; padding-left: 1em; color: #666; background: #f8f9fa; padding: 12px 16px 12px 20px; border-radius: 4px;">'
// }
// md.renderer.rules.code_inline = (tokens, idx, options, env, self) => {
// const token = tokens[idx]
// return `<code style="background: #f6f8fa; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; color: #e74c3c;">${md.utils.escapeHtml(token.content)}</code>`
// }
// // 链接处理
// md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
// const token = tokens[idx]
// const hrefIndex = token.attrIndex('href')
// if (hrefIndex >= 0) {
// const href = token.attrs[hrefIndex][1]
// token.attrPush(['target', '_blank'])
// token.attrPush(['rel', 'noopener noreferrer'])
// token.attrPush(['style', 'color: #1890ff; text-decoration: none;'])
// }
// return self.renderToken(tokens, idx, options)
// }
// 预处理:修复带空格的强调符号
// const originalParse = md.parse.bind(md)
// md.parse = function (src, env) {
// // 预处理:修复 **文本 ** 这种带空格的粗体
// let processedSrc = src
// // 修复 **文本 ** -> **文本**
// .replace(/\*\*([^*]+?)\s+\*\*/g, '**$1**')
// // // 修复 __文本 __ -> __文本__
// // .replace(/__([^_]+?)\s+__/g, '__$1__')
// // // 修复 *文本 * -> *文本*
// // .replace(/\*([^*]+?)\s+\*/g, '*$1*')
// // // 修复 _文本 _ -> _文本_
// // .replace(/_([^_]+?)\s+_/g, '_$1_')
// return originalParse(processedSrc, env)
// }
// // 自定义渲染规则 - 支持所有 Markdown 格式
// md.renderer.rules.strong_open = () => '<strong style="font-weight: 600;">'
// md.renderer.rules.em_open = () => '<em style="font-style: italic;">'
// md.renderer.rules.heading_open = (tokens, idx) => {
// const token = tokens[idx]
// const level = token.tag.slice(1)
// const styles = {
// '1': 'font-size: 2em; margin: 0.67em 0; font-weight: 700; border-bottom: 2px solid #eaecef; padding-bottom: 0.3em;',
// '2': 'font-size: 1.5em; margin: 0.83em 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em;',
// '3': 'font-size: 1.17em; margin: 1em 0; font-weight: 600;',
// '4': 'font-size: 1em; margin: 1.33em 0; font-weight: 600;',
// '5': 'font-size: 0.83em; margin: 1.67em 0; font-weight: 600;',
// '6': 'font-size: 0.67em; margin: 2.33em 0; font-weight: 600; color: #666;'
// }
// return `<${token.tag} style="${styles[level] || ''}">`
// }
// md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
// const token = tokens[idx]
// const hrefIndex = token.attrIndex('href')
// if (hrefIndex >= 0) {
// token.attrPush(['target', '_blank'])
// token.attrPush(['rel', 'noopener noreferrer'])
// token.attrPush(['style', 'color: #1890ff; text-decoration: none;'])
// }
// return self.renderToken(tokens, idx, options)
// }
// md.renderer.rules.blockquote_open = () =>
// '<blockquote style="border-left: 4px solid #dfe2e5; margin: 1em 0; padding: 0.5em 1em; background: #f8f9fa; border-radius: 4px;">'
// md.renderer.rules.code_inline = (tokens, idx) => {
// const token = tokens[idx]
// return `<code style="background: #f3f4f6; padding: 0.2em 0.4em; border-radius: 3px; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.9em; color: #e53e3e;">${md.utils.escapeHtml(token.content)}</code>`
// }
// 自定义表格渲染规则
md.renderer.rules.table_open = () =>
'<div class="table-container"><table style="border-collapse: collapse; width: 100%; margin: 1em 0; border: 1px solid #dfe2e5;background: rgb(239 241 243); border-radius: 5px">'
md.renderer.rules.table_close = () => '</table></div>'
md.renderer.rules.thead_open = () => '<thead style="background: #e6e7e8">'
md.renderer.rules.th_open = () =>
'<th style="border: 1px solid #dfe2e5; padding: 12px; text-align: left; font-weight: 600;">'
md.renderer.rules.td_open = () =>
'<td style="border: 1px solid #dfe2e5; padding: 12px; text-align: left; vertical-align: top;">'
md.renderer.rules.th_close = () => '</th>'
md.renderer.rules.td_close = () => '</td>'
md.renderer.rules.tr_open = () => '<tr>'
md.renderer.rules.tr_close = () => '</tr>'
md.renderer.rules.thead_close = () => '</thead>'
md.renderer.rules.tbody_open = () => '<tbody>'
md.renderer.rules.tbody_close = () => '</tbody>'
return md
}
function preprocessMarkdown(content) {
if (!content) return ''
return content
// 1. 修复带空格的强调符号
.replace(/\*\*(\S.*?\S?)\s*\*\*/g, '**$1**')
.replace(/\*(\S.*?\S?)\s*\*/g, '*$1*')
.replace(/__(\S.*?\S?)\s*__/g, '__$1__')
.replace(/_(\S.*?\S?)\s*_/g, '_$1_')
// 2. 修复不配对的符号
.replace(/(\S)\*\*(\S)/g, '$1 **$2') // 文本**文本 -> 文本 **文本
.replace(/(\S)\*(\S)/g, '$1 *$2') // 文本*文本 -> 文本 *文本
// 3. 处理流式输出中的不完整标记
.replace(/\*\*([^*]*)$/, '**$1**') // 补全未闭合的粗体
.replace(/\*([^*]*)$/, '*$1*') // 补全未闭合的斜体
.replace(/__([^_]*)$/, '__$1__') // 补全未闭合的粗体(下划线)
.replace(/_([^_]*)$/, '_$1_') // 补全未闭合的斜体(下划线)
// 4. 统一换行符
.replace(/\\n/g, '\n')
.replace(/\r\n/g, '\n')
}
// 渲染 markdown
const renderedContent = computed(() => {
const md = createMd()
// 预处理内容
// const processedContent = preprocessMarkdown(rawContent.value)
return md.render(rawContent.value)
})
// 自动滚动
const scrollToBottom = async (scrollContainer) => {
await nextTick()
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight
}
}
// 更新内容并滚动
const updateContent = async (newContent, scrollContainer) => {
rawContent.value = newContent
await scrollToBottom(scrollContainer)
}
// 清空内容
const clearContent = () => {
rawContent.value = ''
}
return {
rawContent,
renderedContent,
updateContent,
clearContent
}
}
\ No newline at end of file
......@@ -2,7 +2,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
export function useContainerScroll(containerRef, options = {}) {
const {
scrollThreshold = 50,
scrollThreshold = 0,
showOnScrollDown = true,
hideOnTop = true,
useAnimationFrame = true
......
......@@ -188,7 +188,6 @@
<div class="text">{{ "风险信号" }}</div>
<div class="num">{{ warningList.length }}</div>
</div>
<div class="more">{{ "更多 +" }}</div>
</div>
<div class="box2-main">
<div class="box2-main-item" v-for="(item, index) in warningList" :key="index">
......@@ -249,6 +248,7 @@
<img src="./assets/images/box4-header-icon.png" alt="" />
</div>
<div class="header-title">{{ "社交媒体" }}</div>
<div class="more">{{ "更多 +" }}</div>
</div>
<div class="box4-main">
<div class="box4-main-item" v-for="(item, index) in messageList" :key="index">
......@@ -267,7 +267,6 @@
</div>
</div>
<DivideHeader class="divide3" :titleText="'统计概览'"></DivideHeader>
<div class="center-footer">
<div class="box5">
<div class="box5-header">
......@@ -278,14 +277,10 @@
<div class="box5-header-title">{{ "涉华法案数量" }}</div>
</div>
<div class="box5-header-right">
<div class="right-box">
<div class="icon1"></div>
<div class="text">{{ "提出法案" }}</div>
</div>
<div class="right-box">
<div class="icon2"></div>
<div class="text">{{ "通过法案" }}</div>
<div class="header-right-icon">
<img src="./assets/images/tips-icon.png" alt="" />
</div>
<div class="header-right-text">{{ "数据来源:美国国会官方网站" }}</div>
</div>
</div>
<div class="box5-main" id="chart1"></div>
......@@ -296,6 +291,12 @@
<img src="./assets/images/box6-header-icon.png" alt="" />
</div>
<div class="header-title">{{ "关键条款" }}</div>
<div class="box6-header-right">
<div class="header-right-icon">
<img src="./assets/images/tips-icon.png" alt="" />
</div>
<div class="header-right-text">{{ "数据来源:美国国会官方网站" }}</div>
</div>
</div>
<div class="box6-main" id="wordCloudChart"></div>
</div>
......@@ -344,6 +345,33 @@
</div>
<div class="home-main-footer-main">
<div class="main-item" v-for="(bill, index) in curBillList" :key="index">
<div
class="status-box"
:class="{
statusBox1: bill.status === '特别重大风险',
statusBox2: bill.status === '重大风险',
statusBox3: bill.status === '一般风险'
}"
>
<div
class="status-icon"
:class="{
statusIcon1: bill.status === '特别重大风险',
statusIcon2: bill.status === '重大风险',
statusIcon3: bill.status === '一般风险'
}"
></div>
<div
class="status-text"
:class="{
status1: bill.status === '特别重大风险',
status2: bill.status === '重大风险',
status3: bill.status === '一般风险'
}"
>
{{ bill.status }}
</div>
</div>
<div class="main-item-box1">
<img :src="bill.img" alt="" />
</div>
......@@ -351,29 +379,43 @@
{{ bill.billName }}
</div>
<div class="main-item-box3">{{ bill.introductionDate }}</div>
<div class="main-item-box4">
<span>{{ bill.yuan }}</span
><span>{{ bill.dangpai }}</span>
</div>
<div class="main-item-box5">
<div class="tag" v-for="(tag, idx) in bill.tagList" :key="idx">{{ tag }}</div>
</div>
</div>
</div>
<div class="home-main-footer-footer">
<div class="footer-left">
{{ `共${curBillList.length}项调查` }}
{{ `共${billList.length}项调查` }}
</div>
<div class="footer-right">
<el-pagination background layout="prev, pager, next" :total="curBillList.length" />
<el-pagination
@current-change="handleCurrentChange"
:pageSize="12"
:current-page="currentPage"
background
layout="prev, pager, next"
:total="billList.length"
/>
</div>
</div>
</div>
</div>
<div class="ai-btn">
<!-- <div class="ai-btn">
<div class="icon">
<img src="@/assets/icons/ai-icon.png" alt="" />
</div>
<div class="text">智能问答</div>
</div>
</div> -->
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { onMounted, ref, computed } from "vue";
import * as echarts from "echarts";
import router from "@/router/index";
......@@ -414,31 +456,34 @@ import Message1 from "./assets/images/message-icon1.png";
import Message2 from "./assets/images/message-icon2.png";
import Message3 from "./assets/images/message-icon3.png";
const currentPage = ref(1);
// 处理页码改变事件
const handleCurrentChange = page => {
currentPage.value = page;
};
const containerRef = ref(null);
const { isShow } = useContainerScroll(containerRef);
const billList = ref([]);
const curBillListIndex = ref(0);
const handleHomeCommand = command => {
router.push(command);
};
const hotBillList = ref([]); // 热门法案列表
const curHotBillListIndex = ref(0); // 当前热门法案索引
// 切换热门法案
const handleSwithCurBill = name => {
if (name === "left") {
if (curBillListIndex.value === 0) {
curBillListIndex.value = billList.value.length - 1;
if (curHotBillListIndex.value === 0) {
curHotBillListIndex.value = hotBillList.value.length - 1;
} else {
curBillListIndex.value--;
curHotBillListIndex.value--;
}
} else {
if (curBillListIndex.value === billList.value.length - 1) {
curBillListIndex.value = 0;
if (curHotBillListIndex.value === hotBillList.value.length - 1) {
curHotBillListIndex.value = 0;
} else {
curBillListIndex.value++;
curHotBillListIndex.value++;
}
}
curBill.value = billList.value[curBillListIndex.value];
curBill.value = hotBillList.value[curHotBillListIndex.value];
};
// 当前法案
......@@ -451,54 +496,13 @@ const curBill = ref({
introductionDate: ""
});
// 查看详情
const handleClickToDetail = () => {
window.sessionStorage.setItem("billId", curBill.value.billId);
router.push("/billLayout");
};
const billTrendList = ref([
{
title: "限制与中国合作",
no: "H.R.2670"
},
{
title: "限制中国获取技术",
no: "H.R.2471"
},
{
title: "禁止在中国建设半导体产能",
no: "H.R.5895"
},
{
title: "限制中国产燃油进口",
no: "S.870"
},
{
title: "禁止政府部门采购受控半导体或服务",
no: "H.R.6395"
},
{
title: "禁止向部分中国实体提供资金",
no: "H.R.3935"
},
{
title: "限制采购中国生产电池",
no: "H.R.7776"
},
{
title: "重视新兴中国技术公司威胁",
no: "H.R.3935"
},
{
title: "禁止卫星出口至中国",
no: "S.870"
},
{
title: "禁购华为设备",
no: "H.R.5895"
}
]);
// 风险信号
const warningList = ref([
{
title: "美国大而美法案落地,总统签署通过",
......@@ -527,121 +531,135 @@ const warningList = ref([
}
]);
const timelineList = ref([
{
title: "成为公法 No: 119-21。",
time: "2025-07-04",
status: 1
},
{
title: "总统签署",
time: "2025-07-04",
status: 0
},
{
title: "提交总统",
time: "2025-07-03",
status: 0
},
{
title: "重新审议动议搁置案无异议通过。",
time: "2025-07-03 14:31",
status: 0
},
{
title: "关于‘众议院同意参议院修正案’...",
time: "2025-07-02 14:31",
status: 0
}
]);
const areaList = ref([
{
name: "跨境电商",
status: 2
},
{
name: "新能源产业",
status: 4
},
{
name: "半导体产业",
status: 1
},
{
name: "关税",
status: 3
},
{
name: "光伏产业",
status: 2
}
]);
const curBillList = ref([
// 资源库 法案列表
const billList = ref([
{
billName: "大而美法案",
introductionDate: "2025年7月4日",
img: bill1
status: "特别重大风险",
tagList: ["集成电路", "人工智能"],
img: bill1,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "GENIUS稳定币法案",
introductionDate: "2025年7月5日",
img: bill2
status: "",
tagList: ["集成电路", "人工智能"],
img: bill2,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "美越贸易协议",
introductionDate: "2025年7月6日",
img: bill3
status: "",
tagList: ["集成电路", "人工智能"],
img: bill3,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "美越贸易协议",
introductionDate: "2025年7月7日",
img: bill4
status: "特别重大风险",
tagList: ["集成电路", "人工智能"],
img: bill4,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "汽车零部件25%关税实施规则",
introductionDate: "2025年7月10日",
img: bill5
status: "重大风险",
tagList: ["集成电路", "人工智能"],
img: bill5,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "汽车零部件25%关税实施规则",
introductionDate: "2025年7月12日",
img: bill6
status: "",
tagList: ["集成电路", "人工智能"],
img: bill6,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "小额豁免包裹政策调整",
introductionDate: "2025年7月14日",
img: bill7
status: "特别重大风险",
tagList: ["集成电路", "人工智能"],
img: bill7,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "NIH预算否决案",
introductionDate: "2025年7月15日",
img: bill8
status: "重大风险",
tagList: ["集成电路", "人工智能"],
img: bill8,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "得州国会选区重划法案",
introductionDate: "2025年7月17日",
img: bill9
status: "一般风险",
tagList: ["集成电路", "人工智能"],
img: bill9,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "美越贸易协议",
introductionDate: "2025年7月24日",
img: bill10
status: "",
tagList: ["集成电路", "人工智能"],
img: bill10,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "美越贸易协议",
introductionDate: "2025年8月4日",
img: bill11
status: "",
tagList: ["集成电路", "人工智能"],
img: bill11,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "美越贸易协议",
introductionDate: "2025年8月8日",
status: "特别重大风险",
tagList: ["集成电路", "人工智能"],
img: bill12,
yuan: "众议院",
dangpai: "共和党"
},
{
billName: "美越贸易协议",
introductionDate: "2025年8月8日",
img: bill12
status: "特别重大风险",
tagList: ["集成电路", "人工智能"],
img: bill12,
yuan: "众议院",
dangpai: "共和党"
}
]);
const releaseTime = ref("近一年发布");
// 当前展示法案列表
const curBillList = computed(() => {
const startIndex = (currentPage.value - 1) * 12;
const endIndex = startIndex + 12;
return billList.value.slice(startIndex, endIndex);
});
const releaseTime = ref("近一年发布"); // 发布时间
const releaseTimeList = ref([
{
......@@ -706,46 +724,6 @@ const handleClickCate = cate => {
handleGetBillsByType();
};
const navList = ref([
{
icon: headerIcon1,
activeIcno: headerIcon1,
name: "首页",
path: "/home"
},
{
icon: headerIcon2,
activeIcno: headerIcon2,
name: "国家",
path: "/country"
},
{
icon: headerIcon3,
activeIcno: headerIcon3,
name: "领域",
path: "/area"
},
{
icon: headerIcon4,
activeIcno: headerIcon4,
name: "要素",
path: "/home"
},
{
icon: headerIcon5,
activeIcno: headerIcon5,
name: "事件",
path: "/home"
}
]);
const activeNavIndex = ref(0);
const handleClickNav = (index, item) => {
activeNavIndex.value = index;
router.push(item.path);
};
// 绘制echarts图表
const setChart = (option, chartId) => {
let chartDom = document.getElementById(chartId);
......@@ -843,7 +821,7 @@ const handleGetHotBills = async () => {
try {
const res = await getHotBills();
console.log("热门法案", res);
billList.value = res.data;
hotBillList.value = res.data;
} catch (error) {
console.error(error);
}
......@@ -857,7 +835,7 @@ const handleGetBillsByType = async () => {
try {
const res = await getBillsByType(params);
console.log("根据法案类型获取法案列表", res);
curBillList.value = res.data.map(item => {
billList.value = res.data.map(item => {
return {
billId: item.billId,
billName: item.billName,
......@@ -894,7 +872,7 @@ const wordCloudData = ref([
onMounted(async () => {
handleGetHylyList();
await handleGetHotBills();
curBill.value = billList.value[0];
curBill.value = hotBillList.value[0];
// handleGetBillsByType();
let chart1 = getMultiLineChart(chart1Data.value.title, chart1Data.value.data[0].value, chart1Data.value.data[1].value);
setChart(chart1, "chart1");
......@@ -913,29 +891,6 @@ onMounted(async () => {
height: calc(100vh - 96px);
position: relative;
overflow-y: hidden;
.ai-btn {
position: absolute;
top: 50%;
right: 46px;
cursor: pointer;
.icon {
width: 96px;
height: 96px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-align: center;
}
}
.search-header {
width: 100%;
height: 144px;
......@@ -1521,15 +1476,6 @@ onMounted(async () => {
background: rgba(255, 255, 255, 0.3);
}
}
.more {
margin-top: 19px;
margin-left: 224px;
color: rgba(20, 89, 187, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 16px;
}
}
.box2-main {
height: 330px;
......@@ -1747,6 +1693,7 @@ onMounted(async () => {
display: flex;
box-sizing: border-box;
padding-left: 22px;
position: relative;
.header-icon {
margin-top: 15px;
width: 20px;
......@@ -1766,6 +1713,19 @@ onMounted(async () => {
font-weight: 700;
line-height: 22px;
}
.more {
width: 49px;
height: 24px;
position: absolute;
top: 14px;
right: 27px;
color: rgba(20, 89, 187, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
cursor: pointer;
}
}
.box4-main {
height: 402px;
......@@ -1869,37 +1829,25 @@ onMounted(async () => {
.box5-header-right {
display: flex;
justify-content: flex-end;
width: 178px;
height: 22px;
.right-box {
display: flex;
margin-top: 16px;
width: 89px;
height: 22px;
justify-content: flex-end;
.icon1 {
margin-top: 5px;
width: 12px;
height: 12px;
border-radius: 6px;
background: rgba(20, 89, 187, 1);
}
.icon2 {
margin-top: 5px;
width: 12px;
height: 12px;
border-radius: 6px;
background: rgba(250, 140, 22, 1);
}
.text {
margin-left: 5px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 22px;
gap: 8px;
height: 24px;
margin-top: 12px;
.header-right-icon {
margin-top: 4px;
width: 14px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.header-right-text {
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
}
.box5-main {
......@@ -1938,6 +1886,30 @@ onMounted(async () => {
font-weight: 700;
line-height: 22px;
}
.box6-header-right {
margin-left: 130px;
display: flex;
justify-content: flex-end;
gap: 8px;
height: 24px;
margin-top: 12px;
.header-right-icon {
margin-top: 4px;
width: 14px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.header-right-text {
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
}
.box6-main {
width: 452px;
......@@ -1949,12 +1921,11 @@ onMounted(async () => {
}
.home-main-footer {
width: 100%;
height: 1000px;
height: 1160px;
background: rgba(248, 249, 250, 1);
margin-bottom: 20px;
overflow: hidden;
margin-top: 36px;
.divide4 {
margin: 0 auto;
margin-top: 52px;
......@@ -2028,34 +1999,89 @@ onMounted(async () => {
.main-item {
width: 240px;
height: 320px;
border-radius: 4px;
border-radius: 10px;
background: linear-gradient(0deg, rgba(255, 255, 255, 1) 44%, rgba(255, 255, 255, 0) 100%);
box-shadow: 0px 0px 15px 0px rgba(22, 119, 255, 0.1);
background: #fff;
margin-bottom: 24px;
margin-right: 25px;
position: relative;
.status-box {
position: absolute;
top: 15px;
right: 16px;
height: 28px;
padding: 0 8px;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
.status-icon {
width: 4px;
height: 4px;
border-radius: 2px;
}
.statusIcon1 {
background: rgba(206, 79, 81, 1);
}
.statusIcon2 {
background: rgba(255, 149, 77, 1);
}
.statusIcon3 {
background: rgba(33, 129, 57, 1);
}
.status-text {
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
height: 28px;
line-height: 28px;
}
.status1 {
color: rgba(206, 79, 81, 1);
}
.status2 {
color: rgba(255, 149, 77, 1);
}
.status3 {
color: rgba(33, 129, 57, 1);
}
}
.statusBox1 {
background: rgba(206, 79, 81, 0.1);
}
.statusBox2 {
background: rgba(255, 149, 77, 0.1);
}
.statusBox3 {
background: rgba(33, 129, 57, 0.1);
}
.main-item-box1 {
margin-top: 20px;
margin-left: 45px;
width: 150px;
height: 200px;
box-sizing: border-box;
border: 1px solid rgba(240, 242, 244, 1);
img {
width: 100%;
height: 100%;
}
}
.main-item-box2 {
margin-top: 26px;
width: 188px;
height: 50px;
display: flex;
align-items: flex-end;
justify-content: center;
text-align: center;
height: 30px;
margin: 0 auto;
margin-top: -45px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-size: 18px;
font-weight: 700;
line-height: 30px;
line-height: 25px;
}
.main-item-box3 {
text-align: center;
......@@ -2066,6 +2092,38 @@ onMounted(async () => {
font-weight: 400;
line-height: 30px;
}
.main-item-box4 {
text-align: center;
height: 30px;
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 30px;
gap: 15px;
display: flex;
justify-content: center;
}
.main-item-box5 {
display: flex;
width: 188px;
height: 24px;
margin: 0 auto;
overflow: hidden;
justify-content: center;
gap: 8px;
.tag {
height: 24px;
border-radius: 4px;
background: rgba(231, 243, 255, 1);
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 24px;
padding: 0 8px;
}
}
}
}
.home-main-footer-footer {
......
......@@ -20,7 +20,12 @@ const getMultiLineChart = (dataX, dataY1, dataY2) => {
},
legend: {
data: ['提出法案', '通过法案'],
show: false
show: true,
textStyle: {
color: 'rgba(95, 101, 108, 1)',
fontFamily: 'Microsoft YaHei',
fontSize: '16px',
}
},
color: ['#1459bb', '#fa8c16'],
xAxis: [
......
......@@ -7,7 +7,26 @@
</div>
</div>
<div class="edit-box"></div>
<div class="btn-box"></div>
<div class="btn-box">
<div class="btn" @click="exportContent">
<div class="icon">
<img src="./assets/images/export-icon.png" alt="" />
</div>
<div class="text">{{ "导出" }}</div>
</div>
<div class="btn" @click="handleSwitchMode">
<div class="icon">
<img src="./assets/images/preview-icon.png" alt="" />
</div>
<div class="text">{{ isEditMode ? "预览" : "编辑" }}</div>
</div>
<div class="btn btn1">
<div class="icon">
<img src="./assets/images/save-icon.png" alt="" />
</div>
<div class="text text1">{{ "保存" }}</div>
</div>
</div>
</div>
<div class="writting-main">
<div class="sider">
......@@ -42,7 +61,7 @@
:class="{ tempActive: tempActiveIndex === index }"
v-for="(temp, index) in tempList"
:key="index"
@click="handleClickTemp(item,index)"
@click="handleClickTemp(item, index)"
>
<div class="header">
<div class="title">{{ temp.title }}</div>
......@@ -66,25 +85,135 @@
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div>
</div>
</div>
<div class="submit-btn">
<div class="submit-btn" @click="getStreamChat">
<div class="submit-icon">
<img src="./assets/images/ai.png" alt="" />
</div>
<div class="submit-text">生成报文</div>
</div>
</div>
<div class="content-box"></div>
<div class="main-box">
<div v-if="isEditMode" class="edit-panel">
<v-md-editor
v-model="reportContent"
height="calc(100% - 40px)"
:disabled-menus="[]"
@upload-image="handleUploadImage"
@save="handleSave"
left-toolbar="undo redo clear | h bold italic strikethrough quote | ul ol table hr | link image code | save"
right-toolbar="preview toc sync-scroll fullscreen"
/>
</div>
<div v-else class="content-box" ref="scrollContainer" v-html="renderedContent"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { ref, onMounted, onUnmounted } from "vue";
import { useMarkdownStream } from "@/hooks/useMarkdownStream";
import { fetchEventSource } from "@microsoft/fetch-event-source";
const writtingTitle = ref("");
import VMdEditor from "@kangc/v-md-editor";
import "@kangc/v-md-editor/lib/style/base-editor.css";
import vuepressTheme from "@kangc/v-md-editor/lib/theme/vuepress.js";
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
const descText = ref("");
// 引入 Prism 相关依赖
import Prism from "prismjs";
VMdEditor.use(vuepressTheme, {
Prism
});
const isEditMode = ref(false);
const handleSwitchMode = () => {
isEditMode.value = !isEditMode.value;
if (!isEditMode.value) {
console.log(reportContent.value);
updateContent(reportContent.value, scrollContainer.value);
}
};
// 保存处理函数
const handleSave = (text, html) => {
console.log("保存内容:", {
markdown: text,
html: html
});
reportContent.value = text;
// 实际保存逻辑
// saveToLocalStorage(text);
ElMessage.success("保存成功!");
};
// 保存到本地存储
const saveToLocalStorage = text => {
localStorage.setItem("markdown-content", text);
};
const abortController = ref(null);
const scrollContainer = ref(null);
const { renderedContent, updateContent } = useMarkdownStream();
const reportContent = ref("");
const getStreamChat = async (search, inputValue) => {
const params = {
query: "输出一篇报文"
};
abortController.value = new AbortController();
fetchEventSource("/sse/api/v1/chat/stream", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(params),
signal: abortController.value.signal,
openWhenHidden: true,
async onopen(res) {
console.log("流式回答开始", res);
},
async onmessage(res) {
let msgData = JSON.parse(res.data);
console.log("ress", msgData.content);
let str = msgData.content;
reportContent.value += str;
updateContent(reportContent.value, scrollContainer.value);
},
onerror(error) {
ElMessage({
message: "问题回答失败请重试",
type: "warning"
});
abortController.value.abort();
abortController.value = new AbortController();
throw new Error(error);
}
}).catch(error => {
ElMessage({
message: "问题回答失败请重试",
type: "warning"
});
abortController.value.abort();
abortController.value = new AbortController();
throw new Error(error);
});
};
const writtingTitle = ref("");
const descText = ref("");
const tabList = ref([
{
name: "写报",
......@@ -99,7 +228,6 @@ const tabList = ref([
active: false
}
]);
const tempList = ref([
{
title: "主题报",
......@@ -113,18 +241,37 @@ const tempList = ref([
const tempActiveIndex = ref(0);
const handleClickTemp = (item,index) => {
tempActiveIndex.value = index
}
const handleClickTemp = (item, index) => {
tempActiveIndex.value = index;
};
// 导出
const exportContent = () => {
const blob = new Blob([reportContent.value], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `markdown-${new Date().getTime()}.md`;
a.click();
URL.revokeObjectURL(url);
};
onMounted(() => {
// const str = '# 欢迎使用 Markdown 编辑器\n\n这是一个功能丰富的 Markdown 编辑器示例。\n\n## 功能特性\n\n- ✨ **实时预览**\n- 🎨 **代码高亮**\n- 📁 **图片上传**\n- 📊 **表格支持**\n- 🔗 **链接管理**\n\n```javascript\n// 示例代码\nconst message = "Hello Markdown!";\nconsole.log(message);\n```'
// updateContent(str, scrollContainer.value);
});
onUnmounted(() => {
if (abortController.value) {
abortController.value.abort();
}
});
</script>
<style lang="scss" scoped>
.writting-wrapper {
width: 100%;
height: 100%;
background: orange;
.writting-header {
height: 60px;
box-sizing: border-box;
......@@ -133,6 +280,7 @@ const handleClickTemp = (item,index) => {
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
display: flex;
position: relative;
.tab-box {
display: flex;
margin-left: 130px;
......@@ -156,6 +304,49 @@ const handleClickTemp = (item,index) => {
border-bottom: 4px solid rgba(5, 95, 194, 1);
}
}
.btn-box {
position: absolute;
top: 12px;
right: 39px;
display: flex;
gap: 12px;
.btn {
display: flex;
justify-content: center;
align-items: center;
width: 88px;
height: 36px;
gap: 8px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
cursor: pointer;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
width: 32px;
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
.text1 {
color: #fff;
}
}
.btn1 {
background: rgba(5, 95, 194, 1);
}
}
}
.writting-main {
display: flex;
......@@ -230,7 +421,7 @@ const handleClickTemp = (item,index) => {
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 4px;
position: relative;
cursor: pointer;
cursor: pointer;
.active-icon {
width: 24px;
height: 24px;
......@@ -335,6 +526,10 @@ const handleClickTemp = (item,index) => {
background: rgba(5, 95, 194, 1);
display: flex;
justify-content: center;
cursor: pointer;
&:hover {
background: rgba(5, 95, 194, 0.8);
}
.submit-icon {
/* AI-logo */
width: 21px;
......@@ -357,9 +552,28 @@ const handleClickTemp = (item,index) => {
}
}
}
.content-box {
.main-box {
flex: 1;
background: #f7f8f9;
.edit-panel {
width: calc(100% - 100px);
height: calc(100% - 40px);
margin: 20px 50px;
}
.content-box {
width: calc(100% - 100px);
height: calc(100% - 40px);
margin: 20px 50px;
// height: 500px;
overflow-y: auto;
padding: 20px;
border: 1px solid #dcdfe6;
border-radius: 8px;
background: #fafafa;
line-height: 1.7;
// font-size: 20px;
font-size: 16px;
}
}
}
}
......
......@@ -38,6 +38,11 @@ export default defineConfig({
target: 'http://8.140.26.4:9085/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
'/sse': {
target: 'http://192.168.26.70:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/sse/, '')
}
}
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论