cyber/src/pages/Editor.vue

585 lines
13 KiB
Vue
Raw Normal View History

2025-03-07 18:26:36 +08:00
<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>
2025-03-07 18:26:36 +08:00
<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";
2025-02-22 19:21:25 +08:00
import swal from "../utils/sweetalert.js";
2025-03-04 19:02:35 +08:00
import Swal from "sweetalert2";
2025-03-08 12:49:38 +08:00
import axios from "axios";
2025-02-22 19:21:25 +08:00
2025-03-07 18:26:36 +08:00
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();
// 内容 HTML
const valueHtml = ref('<p>hello</p>');
const imagesCache = ref([]);
// 工具栏和编辑器配置
const toolbarConfig = {};
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {},
2025-02-22 19:21:25 +08:00
};
2025-03-07 18:26:36 +08:00
// 模式
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);
// 存入缓存
imagesCache.value.push({
file,
originalWidth: imageSize.width,
originalHeight: imageSize.height
});
// 生成临时预览 URL
const objectURL = URL.createObjectURL(file);
insertFn(objectURL, `image-${index}`, objectURL); // 插入编辑器
2025-02-22 19:21:25 +08:00
}
};
2025-03-07 18:26:36 +08:00
// 🎯 读取图片原始尺寸的函数
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);
});
2025-02-22 19:21:25 +08:00
2025-03-07 18:26:36 +08:00
// 组件销毁时销毁编辑器实例
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
2025-03-08 12:49:38 +08:00
async function titleInputWindow() {
2025-03-07 18:26:36 +08:00
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)
2025-03-04 19:02:35 +08:00
}
2025-03-07 18:26:36 +08:00
}
2025-03-08 12:49:38 +08:00
async function inputDirectSubmitURL() {
const result = await Swal.fire({
title: '测试: 上传接口',
input: 'text',
inputLabel: '输入上传博客调用的POST接口url',
inputPlaceholder: '如"http://localhost:1234/blogs"',
showCancelButton: true,
cancelButtonText: '使用cyberURL',
confirmButtonText: '确定',
inputValidator: (value) => {
if (!value) {
return '输一下'
}
}
})
if (!result.isConfirmed) {
return 'https://mva-cyber.club:5001/blogs';
}
return result.value;
}
2025-03-07 18:26:36 +08:00
// 🚀 提交博客
const submitBlog = async () => {
if (!editorRef.value) return;
2025-03-08 12:49:38 +08:00
const title = await titleInputWindow();
2025-03-07 18:26:36 +08:00
if (title === -1) {
2025-03-04 19:02:35 +08:00
return;
}
2025-03-07 18:26:36 +08:00
const response = await swal.window('info', '允许评论吗?', '其他用户可以在你的博客下留言', '允许', '不允许');
let allowComments = response.isConfirmed;
2025-03-08 12:49:38 +08:00
const submitURL = await inputDirectSubmitURL();
2025-03-07 18:26:36 +08:00
let content = editorRef.value.getHtml(); // 获取 HTML 内容
const images = [...imagesCache.value]; // 复制图片数组
// 解析 `<img>` 并处理宽高
const imgTags = content.match(/<img[^>]+>/g) || [];
// 创建 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++;
}
}
2025-03-04 19:02:35 +08:00
});
2025-03-07 18:26:36 +08:00
// 第二步:替换 `<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 中根据 blob URL 匹配)
const imageData = images.find((img) => URL.createObjectURL(img.file) === src);
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;
2025-03-04 19:02:35 +08:00
}
2025-03-07 18:26:36 +08:00
// 替换为占位符,使用相同的索引
content = content.replace(imgTag, `<preholder image ${index} width=${width} height=${height}>`);
}
});
2025-02-22 19:21:25 +08:00
2025-03-07 18:26:36 +08:00
// 2⃣ 构造表单数据
const formData = new FormData();
formData.append("title", title);
formData.append("content", content);
2025-03-08 16:47:44 +08:00
formData.append("allowComments", allowComments);
formData.append("draft", true);
2025-03-07 18:26:36 +08:00
images.forEach((imgData, index) => {
formData.append(`images[${index}]`, imgData.file); // 按索引存储图片
});
console.log(Object.fromEntries(formData.entries()));
// 3⃣ 发送请求
2025-03-08 12:49:38 +08:00
2025-03-08 13:13:18 +08:00
const tempURL = api.defaults.baseURL;
api.defaults.baseURL = submitURL;
2025-03-08 15:59:22 +08:00
api.post('', formData, {
headers: {
"Content-Type": "multipart/form-data",
},
}).then(response => {
2025-03-08 12:49:38 +08:00
if (response.code === 0) {
swal.window('success', '提交成功',`博客id: ${response.blogId || '未找到blogId字段'}`,'ok','好的');
return;
}
2025-03-08 16:27:56 +08:00
swal.window('error', '错误', `code${ ('为' + response.code) || '未返回'}\n返回信息: ${response.message}`, 'ok','好的');
2025-03-08 12:49:38 +08:00
}).catch((e) => {
2025-03-08 16:27:56 +08:00
swal.tip('error', `${e.message}\n${e.code}`);
2025-03-08 12:49:38 +08:00
});
2025-03-08 13:13:18 +08:00
api.defaults.baseURL = tempURL;
2025-03-08 12:49:38 +08:00
// api.post('/blogs', formData).then(response => {
// if (response.status !== 200) {
// swal.tip('error', `404'}`);
// return;
// }
// if (response.code === 0) {
// swal.window('success', `提交成功, 博客id${response.blogId || '未找到blogId字段'}`);
// return;
// }
// swal.tip('error', '提交失败, code字段不为0')
// }).catch((e) => {
// swal.tip('error', `错误${e.message}`)
// });
2025-02-22 19:21:25 +08:00
2025-03-07 18:26:36 +08:00
};
2025-02-22 19:21:25 +08:00
2025-03-07 18:26:36 +08:00
// 编辑器回调函数
const handleCreated = (editor) => {
// console.log('created', editor);
editorRef.value = editor; // 记录 editor 实例
};
2025-03-07 18:26:36 +08:00
const handleChange = (editor) => {
// console.log('change:', editor.getHtml());
};
2025-03-07 18:26:36 +08:00
const handleDestroyed = (editor) => {
// console.log('destroyed', editor);
};
2025-03-07 18:26:36 +08:00
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>
2025-02-22 19:21:25 +08:00
.container {
2025-03-07 18:26:36 +08:00
height: calc(100vh - 65px);
max-width: 1300px;
width: 100%;
2025-02-22 19:21:25 +08:00
display: flex;
flex-direction: column;
2025-03-07 18:26:36 +08:00
justify-content: flex-start;
2025-02-22 19:21:25 +08:00
align-items: center;
2025-03-07 18:26:36 +08:00
flex-wrap: wrap;
gap: 20px;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.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;
}
2025-03-07 18:26:36 +08:00
.submit-btn:hover {
opacity: 1;
background: #ffb74d;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.editor-container {
width: 99%;
height: 100vh;
max-height: 100%;
margin-top: 10px;
2025-02-22 19:21:25 +08:00
display: flex;
2025-03-07 18:26:36 +08:00
flex-direction: column;
border: 1px solid #5d5d5d;
overflow: hidden;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .editor-container {
border: 1px solid #c4c4c4;
}
</style>
<style>
div[data-slate-editor] {
max-height: 0;
}
.w-e-text-container {
2025-02-22 19:21:25 +08:00
background: #2a2a2a;
2025-03-07 18:26:36 +08:00
}
.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 {
2025-02-22 19:21:25 +08:00
color: white;
2025-03-07 18:26:36 +08:00
background-color: #494949;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.w-e-bar-item .disabled:hover {
background-color: #494949;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.w-e-bar-item-group .w-e-bar-item-menus-container {
background-color: #262626;
border: #464646 solid 1px;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.w-e-toolbar svg {
fill: #e5e5e5;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.w-e-drop-panel {
background: #262626;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.w-e-panel-content-table {
background-color: #262626;
color: white;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.w-e-panel-content-table td {
background-color: #262626;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.w-e-panel-content-table td.active {
background-color: #494949;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
#w-e-textarea-1 {
background: #000000;
2025-02-22 19:21:25 +08:00
color: white;
}
2025-03-07 18:26:36 +08:00
.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;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-bar-item button:hover {
2025-02-22 19:21:25 +08:00
color: black;
2025-03-07 18:26:36 +08:00
background-color: #dcdcdc;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-bar-item .disabled:hover {
color: #b6b6b6;
background-color: #dcdcdc;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-bar-item-group .w-e-bar-item-menus-container {
background-color: #ebebeb;
border: #d4d4d4 solid 1px;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-toolbar svg {
fill: #333333;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-drop-panel {
background: #ebebeb;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-panel-content-table {
background-color: #ffffff;
color: black;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-panel-content-table td {
background-color: #ffffff;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-panel-content-table td.active {
background-color: #dcdcdc;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light #w-e-textarea-1 {
background: #ffffff;
color: black;
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-bar-divider {
background: #d4d4d4;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-select-list {
background: #ebebeb;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-select-list ul {
background: #ebebeb;
2025-02-22 19:21:25 +08:00
color: black;
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-select-list ul .selected {
background: #dcdcdc;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-select-list ul li:hover {
background: #d4d4d4;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.theme-light .w-e-drop-panel, .theme-light .w-e-select-list {
border: #d4d4d4 solid 1px;
2025-02-22 19:21:25 +08:00
}
2025-03-07 18:26:36 +08:00
.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 {
2025-02-22 19:21:25 +08:00
background: white;
}
2025-03-07 18:26:36 +08:00
</style>