4 Commits

Author SHA1 Message Date
ccnetcore
0f90b8fd91 feat: 合并分支 2025-12-16 23:33:26 +08:00
ccnetcore
f175c3e5d6 Merge branch 'ai-hub' into markdown
# Conflicts:
#	Yi.Ai.Vue3/package.json
#	Yi.Ai.Vue3/pnpm-lock.yaml
#	Yi.Ai.Vue3/src/pages/chat/layouts/chatWithId/index.vue
2025-12-16 23:15:03 +08:00
ccnetcore
89b19cf541 fix: 修复渲染bug 2025-12-14 12:39:58 +08:00
chenchun
21ef1d51a6 feat: 测试markdown 2025-12-12 19:38:27 +08:00
14 changed files with 198 additions and 15533 deletions

View File

@@ -113,7 +113,7 @@
<!-- 加载动画容器 --> <!-- 加载动画容器 -->
<div id="yixinai-loader" class="loader-container"> <div id="yixinai-loader" class="loader-container">
<div class="loader-title">意心Ai 2.8</div> <div class="loader-title">意心Ai 2.8</div>
<div class="loader-subtitle">海外地址仅首次访问预计加载约10秒,无需梯子</div> <div class="loader-subtitle">海外地址仅首次访问预计加载约10秒</div>
<div class="loader-logo"> <div class="loader-logo">
<div class="pulse-box"></div> <div class="pulse-box"></div>
</div> </div>

View File

