diff --git a/dist.zip b/dist.zip index 73d278b..bd297b3 100644 Binary files a/dist.zip and b/dist.zip differ diff --git a/src/api/index.ts b/src/api/index.ts index f0fb503..e6836e1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -71,16 +71,25 @@ class RequestHttp { */ this.service.interceptors.response.use( (response: AxiosResponse) => { - const { data } = response; + const { data, request } = response; + tryHideFullScreenLoading(); + const imgId = request?.responseURL?.split("imgId=")[1]; //获取导出表格名称 getDispositionName(response); // 获取响应头中的 Authorization 信息 const authorization = response.headers["authorization"]; + if (imgId) { + return { + imgId, + data + }; + } if (authorization) { // 可以在这里更新用户的 token 信息 const userStore = useUserStore(); userStore.setToken(authorization); + console.log("123232323"); return data; } diff --git a/src/api/modules/upload.ts b/src/api/modules/upload.ts index bf988df..63df1b9 100644 --- a/src/api/modules/upload.ts +++ b/src/api/modules/upload.ts @@ -11,8 +11,8 @@ import http from "@/api"; * @name 文件上传模块 */ // 图片上传 -export const uploadImg = (params: any, name?: any) => { - return http.post(`/images/${name}/upload`, params); +export const uploadImg = (params: any, name?: any, id?: any) => { + return http.post(`/images/${name}/upload?imgId=${id}`, params); }; // 视频上传 diff --git a/src/components.d.ts b/src/components.d.ts index 207249a..fb962c0 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -5,62 +5,62 @@ // Read more: https://github.com/vuejs/core/pull/3399 export {} -declare module "vue" { - export interface GlobalComponents { - ElAside: typeof import("element-plus/es")["ElAside"]; - ElAutocomplete: typeof import("element-plus/es")["ElAutocomplete"]; - ElBreadcrumb: typeof import("element-plus/es")["ElBreadcrumb"]; - ElBreadcrumbItem: typeof import("element-plus/es")["ElBreadcrumbItem"]; - ElButton: typeof import("element-plus/es")["ElButton"]; - ElCarousel: typeof import("element-plus/es")["ElCarousel"]; - ElCarouselItem: typeof import("element-plus/es")["ElCarouselItem"]; - ElCheckbox: typeof import("element-plus/es")["ElCheckbox"]; - ElCheckboxGroup: typeof import("element-plus/es")["ElCheckboxGroup"]; - ElColorPicker: typeof import("element-plus/es")["ElColorPicker"]; - ElContainer: typeof import("element-plus/es")["ElContainer"]; - ElDatePicker: typeof import("element-plus/es")["ElDatePicker"]; - ElDialog: typeof import("element-plus/es")["ElDialog"]; - ElDivider: typeof import("element-plus/es")["ElDivider"]; - ElDrawer: typeof import("element-plus/es")["ElDrawer"]; - ElDropdown: typeof import("element-plus/es")["ElDropdown"]; - ElDropdownItem: typeof import("element-plus/es")["ElDropdownItem"]; - ElDropdownMenu: typeof import("element-plus/es")["ElDropdownMenu"]; - ElForm: typeof import("element-plus/es")["ElForm"]; - ElFormItem: typeof import("element-plus/es")["ElFormItem"]; - ElHeader: typeof import("element-plus/es")["ElHeader"]; - ElIcon: typeof import("element-plus/es")["ElIcon"]; - ElImage: typeof import("element-plus/es")["ElImage"]; - ElInput: typeof import("element-plus/es")["ElInput"]; - ElInputNumber: typeof import("element-plus/es")["ElInputNumber"]; - ElMain: typeof import("element-plus/es")["ElMain"]; - ElMenu: typeof import("element-plus/es")["ElMenu"]; - ElMenuItem: typeof import("element-plus/es")["ElMenuItem"]; - ElOption: typeof import("element-plus/es")["ElOption"]; - ElPagination: typeof import("element-plus/es")["ElPagination"]; - ElRadio: typeof import("element-plus/es")["ElRadio"]; - ElRadioButton: typeof import("element-plus/es")["ElRadioButton"]; - ElRadioGroup: typeof import("element-plus/es")["ElRadioGroup"]; - ElScrollbar: typeof import("element-plus/es")["ElScrollbar"]; - ElSelect: typeof import("element-plus/es")["ElSelect"]; - ElSubMenu: typeof import("element-plus/es")["ElSubMenu"]; - ElSwitch: typeof import("element-plus/es")["ElSwitch"]; - ElTable: typeof import("element-plus/es")["ElTable"]; - ElTableColumn: typeof import("element-plus/es")["ElTableColumn"]; - ElTabPane: typeof import("element-plus/es")["ElTabPane"]; - ElTabs: typeof import("element-plus/es")["ElTabs"]; - ElTag: typeof import("element-plus/es")["ElTag"]; - ElTooltip: typeof import("element-plus/es")["ElTooltip"]; - ElTree: typeof import("element-plus/es")["ElTree"]; - ElTreeSelect: typeof import("element-plus/es")["ElTreeSelect"]; - ElUpload: typeof import("element-plus/es")["ElUpload"]; - IEpArrowDown: typeof import("~icons/ep/arrow-down")["default"]; - IEpCircleClose: typeof import("~icons/ep/circle-close")["default"]; - IEpFolderDelete: typeof import("~icons/ep/folder-delete")["default"]; - IEpFullScreen: typeof import("~icons/ep/full-screen")["default"]; - IEpRemove: typeof import("~icons/ep/remove")["default"]; - IEpSearch: typeof import("~icons/ep/search")["default"]; - IEpSwitchButton: typeof import("~icons/ep/switch-button")["default"]; - RouterLink: typeof import("vue-router")["RouterLink"]; - RouterView: typeof import("vue-router")["RouterView"]; - } +declare module 'vue' { + export interface GlobalComponents { + ElAside: typeof import('element-plus/es')['ElAside'] + ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete'] + ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] + ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElCarousel: typeof import("element-plus/es")["ElCarousel"] + ElCarouselItem: typeof import("element-plus/es")["ElCarouselItem"] + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] + ElColorPicker: typeof import("element-plus/es")["ElColorPicker"] + ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDivider: typeof import('element-plus/es')['ElDivider'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElHeader: typeof import('element-plus/es')['ElHeader'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElImage: typeof import('element-plus/es')['ElImage'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElMain: typeof import('element-plus/es')['ElMain'] + ElMenu: typeof import('element-plus/es')['ElMenu'] + ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioButton: typeof import("element-plus/es")["ElRadioButton"] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ElTree: typeof import("element-plus/es")["ElTree"] + ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] + ElUpload: typeof import('element-plus/es')['ElUpload'] + IEpArrowDown: typeof import('~icons/ep/arrow-down')['default'] + IEpCircleClose: typeof import('~icons/ep/circle-close')['default'] + IEpFolderDelete: typeof import('~icons/ep/folder-delete')['default'] + IEpFullScreen: typeof import('~icons/ep/full-screen')['default'] + IEpRemove: typeof import('~icons/ep/remove')['default'] + IEpSearch: typeof import('~icons/ep/search')['default'] + IEpSwitchButton: typeof import('~icons/ep/switch-button')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + } } diff --git a/src/components/Editor/index.vue b/src/components/Editor/index.vue index e5cfb28..8a9633f 100644 --- a/src/components/Editor/index.vue +++ b/src/components/Editor/index.vue @@ -4,14 +4,15 @@ action="#" :multiple="true" :show-file-list="false" - :http-request="() => {}" - :before-upload="() => false" - @change="handleFileChange" + :http-request="handleHttpUpload" + :before-upload="handleBeforeUpload" class="editor-img-uploader" accept=".jpeg,.jpg,.png,.gif" > + + props.content, - set: val => emit("update:content", val) + set: val => { + emit("update:content", val); + } }); - -// 编辑器配置 +//富文本ref +const myQuillEditor = ref(null); +//富文本值 +const oldContent = ref(""); +//富文本配置项 const options = reactive({ theme: "snow", + debug: "warn", modules: { + // 工具栏配置 toolbar: { container: [ - ["bold", "italic", "underline", "strike"], - ["blockquote", "code-block"], - [{ list: "ordered" }, { list: "bullet" }], - [{ indent: "-1" }, { indent: "+1" }], - [{ size: fontSizeStyle.whitelist }], - [{ header: [1, 2, 3, 4, 5, 6, false] }], - [{ color: [] }, { background: [] }], - [{ align: [] }], - ["clean"], - ["link", "image", "video"] + ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线 + ["blockquote", "code-block"], // 引用 代码块 + [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表 + [{ indent: "-1" }, { indent: "+1" }], // 缩进 + [{ size: ["small", false, "large", "huge"] }], // 字体大小 + [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题 + [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色 + [{ align: [] }], // 对齐方式 + ["clean"], // 清除文本格式 + ["link", "image", "video"] // 链接、图片、视频 ], handlers: { - image: () => proxy.$refs.uploadRef.click(), - video: () => document.querySelector("#uploadFileVideo")?.click() + // 重写图片上传事件 + image: function (value) { + if (value) { + //调用图片上传 + proxy.$refs.uploadRef.click(); + } else { + Quill.format("image", true); + } + }, + video: function (value) { + if (value) { + // 劫持原来的视频点击按钮事件 + document.querySelector("#uploadFileVideo")?.click(); + } else { + Quill.format("video", true); + } + } } } }, placeholder: "请输入内容...", - readOnly: props.readOnly + readOnly: props.readOnly, + clipboard: { + matchers: [ + [ + "img", + (node, delta) => { + const src = node.getAttribute("src"); + const id = node.getAttribute("id"); + delta.insert({ image: { src, id: id } }); + } + ] + ] + } }); -// 处理文件选择(临时去重,仅当前批次有效) -const handleFileChange = (file, fileList) => { - if (!fileList.length) return; +// 上传前的钩子 +const handleBeforeUpload = file => { + const fileType = file.type; + console.log(file, "====file===="); - const rawEditor = toRaw(myQuillEditor.value); - const quill = rawEditor?.getQuill(); - if (!quill) return; + // 为文件添加唯一标识 + file.customUid = generateUUID(); // 确保有唯一ID + imageListDb.value.push(file); - // 获取初始光标位置 - let baseCursorIndex = quill.selection.savedRange?.index || 0; + // 图片和视频格式校验 + const validTypes = [ + "image/jpeg", + "image/png", + "image/gif", + "image/jpg", + "image/bmp", + "image/webp", + "video/mov", + "video/ts", + "video/mp4", + "video/avi" + ]; - // 生成文件唯一标识(用于临时去重) - const getFileKey = file => `${file.name}-${file.size}-${file.lastModified}`; - - // 过滤当前批次中已选择的重复文件(但允许和历史批次重复) - const newFiles = fileList.filter(item => { - const file = item.raw; - const key = getFileKey(file); - // 仅在当前批次内去重(避免单次选择中重复添加) - if (tempProcessedFiles.value.has(key)) { - console.log(`当前批次中已包含文件: ${file.name}`); + if (validTypes.includes(fileType)) { + // 校检文件大小 + const isLt = file.size / 1024 / 1024 < props.fileSizeLimit; + if (!isLt) { + console.log(`上传文件大小不能超过 ${props.fileSizeLimit} MB!`); + alert(`上传文件大小不能超过 ${props.fileSizeLimit} MB!`); return false; } - // 校验文件合法性 - if (!validateFile(file)) return false; - // 添加到临时去重集合 - tempProcessedFiles.value.add(key); return true; - }); + } else { + alert(`文件格式不正确!`); + return false; + } +}; - if (!newFiles.length) { - alert("所选文件已在当前上传批次中或不合法"); +// 图片上传 +const handleHttpUpload = async options => { + console.log(imageListDb.value.length, "==============length============"); + let formData = new FormData(); + formData.append("image", options.file); + imageList.value.push(options.file); + + try { + const result = await uploadImg(formData, routerName.value, options.file.customUid); + + // 假设服务器返回格式为 { imgId: 'xxx', data: { code: 0, data: { path: 'xxx' } } } + const { imgId } = result; + console.log(imgId, "==========imgId from server"); + + if (result?.data?.code === 0) { + const { data } = result?.data; + + // 1. 通过customUid查找对应的文件对象 + const fileItem = imageListDb.value.find(item => item.customUid === options.file.customUid); + if (fileItem) { + fileItem.serverImgId = imgId; // 保存服务器返回的imgId + fileItem.path = data.path; // 保存图片路径 + console.log(`成功为文件 ${fileItem.name} 设置路径: ${data.path}`); + } else { + console.error(`找不到对应的文件对象,customUid: ${options.file.customUid}`); + } + + // 2. 检查是否所有文件都已上传完成 + const allFilesUploaded = imageListDb.value.every(item => item.path); + if (allFilesUploaded) { + console.log("所有文件上传完成,准备插入到富文本编辑器"); + + // 获取富文本实例 + const rawMyQuillEditor = toRaw(myQuillEditor.value); + const quill = rawMyQuillEditor.getQuill(); + + // 按上传顺序插入图片 + imageListDb.value.forEach((item, index) => { + // 获取光标位置(每次插入后光标会移动) + const length = quill.getLength() - 1; // 文本末尾 + console.log(length, "=插入位置="); + + quill.insertEmbed(length, "image", { + url: h + item.path, + id: item.serverImgId || generateUUID() + }); + + // 移动光标到插入后的位置 + quill.setSelection(length + 1); + + console.log(`已插入图片 ${index + 1}/${imageListDb.value.length}: ${item.name}`); + }); + + // 清空临时数组 + imageList.value = []; + imageListDb.value = []; + console.log("所有图片已插入富文本,数组已清空"); + } + } + } catch (error) { + console.error("图片上传失败:", error); + } +}; + +// //上传前的钩子 +// const handleBeforeUpload = file => { +// const fileType = file.type; +// console.log(file, "====file===="); +// imageListDb.value.push(file); +// // 图片 +// if ( +// fileType == "image/jpeg" || +// fileType == "image/png" || +// fileType == "image/gif" || +// fileType == "image/jpg" || +// fileType == "image/bmp" || +// fileType == "image/webp" || +// fileType == "video/mov" || +// fileType == "video/ts" || +// fileType == "video/mp4" || +// fileType == "video/avi" +// ) { +// const fileSizeLimit = file.size; +// // 校检文件大小 +// const isLt = fileSizeLimit / 1024 / 1024 < props.fileSizeLimit; +// if (!isLt) { +// console.log(`上传文件大小不能超过 ${props.fileSizeLimit} MB!`); +// alert(`上传文件大小不能超过 ${props.fileSizeLimit} MB!`); +// return false; +// } else { +// console.log(`RIch MB!`); +// return true; +// } +// } else { +// alert(`文件格式不正确!`); +// return false; +// } +// }; +// //图片上传 +// const handleHttpUpload = async options => { +// console.log(imageListDb.value.length, "==============length============"); +// let formData = new FormData(); +// //这里要根据后端设置的name设置key值,如果name是file就传file是image就传image +// formData.append("image", options.file); +// imageList.value.push(options.file); +// try { +// const result = await uploadImg(formData, routerName.value, options.file.uid); +// const { imgId } = result; +// console.log(imgId, "==========sdsdsd"); +// if (result?.data?.code === 0) { +// const { data } = result?.data; +// //获取到imgId,将data.path匹配 +// let iLength = imageListDb.value.length; +// for (let i = 0; i < iLength; i++) { +// let item = imageListDb.value[i]; +// if (item.uid == imgId) { +// item.path = data.path; +// } +// } +// if (imageList.value.length === imageListDb.value.length) { +// console.log("走进来了"); +// let tLength = imageListDb.value.length; +// for (let j = 0; j < tLength; j++) { +// let rawMyQuillEditor = toRaw(myQuillEditor.value); +// // 获取富文本实例 +// let quill = rawMyQuillEditor.getQuill(); +// // 获取光标位置 +// let length = quill.selection.savedRange.index; +// console.log(length, "=光标length="); +// quill.insertEmbed(length, "image", { +// url: h + imageListDb.value[j].path, +// id: generateUUID() +// }); +// quill.setSelection(length + 1); +// if (j === tLength - 1) { +// imageList.value = []; +// imageListDb.value = []; +// } +// } +// } +// } +// } catch (error) {} +// }; + +//视频上传 +const handleVideoUpload = async evt => { + if (evt.target.files.length === 0) { return; } - - // 按选择顺序添加到队列 - newFiles.forEach((item, index) => { - uploadQueue.value.push({ - file: item.raw, - cursorIndex: baseCursorIndex + index - }); - }); - - // 启动队列处理 - if (!isProcessing.value) { - processQueue(); - } -}; - -// 文件校验 -const validateFile = file => { - const isImage = file.type.startsWith("image/"); - if (!isImage) { - alert("请上传图片文件"); - return false; - } - - const fileSizeMB = file.size / 1024 / 1024; - if (fileSizeMB > props.fileSizeLimit) { - alert(`文件 ${file.name} 大小超过 ${props.fileSizeLimit} MB`); - return false; - } - - return true; -}; - -// 处理上传队列(全部完成后清空临时去重集合) -const processQueue = async () => { - if (isProcessing.value || uploadQueue.value.length === 0) return; - - isProcessing.value = true; - const { file, cursorIndex } = uploadQueue.value[0]; - - try { - // 上传文件 - const formData = new FormData(); - formData.append("image", file); - const result = await uploadImg(formData, routerName.value); - - if (result?.code === 0) { - const rawEditor = toRaw(myQuillEditor.value); - const quill = rawEditor.getQuill(); - // 插入图片 - quill.insertEmbed(cursorIndex, "image", { - url: h + result.data.path, - id: generateUUID() - }); - quill.setSelection(cursorIndex + 1); - } - } catch (error) { - console.error(`文件 ${file.name} 上传失败:`, error); - } finally { - uploadQueue.value.shift(); - // 若队列已空,清空临时去重集合(允许下次上传相同文件) - if (uploadQueue.value.length === 0) { - tempProcessedFiles.value.clear(); - } - isProcessing.value = false; - // 继续处理下一个 - processQueue(); - } -}; - -// 视频上传(保持不变) -const handleVideoUpload = async evt => { - if (evt.target.files.length === 0) return; - - const file = evt.target.files[0]; const formData = new FormData(); - formData.append("video", file); - + formData.append("video", evt.target.files[0]); try { - const rawEditor = toRaw(myQuillEditor.value); - const quill = rawEditor.getQuill(); - const cursorIndex = quill.selection.savedRange?.index || 0; - + let quill = toRaw(myQuillEditor.value).getQuill(); + // 获取光标位置 + let length = quill.selection.savedRange.index; const { data } = await uploadVideo(formData); - quill.insertEmbed(cursorIndex, "video", { - url: h + data.path, + quill.insertEmbed(length, "video", { + url: h + data.path, //h + data.fileUrl, // id: generateUUID() }); - quill.setSelection(cursorIndex + 1); + uploadFileVideo.value.value = ""; } catch (error) { - console.error("视频上传失败:", error); - } finally { - evt.target.value = ""; + console.log(error); } }; -// 内容变化监听 +// 监听富文本内容变化,删除被服务器中被用户回车删除的图片 const onContentChange = content => { emit("handleRichTextContentChange", content); }; - -// 初始化标题提示 +// 增加hover工具栏有中文提示 const initTitle = () => { - for (const item of titleConfig.value) { - const tip = document.querySelector(`.ql-toolbar ${item.Choice}`); - if (tip) tip.setAttribute("title", item.title); + document.getElementsByClassName("ql-editor")[0].dataset.placeholder = ""; + for (let item of titleConfig.value) { + let tip = document.querySelector(".ql-toolbar " + item.Choice); + if (!tip) continue; + tip.setAttribute("title", item.title); } }; - onMounted(() => { initTitle(); oldContent.value = props.content; diff --git a/src/components/Editor/index2.vue b/src/components/Editor/index2.vue new file mode 100644 index 0000000..b3aba57 --- /dev/null +++ b/src/components/Editor/index2.vue @@ -0,0 +1,260 @@ + + + + diff --git a/src/components/Upload/UploadImg.vue b/src/components/Upload/UploadImg.vue index a306907..8d81e13 100644 --- a/src/components/Upload/UploadImg.vue +++ b/src/components/Upload/UploadImg.vue @@ -119,7 +119,7 @@ const handleHttpUpload = async (options: UploadRequestOptions) => { const api = props.api ?? uploadImg; const result = await api(formData, routerName.value); - if (result?.code === 0) { + if (result?.data?.code === 0) { const { data } = result; emit("update:imageUrl", data.path); } diff --git a/src/components/Upload/UploadImgs.vue b/src/components/Upload/UploadImgs.vue index 7c9784b..69ffd9f 100644 --- a/src/components/Upload/UploadImgs.vue +++ b/src/components/Upload/UploadImgs.vue @@ -134,7 +134,10 @@ const handleHttpUpload = async (options: UploadRequestOptions) => { formData.append("image", options.file); try { const api = props.api ?? uploadImg; + const { data } = await api(formData, routerName.value); + console.log(data, "============>>>."); + console.log(data.path, "========data=========="); options.onSuccess(data.path); } catch (error) {