提交
This commit is contained in:
@@ -391,7 +391,8 @@ str = str.replace(regex, function (match, content, offset, fullString) {
|
|||||||
const allTables = [];
|
const allTables = [];
|
||||||
if (!tables || tables.length === 0) {
|
if (!tables || tables.length === 0) {
|
||||||
console.warn("未找到表格内容,请检查 XML 结构");
|
console.warn("未找到表格内容,请检查 XML 结构");
|
||||||
return [];
|
callback([]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
const rows = table.getElementsByTagNameNS(namespace, "tr");
|
const rows = table.getElementsByTagNameNS(namespace, "tr");
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.initORCID();
|
this.initORCID();
|
||||||
|
bus.$on('editorSessionLocalRestored', this.syncLsUserToHeader);
|
||||||
if (this.user_id == 24) {
|
if (this.user_id == 24) {
|
||||||
this.daojishi = '2021.9.3 - 2021.9.30';
|
this.daojishi = '2021.9.3 - 2021.9.30';
|
||||||
this.curStartTime = '2021-10-01 00:00:00';
|
this.curStartTime = '2021-10-01 00:00:00';
|
||||||
@@ -162,6 +163,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
syncLsUserToHeader() {
|
||||||
|
this.user_id = localStorage.getItem('U_id');
|
||||||
|
this.user_cap = localStorage.getItem('U_role');
|
||||||
|
this.$forceUpdate();
|
||||||
|
},
|
||||||
goHome() {
|
goHome() {
|
||||||
this.$router.push('/');
|
this.$router.push('/');
|
||||||
},
|
},
|
||||||
@@ -295,6 +301,9 @@ export default {
|
|||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
bus.$off('editorSessionLocalRestored', this.syncLsUserToHeader);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.initORCID();
|
this.initORCID();
|
||||||
|
bus.$on('editorSessionLocalRestored', this.onEditorSessionLocalRestored);
|
||||||
if (this.user_id == 24) {
|
if (this.user_id == 24) {
|
||||||
this.daojishi = '2021.9.3 - 2021.9.30';
|
this.daojishi = '2021.9.3 - 2021.9.30';
|
||||||
this.curStartTime = '2021-10-01 00:00:00';
|
this.curStartTime = '2021-10-01 00:00:00';
|
||||||
@@ -155,6 +156,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onEditorSessionLocalRestored() {
|
||||||
|
this.updateUsername();
|
||||||
|
this.$forceUpdate();
|
||||||
|
},
|
||||||
updateUsername() {
|
updateUsername() {
|
||||||
|
|
||||||
this.localUsername = localStorage.getItem('U_name');
|
this.localUsername = localStorage.getItem('U_name');
|
||||||
@@ -290,6 +295,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
bus.$off('editorSessionLocalRestored', this.onEditorSessionLocalRestored);
|
||||||
// 步骤 C2: 销毁前移除监听器
|
// 步骤 C2: 销毁前移除监听器
|
||||||
if (this.$bus) {
|
if (this.$bus) {
|
||||||
this.$bus.$off('user-name-updated', this.updateUsername);
|
this.$bus.$off('user-name-updated', this.updateUsername);
|
||||||
|
|||||||
@@ -657,15 +657,72 @@ colTitle: 'Template title',
|
|||||||
printBtn: 'Print',
|
printBtn: 'Print',
|
||||||
previewNotSupported: 'This file format cannot be previewed online',
|
previewNotSupported: 'This file format cannot be previewed online',
|
||||||
downloadToView: 'Download to view locally',
|
downloadToView: 'Download to view locally',
|
||||||
registerAuthorBtn: 'Create author account',
|
registerAuthorBtn: 'Auto submit',
|
||||||
registerAuthorConfirm:
|
registerAuthorConfirm:
|
||||||
'Create an account via the same admin API as User Management: login name "{account}", display name "{realname}", email "{email}", initial password 123456qwe (no captcha). Continue?',
|
'Create an account via the same admin API as User Management: login name "{account}", display name "{realname}", email "{email}", initial password 123456qwe (no captcha). Continue?',
|
||||||
registerAuthorSuccess: 'Author account created.',
|
registerAuthorSuccess: 'Author account created.',
|
||||||
|
registerAuthorSuccessWithEmail: 'Created: {email}, password: 123456qwe',
|
||||||
registerAuthorFail: 'Creation failed. Try again later or add the user manually in User Management.',
|
registerAuthorFail: 'Creation failed. Try again later or add the user manually in User Management.',
|
||||||
registerAuthorExistsEmail: 'This email is already registered.',
|
registerAuthorExistsEmail: 'This email is already registered.',
|
||||||
registerAuthorExistsAccount: 'This login name is already taken. Edit the sender display name or add the user manually.',
|
registerAuthorExistsAccount: 'This login name is already taken. Edit the sender display name or add the user manually.',
|
||||||
registerAuthorNeedEmail: 'Sender email is missing; cannot create an account.',
|
registerAuthorNeedEmail: 'Sender email is missing; cannot create an account.',
|
||||||
registerAuthorNoQq: 'QQ Mail is not supported for author accounts. Please add the user manually.',
|
registerAuthorNoQq: 'QQ Mail is not supported for author accounts. Please add the user manually.',
|
||||||
|
registerAuthorConfirmShort: 'Email: {email}\nPassword: {password}',
|
||||||
|
registerAuthorPickEmailFail: 'Could not allocate an available email after several attempts. Try again later or add the user manually.',
|
||||||
|
autoSubmitBtn: 'Submit first .docx as manuscript',
|
||||||
|
autoSubmitTitle: 'Log in as author and upload the first .docx attachment',
|
||||||
|
autoSubmitSessionTip:
|
||||||
|
'Author checkLogin does not change your U_* local account. The server cookie is the author during upload. After finishing, enter your editor password below and log in again to restore the editor server session.',
|
||||||
|
autoSubmitUsername: 'Username',
|
||||||
|
autoSubmitPassword: 'Password',
|
||||||
|
autoSubmitSenderEmailLabel: 'Sender email',
|
||||||
|
autoSubmitSenderEmailPlaceholder: '(No sender email)',
|
||||||
|
autoSubmitCode: 'Captcha',
|
||||||
|
autoSubmitCodePh: 'Fill only if the server requires captcha; often leave empty',
|
||||||
|
autoSubmitCancel: 'Cancel',
|
||||||
|
autoSubmitDialogTitle: 'Auto submit',
|
||||||
|
autoSubmitConfirm: 'Auto submit',
|
||||||
|
autoSubmitExistingAccountTip:
|
||||||
|
'This sender email may already have an account. Enter your password. Re-selecting a local file replaces the previous one.',
|
||||||
|
autoSubmitNotifyMailSubject: '[{journal}] Please complete your submission',
|
||||||
|
autoSubmitNotifyMailFail: 'Could not send the notification email. You can resend from the compose page.',
|
||||||
|
autoSubmitNotifyMailSkipped: 'No sender mailbox (j_email_id) found; skipped automatic email.',
|
||||||
|
autoSubmitNoDocx: 'No .docx attachment in this message (only .docx is supported, same as new submission).',
|
||||||
|
autoSubmitDownloadFail: 'Could not download the attachment. Try again later.',
|
||||||
|
autoSubmitSuccessTitle: 'Manuscript created',
|
||||||
|
autoSubmitDialogClose: 'Close',
|
||||||
|
autoSubmitSuccessLineAccount: 'Account: {account}',
|
||||||
|
autoSubmitSuccessLinePassword: 'Password: {password}',
|
||||||
|
autoSubmitSuccessLineDraft: 'Manuscript ID: {id}. Draft created in staging.',
|
||||||
|
autoSubmitSuccessLineLinkPrefix: 'Submission link: ',
|
||||||
|
autoSubmitSuccessMailSent: 'Notification email sent',
|
||||||
|
autoSubmitSuccessMailSkipped: 'No notification email (journal sender mailbox not configured).',
|
||||||
|
autoSubmitSuccessMailSkippedRecipient: 'No notification email (no valid From address on this message).',
|
||||||
|
autoSubmitSuccessMailFailed: 'Notification email was not sent; try again from the compose page.',
|
||||||
|
autoSubmitSuccessBodyLocalOnly:
|
||||||
|
'Article ID: {id}. The UI still shows your editor account; the server session may still be the author after author login. Refresh or log in again as editor.',
|
||||||
|
autoSubmitSuccessBodyServerRestored:
|
||||||
|
'Article ID: {id}. Logged in again as editor; both local storage and server session should match your editor account.',
|
||||||
|
autoSubmitSuccessNotify:
|
||||||
|
'Article ID: {id}<br/><a href="{link}" target="_blank" rel="noopener noreferrer">Open articleAdd</a>',
|
||||||
|
autoSubmitEditorRestorePwd: 'Editor password (restore session)',
|
||||||
|
autoSubmitEditorRestorePwdPh:
|
||||||
|
'Optional: password for the current editor account shown in the header, used after success to restore the server session',
|
||||||
|
autoSubmitEditorReloginFail:
|
||||||
|
'Could not restore the editor server session; local values were restored where possible. Refresh the page or log in again as editor.',
|
||||||
|
autoSubmitFail: 'Submission failed. Check credentials or network and try again.',
|
||||||
|
autoSubmitUsernameRequired: 'Username is required',
|
||||||
|
autoSubmitPasswordRequired: 'Password is required',
|
||||||
|
autoSubmitJournalLabel: 'Journal',
|
||||||
|
autoSubmitJournalUnknown: 'No journal (switch to a mailbox account that is bound to a journal)',
|
||||||
|
autoSubmitNeedJournal:
|
||||||
|
'This mailbox has no journal ID; staging backfill cannot match the submission page. Switch mailbox account first.',
|
||||||
|
autoSubmitFailPartial: '(If contribute succeeded, article ID may be {id}; please verify in admin.)',
|
||||||
|
autoSubmitManuscriptSource: 'Manuscript file',
|
||||||
|
autoSubmitPickLocalDocx: 'Upload .docx from disk',
|
||||||
|
autoSubmitSourceHint: 'Optional: upload a file; otherwise the first .docx in the email is used. Choosing again replaces the current local file.',
|
||||||
|
autoSubmitLocalPicked: 'Local file: {name}',
|
||||||
|
autoSubmitNeedDocxSource: 'Upload a .docx file, or ensure the email has a .docx attachment.',
|
||||||
},
|
},
|
||||||
crawlerKeywords: {
|
crawlerKeywords: {
|
||||||
pageTitle: 'Keyword Configuration',
|
pageTitle: 'Keyword Configuration',
|
||||||
|
|||||||
@@ -646,15 +646,68 @@ const zh = {
|
|||||||
printBtn: '打印',
|
printBtn: '打印',
|
||||||
previewNotSupported: '该文件格式无法在线预览',
|
previewNotSupported: '该文件格式无法在线预览',
|
||||||
downloadToView: '下载到本地查看',
|
downloadToView: '下载到本地查看',
|
||||||
registerAuthorBtn: '创建作者账号',
|
registerAuthorBtn: '自动投稿',
|
||||||
registerAuthorConfirm:
|
registerAuthorConfirm:
|
||||||
'将使用推广后台「添加用户」接口创建账号:登录名「{account}」,显示名「{realname}」,邮箱「{email}」,初始密码 123456qwe(无需验证码)。是否继续?',
|
'将使用推广后台「添加用户」接口创建账号:登录名「{account}」,显示名「{realname}」,邮箱「{email}」,初始密码 123456qwe(无需验证码)。是否继续?',
|
||||||
registerAuthorSuccess: '作者账号已创建。',
|
registerAuthorSuccess: '作者账号已创建。',
|
||||||
|
registerAuthorSuccessWithEmail: '已创建:{email},密码:123456qwe',
|
||||||
registerAuthorFail: '创建失败,请稍后重试或到用户管理中手动添加。',
|
registerAuthorFail: '创建失败,请稍后重试或到用户管理中手动添加。',
|
||||||
registerAuthorExistsEmail: '该邮箱已被注册。',
|
registerAuthorExistsEmail: '该邮箱已被注册。',
|
||||||
registerAuthorExistsAccount: '该登录名已被占用,请人工处理或修改发件人显示名后重试。',
|
registerAuthorExistsAccount: '该登录名已被占用,请人工处理或修改发件人显示名后重试。',
|
||||||
registerAuthorNeedEmail: '缺少发件人邮箱,无法创建账号。',
|
registerAuthorNeedEmail: '缺少发件人邮箱,无法创建账号。',
|
||||||
registerAuthorNoQq: '本站不支持 QQ 邮箱作为作者账号,请在用户管理中手动添加。',
|
registerAuthorNoQq: '本站不支持 QQ 邮箱作为作者账号,请在用户管理中手动添加。',
|
||||||
|
registerAuthorConfirmShort: '邮箱:{email}\n密码:{password}',
|
||||||
|
registerAuthorPickEmailFail: '多次尝试后仍无法分配到可用邮箱,请稍后重试或手动添加用户。',
|
||||||
|
autoSubmitBtn: '附件一键建稿',
|
||||||
|
autoSubmitTitle: '用作者账号登录并上传首份 .docx 附件',
|
||||||
|
autoSubmitSessionTip:
|
||||||
|
'作者 checkLogin 不会修改顶部 U_* 本地账号;上传期间服务端 Cookie 为作者。完成后请填写「编辑密码」再登录一次以恢复编辑服务端会话。',
|
||||||
|
autoSubmitUsername: '登录名',
|
||||||
|
autoSubmitPassword: '密码',
|
||||||
|
autoSubmitSenderEmailLabel: '发件人邮箱',
|
||||||
|
autoSubmitSenderEmailPlaceholder: '(未识别发件人邮箱)',
|
||||||
|
autoSubmitCode: '验证码',
|
||||||
|
autoSubmitCodePh: '若后台要求验证码则填写,一般可留空',
|
||||||
|
autoSubmitCancel: '取消',
|
||||||
|
autoSubmitDialogTitle: '自动投稿',
|
||||||
|
autoSubmitConfirm: '自动投稿',
|
||||||
|
autoSubmitExistingAccountTip:
|
||||||
|
'该发件邮箱可能已有账号。请填写密码;本地文件可多次重选,新文件会替换上一份。',
|
||||||
|
autoSubmitNotifyMailSubject: '【{journal}】投稿完善提醒',
|
||||||
|
autoSubmitNotifyMailFail: '通知邮件发送失败,可稍后在发件页手动补发。',
|
||||||
|
autoSubmitNotifyMailSkipped: '未识别发件邮箱账号,已跳过自动发信。',
|
||||||
|
autoSubmitNoDocx: '邮件中无 .docx 附件(仅支持 docx,与新增稿件一致)。',
|
||||||
|
autoSubmitDownloadFail: '无法下载附件,请稍后重试。',
|
||||||
|
autoSubmitSuccessTitle: '建稿成功',
|
||||||
|
autoSubmitDialogClose: '关闭',
|
||||||
|
autoSubmitSuccessLineAccount: '账号{account}',
|
||||||
|
autoSubmitSuccessLinePassword: '密码{password}',
|
||||||
|
autoSubmitSuccessLineDraft: '稿号id:{id} 已创建草稿箱',
|
||||||
|
autoSubmitSuccessLineLinkPrefix: '稿件链接地址是:',
|
||||||
|
autoSubmitSuccessMailSent: '邮件通知已发送',
|
||||||
|
autoSubmitSuccessMailSkipped: '未发送通知邮件(未配置期刊发件邮箱)',
|
||||||
|
autoSubmitSuccessMailSkippedRecipient: '未发送通知邮件(邮件中缺少有效发件人邮箱)',
|
||||||
|
autoSubmitSuccessMailFailed: '邮件通知未发送,请稍后在发件页补发',
|
||||||
|
autoSubmitSuccessBodyLocalOnly:
|
||||||
|
'文章 ID:{id}。界面仍为当前编辑账号;服务端在作者登录后可能仍为作者会话,请刷新或重新登录编辑账号。',
|
||||||
|
autoSubmitSuccessBodyServerRestored: '文章 ID:{id}。已通过编辑账号重新登录,本地与服务端会话均已恢复为编辑。',
|
||||||
|
autoSubmitSuccessNotify:
|
||||||
|
'文章 ID:{id}<br/><a href="{link}" target="_blank" rel="noopener noreferrer">打开 articleAdd 继续编辑</a>',
|
||||||
|
autoSubmitEditorRestorePwd: '编辑密码(恢复会话)',
|
||||||
|
autoSubmitEditorRestorePwdPh: '选填:与顶部当前登录名对应的编辑密码,用于上传成功后恢复服务端会话',
|
||||||
|
autoSubmitEditorReloginFail: '恢复编辑服务端会话失败,已尽量恢复本地信息,请刷新页面或重新登录。',
|
||||||
|
autoSubmitFail: '建稿失败,请检查账号密码或网络后重试。',
|
||||||
|
autoSubmitUsernameRequired: '请填写登录名',
|
||||||
|
autoSubmitPasswordRequired: '请填写密码',
|
||||||
|
autoSubmitJournalLabel: '目标期刊',
|
||||||
|
autoSubmitJournalUnknown: '未识别期刊(请先在邮件列表切换绑定期刊的邮箱账号)',
|
||||||
|
autoSubmitNeedJournal: '当前邮箱账号没有期刊信息,无法与投稿页一致回填。请通过「切换邮箱账号」选择绑定期刊的账号。',
|
||||||
|
autoSubmitFailPartial: '(若 contribute 已成功,文章 ID 可能为:{id},请到后台核对)',
|
||||||
|
autoSubmitManuscriptSource: '稿件文件',
|
||||||
|
autoSubmitPickLocalDocx: '本地上传 .docx',
|
||||||
|
autoSubmitSourceHint: '可选本地上传;否则使用邮件中第一份 .docx。再次选择会替换当前本机文件。',
|
||||||
|
autoSubmitLocalPicked: '当前本机文件:{name}',
|
||||||
|
autoSubmitNeedDocxSource: '请在本地上传 .docx,或确保邮件中带有 .docx 附件。',
|
||||||
},
|
},
|
||||||
crawlerKeywords: {
|
crawlerKeywords: {
|
||||||
pageTitle: '关键词配置',
|
pageTitle: '关键词配置',
|
||||||
|
|||||||
@@ -10,13 +10,24 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
plain
|
plain
|
||||||
icon="el-icon-user-solid"
|
icon="el-icon-upload2"
|
||||||
:loading="registerAuthorLoading"
|
:loading="registerAuthorLoading"
|
||||||
class="register-author-btn"
|
class="register-author-btn"
|
||||||
@click="registerAuthorFromMail"
|
@click="registerAuthorFromMail"
|
||||||
>
|
>
|
||||||
{{ $t('mailboxCollect.registerAuthorBtn') }}
|
{{ $t('mailboxCollect.registerAuthorBtn') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<!-- <el-button
|
||||||
|
v-if="resolvedJournalId"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
plain
|
||||||
|
icon="el-icon-upload2"
|
||||||
|
class="register-author-btn"
|
||||||
|
@click="openAutoSubmitDialog"
|
||||||
|
>
|
||||||
|
{{ $t('mailboxCollect.autoSubmitBtn') }}
|
||||||
|
</el-button> -->
|
||||||
<!-- <i class="el-icon-star-off action-icon"></i> -->
|
<!-- <i class="el-icon-star-off action-icon"></i> -->
|
||||||
<i class="el-icon-close action-icon" @click="$emit('close')"></i>
|
<i class="el-icon-close action-icon" @click="$emit('close')"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,18 +93,6 @@
|
|||||||
<el-link type="primary" :underline="false" @click="scrollToAttachments" class="jump-link">
|
<el-link type="primary" :underline="false" @click="scrollToAttachments" class="jump-link">
|
||||||
{{ $t('mailboxCollect.viewAttachments') }}
|
{{ $t('mailboxCollect.viewAttachments') }}
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-button
|
|
||||||
v-if="hasWordAttachment"
|
|
||||||
type="primary"
|
|
||||||
size="mini"
|
|
||||||
plain
|
|
||||||
icon="el-icon-user-solid"
|
|
||||||
:loading="registerAuthorLoading"
|
|
||||||
class="register-author-btn-inline"
|
|
||||||
@click="registerAuthorFromMail"
|
|
||||||
>
|
|
||||||
{{ $t('mailboxCollect.registerAuthorBtn') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,15 +141,98 @@
|
|||||||
ref="previewDialog"
|
ref="previewDialog"
|
||||||
@download="downloadAttachment"
|
@download="downloadAttachment"
|
||||||
/>
|
/>
|
||||||
|
<el-dialog
|
||||||
|
:title="
|
||||||
|
autoSubmitSuccessMode
|
||||||
|
? $t('mailboxCollect.autoSubmitSuccessTitle')
|
||||||
|
: $t('mailboxCollect.autoSubmitDialogTitle')
|
||||||
|
"
|
||||||
|
:visible.sync="autoSubmitDialogVisible"
|
||||||
|
width="580px"
|
||||||
|
append-to-body
|
||||||
|
@close="onAutoSubmitDialogClose"
|
||||||
|
>
|
||||||
|
<div v-show="!autoSubmitSuccessMode">
|
||||||
|
<el-alert
|
||||||
|
v-if="autoSubmitPrefillFromRegisterConflict"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="auto-submit-existing-tip"
|
||||||
|
:title="$t('mailboxCollect.autoSubmitExistingAccountTip')"
|
||||||
|
/>
|
||||||
|
<el-form ref="autoSubmitFormRef" :model="autoSubmitForm" label-width="120px" class="auto-submit-form">
|
||||||
|
<el-form-item :label="$t('mailboxCollect.autoSubmitSenderEmailLabel')">
|
||||||
|
<el-input
|
||||||
|
class="auto-submit-email-readonly"
|
||||||
|
:value="autoSubmitReadonlyEmailDisplay || $t('mailboxCollect.autoSubmitSenderEmailPlaceholder')"
|
||||||
|
readonly
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('mailboxCollect.autoSubmitPassword')">
|
||||||
|
<el-input
|
||||||
|
v-model="autoSubmitForm.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('mailboxCollect.autoSubmitManuscriptSource')">
|
||||||
|
<el-upload
|
||||||
|
:key="'mail-auto-docx-' + autoSubmitUploadKey"
|
||||||
|
ref="autoSubmitLocalUpload"
|
||||||
|
class="auto-submit-local-upload"
|
||||||
|
action=""
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
:on-change="onAutoSubmitLocalDocxChange"
|
||||||
|
:on-remove="onAutoSubmitLocalDocxRemove"
|
||||||
|
:on-exceed="onAutoSubmitLocalDocxExceed"
|
||||||
|
>
|
||||||
|
<el-button size="small" type="default" icon="el-icon-folder-open">{{
|
||||||
|
$t('mailboxCollect.autoSubmitPickLocalDocx')
|
||||||
|
}}</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<p class="auto-submit-source-hint">{{ $t('mailboxCollect.autoSubmitSourceHint') }}</p>
|
||||||
|
<p v-if="localDocxDisplayName" class="auto-submit-local-picked">
|
||||||
|
{{ $t('mailboxCollect.autoSubmitLocalPicked', { name: localDocxDisplayName }) }}
|
||||||
|
</p>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div v-show="autoSubmitSuccessMode" class="auto-submit-success-body" v-html="autoSubmitSuccessBodyHtml"></div>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<template v-if="autoSubmitSuccessMode">
|
||||||
|
<el-button type="primary" @click="autoSubmitDialogVisible = false">{{
|
||||||
|
$t('mailboxCollect.autoSubmitDialogClose')
|
||||||
|
}}</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-button @click="autoSubmitDialogVisible = false">{{ $t('mailboxCollect.autoSubmitCancel') }}</el-button>
|
||||||
|
<el-button type="primary" :loading="autoSubmitLoading" @click="submitAutoSubmitManuscript">{{
|
||||||
|
$t('mailboxCollect.autoSubmitConfirm')
|
||||||
|
}}</el-button>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
import Common from '@/components/common/common';
|
import Common from '@/components/common/common';
|
||||||
|
import bus from '@/components/common/bus';
|
||||||
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
||||||
|
import { runMailManuscriptPipeline, clearAuthorShadowSession } from '@/utils/mailManuscriptAutoSubmit';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import FilePreviewDialog from './FilePreviewDialog.vue';
|
import FilePreviewDialog from './FilePreviewDialog.vue';
|
||||||
|
|
||||||
|
/** 投稿页对外链接域名(邮件与成功通知统一用线上地址,避免本地 origin) */
|
||||||
|
const SUBMISSION_SITE_ORIGIN = 'https://submission.tmrjournals.com';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FilePreviewDialog
|
FilePreviewDialog
|
||||||
@@ -169,6 +251,20 @@ export default {
|
|||||||
content: '',
|
content: '',
|
||||||
attachments: []
|
attachments: []
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
/** 当前邮件列表所选邮箱账号的期刊 ID(与 mailboxCollect 一致) */
|
||||||
|
journalId: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
journalTitle: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
/** 当前邮箱账号 j_email_id,与 mailboxSend 发信参数一致 */
|
||||||
|
jEmailId: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -177,7 +273,22 @@ export default {
|
|||||||
isDetailExpanded: false,
|
isDetailExpanded: false,
|
||||||
downloadingIndex: -1,
|
downloadingIndex: -1,
|
||||||
packingAll: false,
|
packingAll: false,
|
||||||
registerAuthorLoading: false
|
registerAuthorLoading: false,
|
||||||
|
autoSubmitDialogVisible: false,
|
||||||
|
autoSubmitLoading: false,
|
||||||
|
/** 建稿成功后在同一弹窗内展示摘要,仅保留关闭 */
|
||||||
|
autoSubmitSuccessMode: false,
|
||||||
|
autoSubmitSuccessBodyHtml: '',
|
||||||
|
autoSubmitForm: {
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
/** 弹窗内本机选择的 .docx,优先于邮件附件下载 */
|
||||||
|
autoSubmitLocalDocxFile: null,
|
||||||
|
/** 因「邮箱/账号已存在」打开建稿弹窗时展示提示条 */
|
||||||
|
autoSubmitPrefillFromRegisterConflict: false,
|
||||||
|
/** 本地上传组件强制重建 key,超限选新文件时替换 */
|
||||||
|
autoSubmitUploadKey: 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -190,11 +301,71 @@ export default {
|
|||||||
return /\.(doc|docx)$/i.test(n);
|
return /\.(doc|docx)$/i.test(n);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/** 新增稿件上传链路仅接受 .docx */
|
||||||
|
hasDocxAttachment() {
|
||||||
|
const att = (this.mailData && this.mailData.attachments) || [];
|
||||||
|
if (!att.length) return false;
|
||||||
|
return att.some((f) => {
|
||||||
|
const n = (f && f.name) || '';
|
||||||
|
return /\.docx$/i.test(n);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolvedJournalId() {
|
||||||
|
if (this.journalId != null && String(this.journalId).trim() !== '') {
|
||||||
|
const n = Number(this.journalId);
|
||||||
|
if (!Number.isNaN(n) && n > 0) return n;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const s = localStorage.getItem('mailboxCollect_journal_id');
|
||||||
|
if (s != null && String(s).trim() !== '') {
|
||||||
|
const n = Number(s);
|
||||||
|
if (!Number.isNaN(n) && n > 0) return n;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
resolvedJournalLabel() {
|
||||||
|
const id = this.resolvedJournalId;
|
||||||
|
if (id == null) return this.$t('mailboxCollect.autoSubmitJournalUnknown');
|
||||||
|
const t = this.journalTitle && String(this.journalTitle).trim();
|
||||||
|
return t ? `${t}(ID: ${id})` : `ID: ${id}`;
|
||||||
|
},
|
||||||
|
/** 发件接口 sendOne 所需 j_email_id */
|
||||||
|
resolvedJEmailId() {
|
||||||
|
if (this.jEmailId != null && String(this.jEmailId).trim() !== '') {
|
||||||
|
return String(this.jEmailId).trim();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const s = localStorage.getItem('mailboxCollect_j_email_id');
|
||||||
|
if (s != null && String(s).trim() !== '') return String(s).trim();
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
/** 通知邮件正文中的期刊名称(不含「ID:」后缀) */
|
||||||
|
resolvedNotifyJournalName() {
|
||||||
|
const t = this.journalTitle && String(this.journalTitle).trim();
|
||||||
|
if (t) return t;
|
||||||
|
const id = this.resolvedJournalId;
|
||||||
|
return id != null ? `ID ${id}` : this.$t('mailboxCollect.autoSubmitJournalUnknown');
|
||||||
|
},
|
||||||
totalAttachmentSize() {
|
totalAttachmentSize() {
|
||||||
if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B';
|
if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B';
|
||||||
const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
||||||
return this.formatFileSize(total);
|
return this.formatFileSize(total);
|
||||||
},
|
},
|
||||||
|
localDocxDisplayName() {
|
||||||
|
const f = this.autoSubmitLocalDocxFile;
|
||||||
|
if (!f || !f.name) return '';
|
||||||
|
return f.name;
|
||||||
|
},
|
||||||
|
/** 弹窗内只读展示:当前邮件发件人邮箱 */
|
||||||
|
autoSubmitReadonlyEmailDisplay() {
|
||||||
|
return this.getPrimaryMailSenderEmail() || '';
|
||||||
|
},
|
||||||
/** 正文:兼容 content_html / body / html,纯文本时包一层 pre */
|
/** 正文:兼容 content_html / body / html,纯文本时包一层 pre */
|
||||||
mailBodyHtml() {
|
mailBodyHtml() {
|
||||||
const m = this.mailData || {};
|
const m = this.mailData || {};
|
||||||
@@ -236,12 +407,13 @@ export default {
|
|||||||
if (labeled) return labeled[1].trim().slice(0, 32);
|
if (labeled) return labeled[1].trim().slice(0, 32);
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
buildAuthorRegisterPayload() {
|
/** 发件人显示名(用于 realname),与 partyList 校验规则一致 */
|
||||||
|
deriveRealnameFromMail() {
|
||||||
const m = this.mailData || {};
|
const m = this.mailData || {};
|
||||||
const email = String(m.from_email || '')
|
const fromEmail = String(m.from_email || '')
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
const localPart = email.split('@')[0] || 'user';
|
const localPart = fromEmail.split('@')[0] || 'user';
|
||||||
let rawName = String(m.from_name || '').trim();
|
let rawName = String(m.from_name || '').trim();
|
||||||
let realname = rawName || localPart;
|
let realname = rawName || localPart;
|
||||||
if (this.$validateString && !this.$validateString(realname)) {
|
if (this.$validateString && !this.$validateString(realname)) {
|
||||||
@@ -250,78 +422,123 @@ export default {
|
|||||||
realname = 'Author';
|
realname = 'Author';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let account = rawName.replace(/[^a-zA-Z0-9_-]/g, '');
|
return realname;
|
||||||
if (!account || account.length < 2) {
|
},
|
||||||
account = localPart.replace(/[^a-zA-Z0-9_-]/g, '');
|
/** 当前邮件发件人邮箱(来稿真实地址),用于注册作者与通知收件人 */
|
||||||
|
getPrimaryMailSenderEmail() {
|
||||||
|
const raw = this.mailData && this.mailData.from_email != null ? String(this.mailData.from_email).trim() : '';
|
||||||
|
if (!raw) return '';
|
||||||
|
const lower = raw.toLowerCase();
|
||||||
|
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(lower)) return lower;
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
/** 由邮箱推导登录名(与 checkUserByAccount / addUser 常用规则一致) */
|
||||||
|
deriveLoginAccountFromEmail(email) {
|
||||||
|
const e = String(email || '').toLowerCase().trim();
|
||||||
|
const at = e.indexOf('@');
|
||||||
|
if (at < 1) return 'author';
|
||||||
|
let acc = e
|
||||||
|
.slice(0, at)
|
||||||
|
.replace(/[^a-z0-9_]/gi, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.replace(/^_|_$/g, '');
|
||||||
|
if (!acc || acc.length < 2) {
|
||||||
|
acc = 'u_' + e.replace(/[^\w]/g, '').slice(0, 24);
|
||||||
}
|
}
|
||||||
if (!account || account.length < 2) {
|
if (!acc || acc.length < 2) {
|
||||||
account = `u${email.replace(/[^a-zA-Z0-9]/g, '').slice(0, 12) || 'ser'}`;
|
acc = 'author_' + String(Date.now()).slice(-8);
|
||||||
}
|
}
|
||||||
const phone = this.extractPhoneFromMailBody() || '';
|
if (acc.length > 32) acc = acc.slice(0, 32);
|
||||||
return { email, account, realname, phone };
|
return acc;
|
||||||
|
},
|
||||||
|
openAutoSubmitDialogWithCredentialsPrefill({ usernameHint }) {
|
||||||
|
if (this.resolvedJournalId == null) {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.autoSubmitNeedJournal'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.autoSubmitPrefillFromRegisterConflict = true;
|
||||||
|
this.autoSubmitSuccessMode = false;
|
||||||
|
this.autoSubmitSuccessBodyHtml = '';
|
||||||
|
this.autoSubmitForm.username = usernameHint != null ? String(usernameHint).trim() : '';
|
||||||
|
this.autoSubmitForm.password = '';
|
||||||
|
this.autoSubmitDialogVisible = true;
|
||||||
|
},
|
||||||
|
/** addUser 返回「邮箱已存在」类文案时改走手动录入 */
|
||||||
|
isRegisterDuplicateResponse(res, msg) {
|
||||||
|
const s = String(msg || '');
|
||||||
|
const lower = s.toLowerCase();
|
||||||
|
if (lower.includes('already registered') || lower.includes('email is already')) return true;
|
||||||
|
if (s.includes('已注册') && (s.includes('邮箱') || s.includes('邮件'))) return true;
|
||||||
|
const hint = String(this.$t('mailboxCollect.registerAuthorExistsEmail') || '').toLowerCase();
|
||||||
|
if (hint && lower.includes(hint.slice(0, 12))) return true;
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
async registerAuthorFromMail() {
|
async registerAuthorFromMail() {
|
||||||
if (!this.hasWordAttachment) return;
|
if (!this.hasWordAttachment) return;
|
||||||
const { email, account, realname, phone } = this.buildAuthorRegisterPayload();
|
const realname = this.deriveRealnameFromMail();
|
||||||
if (!email) {
|
const email2 = this.getPrimaryMailSenderEmail();
|
||||||
this.$message.warning(this.$t('mailboxCollect.registerAuthorNeedEmail'));
|
if (!email2) {
|
||||||
return;
|
this.$message.error(this.$t('mailboxCollect.registerAuthorNeedEmail'));
|
||||||
}
|
|
||||||
if (email.endsWith('@qq.com')) {
|
|
||||||
this.$message.warning(this.$t('mailboxCollect.registerAuthorNoQq'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pwd = '123456qwe';
|
|
||||||
try {
|
|
||||||
await this.$confirm(
|
|
||||||
this.$t('mailboxCollect.registerAuthorConfirm', { email, account, realname }),
|
|
||||||
this.$t('mailboxCollect.registerAuthorBtn'),
|
|
||||||
{ type: 'warning', distinguishCancelAndClose: true }
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const account2 = this.deriveLoginAccountFromEmail(email2);
|
||||||
this.registerAuthorLoading = true;
|
this.registerAuthorLoading = true;
|
||||||
try {
|
try {
|
||||||
const p = this.buildAuthorRegisterPayload();
|
const r1 = await this.$api.post('api/User/checkUserByEmail', { email: email2, account: account2 });
|
||||||
const email2 = p.email;
|
const r2 = await this.$api.post('api/User/checkUserByAccount', { email: email2, account: account2 });
|
||||||
const account2 = p.account;
|
if (!(r1 && r2 && Number(r1.code) === 0 && Number(r2.code) === 0)) {
|
||||||
const realname2 = p.realname;
|
this.$message.error((r1 && r1.msg) || (r2 && r2.msg) || this.$t('mailboxCollect.registerAuthorFail'));
|
||||||
const phone2 = p.phone;
|
|
||||||
const ef = { email: email2, account: account2 };
|
|
||||||
const r1 = await this.$api.post('api/User/checkUserByEmail', ef);
|
|
||||||
if (!r1 || Number(r1.code) !== 0) {
|
|
||||||
this.$message.error((r1 && r1.msg) || this.$t('mailboxCollect.registerAuthorFail'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const r2 = await this.$api.post('api/User/checkUserByAccount', ef);
|
if (r1.data && Number(r1.data.has) !== 0) {
|
||||||
if (!r2 || Number(r2.code) !== 0) {
|
this.openAutoSubmitDialogWithCredentialsPrefill({ usernameHint: email2 });
|
||||||
this.$message.error((r2 && r2.msg) || this.$t('mailboxCollect.registerAuthorFail'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hasEmail = r1.data && Number(r1.data.has) === 1;
|
if (r2.data && Number(r2.data.has) !== 0) {
|
||||||
const hasAccount = r2.data && Number(r2.data.has) === 1;
|
this.openAutoSubmitDialogWithCredentialsPrefill({ usernameHint: account2 });
|
||||||
if (hasEmail) {
|
|
||||||
this.$message.warning(this.$t('mailboxCollect.registerAuthorExistsEmail'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hasAccount) {
|
} catch (err) {
|
||||||
this.$message.warning(this.$t('mailboxCollect.registerAuthorExistsAccount'));
|
console.error(err);
|
||||||
|
this.$message.error(this.$t('mailboxCollect.registerAuthorFail'));
|
||||||
return;
|
return;
|
||||||
|
} finally {
|
||||||
|
this.registerAuthorLoading = false;
|
||||||
}
|
}
|
||||||
|
const pwd = '123456qwe';
|
||||||
|
this.$notify({
|
||||||
|
title: this.$t('mailboxCollect.registerAuthorBtn'),
|
||||||
|
message: this.$t('mailboxCollect.registerAuthorConfirmShort', { email: email2, password: pwd }),
|
||||||
|
type: 'info',
|
||||||
|
duration: 8000
|
||||||
|
});
|
||||||
|
this.registerAuthorLoading = true;
|
||||||
|
try {
|
||||||
|
const phone2 = this.extractPhoneFromMailBody() || '';
|
||||||
const addForm = {
|
const addForm = {
|
||||||
email: email2,
|
email: email2,
|
||||||
account: account2,
|
account: account2,
|
||||||
password: pwd,
|
password: pwd,
|
||||||
repassword: pwd,
|
repassword: pwd,
|
||||||
realname: realname2,
|
realname,
|
||||||
phone: phone2 || ''
|
phone: phone2 || ''
|
||||||
};
|
};
|
||||||
const res = await this.$api.post('api/User/addUser', addForm);
|
const res = await this.$api.post('api/User/addUser', addForm);
|
||||||
if (res && Number(res.code) === 0) {
|
if (res && Number(res.code) === 0) {
|
||||||
this.$message.success(this.$t('mailboxCollect.registerAuthorSuccess'));
|
this.autoSubmitForm.username = account2;
|
||||||
|
this.autoSubmitForm.password = pwd;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.resolvedJournalId != null) {
|
||||||
|
this.openAutoSubmitDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message.error((res && res.msg) || this.$t('mailboxCollect.registerAuthorFail'));
|
const msg = (res && res.msg) || '';
|
||||||
|
if (this.isRegisterDuplicateResponse(res, msg)) {
|
||||||
|
this.openAutoSubmitDialogWithCredentialsPrefill({ usernameHint: email2 });
|
||||||
|
} else {
|
||||||
|
this.$message.error(msg || this.$t('mailboxCollect.registerAuthorFail'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -338,6 +555,297 @@ export default {
|
|||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"');
|
.replace(/"/g, '"');
|
||||||
},
|
},
|
||||||
|
buildAbsoluteArticleAddUrl(articleId) {
|
||||||
|
const id = articleId == null ? '' : String(articleId);
|
||||||
|
const base = SUBMISSION_SITE_ORIGIN.replace(/\/+$/, '');
|
||||||
|
return `${base}/articleAdd?id=${encodeURIComponent(id)}`;
|
||||||
|
},
|
||||||
|
buildAuthorNotifyMailHtml(articleUrl, username, passwordPlain) {
|
||||||
|
const j = this.escapeHtml(this.resolvedNotifyJournalName);
|
||||||
|
const a = this.escapeHtml(username);
|
||||||
|
const p = this.escapeHtml(passwordPlain);
|
||||||
|
const href = this.escapeHtml(articleUrl);
|
||||||
|
const link = `<a href="${href}" target="_blank" rel="noopener noreferrer">${href}</a>`;
|
||||||
|
const loc = (this.$i18n && this.$i18n.locale) || '';
|
||||||
|
if (String(loc).toLowerCase().startsWith('en')) {
|
||||||
|
return `<p>I am an editor of <strong>${j}</strong>. <br/>Thank you for your submission. An author account has been created for you: username <strong>${a}</strong>, default password <strong>${p}</strong>. <br/>Please complete your manuscript here: ${link}</p>`;
|
||||||
|
}
|
||||||
|
return `<p>我是《${j}》的编辑,很高兴收到你的投稿。<br/>已帮您创建好了投稿账号,登录名为 <strong>${a}</strong>,默认密码为 <strong>${p}</strong>。<br/>请您进一步完善您的稿件,投稿地址:${link}</p>`;
|
||||||
|
},
|
||||||
|
async getAutoSubmitNotifyMailResult({ articleUrl, username, passwordPlain }) {
|
||||||
|
const jEmailId = this.resolvedJEmailId;
|
||||||
|
const journalId = this.resolvedJournalId;
|
||||||
|
if (!jEmailId || journalId == null) {
|
||||||
|
return 'skipped_config';
|
||||||
|
}
|
||||||
|
const toEmail = this.getPrimaryMailSenderEmail();
|
||||||
|
if (!toEmail) {
|
||||||
|
return 'skipped_recipient';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const journalName = this.resolvedNotifyJournalName;
|
||||||
|
const subject = this.$t('mailboxCollect.autoSubmitNotifyMailSubject', { journal: journalName });
|
||||||
|
const content = this.buildAuthorNotifyMailHtml(articleUrl, username, passwordPlain);
|
||||||
|
const res = await this.$api.post('api/email_client/sendOne', {
|
||||||
|
to_email: toEmail,
|
||||||
|
subject: String(subject || '').trim(),
|
||||||
|
content,
|
||||||
|
j_email_id: String(jEmailId),
|
||||||
|
journal_id: String(journalId)
|
||||||
|
});
|
||||||
|
if (!res || Number(res.code) !== 0) {
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
return 'sent';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildAutoSubmitSuccessDialogHtml({ username, passwordPlain, articleId, articleUrl, mailResult }) {
|
||||||
|
const href = this.escapeHtml(articleUrl);
|
||||||
|
const linkHtml = `<a href="${href}" target="_blank" rel="noopener noreferrer">${href}</a>`;
|
||||||
|
const line1 = this.escapeHtml(
|
||||||
|
this.$t('mailboxCollect.autoSubmitSuccessLineAccount', { account: username })
|
||||||
|
);
|
||||||
|
const line2 = this.escapeHtml(
|
||||||
|
this.$t('mailboxCollect.autoSubmitSuccessLinePassword', { password: passwordPlain })
|
||||||
|
);
|
||||||
|
const line3 = this.escapeHtml(
|
||||||
|
this.$t('mailboxCollect.autoSubmitSuccessLineDraft', { id: String(articleId == null ? '' : articleId) })
|
||||||
|
);
|
||||||
|
const linkPrefix = this.escapeHtml(this.$t('mailboxCollect.autoSubmitSuccessLineLinkPrefix'));
|
||||||
|
let line5;
|
||||||
|
if (mailResult === 'sent') {
|
||||||
|
line5 = `<p class="auto-submit-success-mail-sent">${this.escapeHtml(
|
||||||
|
this.$t('mailboxCollect.autoSubmitSuccessMailSent')
|
||||||
|
)}</p>`;
|
||||||
|
} else if (mailResult === 'skipped_config') {
|
||||||
|
line5 = `<p class="auto-submit-success-mail-muted">${this.escapeHtml(
|
||||||
|
this.$t('mailboxCollect.autoSubmitSuccessMailSkipped')
|
||||||
|
)}</p>`;
|
||||||
|
} else if (mailResult === 'skipped_recipient') {
|
||||||
|
line5 = `<p class="auto-submit-success-mail-muted">${this.escapeHtml(
|
||||||
|
this.$t('mailboxCollect.autoSubmitSuccessMailSkippedRecipient')
|
||||||
|
)}</p>`;
|
||||||
|
} else {
|
||||||
|
line5 = `<p class="auto-submit-success-mail-muted">${this.escapeHtml(
|
||||||
|
this.$t('mailboxCollect.autoSubmitSuccessMailFailed')
|
||||||
|
)}</p>`;
|
||||||
|
}
|
||||||
|
return `<div class="auto-submit-success-lines"><p>${line1}</p><p>${line2}</p><p>${line3}</p><p>${linkPrefix}${linkHtml}</p>${line5}</div>`;
|
||||||
|
},
|
||||||
|
openAutoSubmitDialog() {
|
||||||
|
if (this.resolvedJournalId == null) {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.autoSubmitNeedJournal'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.autoSubmitPrefillFromRegisterConflict = false;
|
||||||
|
this.autoSubmitSuccessMode = false;
|
||||||
|
this.autoSubmitSuccessBodyHtml = '';
|
||||||
|
this.autoSubmitDialogVisible = true;
|
||||||
|
},
|
||||||
|
onAutoSubmitLocalDocxChange(file, fileList) {
|
||||||
|
const ref = this.$refs.autoSubmitLocalUpload;
|
||||||
|
if (ref && fileList && fileList.length > 1) {
|
||||||
|
try {
|
||||||
|
ref.handleRemove(fileList[0]);
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = file && file.raw;
|
||||||
|
if (!raw) return;
|
||||||
|
const name = raw.name || '';
|
||||||
|
if (!/\.docx$/i.test(name)) {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.autoSubmitNoDocx'));
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.autoSubmitLocalUpload) {
|
||||||
|
this.$refs.autoSubmitLocalUpload.clearFiles();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.autoSubmitLocalDocxFile = raw;
|
||||||
|
},
|
||||||
|
onAutoSubmitLocalDocxExceed(files) {
|
||||||
|
const wrap = files && files[0];
|
||||||
|
const raw = wrap && wrap.raw;
|
||||||
|
if (!raw || !/\.docx$/i.test(raw.name || '')) {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.autoSubmitNoDocx'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.$refs.autoSubmitLocalUpload) {
|
||||||
|
this.$refs.autoSubmitLocalUpload.clearFiles();
|
||||||
|
}
|
||||||
|
this.autoSubmitLocalDocxFile = raw;
|
||||||
|
this.autoSubmitUploadKey += 1;
|
||||||
|
},
|
||||||
|
onAutoSubmitLocalDocxRemove() {
|
||||||
|
this.autoSubmitLocalDocxFile = null;
|
||||||
|
},
|
||||||
|
onAutoSubmitDialogClose() {
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
this.autoSubmitPrefillFromRegisterConflict = false;
|
||||||
|
this.autoSubmitSuccessMode = false;
|
||||||
|
this.autoSubmitSuccessBodyHtml = '';
|
||||||
|
this.autoSubmitLocalDocxFile = null;
|
||||||
|
this.autoSubmitUploadKey = 0;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.autoSubmitLocalUpload) {
|
||||||
|
this.$refs.autoSubmitLocalUpload.clearFiles();
|
||||||
|
}
|
||||||
|
if (this.$refs.autoSubmitFormRef) {
|
||||||
|
this.$refs.autoSubmitFormRef.clearValidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 从邮件附件公网路径拉取 docx(axios blob + withCredentials),再走 manuscirpt 上传。
|
||||||
|
*/
|
||||||
|
async fetchMailDocxBlobFromAttachment(mailFile) {
|
||||||
|
const rel = await this.ensureFileUrl(mailFile);
|
||||||
|
if (!rel) {
|
||||||
|
throw new Error('NO_ATTACHMENT_URL');
|
||||||
|
}
|
||||||
|
const url = this.resolvePublicFileUrl(rel);
|
||||||
|
const res = await axios.get(url, {
|
||||||
|
responseType: 'blob',
|
||||||
|
withCredentials: true,
|
||||||
|
timeout: 180000
|
||||||
|
});
|
||||||
|
if (!res || res.status < 200 || res.status >= 300 || !res.data) {
|
||||||
|
throw new Error('DOWNLOAD_HTTP');
|
||||||
|
}
|
||||||
|
const blob = res.data;
|
||||||
|
if (!(blob instanceof Blob) || blob.size === 0) {
|
||||||
|
throw new Error('EMPTY_BLOB');
|
||||||
|
}
|
||||||
|
return blob;
|
||||||
|
},
|
||||||
|
getFirstDocxAttachment() {
|
||||||
|
const att = (this.mailData && this.mailData.attachments) || [];
|
||||||
|
for (let i = 0; i < att.length; i++) {
|
||||||
|
const f = att[i];
|
||||||
|
const n = (f && f.name) || '';
|
||||||
|
if (/\.docx$/i.test(n)) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
submitAutoSubmitManuscript() {
|
||||||
|
let username = String((this.autoSubmitForm && this.autoSubmitForm.username) || '').trim();
|
||||||
|
if (!username) {
|
||||||
|
const em = this.getPrimaryMailSenderEmail();
|
||||||
|
if (em) username = em;
|
||||||
|
}
|
||||||
|
if (!username) {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.registerAuthorNeedEmail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.autoSubmitForm.username = username;
|
||||||
|
if (!(this.autoSubmitForm && this.autoSubmitForm.password)) {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.autoSubmitPasswordRequired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.resolvedJournalId == null) {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.autoSubmitNeedJournal'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mailDocx = this.getFirstDocxAttachment();
|
||||||
|
const localFile = this.autoSubmitLocalDocxFile;
|
||||||
|
if (!localFile && !mailDocx) {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.autoSubmitNeedDocxSource'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.autoSubmitLoading = true;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const docxMime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||||
|
let blob;
|
||||||
|
let fileName;
|
||||||
|
if (localFile) {
|
||||||
|
blob = localFile;
|
||||||
|
fileName = localFile.name || 'manuscript.docx';
|
||||||
|
} else {
|
||||||
|
blob = await this.fetchMailDocxBlobFromAttachment(mailDocx);
|
||||||
|
fileName = mailDocx.name || 'manuscript.docx';
|
||||||
|
}
|
||||||
|
const r = await runMailManuscriptPipeline({
|
||||||
|
api: this.$api,
|
||||||
|
baseUrl: Common.baseUrl,
|
||||||
|
preserveEditorLocalStorage: true,
|
||||||
|
journal_id: this.resolvedJournalId,
|
||||||
|
extractWordTables: (consumer) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const f = new File([blob], fileName, { type: docxMime });
|
||||||
|
this.$commonJS.extractWordTablesToArrays(f, async (wordTables) => {
|
||||||
|
try {
|
||||||
|
await consumer(wordTables || []);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
login: {
|
||||||
|
username,
|
||||||
|
password: this.autoSubmitForm.password,
|
||||||
|
code: ''
|
||||||
|
},
|
||||||
|
blob,
|
||||||
|
fileName
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
bus.$emit('editorSessionLocalRestored');
|
||||||
|
const link = this.buildAbsoluteArticleAddUrl(r.article_id);
|
||||||
|
const passwordPlain = this.autoSubmitForm.password;
|
||||||
|
const mailResult = await this.getAutoSubmitNotifyMailResult({
|
||||||
|
articleUrl: link,
|
||||||
|
username,
|
||||||
|
passwordPlain
|
||||||
|
});
|
||||||
|
if (mailResult === 'failed') {
|
||||||
|
this.$message.warning(this.$t('mailboxCollect.autoSubmitNotifyMailFail'));
|
||||||
|
}
|
||||||
|
this.autoSubmitSuccessBodyHtml = this.buildAutoSubmitSuccessDialogHtml({
|
||||||
|
username,
|
||||||
|
passwordPlain,
|
||||||
|
articleId: r.article_id,
|
||||||
|
articleUrl: link,
|
||||||
|
mailResult
|
||||||
|
});
|
||||||
|
this.autoSubmitSuccessMode = true;
|
||||||
|
} else {
|
||||||
|
const base = r.msg || this.$t('mailboxCollect.autoSubmitFail');
|
||||||
|
const tail =
|
||||||
|
r.article_id != null
|
||||||
|
? ' ' + this.$t('mailboxCollect.autoSubmitFailPartial', { id: r.article_id })
|
||||||
|
: '';
|
||||||
|
this.$message.error(base + tail);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const msg = (err && err.message) || '';
|
||||||
|
const httpErr = err && err.response && err.response.status;
|
||||||
|
if (
|
||||||
|
httpErr ||
|
||||||
|
msg === 'NO_ATTACHMENT_URL' ||
|
||||||
|
msg === 'DOWNLOAD_HTTP' ||
|
||||||
|
msg === 'EMPTY_BLOB'
|
||||||
|
) {
|
||||||
|
this.$message.error(this.$t('mailboxCollect.autoSubmitDownloadFail'));
|
||||||
|
} else {
|
||||||
|
this.$message.error(this.$t('mailboxCollect.autoSubmitFail'));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.autoSubmitLoading = false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
scrollToAttachments() {
|
scrollToAttachments() {
|
||||||
// 1. 获取目标元素的 DOM
|
// 1. 获取目标元素的 DOM
|
||||||
const target = this.$el.querySelector('.attachment-section');
|
const target = this.$el.querySelector('.attachment-section');
|
||||||
@@ -389,6 +897,22 @@ export default {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 将 Common.mediaUrl 与后端返回的 file_url 拼成可访问 URL。
|
||||||
|
* file_url 常为 /attachments/…,mediaUrl 常为 …/public/,直接拼接会得到 …/public//attachments/… 导致 404。
|
||||||
|
* 稿件上传仍走 up_file/type/manuscirpt;此处仅用于先把附件下载为 Blob。
|
||||||
|
*/
|
||||||
|
resolvePublicFileUrl(pathOrUrl) {
|
||||||
|
const p = pathOrUrl == null ? '' : String(pathOrUrl).trim();
|
||||||
|
if (!p) return '';
|
||||||
|
if (/^https?:\/\//i.test(p)) return p;
|
||||||
|
let base = String(this.mediaUrl || '').replace(/\/+$/, '');
|
||||||
|
let path = p.replace(/^\/+/, '');
|
||||||
|
if (/\/public$/i.test(base) && /^public\//i.test(path)) {
|
||||||
|
path = path.replace(/^public\//i, '');
|
||||||
|
}
|
||||||
|
return `${base}/${path}`;
|
||||||
|
},
|
||||||
async downloadAll() {
|
async downloadAll() {
|
||||||
if (this.packingAll) return;
|
if (this.packingAll) return;
|
||||||
this.packingAll = true;
|
this.packingAll = true;
|
||||||
@@ -397,7 +921,7 @@ export default {
|
|||||||
const promises = this.mailData.attachments.map(async (file) => {
|
const promises = this.mailData.attachments.map(async (file) => {
|
||||||
const url = await this.ensureFileUrl(file);
|
const url = await this.ensureFileUrl(file);
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const fileUrl = this.mediaUrl + url;
|
const fileUrl = this.resolvePublicFileUrl(url);
|
||||||
const resp = await fetch(fileUrl);
|
const resp = await fetch(fileUrl);
|
||||||
const blob = await resp.blob();
|
const blob = await resp.blob();
|
||||||
zip.file(file.name || 'attachment', blob);
|
zip.file(file.name || 'attachment', blob);
|
||||||
@@ -416,7 +940,7 @@ export default {
|
|||||||
console.log("🚀 ~ previewAttachment ~ file:", file);
|
console.log("🚀 ~ previewAttachment ~ file:", file);
|
||||||
|
|
||||||
var that = this;
|
var that = this;
|
||||||
that.$refs.previewDialog.init(file, this.mediaUrl+file.file_url || '');
|
that.$refs.previewDialog.init(file, this.resolvePublicFileUrl(file.file_url) || '');
|
||||||
// 1. 获取后端返回的 URL
|
// 1. 获取后端返回的 URL
|
||||||
const res = await this.$api.post('api/email_client/getAttachment', {
|
const res = await this.$api.post('api/email_client/getAttachment', {
|
||||||
inbox_id: String(this.mailData.inbox_id),
|
inbox_id: String(this.mailData.inbox_id),
|
||||||
@@ -424,7 +948,7 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.data && res.data.file_url) {
|
if (res.data && res.data.file_url) {
|
||||||
const fullUrl = this.mediaUrl + res.data.file_url;
|
const fullUrl = this.resolvePublicFileUrl(res.data.file_url);
|
||||||
|
|
||||||
// 2. 调用弹窗组件的 init 方法
|
// 2. 调用弹窗组件的 init 方法
|
||||||
// 注意:微软预览要求 fullUrl 必须是公网可访问的!
|
// 注意:微软预览要求 fullUrl 必须是公网可访问的!
|
||||||
@@ -442,7 +966,7 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
this.$message.warning(this.$t('mailboxCollect.downloadFail') || '下载失败');
|
this.$message.warning(this.$t('mailboxCollect.downloadFail') || '下载失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fileUrl = this.mediaUrl + url;
|
const fileUrl = this.resolvePublicFileUrl(url);
|
||||||
const fileName = file.name || 'attachment';
|
const fileName = file.name || 'attachment';
|
||||||
const resp = await fetch(fileUrl);
|
const resp = await fetch(fileUrl);
|
||||||
const blob = await resp.blob();
|
const blob = await resp.blob();
|
||||||
@@ -545,11 +1069,64 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.register-author-btn {
|
.register-author-btn {
|
||||||
flex-shrink: 0;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
.register-author-btn-inline {
|
.auto-submit-email-readonly /deep/ .el-input__inner {
|
||||||
margin-left: 10px;
|
color: #606266;
|
||||||
vertical-align: middle;
|
cursor: default;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-color: #e4e7ed;
|
||||||
|
}
|
||||||
|
.auto-submit-existing-tip {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.auto-submit-local-picked {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #67c23a;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.auto-submit-source-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.auto-submit-success-body {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.65;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.auto-submit-success-body p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.auto-submit-success-body a {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.auto-submit-success-lines p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.auto-submit-success-lines p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.auto-submit-success-mail-sent {
|
||||||
|
color: #67c23a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.auto-submit-success-mail-muted {
|
||||||
|
color: #e6a23c;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.auto-submit-local-upload {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.auto-submit-journal-readonly {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.action-icon:hover {
|
.action-icon:hover {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
@@ -844,7 +1421,10 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
|
|
||||||
}
|
}
|
||||||
.attachment-brief-bar .brief-info {
|
.attachment-brief-bar .brief-info {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.attachment-brief-bar .first-file-name {
|
.attachment-brief-bar .first-file-name {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
|
|||||||
@@ -112,6 +112,9 @@
|
|||||||
<mail-detail
|
<mail-detail
|
||||||
v-if="detailMail && String(detailMail.inbox_id || '') !== ''"
|
v-if="detailMail && String(detailMail.inbox_id || '') !== ''"
|
||||||
:mailData="detailMail"
|
:mailData="detailMail"
|
||||||
|
:journal-id="mailDetailJournalId"
|
||||||
|
:journal-title="mailDetailJournalTitle"
|
||||||
|
:j-email-id="mailDetailJEmailId"
|
||||||
@close="closeDetail"
|
@close="closeDetail"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,6 +223,39 @@ import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
|||||||
},
|
},
|
||||||
selectedAccountEmail() {
|
selectedAccountEmail() {
|
||||||
return this.selectedAccount ? this.selectedAccount.smtp_user : '';
|
return this.selectedAccount ? this.selectedAccount.smtp_user : '';
|
||||||
|
},
|
||||||
|
/** 当前邮箱账号绑定的期刊,供邮件内一键建稿与 articleAdd 回填一致 */
|
||||||
|
mailDetailJournalId() {
|
||||||
|
const a = this.selectedAccount;
|
||||||
|
if (a && a.journal_id != null && String(a.journal_id).trim() !== '') {
|
||||||
|
return a.journal_id;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const s = localStorage.getItem('mailboxCollect_journal_id');
|
||||||
|
return s != null && String(s).trim() !== '' ? s : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mailDetailJournalTitle() {
|
||||||
|
const id = this.mailDetailJournalId;
|
||||||
|
if (id == null) return '';
|
||||||
|
const list = this.journalList || [];
|
||||||
|
const j = list.find((x) => String(x.journal_id) === String(id));
|
||||||
|
return j && j.title ? j.title : '';
|
||||||
|
},
|
||||||
|
/** 当前发件/收件箱账号,供 sendOne 与 mailboxSend 一致 */
|
||||||
|
mailDetailJEmailId() {
|
||||||
|
const a = this.selectedAccount;
|
||||||
|
if (a && a.j_email_id != null && String(a.j_email_id).trim() !== '') {
|
||||||
|
return a.j_email_id;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const s = localStorage.getItem('mailboxCollect_j_email_id');
|
||||||
|
return s != null && String(s).trim() !== '' ? s : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|||||||
381
src/utils/mailManuscriptAutoSubmit.js
Normal file
381
src/utils/mailManuscriptAutoSubmit.js
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
/**
|
||||||
|
* 邮件 Word → checkLogin(作者)→ multipart manuscirpt → contribute → 回填。
|
||||||
|
* 默认 preserveEditorLocalStorage:不覆盖 U_*,作者 user_id 仅用接口返回值;作者账号写入 sessionStorage 备用键。
|
||||||
|
* 服务端会话在作者 checkLogin 后仍为作者,需用编辑密码再次 checkLogin 恢复(见 MailDetail)。
|
||||||
|
*/
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const LS_KEYS = ['U_status', 'U_role', 'U_name', 'U_id', 'U_email', 'U_relname'];
|
||||||
|
|
||||||
|
export function backupEditorLocalStorage() {
|
||||||
|
const b = {};
|
||||||
|
LS_KEYS.forEach((k) => {
|
||||||
|
b[k] = localStorage.getItem(k);
|
||||||
|
});
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreLocalStorage(backup) {
|
||||||
|
if (!backup) return;
|
||||||
|
LS_KEYS.forEach((k) => {
|
||||||
|
if (backup[k] != null) localStorage.setItem(k, backup[k]);
|
||||||
|
else localStorage.removeItem(k);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {*} api - this.$api
|
||||||
|
* @param {{ username: string, password: string, code?: string, baseUrl?: string }} login
|
||||||
|
*/
|
||||||
|
export async function postCheckLogin(api, login) {
|
||||||
|
const baseUrl = login.baseUrl != null ? login.baseUrl : '/api';
|
||||||
|
const random_num = Math.random();
|
||||||
|
const image = `${baseUrl}api/User/retrieveCaptcha?a=${random_num}`;
|
||||||
|
return api.post('api/User/checkLogin', {
|
||||||
|
username: login.username,
|
||||||
|
password: login.password,
|
||||||
|
random_num,
|
||||||
|
image,
|
||||||
|
code: login.code != null && login.code !== '' ? login.code : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验作者 checkLogin 响应,不写 localStorage */
|
||||||
|
export function validateAuthorLoginResponse(res) {
|
||||||
|
if (!res || Number(res.code) === 1) {
|
||||||
|
return { ok: false, msg: (res && res.msg) || 'Login failed' };
|
||||||
|
}
|
||||||
|
const roles = res.data && res.data.roles;
|
||||||
|
const userinfo = res.data && res.data.userinfo;
|
||||||
|
if (!userinfo) {
|
||||||
|
return { ok: false, msg: 'Invalid login response' };
|
||||||
|
}
|
||||||
|
if (roles && String(roles).includes('editor')) {
|
||||||
|
return { ok: false, msg: 'Editor accounts cannot use manuscript auto-submit in this flow' };
|
||||||
|
}
|
||||||
|
return { ok: true, userinfo };
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHADOW_AUTHOR_ID = 'mailManuscript_author_id';
|
||||||
|
const SHADOW_AUTHOR_ACCOUNT = 'mailManuscript_author_account';
|
||||||
|
|
||||||
|
export function persistAuthorShadowSession(userinfo) {
|
||||||
|
if (!userinfo || typeof sessionStorage === 'undefined') return;
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(SHADOW_AUTHOR_ID, String(userinfo.user_id));
|
||||||
|
sessionStorage.setItem(SHADOW_AUTHOR_ACCOUNT, String(userinfo.account || ''));
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAuthorShadowSession() {
|
||||||
|
if (typeof sessionStorage === 'undefined') return;
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem(SHADOW_AUTHOR_ID);
|
||||||
|
sessionStorage.removeItem(SHADOW_AUTHOR_ACCOUNT);
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAuthorSessionFromLoginResponse(res) {
|
||||||
|
const v = validateAuthorLoginResponse(res);
|
||||||
|
if (!v.ok) {
|
||||||
|
return { ok: false, msg: v.msg };
|
||||||
|
}
|
||||||
|
const { userinfo } = v;
|
||||||
|
const roles = res.data && res.data.roles;
|
||||||
|
localStorage.setItem('U_status', '2');
|
||||||
|
localStorage.setItem('U_role', roles != null && roles !== '' ? roles : '');
|
||||||
|
localStorage.setItem('U_name', userinfo.account);
|
||||||
|
localStorage.setItem('U_id', userinfo.user_id);
|
||||||
|
localStorage.setItem('U_relname', userinfo.realname || '');
|
||||||
|
localStorage.setItem('U_email', userinfo.email || '');
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与 Login.vue 编辑账号成功分支一致,用于一键建稿后恢复编辑服务端会话 */
|
||||||
|
export function applyEditorSessionFromLoginResponse(res) {
|
||||||
|
if (!res || Number(res.code) === 1) {
|
||||||
|
return { ok: false, msg: (res && res.msg) || 'Login failed' };
|
||||||
|
}
|
||||||
|
const roles = res.data && res.data.roles;
|
||||||
|
const userinfo = res.data && res.data.userinfo;
|
||||||
|
if (!userinfo) {
|
||||||
|
return { ok: false, msg: 'Invalid login response' };
|
||||||
|
}
|
||||||
|
const roleStr = Array.isArray(roles) ? roles.join(',') : String(roles || '');
|
||||||
|
if (!roleStr.includes('editor')) {
|
||||||
|
return { ok: false, msg: 'Not an editor account' };
|
||||||
|
}
|
||||||
|
localStorage.setItem('U_status', '1');
|
||||||
|
localStorage.setItem('U_role', roles != null && roles !== '' ? roles : '');
|
||||||
|
localStorage.setItem('U_name', userinfo.account);
|
||||||
|
localStorage.setItem('U_id', userinfo.user_id);
|
||||||
|
localStorage.setItem('U_email', userinfo.email || '');
|
||||||
|
if (userinfo.realname) {
|
||||||
|
localStorage.setItem('U_relname', userinfo.realname);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('U_relname');
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与 articleAdd.upload_manuscirpt:`baseUrl + 'api/Article/up_file/type/manuscirpt'` */
|
||||||
|
export async function uploadManuscriptDocxMultipart(baseUrl, blob, filename) {
|
||||||
|
const url = `${baseUrl}api/Article/up_file/type/manuscirpt`;
|
||||||
|
const fd = new FormData();
|
||||||
|
let name = filename || 'manuscript.docx';
|
||||||
|
if (!String(name).toLowerCase().endsWith('.docx')) {
|
||||||
|
name = `${name}.docx`;
|
||||||
|
}
|
||||||
|
fd.append('manuscirpt', blob, name);
|
||||||
|
const { data } = await axios.post(url, fd, {
|
||||||
|
withCredentials: true,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postContribute(api, { upurl, user_id, article_id }) {
|
||||||
|
const payload = {
|
||||||
|
file_url: '/public/manuscirpt/' + upurl,
|
||||||
|
user_id: String(user_id)
|
||||||
|
};
|
||||||
|
if (article_id != null && article_id !== '') {
|
||||||
|
payload.article_id = String(article_id);
|
||||||
|
}
|
||||||
|
return api.post('api/Contribute/contribute', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildKeywordsString(article) {
|
||||||
|
if (!article || article.keywords == null || String(article.keywords).trim() === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(article.keywords)
|
||||||
|
.split(',')
|
||||||
|
.map((k) => k.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与 articleAdd.addWordTablesList → api/Article/addArticleTable */
|
||||||
|
export async function postAddArticleTableFromWordTables(api, article_id, wordTables) {
|
||||||
|
if (!wordTables || !wordTables.length) return { code: 0 };
|
||||||
|
const data = {
|
||||||
|
article_id,
|
||||||
|
list: wordTables.map((e) => ({
|
||||||
|
table: JSON.stringify([...e]),
|
||||||
|
type: 0,
|
||||||
|
html_data: ''
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
return api.post('api/Article/addArticleTable', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {*} api
|
||||||
|
* @param {{ user_id: string, article_id: string, journal_id: number, article: object, upurl: string, staging_username?: string, extractWordTables?: (consumer: (tables: any[]) => Promise<void>) => Promise<void> }} opts
|
||||||
|
*/
|
||||||
|
export async function runPostContributeArticleAddBackfill(api, opts) {
|
||||||
|
const { user_id, article_id, journal_id, article, upurl, extractWordTables, staging_username } = opts;
|
||||||
|
const relPath = 'manuscirpt/' + upurl;
|
||||||
|
const jid = journal_id != null && String(journal_id).trim() !== '' ? Number(journal_id) : NaN;
|
||||||
|
const hasJournal = !Number.isNaN(jid) && jid > 0;
|
||||||
|
|
||||||
|
if (hasJournal) {
|
||||||
|
const cj = await api.post('api/Article/changeJournal', {
|
||||||
|
article_id,
|
||||||
|
journal_id: jid
|
||||||
|
});
|
||||||
|
if (cj && Number(cj.code) !== 0) {
|
||||||
|
return { ok: false, step: 'changeJournal', msg: (cj && cj.msg) || 'changeJournal failed' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyWords = buildKeywordsString(article);
|
||||||
|
const stagingPayload = {
|
||||||
|
article_id,
|
||||||
|
journal: hasJournal ? jid : article.journal_id != null ? article.journal_id : '',
|
||||||
|
title: (article && article.title) || '',
|
||||||
|
keywords: keyWords,
|
||||||
|
abstrart: (article && article.abstrart) || '',
|
||||||
|
type: article && article.type != null ? article.type : '',
|
||||||
|
username:
|
||||||
|
staging_username != null && String(staging_username).trim() !== ''
|
||||||
|
? String(staging_username).trim()
|
||||||
|
: (typeof localStorage !== 'undefined' && localStorage.getItem('U_name')) || '',
|
||||||
|
user_id,
|
||||||
|
approval: '',
|
||||||
|
approval_file: '',
|
||||||
|
approval_content: '',
|
||||||
|
fund: (article && article.fund) || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const st = await api.post('api/Article/addArticleStaging', stagingPayload);
|
||||||
|
if (!st || Number(st.status) !== 1) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
step: 'addArticleStaging',
|
||||||
|
msg: (st && st.msg) || 'addArticleStaging failed'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const af = await api.post('api/Article/addArticlefile', {
|
||||||
|
article_id,
|
||||||
|
type: 'manuscirpt',
|
||||||
|
url: relPath
|
||||||
|
});
|
||||||
|
if (!af || Number(af.code) !== 0) {
|
||||||
|
return { ok: false, step: 'addArticlefile', msg: (af && af.msg) || 'addArticlefile failed' };
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.post('api/Article/reloadArticleImages', { article_id });
|
||||||
|
await api.post('api/Article/reloadArticleTable', { article_id });
|
||||||
|
|
||||||
|
if (typeof extractWordTables === 'function') {
|
||||||
|
try {
|
||||||
|
await extractWordTables(async (tables) => {
|
||||||
|
if (tables && tables.length > 0) {
|
||||||
|
await postAddArticleTableFromWordTables(api, article_id, tables);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
step: 'addArticleTable',
|
||||||
|
msg: (e && e.message) || String(e)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.post('api/Article/getAuthors', { article_id });
|
||||||
|
await api.post('api/Article/getArticleState', { article_id, user_id });
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} ctx
|
||||||
|
* @param {*} ctx.api
|
||||||
|
* @param {string} ctx.baseUrl
|
||||||
|
* @param {object} ctx.login
|
||||||
|
* @param {Blob} ctx.blob
|
||||||
|
* @param {string} ctx.fileName
|
||||||
|
* @param {number|string} [ctx.journal_id] — 当前邮件账号期刊(必填,与 mailbox 一致)
|
||||||
|
* @param {(consumer: (tables: any[]) => Promise<void>) => Promise<void>} [ctx.extractWordTables]
|
||||||
|
* @param {boolean} [ctx.preserveEditorLocalStorage=true] 为 true 时不改 U_*,仅用作者登录返回的 user_id
|
||||||
|
*/
|
||||||
|
export async function runMailManuscriptPipeline(ctx) {
|
||||||
|
const preserve = ctx.preserveEditorLocalStorage !== false;
|
||||||
|
const backup = backupEditorLocalStorage();
|
||||||
|
const { api, baseUrl, login, blob, fileName, journal_id, extractWordTables } = ctx;
|
||||||
|
|
||||||
|
const restoreIfNeeded = () => {
|
||||||
|
if (!preserve) {
|
||||||
|
restoreLocalStorage(backup);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loginRes = await postCheckLogin(api, { ...login, baseUrl });
|
||||||
|
let authorUserId;
|
||||||
|
let authorAccount = '';
|
||||||
|
|
||||||
|
if (preserve) {
|
||||||
|
const v = validateAuthorLoginResponse(loginRes);
|
||||||
|
if (!v.ok) {
|
||||||
|
return { ok: false, msg: v.msg, backup };
|
||||||
|
}
|
||||||
|
authorUserId = String(v.userinfo.user_id);
|
||||||
|
authorAccount = String(v.userinfo.account || '').trim();
|
||||||
|
persistAuthorShadowSession(v.userinfo);
|
||||||
|
} else {
|
||||||
|
const applied = applyAuthorSessionFromLoginResponse(loginRes);
|
||||||
|
if (!applied.ok) {
|
||||||
|
restoreIfNeeded();
|
||||||
|
return { ok: false, msg: applied.msg, backup };
|
||||||
|
}
|
||||||
|
authorUserId = localStorage.getItem('U_id');
|
||||||
|
authorAccount = String(localStorage.getItem('U_name') || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authorUserId) {
|
||||||
|
restoreIfNeeded();
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
return { ok: false, msg: 'Missing user_id after login', backup };
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadRes = await uploadManuscriptDocxMultipart(baseUrl, blob, fileName);
|
||||||
|
if (!uploadRes || Number(uploadRes.code) !== 0) {
|
||||||
|
restoreIfNeeded();
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
msg: (uploadRes && uploadRes.msg) || 'Manuscript upload failed',
|
||||||
|
backup
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const upurl = uploadRes.upurl;
|
||||||
|
if (!upurl) {
|
||||||
|
restoreIfNeeded();
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
return { ok: false, msg: 'Upload response missing upurl', backup };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cr = await postContribute(api, { upurl, user_id: authorUserId });
|
||||||
|
if (!cr || Number(cr.status) !== 1 || !cr.article || cr.article.article_id == null) {
|
||||||
|
restoreIfNeeded();
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
msg: (cr && cr.msg) || 'Contribute failed',
|
||||||
|
backup,
|
||||||
|
contributeRes: cr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleId = String(cr.article.article_id);
|
||||||
|
const jid = journal_id != null && String(journal_id).trim() !== '' ? Number(journal_id) : NaN;
|
||||||
|
if (Number.isNaN(jid) || jid <= 0) {
|
||||||
|
restoreIfNeeded();
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
msg: 'Missing journal_id for post-contribute backfill',
|
||||||
|
backup,
|
||||||
|
contributeRes: cr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bf = await runPostContributeArticleAddBackfill(api, {
|
||||||
|
user_id: authorUserId,
|
||||||
|
article_id: articleId,
|
||||||
|
journal_id: jid,
|
||||||
|
article: cr.article,
|
||||||
|
upurl,
|
||||||
|
staging_username: authorAccount,
|
||||||
|
extractWordTables
|
||||||
|
});
|
||||||
|
if (!bf.ok) {
|
||||||
|
restoreIfNeeded();
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
msg: bf.msg || 'Post-contribute backfill failed',
|
||||||
|
backup,
|
||||||
|
step: bf.step,
|
||||||
|
article_id: articleId,
|
||||||
|
contributeRes: cr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
return { ok: true, article_id: articleId, backup };
|
||||||
|
} catch (e) {
|
||||||
|
restoreIfNeeded();
|
||||||
|
clearAuthorShadowSession();
|
||||||
|
return { ok: false, msg: (e && e.message) || String(e), backup };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user