@@ -35,37 +35,32 @@
"@floating-ui/dom": "^1.7.2", "@floating-ui/dom": "^1.7.2",
"@floating-ui/vue": "^1.1.7", "@floating-ui/vue": "^1.1.7",
"@jsonlee_12138/enum": "^1.0.4", "@jsonlee_12138/enum": "^1.0.4",
"@shikijs/transformers": "^3.7.0",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0", "@vueuse/integrations": "^13.5.0",
"chatarea": "^6.0.3",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"deepmerge": "^4.3.1",
"dompurify": "^3.2.6",
"driver.js": "^1.3.6", "driver.js": "^1.3.6",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.10.4", "element-plus": "^2.10.4",
"fingerprintjs": "^0.5.3", "fingerprintjs": "^0.5.3",
"github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"hook-fetch": "^2.0.4-beta.1", "hook-fetch": "^2.0.4-beta.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mammoth": "^1.11.0", "mammoth": "^1.11.0",
"mermaid": "11.12.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pdfjs-dist": "^5.4.449", "pdfjs-dist": "^5.4.449",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1", "pinia-plugin-persistedstate": "^4.4.1",
"qrcode": "^1.5.4",
"radash": "^12.1.1",
"reset-css": "^5.0.2",
"vue": "^3.5.17",
"vue-element-plus-x": "1.3.7",
"vue-router": "4",
"xlsx": "^0.18.5",
"@shikijs/transformers": "^3.7.0",
"chatarea": "^6.0.3",
"deepmerge": "^4.3.1",
"dompurify": "^3.2.6",
"github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"lodash": "^4.17.21",
"mermaid": "11.12.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"property-information": "^7.1.0", "property-information": "^7.1.0",
"qrcode": "^1.5.4",
"radash": "^12.1.1",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
@@ -74,46 +69,21 @@
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2", "remark-rehype": "^11.1.2",
"reset-css": "^5.0.2",
"shiki": "^3.7.0", "shiki": "^3.7.0",
"ts-md5": "^2.0.1", "ts-md5": "^2.0.1",
"unified": "^11.0.5", "unified": "^11.0.5",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0",
"vue": "^3.5.17",
"vue-element-plus-x": "1.3.7",
"vue-router": "4",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^4.16.2", "@antfu/eslint-config": "^4.16.2",
"@changesets/cli": "^2.29.5", "@changesets/cli": "^2.29.5",
"@commitlint/config-conventional": "^19.8.1",
"@types/fingerprintjs__fingerprintjs": "^3.0.2",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"commitlint": "^19.8.1",
"cz-git": "^1.12.0",
"eslint": "^9.31.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"postcss": "8.4.31",
"postcss-html": "1.5.0",
"prettier": "^3.6.2",
"rollup-plugin-visualizer": "^6.0.3",
"sass-embedded": "^1.89.2",
"stylelint": "^16.21.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended-scss": "^15.0.1",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-standard-scss": "^15.0.1",
"typescript": "~5.8.3",
"typescript-api-pro": "^0.0.7",
"unocss": "66.3.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-env-typed": "^0.0.2",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^3.0.1",
"@chromatic-com/storybook": "^3.2.7", "@chromatic-com/storybook": "^3.2.7",
"@commitlint/config-conventional": "^19.8.1",
"@jsonlee_12138/markdown-it-mermaid": "0.0.6", "@jsonlee_12138/markdown-it-mermaid": "0.0.6",
"@storybook/addon-essentials": "^8.6.14", "@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-onboarding": "^8.6.14", "@storybook/addon-onboarding": "^8.6.14",
@@ -127,22 +97,52 @@
"@storybook/vue3": "^8.6.14", "@storybook/vue3": "^8.6.14",
"@storybook/vue3-vite": "^8.6.14", "@storybook/vue3-vite": "^8.6.14",
"@types/dom-speech-recognition": "^0.0.4", "@types/dom-speech-recognition": "^0.0.4",
"@types/fingerprintjs__fingerprintjs": "^3.0.2",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@vitejs/plugin-vue": "^6.0.0",
"@vitest/browser": "^3.2.4", "@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"@vue/tsconfig": "^0.7.0",
"commitlint": "^19.8.1",
"cz-git": "^1.12.0",
"eslint": "^9.31.0",
"esno": "^4.8.0", "esno": "^4.8.0",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"playwright": "^1.53.2", "playwright": "^1.53.2",
"postcss": "8.4.31",
"postcss-html": "1.5.0",
"prettier": "^3.6.2",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^6.0.3",
"sass": "^1.89.2", "sass": "^1.89.2",
"sass-embedded": "^1.89.2",
"storybook": "^8.6.14", "storybook": "^8.6.14",
"storybook-dark-mode": "^4.0.2", "storybook-dark-mode": "^4.0.2",
"stylelint": "^16.21.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended-scss": "^15.0.1",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-standard-scss": "^15.0.1",
"terser": "^5.43.1", "terser": "^5.43.1",
"typescript": "~5.8.3",
"typescript-api-pro": "^0.0.7",
"unocss": "66.3.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-dts": "^4.5.4", "vite-plugin-dts": "^4.5.4",
"vite-plugin-env-typed": "^0.0.2",
"vite-plugin-lib-inject-css": "^2.2.2", "vite-plugin-lib-inject-css": "^2.2.2",
"vitest": "^3.2.4" "vite-plugin-svg-icons": "^2.0.1",
"vitest": "^3.2.4",
"vue-tsc": "^3.0.1"
}, },
"config": { "config": {
"commitizen": { "commitizen": {

14992
Yi.Ai.Vue3/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,355 +0,0 @@
<template>
<div class="chat-message-list" :style="{ maxHeight }">
<div ref="scrollContainer" class="chat-message-list__container">
<div class="chat-message-list__content">
<div
v-for="(item, index) in list"
:key="item.key || index"
class="chat-message-item"
:class="{
'chat-message-item--user': item.placement === 'end',
'chat-message-item--assistant': item.placement === 'start'
}"
>
<!-- 消息头部 -->
<div v-if="$slots.header" class="chat-message-item__header">
<slot name="header" :item="item" :index="index" />
</div>
<!-- 消息主体 -->
<div class="chat-message-item__body">
<!-- 头像 -->
<div v-if="item.avatar" class="chat-message-item__avatar">
<img
:src="item.avatar"
:style="{
width: item.avatarSize || '40px',
height: item.avatarSize || '40px'
}"
alt="avatar"
>
</div>
<!-- 消息内容 -->
<div
class="chat-message-item__content"
:class="{
'chat-message-item__content--no-style': item.noStyle
}"
>
<slot name="content" :item="item" :index="index">
{{ item.content }}
</slot>
</div>
</div>
<!-- 消息底部 -->
<div v-if="$slots.footer" class="chat-message-item__footer">
<slot name="footer" :item="item" :index="index" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted, onUnmounted } from 'vue';
interface MessageItem {
key?: number;
avatar?: string;
avatarSize?: string;
placement?: 'start' | 'end';
content?: string;
noStyle?: boolean;
[key: string]: any;
}
interface Props {
list: MessageItem[];
maxHeight?: string;
}
const props = withDefaults(defineProps<Props>(), {
list: () => [],
maxHeight: '100%'
});
const scrollContainer = ref<HTMLDivElement | null>(null);
const autoScroll = ref(true); // 是否自动滚动到底部
const isUserScrolling = ref(false); // 用户是否正在手动滚动
let scrollTimeout: any = null;
let mutationObserver: MutationObserver | null = null;
let scrollRAF: number | null = null; // 用于存储 requestAnimationFrame ID
/**
* 检查是否滚动到底部
* 允许一定的误差范围5px
*/
function isScrolledToBottom(): boolean {
if (!scrollContainer.value) return false;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer.value;
return scrollHeight - scrollTop - clientHeight < 5;
}
/**
* 立即滚动到底部(用于内容快速变化时)
*/
function scrollToBottomImmediate() {
if (!scrollContainer.value || !autoScroll.value) return;
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight;
}
/**
* 平滑滚动到底部(使用 RAF 避免过度调用)
*/
function scrollToBottomSmooth() {
if (!scrollContainer.value || !autoScroll.value) return;
// 取消之前的 RAF避免重复调用
if (scrollRAF !== null) {
cancelAnimationFrame(scrollRAF);
}
scrollRAF = requestAnimationFrame(() => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight;
}
scrollRAF = null;
});
}
/**
* 滚动到底部(立即)
*/
function scrollToBottom() {
nextTick(() => {
if (scrollContainer.value) {
autoScroll.value = true; // 重新启用自动滚动
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight;
}
});
}
/**
* 处理滚动事件
* 检测用户是否手动滚动离开底部
*/
function handleScroll() {
if (!scrollContainer.value) return;
// 清除之前的定时器
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
// 标记用户正在滚动
isUserScrolling.value = true;
// 检查是否在底部
const atBottom = isScrolledToBottom();
if (atBottom) {
// 如果滚动到底部,启用自动滚动
autoScroll.value = true;
} else {
// 如果不在底部,禁用自动滚动
autoScroll.value = false;
}
// 300ms 后标记用户停止滚动
scrollTimeout = setTimeout(() => {
isUserScrolling.value = false;
}, 300);
}
/**
* 监听内容变化,自动滚动到底部
*/
function observeContentChanges() {
if (!scrollContainer.value) return;
const contentElement = scrollContainer.value.querySelector('.chat-message-list__content');
if (!contentElement) return;
// 创建 MutationObserver 监听内容变化
mutationObserver = new MutationObserver((mutations) => {
// 如果启用了自动滚动且用户没有在滚动,则滚动到底部
if (autoScroll.value && !isUserScrolling.value) {
// 检查是否有文本内容变化characterData这通常是 SSE 流式输出
const hasCharacterDataChange = mutations.some(
mutation => mutation.type === 'characterData'
);
if (hasCharacterDataChange) {
// SSE 流式输出时使用立即滚动,避免跳动
scrollToBottomImmediate();
} else {
// 其他情况使用平滑滚动
scrollToBottomSmooth();
}
}
});
// 监听子元素的变化和文本内容的变化
mutationObserver.observe(contentElement, {
childList: true, // 监听子元素的添加/删除
subtree: true, // 监听所有后代元素
characterData: true, // 监听文本内容变化
attributes: false // 不监听属性变化
});
}
// 组件挂载时初始化
onMounted(() => {
if (scrollContainer.value) {
scrollContainer.value.addEventListener('scroll', handleScroll, { passive: true });
}
// 初始化时滚动到底部
nextTick(() => {
scrollToBottom();
// 开始监听内容变化
observeContentChanges();
});
});
// 组件卸载时清理
onUnmounted(() => {
if (scrollContainer.value) {
scrollContainer.value.removeEventListener('scroll', handleScroll);
}
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
if (scrollRAF !== null) {
cancelAnimationFrame(scrollRAF);
}
if (mutationObserver) {
mutationObserver.disconnect();
}
});
// 暴露方法给父组件调用
defineExpose({
scrollToBottom
});
</script>
<style scoped lang="scss">
.chat-message-list {
width: 100%;
height: 100%;
overflow: hidden;
&__container {
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
/* 美化滚动条 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.3);
}
}
}
&__content {
width: 100%;
min-height: 100%;
display: flex;
flex-direction: column;
}
}
.chat-message-item {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 16px;
&__header {
width: 100%;
margin-bottom: 8px;
}
&__body {
display: flex;
gap: 12px;
width: 100%;
}
&__avatar {
flex-shrink: 0;
img {
border-radius: 50%;
object-fit: cover;
}
}
&__content {
flex: 1;
min-width: 0;
padding: 12px 16px;
background-color: #f5f5f5;
border-radius: 12px;
word-wrap: break-word;
overflow-wrap: break-word;
&--no-style {
background-color: transparent;
padding: 0;
}
}
&__footer {
width: 100%;
margin-top: 8px;
padding-left: 52px; /* 头像宽度 + gap */
}
/* 用户消息样式 */
&--user {
.chat-message-item__body {
flex-direction: row-reverse;
}
.chat-message-item__content {
background-color: #409eff;
color: white;
}
.chat-message-item__footer {
padding-left: 0;
padding-right: 52px;
text-align: right;
}
}
/* 助手消息样式 */
&--assistant {
.chat-message-item__body {
flex-direction: row;
}
.chat-message-item__content {
background-color: #f5f5f5;
}
}
}
</style>

