This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,336 @@
<template>
<div :style="containerStyle" @submit.prevent class="rounded-lg border shadow-sm bg-card text-card-foreground">
<aurora-editor
v-model="localModelValue"
:extensions="mergedExtensions"
:hideToolbar="hideToolbar"
:hideMenubar="hideMenubar"
:disabled="disabled"
:maxHeight="maxHeight"
:minHeight="minHeight"
:output="output"
:placeholder="placeholder"
>
</aurora-editor>
</div>
</template>
<script setup lang="ts" name="aiEditorComponent">
import { computed, toRefs } from 'vue';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import request from '/@/utils/request';
import { Session } from '/@/utils/storage';
import {
Bold,
BulletList,
Italic,
BaseKit,
Underline,
Strike,
LineHeight,
Image,
History,
Heading,
CodeBlock,
FontSize,
Highlight,
Table,
Clear,
Blockquote,
Link,
Color,
Video,
OrderedList,
HorizontalRule,
Fullscreen,
TaskList,
MoreMark,
FormatPainter,
SlashCommand,
Indent,
locale,
ImportWord,
Columns,
TextAlign,
ImageUpload,
VideoUpload,
FontFamily,
FindAndReplace,
Code,
AI,
Preview,
Printer,
AuroraEditor,
} from 'aurora-editor';
import 'aurora-editor/style.css';
import other from '/@/utils/other';
import { useThemeConfig } from '/@/stores/themeConfig';
import { storeToRefs } from 'pinia';
// 获取主题配置
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
});
// 设置编辑器语言为简体中文
locale.setLang('zhHans');
// 定义组件 Props
const props = withDefaults(
defineProps<{
modelValue?: string; // 编辑器内容,用于 v-model
width?: string | number; // 编辑器容器宽度
maxHeight?: number; // 编辑器最大高度
minHeight?: number; // 编辑器最小高度
hideToolbar?: boolean; // 是否隐藏工具栏
hideMenubar?: boolean; // 是否隐藏菜单栏
disabled?: boolean; // 是否禁用编辑器
customExtensions?: any[]; // 自定义扩展
placeholder?: string; // 编辑器占位符文本
characterLimit?: number; // 编辑器字符数限制
enableImage?: boolean; // 是否启用图片功能
enableVideo?: boolean; // 是否启用视频功能
output?: string; // 是否启用输出功能
}>(),
{
modelValue: '',
width: '100%', // 默认宽度
maxHeight: 800, // 默认最大高度
minHeight: 800, // 默认最小高度
hideToolbar: false, // 默认不隐藏工具栏
hideMenubar: true, // 默认不隐藏菜单栏
disabled: false, // 默认不禁用
customExtensions: () => [], // 默认无自定义扩展
placeholder: '', // 默认无占位符
characterLimit: 50000, // 默认字符限制
enableImage: false, // 默认禁用图片功能
enableVideo: false, // 默认禁用视频功能
output: 'html', // 默认输出 HTML
}
);
// 提取props中的属性以直接在模板中使用避免使用props.xxx语法
const { hideToolbar, hideMenubar, disabled, maxHeight, minHeight, output } = toRefs(props);
// 使用计算属性创建一个双向绑定的本地变量避免直接修改props
const localModelValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
// 定义emit
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
// 计算编辑器容器样式
const containerStyle = computed(() => ({
width: typeof props.width === 'number' ? `${props.width}px` : props.width, // 处理数字和字符串类型的宽度
}));
// 定义默认的编辑器扩展
const defaultExtensions = computed(() => {
// Base configurations reused in both modes
const baseKitConfig = {
placeholder: {
showOnlyCurrent: true,
placeholder: props.placeholder,
},
characterCount: {
limit: props.characterLimit,
},
};
const aiConfig = {
completions: AICompletions,
shortcuts: [
{
label: '自定义选项',
children: [
{
label: '重写文案',
prompt:
'Rewrite this content with no spelling mistakes, proper grammar, and with more descriptive language, using best writing practices without losing the original meaning.',
},
],
},
],
};
// Core extensions used in both modes (except Fullscreen which is conditional)
const coreExtensions = [BaseKit.configure(baseKitConfig), AI.configure(aiConfig)];
// Base list for the full editor experience
let fullExtensions: any[] = [
...coreExtensions,
History,
Columns,
FormatPainter,
Clear,
Heading.configure({ spacer: true }),
FontSize,
FontFamily,
Bold,
Italic,
Underline,
Strike,
MoreMark,
Color.configure({ spacer: true }),
Highlight,
BulletList,
OrderedList,
TextAlign.configure({ types: ['heading', 'paragraph', 'image'], spacer: true }),
Indent,
LineHeight,
TaskList.configure({
spacer: true,
taskItem: {
nested: true,
},
}),
Link,
Blockquote,
SlashCommand,
HorizontalRule,
CodeBlock,
Table,
Code,
ImportWord.configure({
upload: handleFileUpload,
}),
FindAndReplace.configure({ spacer: true }),
Printer.configure({ pageTitle: getThemeConfig.value.globalTitle }),
Preview,
Fullscreen, // Fullscreen is also part of the full set
];
// Conditionally add image extensions
if (props.enableImage) {
fullExtensions.push(
Image, // 图片
ImageUpload.configure({
// 图片上传配置
upload: (file: File) => {
return new Promise<string>((resolve) => {
setTimeout(() => {
const fileUrl = URL.createObjectURL(file);
resolve(fileUrl);
}, 1000);
});
},
})
);
}
// Conditionally add video extensions
if (props.enableVideo) {
fullExtensions.push(
Video, // 视频
VideoUpload.configure({
// 视频上传配置
upload: handleFileUpload,
})
);
}
return fullExtensions;
});
// 合并默认扩展和自定义扩展
const mergedExtensions = computed(() => {
return [...defaultExtensions.value, ...props.customExtensions];
});
// 统一文件上传处理 (视频、Word导入等)
async function handleFileUpload(files: File[]) {
// 生成预览 URL
const uploads = files.map((file) => ({
src: URL.createObjectURL(file),
alt: file.name,
}));
// 模拟上传延迟
await new Promise((resolve) => setTimeout(resolve, 1000));
// 返回上传结果 (实际应返回服务器 URL)
return Promise.resolve(uploads);
}
// AI 补全处理函数 (使用 Server-Sent Events)
async function AICompletions(history: Array<{ role: string; content: string }> = [], signal?: AbortSignal): Promise<ReadableStream> {
try {
// 构建后端 API 地址
const backendApiUrl = `${request.defaults.baseURL}${other.adaptationUrl('/knowledge/completions/text')}`;
// 获取认证信息
const token = Session.getToken();
const tenantId = Session.getTenant();
// 准备发送到后端的消息 (假设后端处理系统提示)
const messagesToSend = history;
// 创建 ReadableStream 用于流式传输
let streamController: ReadableStreamDefaultController<any>;
const stream = new ReadableStream({
start(controller) {
streamController = controller; // 保存控制器以推送数据
},
cancel() {
// 处理流取消的逻辑 (例如,清理资源)
},
});
// 使用 fetchEventSource 发起 SSE 请求
fetchEventSource(backendApiUrl, {
method: 'POST', // 请求方法
headers: {
// 请求头
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`, // 认证 Token
'TENANT-ID': tenantId, // 租户 ID
},
body: JSON.stringify(messagesToSend), // 请求体 (发送的消息)
signal, // 用于中止请求的 AbortSignal
openWhenHidden: true, // 保持连接即使页面不可见
onmessage(event) {
const { message } = JSON.parse(event.data);
// 接收到消息时的回调
// 检查是否是结束标记
if (message === '[DONE]') {
streamController.close(); // 关闭流
return;
}
try {
// 获取消息内容
if (message) {
streamController.enqueue({
choices: [{ delta: { content: message } }], // 模拟 OpenAI 流格式
});
}
} catch (error) {
// 移除 console.error 以修复 linter 错误
streamController.error(error); // 将错误推送到流中
}
},
onclose() {
// 连接关闭时的回调
streamController.close(); // 关闭流
},
onerror(error) {
// 发生错误时的回调
streamController.error(error); // 将错误推送到流中
},
});
// 返回创建的 ReadableStream
return stream;
} catch (error) {
// 创建一个包含错误信息的流
return new ReadableStream({
start(controller) {
controller.error(error);
},
});
}
}
</script>