<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); // 存入缓存 imagesCache.value.push({ file, originalWidth: imageSize.width, originalHeight: imageSize.height }); // 生成临时预览 URL const objectURL = URL.createObjectURL(file); 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) } } 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; } // 🚀 提交博客 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; const submitURL = await inputDirectSubmitURL(); 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++; } } }); // 第二步:替换 `<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; } // 替换为占位符,使用相同的索引 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); images.forEach((imgData, index) => { formData.append(`images[${index}]`, imgData.file); // 按索引存储图片 }); console.log(Object.fromEntries(formData.entries())); // 3️⃣ 发送请求 const testAxiosAPI = api; testAxiosAPI.defaults.baseURL = submitURL; testAxiosAPI.post('', formData).then(response => { if (response.code === 0) { swal.window('success', '提交成功',`博客id: ${response.blogId || '未找到blogId字段'}`,'ok','好的'); return; } swal.tip('error', '提交失败, code字段不为0') }).catch((e) => { swal.tip('error', `错误${e.message}`) }); // 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}`) // }); }; // 编辑器回调函数 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>