This commit is contained in:
2026-01-26 09:44:48 +08:00
parent 42be40ee9f
commit 01e87acfd1
28 changed files with 1016 additions and 1050 deletions
+66 -66
View File
@@ -1,144 +1,144 @@
export default class UploadAdapter {
constructor(loader, options) {
this.loader = loader;
this.options = options;
this.timeout = 60000; // 60秒超时
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);
this._initRequest()
this._initListeners(resolve, reject, file)
this._sendRequest(file)
this._initTimeout(reject)
}),
);
)
}
abort() {
if (this.xhr) {
this.xhr.abort();
this.xhr.abort()
}
if (this.timeoutId) {
clearTimeout(this.timeoutId);
clearTimeout(this.timeoutId)
}
}
_initRequest() {
const xhr = (this.xhr = new XMLHttpRequest());
const xhr = (this.xhr = new XMLHttpRequest())
xhr.open("POST", this.options.upload.uploadUrl, true);
xhr.responseType = "json";
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}.`;
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('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('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('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;
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;
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;
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;
console.error('[UploadAdapter] No URL in response for file:', file.name, response)
reject('Upload succeeded but no URL returned')
return
}
resolve({ default: url });
resolve({ default: url })
} else {
const errorMessage = response.message || genericErrorText;
console.error("[UploadAdapter] Upload failed for file:", file.name, "Error:", errorMessage);
reject(errorMessage);
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}`);
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) => {
xhr.upload.addEventListener('progress', (evt) => {
if (evt.lengthComputable) {
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
loader.uploadTotal = evt.total
loader.uploaded = evt.loaded
}
});
})
}
}
_initTimeout(reject) {
// 清除之前的超时定时器(如果有)
if (this.timeoutId) {
clearTimeout(this.timeoutId);
clearTimeout(this.timeoutId)
}
// 设置新的超时定时器
this.timeoutId = setTimeout(() => {
if (this.xhr) {
this.xhr.abort();
reject(new Error("Upload timeout"));
this.xhr.abort()
reject(new Error('Upload timeout'))
}
}, this.timeout);
}, this.timeout)
}
_sendRequest(file) {
// 设置请求超时
this.xhr.timeout = this.timeout;
this.xhr.timeout = this.timeout
// Set headers if specified.
const headers = this.options.upload.headers || {};
const extendData = this.options.upload.extendData || {};
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";
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.setRequestHeader(headerName, headers[headerName])
}
this.xhr.withCredentials = withCredentials;
this.xhr.withCredentials = withCredentials
const data = new FormData();
const data = new FormData()
for (const key of Object.keys(extendData)) {
data.append(key, extendData[key]);
data.append(key, extendData[key])
}
data.append(uploadName, file);
data.append(uploadName, file)
this.xhr.send(data);
this.xhr.send(data)
}
}
export function UploadAdapterPlugin(editor) {
editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
return new UploadAdapter(loader, editor.config._config);
};
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new UploadAdapter(loader, editor.config._config)
}
}
+114 -144
View File
@@ -1,7 +1,6 @@
<template>
<div :style="{ '--editor-height': editorHeight }">
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur"
@focus="onFocus"></ckeditor>
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur" @focus="onFocus"></ckeditor>
</div>
</template>
@@ -69,137 +68,119 @@ import {
Underline,
Undo,
WordCount,
} from "ckeditor5";
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import { UploadAdapterPlugin } from "./UploadAdapter.js";
} 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 { ref, computed, watch } from 'vue'
import { useCurrentInstance } from '@/utils/tool'
import coreTranslations from "ckeditor5/translations/zh-cn.js";
import "ckeditor5/ckeditor5.css";
import coreTranslations from 'ckeditor5/translations/zh-cn.js'
import 'ckeditor5/ckeditor5.css'
const { proxy } = useCurrentInstance();
const { proxy } = useCurrentInstance()
// 组件名称
defineOptions({
name: "scCkeditor"
});
name: 'scCkeditor',
})
// Props 定义
const props = defineProps({
modelValue: {
type: String,
default: "",
default: '',
},
placeholder: {
type: String,
default: "请输入内容……",
default: '请输入内容……',
},
toolbar: {
type: String,
default: "basic",
default: 'basic',
},
height: {
type: String,
default: "400px",
default: '400px',
},
disabled: {
type: Boolean,
default: false,
},
});
})
// Emits 定义
const emit = defineEmits(["update:modelValue"]);
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",
'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",
'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",
],
};
simple: ['undo', 'redo', 'heading', '|', 'removeFormat', 'bold', 'italic', 'underline', 'link', 'fontBackgroundColor', 'fontFamily', 'fontSize', 'fontColor', '|', 'insertTable', 'imageInsert', 'mediaEmbed'],
}
// 插件配置常量
const PLUGINS = [
@@ -265,16 +246,16 @@ const PLUGINS = [
Undo,
WordCount,
UploadAdapterPlugin,
];
]
// 响应式数据
const editorData = ref("");
const editorHeight = ref(props.height);
const editor = ClassicEditor;
const editorData = ref('')
const editorHeight = ref(props.height)
const editor = ClassicEditor
// 编辑器配置
const editorConfig = computed(() => ({
language: { ui: "zh-cn", content: "zh-cn" },
language: { ui: 'zh-cn', content: 'zh-cn' },
translations: [coreTranslations],
plugins: PLUGINS,
toolbar: {
@@ -283,27 +264,18 @@ const editorConfig = computed(() => ({
},
placeholder: props.placeholder,
image: {
styles: ["alignLeft", "alignCenter", "alignRight"],
toolbar: [
"imageTextAlternative",
"toggleImageCaption",
"|",
"imageStyle:alignLeft",
"imageStyle:alignCenter",
"imageStyle:alignRight",
"|",
"linkImage",
],
styles: ['alignLeft', 'alignCenter', 'alignRight'],
toolbar: ['imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight', '|', 'linkImage'],
},
mediaEmbed: {
previewsInData: true,
providers: [
{
name: "mp4",
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>')
html: (match) => {
const url = match['input']
return '<video controls width="100%" height="100%" src="' + url + '"></video>'
},
},
],
@@ -314,59 +286,57 @@ const editorConfig = computed(() => ({
style: {
definitions: [
{
name: "Article category",
element: "h3",
classes: ["category"],
name: 'Article category',
element: 'h3',
classes: ['category'],
},
{
name: "Info box",
element: "p",
classes: ["info-box"],
name: 'Info box',
element: 'p',
classes: ['info-box'],
},
],
},
upload: {
uploadUrl: proxy?.$API?.common?.upload?.url || "",
uploadUrl: proxy?.$API?.common?.upload?.url || '',
withCredentials: false,
extendData: { type: "images" },
extendData: { type: 'images' },
headers: {
Authorization: "Bearer " + proxy?.$TOOL?.data?.get("TOKEN"),
Authorization: 'Bearer ' + proxy?.$TOOL?.data?.get('TOKEN'),
},
},
}));
}))
// 监听 modelValue 变化
watch(
() => props.modelValue,
(newVal) => {
editorData.value = newVal ?? "";
editorData.value = newVal ?? ''
},
{ immediate: true }
);
{ immediate: true },
)
// 监听 height 变化
watch(
() => props.height,
(newVal) => {
editorHeight.value = newVal;
}
);
editorHeight.value = newVal
},
)
// 移除图片宽高的正则替换函数
const stripImageDimensions = (html) => {
return html.replace(/<img[^>]*>/gi, (match) => {
return match
.replace(/width="[^"]*"/gi, "")
.replace(/height="[^"]*"/gi, "");
});
};
return match.replace(/width="[^"]*"/gi, '').replace(/height="[^"]*"/gi, '')
})
}
// 失去焦点事件 - 移除图片的固定宽高,避免响应式布局问题
const onBlur = () => {
const cleanedData = stripImageDimensions(editorData.value);
editorData.value = cleanedData;
emit("update:modelValue", cleanedData);
};
const cleanedData = stripImageDimensions(editorData.value)
editorData.value = cleanedData
emit('update:modelValue', cleanedData)
}
</script>
<style>