Merge branch 'master' of https://git.nuttyreading.com/wangjinlei/tougao_web into Editorial-Board

This commit is contained in:
2026-01-20 15:26:06 +08:00
2 changed files with 249 additions and 27 deletions

View File

@@ -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 () {
// 在选中的文本周围包裹 <blue> 标签
var selectedText = ed.selection.getContent();
// 必须获取带 HTML 的内容,否则里面的 em/i 标签在拼接前就丢了
var selectedText = ed.selection.getContent({ format: 'html' });
if (selectedText) {
// 这就是你想要的:直接外层套一个 blue
var wrappedText = `<blue>${selectedText}</blue>`;
// 使用 setContent 强行回写
ed.selection.setContent(wrappedText);
} else {
this.$message.error('请选择要添加蓝色的文本');
}
}
});

View File

@@ -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: `
<div id="image-upload-container" style="width: 300px;">
<p id="total-status-text" style="margin: 0 0 12px 0; font-size: 14px; color: #606266; font-weight: 500;">
${t('preparing', { total: this.totalUploadImages })}
</p>
<div class="el-progress el-progress--line" style="margin-bottom: 15px;">
<div class="el-progress-bar">
<div class="el-progress-bar__outer" style="height: 10px; background: #ebeef5; border-radius: 5px;">
<div class="el-progress-bar__inner" style="width: 0%; background: #409EFF; transition: width 0.4s ease-out;"></div>
</div>
</div>
<div class="el-progress__text" style="font-size: 12px; font-weight: bold; margin-top: 2px;">0%</div>
</div>
<ul id="image-individual-status" style="max-height: 400px; overflow-y: auto; padding: 0; margin: 0; list-style: none; border-top: 1px solid #f0f0f0; padding-top: 10px;"></ul>
<p id="manual-close-tip" style="display:none; margin-top:15px; font-size:12px; color:#909399; text-align:right; border-top: 1px dashed #eee; padding-top: 8px;">
${t('manualClose')}
</p>
</div>`,
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 = `
<span style="width: 55px; color: #909399; font-weight: bold;">${t('imgLabel')} ${imgIndex + 1}:</span>
<span class="status-icon" style="margin: 0 10px;"></span>
<span class="status-text" style="flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #666;">...</span>
`;
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 = '<span class="status-spinning">🔄</span>';
statusText.innerText = t('parsing');
break;
case 'uploading':
statusIcon.innerHTML = '<span class="status-spinning">⬆️</span>';
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>.*?<\/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(/(<img[^>]*?)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('<p>' + _this.value + '</p>');
// 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;
}
</style>