Files
FastapiAdmin/frontend/web/src/components/forms/fa-wang-editor/index.vue
T
zhangtao 53d2a9d13e chore: cleanup old frontend code and update project configs
This commit removes the deprecated frontend/web-old directory, updates various project configuration files including tsconfig, vite config, environment variables, and backend data models. It also fixes several API paths, adds tenant awareness to multiple models, and makes minor UI adjustments.
2026-05-27 01:12:03 +08:00

263 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- WangEditor 富文本编辑器 插件地址:https://www.wangeditor.com/ -->
<template>
<div class="editor-wrapper">
<Toolbar
class="editor-toolbar"
:editor="editorRef"
:mode="mode"
:defaultConfig="toolbarConfig"
/>
<Editor
:style="{ height: height, overflowY: 'hidden' }"
v-model="modelValue"
:mode="mode"
:defaultConfig="editorConfig"
@onCreated="onCreateEditor"
/>
</div>
</template>
<script setup lang="ts">
import "@wangeditor-next/editor/dist/css/style.css";
import { onBeforeUnmount, onMounted, shallowRef, computed } from "vue";
import { Editor, Toolbar } from "@wangeditor-next/editor-for-vue";
import { useUserStore } from "@stores";
import { request, EmojiText } from "@utils";
import { IDomEditor, IToolbarConfig, IEditorConfig } from "@wangeditor-next/editor";
import type { AxiosResponse } from "axios";
import { ElMessage } from "element-plus";
defineOptions({ name: "FaWangEditor" });
type InsertFnType = (url: string, alt: string, href: string) => void;
const { VITE_API_URL } = import.meta.env;
// Props 定义
interface Props {
/** 编辑器高度 */
height?: string;
/** 自定义工具栏配置 */
toolbarKeys?: string[];
/** 插入新工具到指定位置 */
insertKeys?: { index: number; keys: string[] };
/** 排除的工具栏项 */
excludeKeys?: string[];
/** 编辑器模式 */
mode?: "default" | "simple";
/** 占位符文本 */
placeholder?: string;
/** 上传配置 */
uploadConfig?: {
maxFileSize?: number;
maxNumberOfFiles?: number;
server?: string;
// 是否开启自定义上传
isCustomUpload?: boolean;
};
}
const props = withDefaults(defineProps<Props>(), {
height: "500px",
mode: "default",
placeholder: "请输入内容...",
excludeKeys: () => ["fontFamily"],
isCustomUpload: false,
});
const modelValue = defineModel<string>({ required: true });
// 编辑器实例
const editorRef = shallowRef<IDomEditor>();
const userStore = useUserStore();
// 常量配置
const DEFAULT_UPLOAD_CONFIG = {
maxFileSize: 3 * 1024 * 1024, // 3MB
maxNumberOfFiles: 10,
fieldName: "file",
allowedFileTypes: ["image/*"],
} as const;
// 计算属性:上传服务器地址
const uploadServer = computed(
() => props.uploadConfig?.server || `${VITE_API_URL}/common/file/upload`
);
// 合并上传配置
const mergedUploadConfig = computed(() => ({
...DEFAULT_UPLOAD_CONFIG,
...props.uploadConfig,
}));
// 工具栏配置
const toolbarConfig = computed((): Partial<IToolbarConfig> => {
const config: Partial<IToolbarConfig> = {};
// 完全自定义工具栏
if (props.toolbarKeys && props.toolbarKeys.length > 0) {
config.toolbarKeys = props.toolbarKeys;
}
// 插入新工具
if (props.insertKeys) {
config.insertKeys = props.insertKeys;
}
// 排除工具
if (props.excludeKeys && props.excludeKeys.length > 0) {
config.excludeKeys = props.excludeKeys;
}
return config;
});
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
placeholder: props.placeholder,
MENU_CONF: {
uploadImage: {
fieldName: mergedUploadConfig.value.fieldName,
maxFileSize: mergedUploadConfig.value.maxFileSize,
maxNumberOfFiles: mergedUploadConfig.value.maxNumberOfFiles,
allowedFileTypes: [...mergedUploadConfig.value.allowedFileTypes],
server: uploadServer.value,
headers: {
Authorization: userStore.accessToken,
},
onSuccess() {
ElMessage.success(`图片上传成功 ${EmojiText[200]}`);
},
onError(_file: unknown, err: any, res: any) {
console.error("图片上传失败:", err, res);
ElMessage.error(`图片上传失败 ${EmojiText[500]}`);
},
},
},
};
// 自定义上传
const uploadConfig = props.uploadConfig;
if (uploadConfig?.isCustomUpload && uploadConfig.server && editorConfig.MENU_CONF) {
const uploadServerUrl = uploadConfig.server;
editorConfig.MENU_CONF.uploadImage!.customUpload = async (file: File, insertFn: InsertFnType) => {
try {
const formData = new FormData();
formData.append(mergedUploadConfig.value.fieldName, file);
type UploadImagePayload = { url: string; alt?: string; href?: string };
const response = await request.post<
ApiResponse<UploadImagePayload>,
AxiosResponse<ApiResponse<UploadImagePayload>>
>(uploadServerUrl, formData, {
headers: {
"Content-Type": "multipart/form-data",
Authorization: userStore.accessToken,
},
});
const { url, alt = "", href = "" } = response.data.data ?? ({} as Record<string, string>);
if (!url) {
throw new Error("上传失败,请检查服务端配置");
}
insertFn(url, alt, href);
ElMessage.success(`图片上传成功 ${EmojiText[200]}`);
} catch (error) {
console.error("图片上传失败:", error);
ElMessage.error(`图片上传失败 ${EmojiText[500]}`);
}
};
}
// 编辑器创建回调
const onCreateEditor = (editor: IDomEditor) => {
editorRef.value = editor;
// 监听全屏事件
editor.on("fullScreen", () => {
// 全屏状态由 wangEditor 内部管理
});
// 确保在编辑器创建后应用自定义图标
applyCustomIcons();
};
// 应用自定义图标(带重试机制)
const applyCustomIcons = () => {
let retryCount = 0;
const maxRetries = 10;
const retryDelay = 100;
const tryApplyIcons = () => {
const editor = editorRef.value;
if (!editor) {
if (retryCount < maxRetries) {
retryCount++;
setTimeout(tryApplyIcons, retryDelay);
}
return;
}
// 获取当前编辑器的工具栏容器
const editorContainer = editor.getEditableContainer().closest(".editor-wrapper");
if (!editorContainer) {
if (retryCount < maxRetries) {
retryCount++;
setTimeout(tryApplyIcons, retryDelay);
}
return;
}
const toolbar = editorContainer.querySelector(".w-e-toolbar");
const toolbarButtons = editorContainer.querySelectorAll(".w-e-bar-item button[data-menu-key]");
if (toolbar && toolbarButtons.length > 0) {
return;
}
// 如果工具栏还没渲染完成,继续重试
if (retryCount < maxRetries) {
retryCount++;
setTimeout(tryApplyIcons, retryDelay);
} else {
console.warn("工具栏渲染超时,无法应用自定义图标 - 编辑器实例:", editor.id);
}
};
// 使用 requestAnimationFrame 确保在下一帧执行
requestAnimationFrame(tryApplyIcons);
};
// 暴露编辑器实例和方法
defineExpose({
/** 获取编辑器实例 */
getEditor: () => editorRef.value,
/** 设置编辑器内容 */
setHtml: (html: string) => editorRef.value?.setHtml(html),
/** 获取编辑器内容 */
getHtml: () => editorRef.value?.getHtml(),
/** 清空编辑器 */
clear: () => editorRef.value?.clear(),
/** 聚焦编辑器 */
focus: () => editorRef.value?.focus(),
});
// 生命周期
onMounted(() => {
// 图标替换已在 onCreateEditor 中处理
});
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor) {
editor.destroy();
}
});
</script>
<style lang="scss">
@use "@styles/fa-wang-editor";
</style>