560 lines
13 KiB
Vue
560 lines
13 KiB
Vue
<template>
|
||
<div class="container">
|
||
<div class="editor-container">
|
||
<Toolbar
|
||
class="tool-bar"
|
||
:editor="editorRef"
|
||
:defaultConfig="toolbarConfig"
|
||
:mode="mode"
|
||
/>
|
||
<Editor
|
||
:defaultConfig="editorConfig"
|
||
:mode="mode"
|
||
v-model="valueHtml"
|
||
@onCreated="handleCreated"
|
||
@onChange="handleChange"
|
||
@onDestroyed="handleDestroyed"
|
||
@onFocus="handleFocus"
|
||
@onBlur="handleBlur"
|
||
@customAlert="customAlert"
|
||
@customPaste="customPaste"
|
||
/>
|
||
</div>
|
||
<button class="submit-btn" @click="submitBlog">提交</button>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import '@wangeditor/editor/dist/css/style.css';
|
||
import {ref, shallowRef, onMounted, onBeforeUnmount} from 'vue';
|
||
import {Editor, Toolbar} from '@wangeditor/editor-for-vue';
|
||
import api from "../utils/axios.js";
|
||
import swal from "../utils/sweetalert.js";
|
||
import Swal from "sweetalert2";
|
||
import axios from "axios";
|
||
|
||
|
||
// 编辑器实例,必须用 shallowRef
|
||
const editorRef = shallowRef();
|
||
|
||
// 内容 HTML
|
||
const valueHtml = ref('<p>hello</p>');
|
||
const imagesCache = ref([]);
|
||
|
||
// 工具栏和编辑器配置
|
||
const toolbarConfig = {};
|
||
const editorConfig = {
|
||
placeholder: '请输入内容...',
|
||
MENU_CONF: {},
|
||
};
|
||
|
||
// 模式
|
||
const mode = 'default';
|
||
|
||
|
||
toolbarConfig.excludeKeys = ["insertImage", "group-video", "fullScreen", "insertTable"];
|
||
toolbarConfig.modalAppendToBody = true;
|
||
|
||
|
||
editorConfig.MENU_CONF.uploadImage = {
|
||
fieldName: "image", // 后端接收字段名
|
||
|
||
async customUpload(file, insertFn) {
|
||
const index = imagesCache.value.length;
|
||
const imageSize = await getImageSize(file);
|
||
const objectURL = URL.createObjectURL(file); // 生成 blob URL
|
||
imagesCache.value.push({
|
||
file,
|
||
url: objectURL, // 保存 blob URL
|
||
originalWidth: imageSize.width,
|
||
originalHeight: imageSize.height,
|
||
});
|
||
insertFn(objectURL, `image-${index}`, objectURL); // 插入编辑器
|
||
}
|
||
};
|
||
// 🎯 读取图片原始尺寸的函数
|
||
const getImageSize = (file) => {
|
||
return new Promise((resolve) => {
|
||
const img = new Image();
|
||
img.src = URL.createObjectURL(file);
|
||
img.onload = () => {
|
||
resolve({width: img.width, height: img.height});
|
||
};
|
||
});
|
||
};
|
||
// 模拟 Ajax 异步获取内容
|
||
onMounted(() => {
|
||
// setTimeout(() => {
|
||
// valueHtml.value = '<p>模拟 Ajax 异步设置内容</p>';
|
||
// // valueHtml.value = valueHtml.value.replaceAll(
|
||
// // '<holder>',
|
||
// // ''
|
||
// // );
|
||
// }, 1500);
|
||
});
|
||
|
||
// 组件销毁时销毁编辑器实例
|
||
onBeforeUnmount(() => {
|
||
const editor = editorRef.value;
|
||
if (editor == null) return;
|
||
editor.destroy();
|
||
});
|
||
|
||
async function titleInputWindow() {
|
||
try {
|
||
while (1) {
|
||
const result = await Swal.fire({
|
||
title: '请输入标题',
|
||
input: 'text',
|
||
inputLabel: '标题',
|
||
inputPlaceholder: '请输入您的标题...',
|
||
showCancelButton: true,
|
||
cancelButtonText: '取消',
|
||
confirmButtonText: '确定',
|
||
inputValidator: (value) => {
|
||
if (!value) {
|
||
return '标题不能为空!'
|
||
}
|
||
}
|
||
})
|
||
|
||
// 判断用户操作
|
||
if (!result.isConfirmed) {
|
||
return -1;
|
||
}
|
||
const title = result.value;
|
||
|
||
if (title) {
|
||
const result = await swal.window('info', `确定吗?`, `用"${title}"作为标题`, '确定', '重输');
|
||
if (result.isConfirmed) {
|
||
return title;
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('输入弹窗出错:', error)
|
||
}
|
||
}
|
||
|
||
// 🚀 提交博客
|
||
const submitBlog = async () => {
|
||
if (!editorRef.value) return;
|
||
|
||
const title = await titleInputWindow();
|
||
if (title === -1) {
|
||
return;
|
||
}
|
||
const response = await swal.window('info', '允许评论吗?', '其他用户可以在你的博客下留言', '允许', '不允许');
|
||
let allowComments = response.isConfirmed;
|
||
|
||
|
||
let content = editorRef.value.getHtml(); // 获取 HTML 内容
|
||
let images = [...imagesCache.value]; // 复制图片数组
|
||
|
||
// 解析 `<img>` 并提取所有 blob URL
|
||
const imgTags = content.match(/<img[^>]+>/g) || [];
|
||
const usedUrls = new Set(); // 记录编辑器中使用的 blob URL
|
||
|
||
imgTags.forEach((imgTag) => {
|
||
const srcMatch = imgTag.match(/src=["']([^"']+)["']/);
|
||
if (srcMatch) {
|
||
usedUrls.add(srcMatch[1]); // 将使用的 blob URL 添加到 Set 中
|
||
}
|
||
});
|
||
|
||
// 清理 imagesCache 中未被使用的图片
|
||
images = images.filter((img) => {
|
||
const isUsed = usedUrls.has(img.url);
|
||
if (!isUsed) {
|
||
URL.revokeObjectURL(img.url); // 释放未使用的 blob URL
|
||
}
|
||
return isUsed; // 只保留编辑器中仍在使用的图片
|
||
});
|
||
|
||
// 更新 imagesCache
|
||
imagesCache.value = images;
|
||
|
||
// 创建 blob URL 到索引的映射
|
||
const urlToIndexMap = new Map();
|
||
let uniqueIndex = 0;
|
||
|
||
// 第一步:为每个唯一的 blob URL 分配索引
|
||
imgTags.forEach((imgTag) => {
|
||
const srcMatch = imgTag.match(/src=["']([^"']+)["']/);
|
||
if (srcMatch) {
|
||
const src = srcMatch[1]; // 提取 blob URL
|
||
if (!urlToIndexMap.has(src)) {
|
||
urlToIndexMap.set(src, uniqueIndex);
|
||
uniqueIndex++;
|
||
}
|
||
}
|
||
});
|
||
|
||
// 第二步:替换 `<img>` 标签并处理宽高
|
||
imgTags.forEach((imgTag) => {
|
||
const srcMatch = imgTag.match(/src=["']([^"']+)["']/);
|
||
if (srcMatch) {
|
||
const src = srcMatch[1];
|
||
const index = urlToIndexMap.get(src); // 获取相同 blob URL 的索引
|
||
const styleMatch = imgTag.match(/style=["']([^"']+)["']/);
|
||
let width = "", height = "";
|
||
|
||
// 从 images 中根据保存的 url 匹配图片数据
|
||
const imageData = images.find((img) => img.url === src); // 使用保存的 url 比较
|
||
const originalWidth = imageData?.originalWidth || 0;
|
||
const originalHeight = imageData?.originalHeight || 0;
|
||
|
||
if (styleMatch && styleMatch[1]) {
|
||
const styleStr = styleMatch[1];
|
||
const widthMatch = styleStr.match(/width:\s*([\d.]+)px/);
|
||
const heightMatch = styleStr.match(/height:\s*([\d.]+)px/);
|
||
const percentWidthMatch = styleStr.match(/width:\s*([\d.]+)%/);
|
||
|
||
if (widthMatch) {
|
||
width = widthMatch[1];
|
||
} else if (percentWidthMatch && originalWidth) {
|
||
width = ((parseFloat(percentWidthMatch[1]) / 100) * originalWidth).toFixed(2);
|
||
}
|
||
|
||
if (heightMatch) {
|
||
height = heightMatch[1];
|
||
} else if (width && originalWidth && originalHeight) {
|
||
height = ((width / originalWidth) * originalHeight).toFixed(2);
|
||
}
|
||
} else {
|
||
width = originalWidth;
|
||
height = originalHeight;
|
||
}
|
||
|
||
// 替换为占位符,使用相同的索引
|
||
content = content.replace(imgTag, `<preholder image ${index} width=${width} height=${height}>`);
|
||
}
|
||
});
|
||
|
||
// 2️⃣ 构造表单数据
|
||
const formData = new FormData();
|
||
formData.append("title", title);
|
||
formData.append("content", content);
|
||
formData.append("allow_comments", allowComments);
|
||
formData.append("draft", false);
|
||
|
||
console.log(images);
|
||
images.forEach((imgData) => {
|
||
formData.append(`images`, imgData.file); // 按索引存储图片
|
||
});
|
||
|
||
console.log(Object.fromEntries(formData.entries()));
|
||
|
||
// 3️⃣ 发送请求
|
||
api.post('/blogs', formData, {
|
||
headers: {
|
||
"Content-Type": "multipart/form-data",
|
||
},
|
||
}).then(response => {
|
||
if (response.code === 0) {
|
||
swal.window('success', `提交成功, 博客id${response.blogId || '未找到blogId字段'}`,'', 'ok', '好的');
|
||
return;
|
||
}
|
||
swal.window('error', '提交失败', `code字段为${response.code}; 错误信息: ${response.message}`, 'ok', '好的');
|
||
}).catch((e) => {
|
||
swal.tip('error', `错误${e.message}`)
|
||
});
|
||
|
||
};
|
||
|
||
// 编辑器回调函数
|
||
const handleCreated = (editor) => {
|
||
// console.log('created', editor);
|
||
editorRef.value = editor; // 记录 editor 实例
|
||
};
|
||
|
||
const handleChange = (editor) => {
|
||
// console.log('change:', editor.getHtml());
|
||
};
|
||
|
||
const handleDestroyed = (editor) => {
|
||
// console.log('destroyed', editor);
|
||
};
|
||
|
||
const handleFocus = (editor) => {
|
||
// console.log('focus', editor);
|
||
};
|
||
|
||
const handleBlur = (editor) => {
|
||
// console.log('blur', editor);
|
||
};
|
||
|
||
const customAlert = (info, type) => {
|
||
// alert(`【自定义提示】${type} - ${info}`);
|
||
};
|
||
|
||
const customPaste = (editor, event, callback) => {
|
||
// console.log('ClipboardEvent 粘贴事件对象', event);
|
||
// editor.insertText('xxx');
|
||
// callback(false); // 阻止默认粘贴行为
|
||
};
|
||
|
||
// 按钮点击事件
|
||
const insertText = () => {
|
||
const editor = editorRef.value;
|
||
if (editor == null) return;
|
||
editor.insertText('hello world');
|
||
};
|
||
|
||
const printHtml = () => {
|
||
const editor = editorRef.value;
|
||
if (editor == null) return;
|
||
console.log(editor.getHtml());
|
||
};
|
||
|
||
const disable = () => {
|
||
const editor = editorRef.value;
|
||
if (editor == null) return;
|
||
editor.disable();
|
||
};
|
||
</script>
|
||
<style scoped>
|
||
.container {
|
||
height: calc(100vh - 65px);
|
||
max-width: 1300px;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 20px;
|
||
}
|
||
|
||
.submit-btn {
|
||
position: absolute;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 80px;
|
||
height: 40px;
|
||
border: #ffb74d solid 3px;
|
||
border-radius: 5px;
|
||
margin: 5px;
|
||
opacity: 0.3;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.submit-btn:hover {
|
||
opacity: 1;
|
||
background: #ffb74d;
|
||
}
|
||
|
||
.editor-container {
|
||
width: 99%;
|
||
height: 100vh;
|
||
max-height: 100%;
|
||
margin-top: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid #5d5d5d;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.theme-light .editor-container {
|
||
border: 1px solid #c4c4c4;
|
||
}
|
||
|
||
</style>
|
||
<style>
|
||
div[data-slate-editor] {
|
||
max-height: 0;
|
||
}
|
||
|
||
.w-e-text-container {
|
||
background: #2a2a2a;
|
||
}
|
||
|
||
.w-e-toolbar {
|
||
background: #1c1c1c;
|
||
border-bottom: 1px solid #595959;
|
||
}
|
||
|
||
.w-e-toolbar button {
|
||
color: #e5e5e5;
|
||
background-color: #262626;
|
||
}
|
||
|
||
.w-e-bar-item button:hover {
|
||
color: white;
|
||
background-color: #494949;
|
||
}
|
||
|
||
.w-e-bar-item .disabled:hover {
|
||
background-color: #494949;
|
||
}
|
||
|
||
.w-e-bar-item-group .w-e-bar-item-menus-container {
|
||
background-color: #262626;
|
||
border: #464646 solid 1px;
|
||
}
|
||
|
||
.w-e-toolbar svg {
|
||
fill: #e5e5e5;
|
||
}
|
||
|
||
.w-e-drop-panel {
|
||
background: #262626;
|
||
}
|
||
|
||
.w-e-panel-content-table {
|
||
background-color: #262626;
|
||
color: white;
|
||
}
|
||
|
||
.w-e-panel-content-table td {
|
||
background-color: #262626;
|
||
}
|
||
|
||
.w-e-panel-content-table td.active {
|
||
background-color: #494949;
|
||
}
|
||
|
||
#w-e-textarea-1 {
|
||
background: #000000;
|
||
color: white;
|
||
}
|
||
|
||
.w-e-bar-divider {
|
||
background: #595959;
|
||
}
|
||
|
||
.w-e-select-list {
|
||
background: #262626;
|
||
}
|
||
|
||
.w-e-select-list ul {
|
||
background: #262626;
|
||
color: white;
|
||
}
|
||
|
||
.w-e-select-list ul .selected {
|
||
background: #494949;
|
||
}
|
||
|
||
.w-e-select-list ul li:hover {
|
||
background: #595959;
|
||
}
|
||
|
||
.w-e-drop-panel, .w-e-select-list {
|
||
border: #464646 solid 1px;
|
||
}
|
||
|
||
.w-e-panel-content-color li {
|
||
border: #464646 solid 1px;
|
||
}
|
||
|
||
.w-e-panel-content-color li .color-block {
|
||
border: #464646 solid 1px;
|
||
}
|
||
|
||
.w-e-hover-bar {
|
||
background: black;
|
||
}
|
||
|
||
/* 亮色模式 */
|
||
.theme-light .w-e-text-container {
|
||
background: #ffffff;
|
||
}
|
||
|
||
.theme-light .w-e-toolbar {
|
||
background: #f5f5f5;
|
||
border-bottom: 1px solid #d4d4d4;
|
||
}
|
||
|
||
.theme-light .w-e-toolbar button {
|
||
color: #333333;
|
||
background-color: #ebebeb;
|
||
}
|
||
|
||
.theme-light .w-e-bar-item .disabled svg {
|
||
fill: #b6b6b6;
|
||
}
|
||
|
||
.theme-light .w-e-bar-item .disabled {
|
||
color: #b6b6b6;
|
||
}
|
||
|
||
.theme-light .w-e-bar-item button:hover {
|
||
color: black;
|
||
background-color: #dcdcdc;
|
||
}
|
||
|
||
.theme-light .w-e-bar-item .disabled:hover {
|
||
color: #b6b6b6;
|
||
background-color: #dcdcdc;
|
||
}
|
||
|
||
.theme-light .w-e-bar-item-group .w-e-bar-item-menus-container {
|
||
background-color: #ebebeb;
|
||
border: #d4d4d4 solid 1px;
|
||
}
|
||
|
||
.theme-light .w-e-toolbar svg {
|
||
fill: #333333;
|
||
}
|
||
|
||
.theme-light .w-e-drop-panel {
|
||
background: #ebebeb;
|
||
}
|
||
|
||
.theme-light .w-e-panel-content-table {
|
||
background-color: #ffffff;
|
||
color: black;
|
||
}
|
||
|
||
.theme-light .w-e-panel-content-table td {
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.theme-light .w-e-panel-content-table td.active {
|
||
background-color: #dcdcdc;
|
||
}
|
||
|
||
.theme-light #w-e-textarea-1 {
|
||
background: #ffffff;
|
||
color: black;
|
||
}
|
||
|
||
.theme-light .w-e-bar-divider {
|
||
background: #d4d4d4;
|
||
}
|
||
|
||
.theme-light .w-e-select-list {
|
||
background: #ebebeb;
|
||
}
|
||
|
||
.theme-light .w-e-select-list ul {
|
||
background: #ebebeb;
|
||
color: black;
|
||
}
|
||
|
||
.theme-light .w-e-select-list ul .selected {
|
||
background: #dcdcdc;
|
||
}
|
||
|
||
.theme-light .w-e-select-list ul li:hover {
|
||
background: #d4d4d4;
|
||
}
|
||
|
||
.theme-light .w-e-drop-panel, .theme-light .w-e-select-list {
|
||
border: #d4d4d4 solid 1px;
|
||
}
|
||
|
||
.theme-light .w-e-panel-content-color li {
|
||
border: #d4d4d4 solid 1px;
|
||
}
|
||
|
||
.theme-light .w-e-panel-content-color li .color-block {
|
||
border: #d4d4d4 solid 1px;
|
||
}
|
||
|
||
.theme-light .w-e-hover-bar {
|
||
background: white;
|
||
}
|
||
|
||
</style> |