View File

@@ -6,6 +6,8 @@ import { ElMessage } from 'element-plus';
import mammoth from 'mammoth'; import mammoth from 'mammoth';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useFilesStore } from '@/stores/modules/files'; import { useFilesStore } from '@/stores/modules/files';
// 配置 PDF.js worker - 使用稳定的 CDN // 配置 PDF.js worker - 使用稳定的 CDN
@@ -411,16 +413,6 @@ onChange(async (files) => {
// 处理图片文件 // 处理图片文件
if (isImage) { if (isImage) {
try { try {
// 控制参数:是否开启图片压缩
const enableImageCompression = true; // 这里可以设置为变量或从配置读取
let finalBlob: Blob = file;
let base64 = '';
let compressionLevel = 0;
const originalSize = (file.size / 1024).toFixed(2);
let finalSize = originalSize;
if (enableImageCompression) {
// 多级压缩策略:逐步降低质量和分辨率 // 多级压缩策略:逐步降低质量和分辨率
const compressionLevels = [ const compressionLevels = [
{ maxWidth: 800, maxHeight: 800, quality: 0.6 }, { maxWidth: 800, maxHeight: 800, quality: 0.6 },
@@ -429,6 +421,8 @@ onChange(async (files) => {
]; ];
let compressedBlob: Blob | null = null; let compressedBlob: Blob | null = null;
let base64 = '';
let compressionLevel = 0;
// 尝试不同级别的压缩 // 尝试不同级别的压缩
for (const level of compressionLevels) { for (const level of compressionLevels) {
@@ -441,14 +435,13 @@ onChange(async (files) => {
if (totalContentLength + estimatedLength <= MAX_TOTAL_CONTENT_LENGTH) { if (totalContentLength + estimatedLength <= MAX_TOTAL_CONTENT_LENGTH) {
// 满足限制,使用当前压缩级别 // 满足限制,使用当前压缩级别
totalContentLength += estimatedLength; totalContentLength += estimatedLength;
finalBlob = compressedBlob;
break; break;
} }
// 如果是最后一级压缩仍然超限,则跳过 // 如果是最后一级压缩仍然超限,则跳过
if (compressionLevel === compressionLevels.length) { if (compressionLevel === compressionLevels.length) {
const fileSizeMB = (file.size / 1024 / 1024).toFixed(2); const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
ElMessage.error(`${file.name} 图片内容过大,请压缩后上传`); ElMessage.error(`${file.name} (${fileSizeMB}MB) 即使压缩后仍超过总内容限制,已跳过`);
compressedBlob = null; compressedBlob = null;
break; break;
} }
@@ -460,24 +453,9 @@ onChange(async (files) => {
} }
// 计算压缩比例 // 计算压缩比例
finalSize = (finalBlob.size / 1024).toFixed(2); const originalSize = (file.size / 1024).toFixed(2);
console.log(`图片压缩: ${file.name} - 原始: ${originalSize}KB, 压缩后: ${finalSize}KB (级别${compressionLevel})`); const compressedSize = (compressedBlob.size / 1024).toFixed(2);
} console.log(`图片压缩: ${file.name} - 原始: ${originalSize}KB, 压缩后: ${compressedSize}KB (级别${compressionLevel})`);
else {
// 不开启压缩时,直接转换原始文件
base64 = await blobToBase64(file);
// 检查总长度限制
const estimatedLength = Math.floor(base64.length * 0.5);
if (totalContentLength + estimatedLength > MAX_TOTAL_CONTENT_LENGTH) {
const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
ElMessage.error(`${file.name} (${fileSizeMB}MB) 超过总长度限制,已跳过`);
continue;
}
totalContentLength += estimatedLength;
console.log(`图片未压缩: ${file.name} - 大小: ${originalSize}KB`);
}
arr.push({ arr.push({
uid: crypto.randomUUID(), uid: crypto.randomUUID(),
@@ -495,8 +473,8 @@ onChange(async (files) => {
}); });
} }
catch (error) { catch (error) {
console.error('处理图片失败:', error); console.error('压缩图片失败:', error);
ElMessage.error(`${file.name} 处理失败`); ElMessage.error(`${file.name} 压缩失败`);
continue; continue;
} }
} }
@@ -515,10 +493,9 @@ onChange(async (files) => {
// 至少保留1000字符才有意义 // 至少保留1000字符才有意义
finalContent = result.content.substring(0, remainingSpace); finalContent = result.content.substring(0, remainingSpace);
wasTruncated = true; wasTruncated = true;
} } else if (remainingSpace <= 1000) {
else if (remainingSpace <= 1000) {
const fileSizeKB = (file.size / 1024).toFixed(2); const fileSizeKB = (file.size / 1024).toFixed(2);
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总长度限制,已跳过`); ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
continue; continue;
} }
@@ -565,10 +542,9 @@ onChange(async (files) => {
if (result.content.length > remainingSpace && remainingSpace > 1000) { if (result.content.length > remainingSpace && remainingSpace > 1000) {
finalContent = result.content.substring(0, remainingSpace); finalContent = result.content.substring(0, remainingSpace);
wasTruncated = true; wasTruncated = true;
} } else if (remainingSpace <= 1000) {
else if (remainingSpace <= 1000) {
const fileSizeKB = (file.size / 1024).toFixed(2); const fileSizeKB = (file.size / 1024).toFixed(2);
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总长度限制,已跳过`); ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
continue; continue;
} }
@@ -615,10 +591,9 @@ onChange(async (files) => {
if (result.content.length > remainingSpace && remainingSpace > 1000) { if (result.content.length > remainingSpace && remainingSpace > 1000) {
finalContent = result.content.substring(0, remainingSpace); finalContent = result.content.substring(0, remainingSpace);
wasTruncated = true; wasTruncated = true;
} } else if (remainingSpace <= 1000) {
else if (remainingSpace <= 1000) {
const fileSizeKB = (file.size / 1024).toFixed(2); const fileSizeKB = (file.size / 1024).toFixed(2);
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总长度限制,已跳过`); ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
continue; continue;
} }
@@ -670,10 +645,9 @@ onChange(async (files) => {
if (finalContent.length > remainingSpace && remainingSpace > 1000) { if (finalContent.length > remainingSpace && remainingSpace > 1000) {
finalContent = finalContent.substring(0, remainingSpace); finalContent = finalContent.substring(0, remainingSpace);
truncated = true; truncated = true;
} } else if (remainingSpace <= 1000) {
else if (remainingSpace <= 1000) {
const fileSizeKB = (file.size / 1024).toFixed(2); const fileSizeKB = (file.size / 1024).toFixed(2);
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总长度限制,已跳过`); ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
continue; continue;
} }

View File

@@ -42,8 +42,12 @@ onMounted(() => {
</script> </script>
<template> <template>
<div> <div v-show="layout === 'blankPage'">
<component :is="LayoutComponent[layout]" /> <LayoutBlankPage />
<!-- <component :is="LayoutComponent[layout]" /> -->
</div>
<div v-show="layout !== 'blankPage'">
<LayoutVertical />
</div> </div>
</template> </template>

View File

@@ -2,6 +2,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AnyObject } from 'typescript-api-pro'; import type { AnyObject } from 'typescript-api-pro';
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble'; import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard'; import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking'; import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
import { ArrowLeftBold, ArrowRightBold, Document, Loading } from '@element-plus/icons-vue'; import { ArrowLeftBold, ArrowRightBold, Document, Loading } from '@element-plus/icons-vue';
@@ -12,8 +13,6 @@ import { Sender } from 'vue-element-plus-x';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { send } from '@/api'; import { send } from '@/api';
import ModelSelect from '@/components/ModelSelect/index.vue'; import ModelSelect from '@/components/ModelSelect/index.vue';
import YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
import ChatMessageList from '@/components/ChatMessageList/index.vue';
import { useGuideTourStore } from '@/stores'; import { useGuideTourStore } from '@/stores';
import { useChatStore } from '@/stores/modules/chat'; import { useChatStore } from '@/stores/modules/chat';
import { useFilesStore } from '@/stores/modules/files'; import { useFilesStore } from '@/stores/modules/files';
@@ -22,6 +21,7 @@ import { useUserStore } from '@/stores/modules/user';
import { getUserProfilePicture, systemProfilePicture } from '@/utils/user.ts'; import { getUserProfilePicture, systemProfilePicture } from '@/utils/user.ts';
import '@/styles/github-markdown.css'; import '@/styles/github-markdown.css';
import '@/styles/yixin-markdown.scss'; import '@/styles/yixin-markdown.scss';
import YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
type MessageItem = BubbleProps & { type MessageItem = BubbleProps & {
key: number; key: number;
@@ -50,7 +50,7 @@ const avatar = computed(() => {
const inputValue = ref(''); const inputValue = ref('');
const senderRef = ref<InstanceType<typeof Sender> | null>(null); const senderRef = ref<InstanceType<typeof Sender> | null>(null);
const bubbleItems = ref<MessageItem[]>([]); const bubbleItems = ref<MessageItem[]>([]);
const bubbleListRef = ref<InstanceType<typeof ChatMessageList> | null>(null); const bubbleListRef = ref<BubbleListInstance | null>(null);
const isSending = ref(false); const isSending = ref(false);
const { stream, loading: isLoading, cancel } = useHookFetch({ const { stream, loading: isLoading, cancel } = useHookFetch({
@@ -446,7 +446,7 @@ function handleImagePreview(url: string) {
<template> <template>
<div class="chat-with-id-container"> <div class="chat-with-id-container">
<div class="chat-warp"> <div class="chat-warp">
<ChatMessageList ref="bubbleListRef" :list="bubbleItems" max-height="calc(100vh - 240px)"> <BubbleList ref="bubbleListRef" :list="bubbleItems" max-height="calc(100vh - 240px)">
<template #header="{ item }"> <template #header="{ item }">
<Thinking <Thinking
v-if="item.reasoning_content" v-model="item.thinlCollapse" :content="item.reasoning_content" v-if="item.reasoning_content" v-model="item.thinlCollapse" :content="item.reasoning_content"
@@ -503,7 +503,7 @@ function handleImagePreview(url: string) {
</div> </div>
</div> </div>
</template> </template>
</ChatMessageList> </BubbleList>
<Sender <Sender
ref="senderRef" v-model="inputValue" class="chat-defaul-sender" data-tour="chat-sender" :auto-size="{ ref="senderRef" v-model="inputValue" class="chat-defaul-sender" data-tour="chat-sender" :auto-size="{
@@ -577,10 +577,10 @@ function handleImagePreview(url: string) {
} }
} }
:deep() { :deep() {
.chat-message-list { .el-bubble-list {
padding-top: 24px; padding-top: 24px;
} }
.chat-message-item { .el-bubble {
padding: 0 12px; padding: 0 12px;
padding-bottom: 24px; padding-bottom: 24px;
} }

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import type { MarkdownProps } from '../XMarkdownCore/shared/types';
import { useShiki } from '@components/XMarkdownCore/hooks/useShiki';
import { MarkdownRendererAsync } from '../XMarkdownCore';
import { useMarkdownContext } from '../XMarkdownCore/components/MarkdownProvider';
import { DEFAULT_PROPS } from '../XMarkdownCore/shared/constants';
const props = withDefaults(defineProps<MarkdownProps>(), DEFAULT_PROPS);
const slots = useSlots();
const customComponents = useMarkdownContext();
const colorReplacementsComputed = computed(() => {
return props.colorReplacements;
});
const needViewCodeBtnComputed = computed(() => {
return props.needViewCodeBtn;
});
useShiki();
</script>
<template>
<div class="elx-xmarkdown-container">
<MarkdownRendererAsync
v-bind="props"
:color-replacements="colorReplacementsComputed"
:need-view-code-btn="needViewCodeBtnComputed"
>
<template
v-for="(slot, name) in customComponents"
:key="name"
#[name]="slotProps"
>
<component :is="slot" v-bind="slotProps" />
</template>
<template v-for="(_, name) in slots" :key="name" #[name]="slotProps">
<slot :name="name" v-bind="slotProps" />
</template>
</MarkdownRendererAsync>
</div>
</template>

View File

@@ -11,8 +11,7 @@ import {
transformerNotationHighlight, transformerNotationHighlight,
transformerNotationWordHighlight transformerNotationWordHighlight
} from '@shikijs/transformers'; } from '@shikijs/transformers';
import { computed, h, reactive, ref, toValue, watch, onUnmounted } from 'vue'; import { computed, h, reactive, ref, toValue, watch } from 'vue';
import { debounce } from 'lodash-es';
import HighLightCode from '../../components/HighLightCode/index.vue'; import HighLightCode from '../../components/HighLightCode/index.vue';
import { SHIKI_SUPPORT_LANGS, shikiThemeDefault } from '../../shared'; import { SHIKI_SUPPORT_LANGS, shikiThemeDefault } from '../../shared';
import { useMarkdownContext } from '../MarkdownProvider'; import { useMarkdownContext } from '../MarkdownProvider';
@@ -89,29 +88,16 @@ async function generateHtml() {
} }
} }
// 使用防抖优化代码块渲染避免SSE流式更新时频繁高亮
const debouncedGenerateHtml = debounce(generateHtml, 100, {
leading: false,
trailing: true,
maxWait: 200
});
watch( watch(
() => props.raw?.content, () => props.raw?.content,
async content => { async content => {
if (content) { if (content) {
debouncedGenerateHtml(); await generateHtml();
} }
}, },
{ immediate: true } { immediate: true }
); );
// 清理防抖定时器
onUnmounted(() => {
debouncedGenerateHtml.cancel();
});
const runCodeOptions = reactive<ElxRunCodeProps>({ const runCodeOptions = reactive<ElxRunCodeProps>({
code: [], code: [],
content: '', content: '',

View File

@@ -4,11 +4,11 @@ import type { PluggableList } from 'unified';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import type { CustomAttrs, SanitizeOptions, TVueMarkdown } from './types'; import type { CustomAttrs, SanitizeOptions, TVueMarkdown } from './types';
import { defineComponent, shallowRef, toRefs, watch, onUnmounted } from 'vue'; import { computed, defineComponent, ref, shallowRef, toRefs, watch } from 'vue';
import { watchDebounced } from '@vueuse/core';
// import { useMarkdownContext } from '../components/MarkdownProvider'; // import { useMarkdownContext } from '../components/MarkdownProvider';
import { render } from './hast-to-vnode'; import { render } from './hast-to-vnode';
import { useMarkdownProcessor } from './useProcessor'; import { useMarkdownProcessor } from './useProcessor';
import { debounce } from 'lodash-es';
export type { CustomAttrs, SanitizeOptions, TVueMarkdown }; export type { CustomAttrs, SanitizeOptions, TVueMarkdown };
@@ -64,10 +64,24 @@ const vueMarkdownImpl = defineComponent({
sanitizeOptions sanitizeOptions
}); });
// 防抖优化控制markdown更新频率避免流式渲染时频繁触发
const debouncedMarkdown = ref(markdown.value);
watchDebounced(
markdown,
(val) => {
debouncedMarkdown.value = val;
},
{ debounce: 50, maxWait: 200 } // 50ms防抖最多200ms必须更新一次
);
// 缓存优化使用computed缓存解析结果避免重复计算
const hast = computed(() => {
const mdast = processor.value.parse(debouncedMarkdown.value);
return processor.value.runSync(mdast) as Root;
});
return () => { return () => {
const mdast = processor.value.parse(markdown.value); return render(hast.value, attrs, slots, customAttrs.value);
const hast = processor.value.runSync(mdast) as Root;
return render(hast, attrs, slots, customAttrs.value);
}; };
} }
}); });
@@ -94,42 +108,25 @@ const vueMarkdownAsyncImpl = defineComponent({
}); });
const hast = shallowRef<Root | null>(null); const hast = shallowRef<Root | null>(null);
const isProcessing = shallowRef(false);
// 防抖优化控制markdown更新频率
const debouncedMarkdown = ref(markdown.value);
const process = async (): Promise<void> => { const process = async (): Promise<void> => {
// 避免重复处理 const mdast = processor.value.parse(debouncedMarkdown.value);
if (isProcessing.value) return;
isProcessing.value = true;
try {
const mdast = processor.value.parse(markdown.value);
hast.value = (await processor.value.run(mdast)) as Root; hast.value = (await processor.value.run(mdast)) as Root;
} finally {
isProcessing.value = false;
}
}; };
// 使用防抖优化SSE流式更新场景 // 使用防抖watch避免频繁触发异步处理
// trailing: true 确保最后一次更新会执行 watchDebounced(
// leading: false 避免首次触发两次渲染 markdown,
const debouncedProcess = debounce(process, 50, { (val) => {
leading: false, debouncedMarkdown.value = val;
trailing: true,
maxWait: 200 // 最长等待200ms避免延迟过大
});
watch(
() => [markdown.value, processor.value],
() => {
debouncedProcess();
}, },
{ flush: 'post' } // 改为post在DOM更新后执行避免阻塞UI { debounce: 50, maxWait: 200 }
); );
// 清理防抖定时器 watch(() => [debouncedMarkdown.value, processor.value], process, { flush: 'post' });
onUnmounted(() => {
debouncedProcess.cancel();
});
await process(); await process();

View File

@@ -1,5 +1,5 @@
import type { TVueMarkdownProps } from '../'; import type { TVueMarkdownProps } from '@components/XMarkdownCore';
import type { CodeBlockHeaderExpose } from '../components/CodeBlock/shiki-header'; import type { CodeBlockHeaderExpose } from '@components/XMarkdownCore/components/CodeBlock/shiki-header';
import type { ElxRunCodeOptions } from '../components/RunCode/type'; import type { ElxRunCodeOptions } from '../components/RunCode/type';
import type { InitShikiOptions } from './shikiHighlighter'; import type { InitShikiOptions } from './shikiHighlighter';

View File

@@ -4,7 +4,8 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"],
"@components/*": ["src/vue-element-plus-y/components/*"]
}, },
"typeRoots": ["./node_modules/@types", "./types"], "typeRoots": ["./node_modules/@types", "./types"],
/* Linting */ /* Linting */

View File

@@ -11,7 +11,11 @@ declare module 'vue' {
AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default'] AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default']
APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default'] APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default']
CardFlipActivity: typeof import('./../src/components/userPersonalCenter/components/CardFlipActivity.vue')['default'] CardFlipActivity: typeof import('./../src/components/userPersonalCenter/components/CardFlipActivity.vue')['default']
ChatMessageList: typeof import('./../src/components/ChatMessageList/index.vue')['default'] CodeBlock: typeof import('./../src/components/YMarkdownCore/components/CodeBlock/index.vue')['default']
CodeLine: typeof import('./../src/components/YMarkdownCore/components/CodeLine/index.vue')['default']
CodeX: typeof import('./../src/components/YMarkdownCore/components/CodeX/index.vue')['default']
CopyCodeButton: typeof import('./../src/components/YMarkdownCore/components/CodeBlock/copy-code-button.vue')['default']
CustomLoading: typeof import('./../src/components/YMarkdownCore/components/RunCode/components/custom-loading.vue')['default']
DailyTask: typeof import('./../src/components/userPersonalCenter/components/DailyTask.vue')['default'] DailyTask: typeof import('./../src/components/userPersonalCenter/components/DailyTask.vue')['default']
DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default'] DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert'] ElAlert: typeof import('element-plus/es')['ElAlert']
@@ -25,8 +29,6 @@ declare module 'vue' {
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -59,9 +61,12 @@ declare module 'vue' {
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default'] FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
HighLightCode: typeof import('./../src/components/YMarkdownCore/components/HighLightCode/index.vue')['default']
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default'] IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
Indexl: typeof import('./../src/components/SupportModelProducts/indexl.vue')['default'] Indexl: typeof import('./../src/components/SupportModelProducts/indexl.vue')['default']
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default'] LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
Mermaid: typeof import('./../src/components/YMarkdownCore/components/Mermaid/index.vue')['default']
MermaidToolbar: typeof import('./../src/components/YMarkdownCore/components/Mermaid/MermaidToolbar.vue')['default']
ModeList: typeof import('./../src/components/modeList/index.vue')['default'] ModeList: typeof import('./../src/components/modeList/index.vue')['default']
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default'] ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default'] NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default']
@@ -75,6 +80,10 @@ declare module 'vue' {
RegistrationForm: typeof import('./../src/components/LoginDialog/components/FormLogin/RegistrationForm.vue')['default'] RegistrationForm: typeof import('./../src/components/LoginDialog/components/FormLogin/RegistrationForm.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
RunCode: typeof import('./../src/components/YMarkdownCore/components/RunCode/index.vue')['default']
RunCodeButton: typeof import('./../src/components/YMarkdownCore/components/CodeBlock/run-code-button.vue')['default']
RunCodeContent: typeof import('./../src/components/YMarkdownCore/components/RunCode/components/run-code-content.vue')['default']
RunCodeHeader: typeof import('./../src/components/YMarkdownCore/components/RunCode/components/run-code-header.vue')['default']
SupportModelList: typeof import('./../src/components/userPersonalCenter/components/SupportModelList.vue')['default'] SupportModelList: typeof import('./../src/components/userPersonalCenter/components/SupportModelList.vue')['default']
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default'] SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
SystemAnnouncementDialog: typeof import('./../src/components/SystemAnnouncementDialog/index.vue')['default'] SystemAnnouncementDialog: typeof import('./../src/components/SystemAnnouncementDialog/index.vue')['default']
@@ -83,6 +92,7 @@ declare module 'vue' {
UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default'] UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default']
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default'] VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default'] WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
YMarkdownAsync: typeof import('./../src/components/YMarkdownAsync/index.vue')['default']
} }
export interface GlobalDirectives { export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import('element-plus/es')['ElLoadingDirective']