Merge branch 'master' of https://git.nuttyreading.com/wangjinlei/tougao_web into Editorial-Board
This commit is contained in:
@@ -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('请选择要添加蓝色的文本');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user