优化仪表盘内容
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
export default class UploadAdapter {
|
||||
constructor(loader, options) {
|
||||
this.loader = loader;
|
||||
this.options = options;
|
||||
this.timeout = 60000; // 60秒超时
|
||||
}
|
||||
|
||||
upload() {
|
||||
return this.loader.file.then(
|
||||
(file) =>
|
||||
new Promise((resolve, reject) => {
|
||||
this._initRequest();
|
||||
this._initListeners(resolve, reject, file);
|
||||
this._sendRequest(file);
|
||||
this._initTimeout(reject);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (this.xhr) {
|
||||
this.xhr.abort();
|
||||
}
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
_initRequest() {
|
||||
const xhr = (this.xhr = new XMLHttpRequest());
|
||||
|
||||
xhr.open("POST", this.options.upload.uploadUrl, true);
|
||||
xhr.responseType = "json";
|
||||
}
|
||||
|
||||
_initListeners(resolve, reject, file) {
|
||||
const xhr = this.xhr;
|
||||
const loader = this.loader;
|
||||
const genericErrorText = `Couldn't upload file: ${file.name}.`;
|
||||
|
||||
xhr.addEventListener("error", () => {
|
||||
console.error("[UploadAdapter] Upload error for file:", file.name);
|
||||
reject(genericErrorText);
|
||||
});
|
||||
|
||||
xhr.addEventListener("abort", () => {
|
||||
console.warn("[UploadAdapter] Upload aborted for file:", file.name);
|
||||
reject();
|
||||
});
|
||||
|
||||
xhr.addEventListener("timeout", () => {
|
||||
console.error("[UploadAdapter] Upload timeout for file:", file.name);
|
||||
reject(`Upload timeout: ${file.name}. Please try again.`);
|
||||
});
|
||||
|
||||
xhr.addEventListener("load", () => {
|
||||
const response = xhr.response;
|
||||
|
||||
// 检查响应状态码
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
if (!response) {
|
||||
console.error("[UploadAdapter] Empty response for file:", file.name);
|
||||
reject(genericErrorText);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查业务状态码(假设 code=1 表示成功)
|
||||
if (response.code == 1 || response.code == undefined) {
|
||||
const url = response.data?.url || response.data?.src;
|
||||
if (!url) {
|
||||
console.error("[UploadAdapter] No URL in response for file:", file.name, response);
|
||||
reject("Upload succeeded but no URL returned");
|
||||
return;
|
||||
}
|
||||
resolve({ default: url });
|
||||
} else {
|
||||
const errorMessage = response.message || genericErrorText;
|
||||
console.error("[UploadAdapter] Upload failed for file:", file.name, "Error:", errorMessage);
|
||||
reject(errorMessage);
|
||||
}
|
||||
} else {
|
||||
console.error("[UploadAdapter] HTTP error for file:", file.name, "Status:", xhr.status);
|
||||
reject(`Server error (${xhr.status}): ${file.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 上传进度监听
|
||||
if (xhr.upload) {
|
||||
xhr.upload.addEventListener("progress", (evt) => {
|
||||
if (evt.lengthComputable) {
|
||||
loader.uploadTotal = evt.total;
|
||||
loader.uploaded = evt.loaded;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_initTimeout(reject) {
|
||||
// 清除之前的超时定时器(如果有)
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
// 设置新的超时定时器
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.xhr) {
|
||||
this.xhr.abort();
|
||||
reject(new Error("Upload timeout"));
|
||||
}
|
||||
}, this.timeout);
|
||||
}
|
||||
|
||||
_sendRequest(file) {
|
||||
// 设置请求超时
|
||||
this.xhr.timeout = this.timeout;
|
||||
|
||||
// Set headers if specified.
|
||||
const headers = this.options.upload.headers || {};
|
||||
const extendData = this.options.upload.extendData || {};
|
||||
// Use the withCredentials flag if specified.
|
||||
const withCredentials = this.options.upload.withCredentials || false;
|
||||
const uploadName = this.options.upload.uploadName || "file";
|
||||
|
||||
for (const headerName of Object.keys(headers)) {
|
||||
this.xhr.setRequestHeader(headerName, headers[headerName]);
|
||||
}
|
||||
|
||||
this.xhr.withCredentials = withCredentials;
|
||||
|
||||
const data = new FormData();
|
||||
for (const key of Object.keys(extendData)) {
|
||||
data.append(key, extendData[key]);
|
||||
}
|
||||
data.append(uploadName, file);
|
||||
|
||||
this.xhr.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
export function UploadAdapterPlugin(editor) {
|
||||
editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
|
||||
return new UploadAdapter(loader, editor.config._config);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div :style="{ '--editor-height': editorHeight }">
|
||||
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur"
|
||||
@focus="onFocus"></ckeditor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ClassicEditor,
|
||||
Alignment,
|
||||
AutoImage,
|
||||
Autoformat,
|
||||
BlockQuote,
|
||||
Bold,
|
||||
CodeBlock,
|
||||
DataFilter,
|
||||
DataSchema,
|
||||
Essentials,
|
||||
FindAndReplace,
|
||||
FontBackgroundColor,
|
||||
FontColor,
|
||||
FontFamily,
|
||||
FontSize,
|
||||
GeneralHtmlSupport,
|
||||
Heading,
|
||||
Highlight,
|
||||
HorizontalLine,
|
||||
Image,
|
||||
ImageCaption,
|
||||
ImageInsert,
|
||||
ImageResize,
|
||||
ImageStyle,
|
||||
ImageToolbar,
|
||||
ImageUpload,
|
||||
Indent,
|
||||
IndentBlock,
|
||||
Italic,
|
||||
Link,
|
||||
LinkImage,
|
||||
List,
|
||||
MediaEmbed,
|
||||
MediaEmbedToolbar,
|
||||
Mention,
|
||||
Paragraph,
|
||||
PasteFromOffice,
|
||||
RemoveFormat,
|
||||
SelectAll,
|
||||
ShowBlocks,
|
||||
SourceEditing,
|
||||
SpecialCharacters,
|
||||
SpecialCharactersArrows,
|
||||
SpecialCharactersCurrency,
|
||||
SpecialCharactersEssentials,
|
||||
SpecialCharactersLatin,
|
||||
SpecialCharactersMathematical,
|
||||
SpecialCharactersText,
|
||||
Style,
|
||||
Subscript,
|
||||
Superscript,
|
||||
Table,
|
||||
TableCaption,
|
||||
TableCellProperties,
|
||||
TableColumnResize,
|
||||
TableProperties,
|
||||
TableToolbar,
|
||||
TextTransformation,
|
||||
TodoList,
|
||||
Underline,
|
||||
Undo,
|
||||
WordCount,
|
||||
} from "ckeditor5";
|
||||
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
|
||||
import { UploadAdapterPlugin } from "./UploadAdapter.js";
|
||||
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useCurrentInstance } from "@/utils/tool";
|
||||
|
||||
import coreTranslations from "ckeditor5/translations/zh-cn.js";
|
||||
import "ckeditor5/ckeditor5.css";
|
||||
|
||||
const { proxy } = useCurrentInstance();
|
||||
|
||||
// 组件名称
|
||||
defineOptions({
|
||||
name: "scCkeditor"
|
||||
});
|
||||
|
||||
// Props 定义
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入内容……",
|
||||
},
|
||||
toolbar: {
|
||||
type: String,
|
||||
default: "basic",
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: "400px",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Emits 定义
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
// 工具栏配置常量
|
||||
const TOOLBARS = {
|
||||
full: [
|
||||
"sourceEditing",
|
||||
"undo",
|
||||
"redo",
|
||||
"heading",
|
||||
"style",
|
||||
"|",
|
||||
"superscript",
|
||||
"subscript",
|
||||
"removeFormat",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"link",
|
||||
"fontBackgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontColor",
|
||||
"|",
|
||||
"outdent",
|
||||
"indent",
|
||||
"alignment",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
"todoList",
|
||||
"|",
|
||||
"blockQuote",
|
||||
"insertTable",
|
||||
"imageInsert",
|
||||
"mediaEmbed",
|
||||
"highlight",
|
||||
"horizontalLine",
|
||||
"selectAll",
|
||||
"showBlocks",
|
||||
"specialCharacters",
|
||||
"codeBlock",
|
||||
"findAndReplace",
|
||||
],
|
||||
basic: [
|
||||
"sourceEditing",
|
||||
"undo",
|
||||
"redo",
|
||||
"heading",
|
||||
"|",
|
||||
"removeFormat",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"link",
|
||||
"fontBackgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontColor",
|
||||
"|",
|
||||
"outdent",
|
||||
"indent",
|
||||
"alignment",
|
||||
"bulletedList",
|
||||
"numberedList",
|
||||
"todoList",
|
||||
"|",
|
||||
"insertTable",
|
||||
"imageInsert",
|
||||
"mediaEmbed",
|
||||
],
|
||||
simple: [
|
||||
"undo",
|
||||
"redo",
|
||||
"heading",
|
||||
"|",
|
||||
"removeFormat",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"link",
|
||||
"fontBackgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontColor",
|
||||
"|",
|
||||
"insertTable",
|
||||
"imageInsert",
|
||||
"mediaEmbed",
|
||||
],
|
||||
};
|
||||
|
||||
// 插件配置常量
|
||||
const PLUGINS = [
|
||||
Alignment,
|
||||
AutoImage,
|
||||
Autoformat,
|
||||
BlockQuote,
|
||||
Bold,
|
||||
CodeBlock,
|
||||
DataFilter,
|
||||
DataSchema,
|
||||
Essentials,
|
||||
FindAndReplace,
|
||||
FontBackgroundColor,
|
||||
FontColor,
|
||||
FontFamily,
|
||||
FontSize,
|
||||
GeneralHtmlSupport,
|
||||
Heading,
|
||||
Highlight,
|
||||
HorizontalLine,
|
||||
Image,
|
||||
ImageCaption,
|
||||
ImageInsert,
|
||||
ImageResize,
|
||||
ImageStyle,
|
||||
ImageToolbar,
|
||||
ImageUpload,
|
||||
Indent,
|
||||
IndentBlock,
|
||||
Italic,
|
||||
Link,
|
||||
LinkImage,
|
||||
List,
|
||||
MediaEmbed,
|
||||
MediaEmbedToolbar,
|
||||
Mention,
|
||||
Paragraph,
|
||||
PasteFromOffice,
|
||||
RemoveFormat,
|
||||
SelectAll,
|
||||
ShowBlocks,
|
||||
SourceEditing,
|
||||
SpecialCharacters,
|
||||
SpecialCharactersArrows,
|
||||
SpecialCharactersCurrency,
|
||||
SpecialCharactersEssentials,
|
||||
SpecialCharactersLatin,
|
||||
SpecialCharactersMathematical,
|
||||
SpecialCharactersText,
|
||||
Style,
|
||||
Subscript,
|
||||
Superscript,
|
||||
Table,
|
||||
TableCaption,
|
||||
TableCellProperties,
|
||||
TableColumnResize,
|
||||
TableProperties,
|
||||
TableToolbar,
|
||||
TextTransformation,
|
||||
TodoList,
|
||||
Underline,
|
||||
Undo,
|
||||
WordCount,
|
||||
UploadAdapterPlugin,
|
||||
];
|
||||
|
||||
// 响应式数据
|
||||
const editorData = ref("");
|
||||
const editorHeight = ref(props.height);
|
||||
const editor = ClassicEditor;
|
||||
|
||||
// 编辑器配置
|
||||
const editorConfig = computed(() => ({
|
||||
language: { ui: "zh-cn", content: "zh-cn" },
|
||||
translations: [coreTranslations],
|
||||
plugins: PLUGINS,
|
||||
toolbar: {
|
||||
shouldNotGroupWhenFull: true,
|
||||
items: TOOLBARS[props.toolbar] || TOOLBARS.basic,
|
||||
},
|
||||
placeholder: props.placeholder,
|
||||
image: {
|
||||
styles: ["alignLeft", "alignCenter", "alignRight"],
|
||||
toolbar: [
|
||||
"imageTextAlternative",
|
||||
"toggleImageCaption",
|
||||
"|",
|
||||
"imageStyle:alignLeft",
|
||||
"imageStyle:alignCenter",
|
||||
"imageStyle:alignRight",
|
||||
"|",
|
||||
"linkImage",
|
||||
],
|
||||
},
|
||||
mediaEmbed: {
|
||||
previewsInData: true,
|
||||
providers: [
|
||||
{
|
||||
name: "mp4",
|
||||
url: /\.(mp4|avi|mov|flv|wmv|mkv)$/i,
|
||||
html: match => {
|
||||
const url = match["input"];
|
||||
return ('<video controls width="100%" height="100%" src="' + url + '"></video>')
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
fontSize: {
|
||||
options: [10, 12, 14, 16, 18, 20, 22, 24, 26, 30, 32, 36],
|
||||
},
|
||||
style: {
|
||||
definitions: [
|
||||
{
|
||||
name: "Article category",
|
||||
element: "h3",
|
||||
classes: ["category"],
|
||||
},
|
||||
{
|
||||
name: "Info box",
|
||||
element: "p",
|
||||
classes: ["info-box"],
|
||||
},
|
||||
],
|
||||
},
|
||||
upload: {
|
||||
uploadUrl: proxy?.$API?.common?.upload?.url || "",
|
||||
withCredentials: false,
|
||||
extendData: { type: "images" },
|
||||
headers: {
|
||||
Authorization: "Bearer " + proxy?.$TOOL?.data?.get("TOKEN"),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
editorData.value = newVal ?? "";
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听 height 变化
|
||||
watch(
|
||||
() => props.height,
|
||||
(newVal) => {
|
||||
editorHeight.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 移除图片宽高的正则替换函数
|
||||
const stripImageDimensions = (html) => {
|
||||
return html.replace(/<img[^>]*>/gi, (match) => {
|
||||
return match
|
||||
.replace(/width="[^"]*"/gi, "")
|
||||
.replace(/height="[^"]*"/gi, "");
|
||||
});
|
||||
};
|
||||
|
||||
// 失去焦点事件 - 移除图片的固定宽高,避免响应式布局问题
|
||||
const onBlur = () => {
|
||||
const cleanedData = stripImageDimensions(editorData.value);
|
||||
editorData.value = cleanedData;
|
||||
emit("update:modelValue", cleanedData);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--ck-z-panel: 9999;
|
||||
}
|
||||
|
||||
.ck-content {
|
||||
height: var(--editor-height);
|
||||
}
|
||||
|
||||
.ck-source-editing-area,
|
||||
.ck-source-editing-area textarea {
|
||||
height: var(--editor-height);
|
||||
}
|
||||
|
||||
.ck-source-editing-area textarea {
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user