refactor(ai): 重构AI模块目录结构并优化代码

将AI模块从module_application迁移到module_ai目录
新增聊天会话和消息的CRUD、服务和控制器
实现WebSocket聊天接口和前端组件
优化代码结构和性能,修复已知问题
This commit is contained in:
zhangtao
2026-02-15 01:24:38 +08:00
parent 815bb31d52
commit aad10384d4
32 changed files with 2789 additions and 1325 deletions
@@ -0,0 +1,141 @@
<template>
<div class="markdown-content" v-html="renderedContent"></div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import MarkdownIt from "markdown-it";
import markdownItHighlightjs from "markdown-it-highlightjs";
import hljs from "highlight.js";
import "highlight.js/styles/atom-one-light.css";
interface Props {
content: string;
maxLength?: number;
}
const props = withDefaults(defineProps<Props>(), {
maxLength: undefined,
});
const md: MarkdownIt = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
breaks: true,
highlight(str: string, lang: string): string {
if (lang && hljs.getLanguage(lang)) {
try {
return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
} catch {
return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`;
}
}
return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`;
},
}).use(markdownItHighlightjs);
const defaultRender =
md.renderer.rules.link_open ||
function (tokens: any[], idx: number, options: any, env: any, self: any) {
return self.renderToken(tokens, idx, options, env, self);
};
md.renderer.rules.link_open = function (
tokens: any[],
idx: number,
options: any,
env: any,
self: any
) {
tokens[idx].attrPush(["target", "_blank"]);
tokens[idx].attrPush(["rel", "noopener noreferrer"]);
return defaultRender(tokens, idx, options, env, self);
};
const renderedContent = computed(() => {
if (!props.content) return "";
const content =
props.maxLength && props.content.length > props.maxLength
? props.content.substring(0, props.maxLength) + "..."
: props.content;
return md.render(content);
});
</script>
<style lang="scss" scoped>
.markdown-content {
font-size: 14px;
line-height: 1.6;
color: var(--el-text-color-primary);
word-wrap: break-word;
:deep(pre) {
padding: 12px;
margin: 12px 0;
overflow-x: auto;
background: var(--el-fill-color-light);
border-radius: 6px;
code {
font-family: "Courier New", Courier, monospace;
font-size: 13px;
}
}
:deep(code) {
padding: 2px 6px;
font-family: "Courier New", Courier, monospace;
font-size: 13px;
background: var(--el-fill-color-light);
border-radius: 3px;
}
:deep(p) {
margin: 8px 0;
}
:deep(ul),
:deep(ol) {
padding-left: 24px;
margin: 8px 0;
}
:deep(li) {
margin: 4px 0;
}
:deep(a) {
color: var(--el-color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
:deep(blockquote) {
padding: 8px 16px;
margin: 12px 0;
background: var(--el-fill-color-light);
border-left: 4px solid var(--el-color-primary);
}
:deep(table) {
width: 100%;
margin: 12px 0;
border-collapse: collapse;
th,
td {
padding: 8px 12px;
border: 1px solid var(--el-border-color-light);
}
th {
font-weight: 600;
background: var(--el-fill-color-light);
}
}
}
</style>