diff --git a/src/common/js/commonJS.js b/src/common/js/commonJS.js index eac128e..04e0b86 100644 --- a/src/common/js/commonJS.js +++ b/src/common/js/commonJS.js @@ -2060,17 +2060,18 @@ export default { ed.ui.registry.addButton('customBlue', { - text: 'Blue', // 按钮文本 - className: 'custom-button-blue', // 添加自定义类 - // shortcut: "Ctrl+J", + text: 'Blue', + className: 'custom-button-blue', onAction: function () { - // 在选中的文本周围包裹 标签 - var selectedText = ed.selection.getContent(); + // 必须获取带 HTML 的内容,否则里面的 em/i 标签在拼接前就丢了 + var selectedText = ed.selection.getContent({ format: 'html' }); + if (selectedText) { + // 这就是你想要的:直接外层套一个 blue var wrappedText = `${selectedText}`; + + // 使用 setContent 强行回写 ed.selection.setContent(wrappedText); - } else { - this.$message.error('请选择要添加蓝色的文本'); } } }); diff --git a/src/components/page/components/Tinymce/index.vue b/src/components/page/components/Tinymce/index.vue index 7d7e009..5b73b72 100644 --- a/src/components/page/components/Tinymce/index.vue +++ b/src/components/page/components/Tinymce/index.vue @@ -67,6 +67,10 @@ export default { }, data() { return { + uploadNotifications: {}, + totalUploadImages: 0, + uploadedImageCount: 0, + uploadNotificationInstance: null, // 全局通知实例 baseUrl: this.Common.baseUrl, mediaUrl: this.Common.mediaUrl, typesettingType: 1, @@ -136,6 +140,15 @@ export default { mounted() { this.typesettingType = 1; this.initTinymce(); + const style = document.createElement('style'); + style.innerHTML = ` + @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } + @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } + .status-spinning { display: inline-block; animation: spin 1s linear infinite; } + .upload-item-animate { animation: slideIn 0.3s ease-out forwards; } + .el-progress-bar__inner { transition: width 0.4s ease-in-out !important; } +`; + document.head.appendChild(style); }, activated() { this.typesettingType = 1; @@ -145,9 +158,139 @@ export default { this.destroyTinymce(); }, methods: { + + onClear() { + if (this.uploadNotificationInstance) { + this.uploadNotificationInstance.close(); this.uploadNotificationInstance = null; + this.uploadedImageCount = 0; + this.totalUploadImages = 0;} + + }, + updateUploadProgressNotification(imgIndex, status = 'processing', message = '') { + // 快捷调用 $t + const t = (key, params) => this.$t(`imageTask.${key}`, params); + + if (!this.uploadNotificationInstance) { + this.uploadNotificationInstance = this.$notify({ + title: t('title'), + dangerouslyUseHTMLString: true, + message: ` +
+

+ ${t('preparing', { total: this.totalUploadImages })} +

+ +
+
+
+
+
+
+
0%
+
+ + + + +
`, + duration: 0, + position: 'bottom-left', + onClose: () => { + this.uploadNotificationInstance = null; + this.uploadedImageCount = 0; + this.totalUploadImages = 0; + } + }); + } + + const listContainer = document.getElementById('image-individual-status'); + if (!listContainer) return; + + let itemEl = document.getElementById(`img-status-${imgIndex}`); + if (!itemEl) { + itemEl = document.createElement('li'); + itemEl.id = `img-status-${imgIndex}`; + itemEl.className = 'upload-item-animate'; + itemEl.style.cssText = + 'font-size: 13px; padding: 8px 5px; display: flex; align-items: center; border-bottom: 1px solid #fafafa;'; + itemEl.innerHTML = ` + ${t('imgLabel')} ${imgIndex + 1}: + + ... + `; + listContainer.appendChild(itemEl); + listContainer.scrollTop = listContainer.scrollHeight; + } + + const statusIcon = itemEl.querySelector('.status-icon'); + const statusText = itemEl.querySelector('.status-text'); + + let isFinished = false; + if (statusIcon && statusText) { + switch (status) { + case 'processing': + statusIcon.innerHTML = '🔄'; + statusText.innerText = t('parsing'); + break; + case 'uploading': + statusIcon.innerHTML = '⬆️'; + statusText.innerText = t('uploading'); + break; + case 'tooLarge': + statusIcon.innerHTML = '⚠️'; + statusText.innerText = t('tooLarge'); + itemEl.style.color = '#E6A23C'; + isFinished = true; + break; + case 'success': + statusIcon.innerHTML = '✅'; + statusText.innerText = t('success'); + itemEl.style.color = '#67C23A'; + isFinished = true; + break; + case 'fail': + statusIcon.innerHTML = '❌'; + statusText.innerText = t('error', { msg: message }); + itemEl.style.color = '#F56C6C'; + isFinished = true; + break; + } + } + + if (isFinished) { + this.uploadedImageCount++; + + const progressPercent = Math.min(100, Math.round((this.uploadedImageCount / this.totalUploadImages) * 100)); + const barInner = this.uploadNotificationInstance.$el.querySelector('.el-progress-bar__inner'); + const barText = this.uploadNotificationInstance.$el.querySelector('.el-progress__text'); + const statusTotalText = document.getElementById('total-status-text'); + + if (barInner) barInner.style.width = `${progressPercent}%`; + if (barText) barText.innerText = `${progressPercent}%`; + if (statusTotalText) { + statusTotalText.innerText = t('progress', { current: this.uploadedImageCount, total: this.totalUploadImages }); + } + + if (this.uploadedImageCount >= this.totalUploadImages) { + // 更新标题为完成状态 + const titleEl = this.uploadNotificationInstance.$el.querySelector('.el-notification__title'); + if (titleEl) titleEl.innerText = t('completed'); + + if (barInner) barInner.style.background = '#67C23A'; + + const tipEl = document.getElementById('manual-close-tip'); + if (tipEl) tipEl.style.display = 'block'; + + if (statusTotalText) { + statusTotalText.innerText = t('allDone', { total: this.totalUploadImages }); + } + } + } + }, openLatexEditor(data) { this.$emit('openLatexEditor', data); - console.log('at line 254:', '打开数字公式'); }, handleSubmit() { this.$refs.uploadImage.handleSubmit(); @@ -210,10 +353,10 @@ export default { const xhr = new XMLHttpRequest(); const formData = new FormData(); // 按照你截图中的参数名,这里假设是 'file' - formData.append('mainImage', blob, `word_img_${index}.png`); + formData.append('file_name', blob, `word_img_${index}.png`); formData.append('article_id', this.articleId); xhr.withCredentials = false; - xhr.open('POST', _this.baseUrl + 'api/Preaccept/up_img_mainImage'); + xhr.open('POST', _this.baseUrl + 'api/Articlemain/uploadTableImage'); xhr.onload = function () { if (xhr.status !== 200) { console.error('HTTP Error: ' + xhr.status); @@ -221,9 +364,9 @@ export default { } try { const json = JSON.parse(xhr.responseText); - if (json.code === 0) { + if (json.status == 1) { // 2. 拼接服务器返回的 URL - const finalUrl = _this.mediaUrl + 'articleImage/' + json.data.upurl; + const finalUrl = _this.mediaUrl + 'articleTableImage/' + json.data; // 3. 找到对应的加载中占位图并替换 const doc = tinymce.activeEditor.getDoc(); const placeholder = doc.querySelector(`img[data-idx="${index}"]`); @@ -231,6 +374,8 @@ export default { placeholder.src = finalUrl; placeholder.removeAttribute('data-idx'); // 任务完成,移除标记 } + } else { + _this.removePlaceholder(index); } } catch (e) { console.error('解析响应失败', e); @@ -240,6 +385,14 @@ export default { xhr.send(formData); }, + removePlaceholder(idx) { + const doc = tinymce.activeEditor.getDoc(); + const placeholder = doc.querySelector(`img[data-idx="${idx}"]`); + if (placeholder) { + placeholder.remove(); // 直接从 DOM 中删除这个 img 标签 + this.$message.error('Upload failed. Removing temporary placeholder.'); + } + }, // 辅助工具:Base64 转 Blob dataURLtoBlob(dataurl) { const arr = dataurl.split(','), @@ -258,6 +411,10 @@ export default { var _this = this; window.tinymce.init({ ..._this.tinymceOtherInit, + extended_valid_elements: 'blue[*]', + custom_elements: 'blue', + valid_children: '+blue[#text|i|em|b|strong|span],+body[blue],+p[blue]', + inline: false, // 使用 iframe 模式 selector: `#${this.tinymceId}`, // noneditable_regexp: "/.*?<\/wmath>/g", @@ -271,8 +428,41 @@ export default { noneditable_editable_class: 'MathJax', height: this.height, content_style: ` + *{ + font-size: 14px; + } ${tableStyle} ${_this.wordStyle} + blue{ + display: inline; + } + myfigure, + mytable { + pointer-events: auto !important; /* 强制允许鼠标点击 */ + display: inline-block; + cursor: pointer; + color: rgb(0, 130, 170) !important; + text-shadow: 0 0 3px #09c2fb, 0 0 4px rgba(0, 130, 170, 0.3); +} + +myfigure *, + mytable * { + color: inherit !important; + text-shadow: inherit !important; + background: transparent !important; /* 防止内部标签背景干扰 */ +} + +@keyframes blueGlow { + 0%, + 100% { + transform: scale(1); + opacity: 0.9; + } + 50% { + transform: scale(1.02); + opacity: 1; + } +} `, formats: { bold: { inline: 'b' }, @@ -304,12 +494,12 @@ export default { // 1. 处理文件名:优先使用原始文件名,没有则生成 let filename = blobInfo.filename() || `upload_${Date.now()}.png`; // 2. 构造符合你后端要求的参数 - formData.append('mainImage', file, filename); + formData.append('file_name', file, filename); // 优先从路由取 id,其次取 data 里的 articleId formData.append('article_id', _this.articleId); xhr.withCredentials = false; // 拼接你的 baseUrl - xhr.open('POST', _this.baseUrl + '/api/Preaccept/up_img_mainImage'); + xhr.open('POST', _this.baseUrl + 'api/Articlemain/uploadTableImage'); // 上传进度(可选) xhr.upload.onprogress = (e) => { progress((e.loaded / e.total) * 100); @@ -321,9 +511,9 @@ export default { } try { const json = JSON.parse(xhr.responseText); - if (json.code === 0) { + if (json.status == 1) { // 3. 按照你的逻辑返回拼接后的完整 URL - const finalUrl = _this.mediaUrl + 'articleImage/' + json.data.upurl; + const finalUrl = _this.mediaUrl + 'articleTableImage/' + json.data; resolve(finalUrl); console.log('手动上传成功:', finalUrl); } else { @@ -374,15 +564,42 @@ export default { ); } }); - // 修改 paste 事件中的逻辑 - ed.on('paste', (event) => { + + ed.on('paste', async (event) => { const rtf = event.clipboardData.getData('text/rtf'); if (rtf && rtf.includes('\\pict')) { const extracted = extractHexImagesFromRTF(rtf); - extracted.forEach((img, i) => { - const base64 = _this.hexToBase64Sync(img.hex, img.mimeType); - _this.uploadSingleImage(base64, i); + _this.totalUploadImages = extracted.length; // 设置总数 + _this.uploadedImageCount = 0; // 重置已上传数 + + if (_this.totalUploadImages === 0) return; + + // 初始化主通知框 + _this.updateUploadProgressNotification(0, 'init'); // 这里的参数只是为了触发通知框的创建 + + const uploadPromises = extracted.map(async (img, i) => { + const isTooLarge = img.hex.length > 2 * 1024 * 1024; // 1MB 限制 + + _this.updateUploadProgressNotification(i, 'processing'); // 更新单张图片状态 + + if (isTooLarge) { + _this.updateUploadProgressNotification(i, 'tooLarge'); + return; // 跳过此图片 + } + + try { + const base64 = _this.hexToBase64Sync(img.hex, img.mimeType); + _this.updateUploadProgressNotification(i, 'uploading'); // 更新为上传中 + await _this.uploadSingleImage(base64, i); + _this.updateUploadProgressNotification(i, 'success'); // 上传成功 + } catch (err) { + console.error('Upload failed:', err); + _this.updateUploadProgressNotification(i, 'fail', err.message || 'Network error'); // 上传失败 + } }); + + await Promise.all(uploadPromises); + // Promise.all 完成后, updateUploadProgressNotification 会自动计算 totalCount === uploadedCount 并关闭 } }); @@ -484,15 +701,16 @@ export default { }, paste_preprocess: function (plugin, args) { let imgIdx = 0; - + const silentPlaceholder = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - + let content = args.content.replace(/(]*?)src="file:\/\/\/[^" ]*"/gi, (match, p1) => { // 保留 data-idx, Word 里的尺寸 return `${p1}src="${silentPlaceholder}" class="word-img-placeholder" data-idx="${imgIdx++}"`; }); let tempDiv = document.createElement('div'); tempDiv.innerHTML = content; + if (tempDiv.querySelector('table')) { console.log('粘贴的内容包含表格'); if (_this.type == 'table') { @@ -567,11 +785,14 @@ export default { window.renderMathJax(_this.tinymceId); }, 10); }, + clear_custom_action: (editor, vm) => { + + vm.onClear(); + }, init_instance_callback: (editor) => { if (_this.value) { - editor.setContent(_this.value); + editor.setContent('

' + _this.value + '

'); - // console.log('at line 489:', ' 页面'); setTimeout(() => { window.renderMathJax(_this.tinymceId); // 初始化时渲染 MathJax }, 10); @@ -648,10 +869,11 @@ export default { editor.focus(); // 聚焦到编辑器// 触发编辑器内容变化后,如果需要,可能还要设置编辑器的样式 }, //销毁富文本 - destroyTinymce() { + destroyTinymce() {this.onClear(); if (window.tinymce.get(this.tinymceId)) { window.tinymce.get(this.tinymceId).destroy(); } + }, //设置内容 setContent(value) { @@ -777,5 +999,4 @@ export default { ::v-deep .tox:not(.tox-tinymce-inline) .tox-editor-header { padding: 0 !important; } -