Compare commits
10 Commits
4d7c230abe
...
similarity
| Author | SHA1 | Date | |
|---|---|---|---|
| d765628bb3 | |||
| be8ea4e486 | |||
| 4426077094 | |||
| 6050dd365d | |||
| 723ec0d190 | |||
| b10de50fdf | |||
| f67d8d5600 | |||
| 67a4875b01 | |||
| 8e59702f0b | |||
| ea1564018e |
@@ -19,8 +19,8 @@ const service = axios.create({
|
|||||||
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
||||||
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
||||||
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
||||||
// baseURL: '/api', //本地
|
baseURL: '/api', //本地
|
||||||
baseURL: '/', //正式
|
// baseURL: '/', //正式
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
//记得切换
|
//记得切换
|
||||||
|
|
||||||
//正式
|
//正式
|
||||||
const mediaUrl = '/public/';
|
// const mediaUrl = '/public/';
|
||||||
const baseUrl = '/';
|
// const baseUrl = '/';
|
||||||
|
|
||||||
//正式环境
|
//正式环境
|
||||||
|
|
||||||
// const mediaUrl = 'https://submission.tmrjournals.com/public/';
|
const mediaUrl = 'https://submission.tmrjournals.com/public/';
|
||||||
// // const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
|
// const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
|
||||||
// const baseUrl = '/api'
|
const baseUrl = '/api'
|
||||||
|
|
||||||
//测试环境
|
//测试环境
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ const en = {
|
|||||||
status: 'Status',
|
status: 'Status',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
deleteInfo: 'Are you sure you want to delete this journal installment?',
|
deleteInfo: 'Are you sure you want to delete this journal installment?',
|
||||||
|
plagiarismNotChecked: 'Not checked',
|
||||||
|
plagiarismChecking: 'Checking…',
|
||||||
|
plagiarismRecheck: 'Re-check',
|
||||||
|
plagiarismCheckFailed: 'Failed to start plagiarism check.',
|
||||||
|
plagiarismStatusFailed: 'Failed to load plagiarism status.',
|
||||||
|
plagiarismNoReportUrl: 'Report link is not available yet.',
|
||||||
|
plagiarismReportDetailFailed: 'Could not load manuscript details. Please try again.',
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
main: 'Personal Center',
|
main: 'Personal Center',
|
||||||
@@ -503,7 +510,7 @@ const en = {
|
|||||||
languagePlaceholder: 'Language',
|
languagePlaceholder: 'Language',
|
||||||
searchBtn: 'Search',
|
searchBtn: 'Search',
|
||||||
createTemplate: 'Create Template',
|
createTemplate: 'Create Template',
|
||||||
colTitle: 'Template title',
|
colTitle: 'Template title',
|
||||||
colSubject: 'Email subject',
|
colSubject: 'Email subject',
|
||||||
colScene: 'Scene',
|
colScene: 'Scene',
|
||||||
colLanguage: 'Language',
|
colLanguage: 'Language',
|
||||||
@@ -519,6 +526,23 @@ colTitle: 'Template title',
|
|||||||
deleteFail: 'Delete failed',
|
deleteFail: 'Delete failed',
|
||||||
previewTitle: 'Template preview',
|
previewTitle: 'Template preview',
|
||||||
previewClose: 'Close',
|
previewClose: 'Close',
|
||||||
|
batchImportBtn: 'Batch import',
|
||||||
|
batchImportTitle: 'Batch import templates (JSON)',
|
||||||
|
batchImportHint:
|
||||||
|
'Paste a JSON array. Each item is saved via the same API as the editor (omit template_id to create; include template_id to update). Fields: title, subject, scene, language (or lang), version, body_html (or body), variables_json (or variables), is_active.',
|
||||||
|
batchImportCommonTip: 'Journal ID is set in the field below; when non-empty it overrides journal_id / journalId on every row.',
|
||||||
|
batchImportJournalId: 'Journal ID',
|
||||||
|
batchImportJournalPlaceholder: 'Match list filter or type manually',
|
||||||
|
batchImportRun: 'Run import',
|
||||||
|
batchImportBadJson: 'Invalid JSON',
|
||||||
|
batchImportEmpty: 'Array must contain at least one object',
|
||||||
|
batchImportMissingJournal: 'Row {index}: missing journal_id (use the input above or put journal_id in JSON)',
|
||||||
|
batchImportMissingField: 'Row {index}: missing {field}',
|
||||||
|
batchImportRowFail: 'Row {index} failed: {msg}',
|
||||||
|
batchImportRowNetwork: 'Row {index}: request error',
|
||||||
|
batchImportDone: 'Done: {ok} succeeded, {fail} failed',
|
||||||
|
batchImportErrorsTitle: 'Errors (first 8)',
|
||||||
|
batchImportSaveFail: 'Save failed',
|
||||||
},
|
},
|
||||||
mailboxStyle: {
|
mailboxStyle: {
|
||||||
title: 'Email Styles',
|
title: 'Email Styles',
|
||||||
@@ -650,6 +674,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: 'Auto submit',
|
||||||
|
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?',
|
||||||
|
registerAuthorSuccess: 'Author account created.',
|
||||||
|
registerAuthorSuccessWithEmail: 'Created: {email}, password: 123456qwe',
|
||||||
|
registerAuthorFail: 'Creation failed. Try again later or add the user manually in User Management.',
|
||||||
|
registerAuthorExistsEmail: 'This email is already registered.',
|
||||||
|
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.',
|
||||||
|
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',
|
||||||
@@ -995,8 +1085,8 @@ colTitle: 'Template title',
|
|||||||
step3: 'References',
|
step3: 'References',
|
||||||
step: 'step',
|
step: 'step',
|
||||||
Information: 'Fill in information',
|
Information: 'Fill in information',
|
||||||
|
startPreAccept: 'Start the pre-acceptance process',
|
||||||
|
startPreAcceptWithPayment: 'Start the pre-acceptance process and complete your payment',
|
||||||
},
|
},
|
||||||
Formula: {
|
Formula: {
|
||||||
FormulaTemplate: 'Formula Template'
|
FormulaTemplate: 'Formula Template'
|
||||||
@@ -1171,6 +1261,55 @@ colTitle: 'Template title',
|
|||||||
onlySaveConfig: 'Save configuration only',
|
onlySaveConfig: 'Save configuration only',
|
||||||
enableNowNextDay: 'Enable auto promotion now (starts next day)',
|
enableNowNextDay: 'Enable auto promotion now (starts next day)',
|
||||||
factoryCreateBtn: 'Create automated promotion task',
|
factoryCreateBtn: 'Create automated promotion task',
|
||||||
|
factoryBatchImportBtn: 'Batch import (JSON)',
|
||||||
|
factoryBatchImportTitle: 'Batch create tasks (JSON)',
|
||||||
|
factoryBatchImportHintShort: 'Submit a JSON array; non-empty fields merge into each row. You can still edit JSON.',
|
||||||
|
factoryBatchImportHint:
|
||||||
|
'Paste a JSON array… Pick journal (getAllJournal) or type ID; template/style/fetch_ids below override JSON when non-empty. Load promotion fields to tick IDs or type comma-separated fetch_ids; load accounts for j_email_id. expert_type "5" needs partitions/countries. Shorthand: zones, countries, email_id_list.',
|
||||||
|
factoryBatchImportCommonTip: 'Journal from cover or ID field; non-empty template ID, style ID, or fetch_ids override JSON on every row.',
|
||||||
|
factoryBatchImportJournalId: 'Journal ID',
|
||||||
|
factoryBatchImportJournalPick: 'Journal',
|
||||||
|
factoryBatchImportJournalEmpty: 'No journals returned. Check api/Journal/getAllJournal.',
|
||||||
|
factoryBatchImportJournalManualPlaceholder: 'Filled when you pick a cover, or type manually',
|
||||||
|
factoryBatchImportTemplateId: 'Template ID',
|
||||||
|
factoryBatchImportStyleId: 'Style ID',
|
||||||
|
factoryBatchImportJournalPlaceholder: 'Merged into payload',
|
||||||
|
factoryBatchImportTemplatePlaceholder: 'Merged into payload',
|
||||||
|
factoryBatchImportStylePlaceholder: 'Merged into payload',
|
||||||
|
factoryBatchImportFetchIdsLabel: 'Promotion fields (fetch_ids)',
|
||||||
|
factoryBatchImportLoadFields: 'Load available fields',
|
||||||
|
factoryBatchImportFetchTip: 'Uses current journal: pick journal, then load. Search by name or ID (multiple tokens: space or comma). “Select all” selects the filtered list when search is set, otherwise all fields. Checkboxes sync with the comma text; when non-empty, overrides fetch_ids on every row.',
|
||||||
|
factoryBatchImportFetchIdsManual: 'Merged IDs (comma-separated, editable)',
|
||||||
|
factoryBatchImportFetchIdsPlaceholder: 'e.g. 1,2,3 or use checkboxes',
|
||||||
|
factoryBatchImportNeedJournalForFields: 'Select or enter journal ID first',
|
||||||
|
factoryBatchImportLoadAccounts: 'Load accounts for journal',
|
||||||
|
factoryBatchImportAccountsApiTip: 'POST api/email_client/getAccounts with journal_id',
|
||||||
|
factoryBatchImportColEmailId: 'j_email_id',
|
||||||
|
factoryBatchImportColAddress: 'Sender address',
|
||||||
|
factoryBatchImportColQuota: 'Remaining / daily limit',
|
||||||
|
factoryBatchImportCopyEmailIds: 'Copy email_ids (comma)',
|
||||||
|
factoryBatchImportNeedJournalForAccounts: 'Enter journal ID first',
|
||||||
|
factoryBatchImportNoAccounts: 'No mailbox accounts for this journal',
|
||||||
|
factoryBatchImportAccountsFail: 'Failed to load accounts',
|
||||||
|
factoryBatchImportCopyEmailIdsEmpty: 'Load the account list first',
|
||||||
|
factoryBatchImportIdsCopied: 'Copied j_email_id list to clipboard',
|
||||||
|
factoryBatchImportCopyFail: 'Copy failed; select and copy manually',
|
||||||
|
factoryBatchImportSyncToJson: 'Apply top form to JSON',
|
||||||
|
factoryBatchImportSyncFromJson: 'Load first row into form',
|
||||||
|
factoryBatchImportSyncIncludeEmails: 'When applying, set each row email_ids from loaded accounts',
|
||||||
|
factoryBatchImportSyncTip: 'You can still edit JSON manually; non-empty top fields are merged again on submit.',
|
||||||
|
factoryBatchImportJsonFromUiOk: 'JSON updated from the form',
|
||||||
|
factoryBatchImportUiFromJsonOk: 'Form updated from the first JSON row',
|
||||||
|
factoryBatchImportRun: 'Run batch create',
|
||||||
|
factoryBatchImportBadJson: 'Invalid JSON; check brackets and quotes',
|
||||||
|
factoryBatchImportEmpty: 'Array must contain at least one object',
|
||||||
|
factoryBatchImportMissing: 'Row {index}: missing field {field}',
|
||||||
|
factoryBatchImportNeedFetch: 'Row {index}: expert database requires fetch_ids',
|
||||||
|
factoryBatchImportNeedZone: 'Row {index}: expert database requires target_partitions or target_country_ids',
|
||||||
|
factoryBatchImportRowFail: 'Row {index} failed: {msg}',
|
||||||
|
factoryBatchImportRowNetwork: 'Row {index}: request error',
|
||||||
|
factoryBatchImportDone: 'Done: {ok} succeeded, {fail} failed',
|
||||||
|
factoryBatchImportErrorsTitle: 'Errors (first 8)',
|
||||||
factoryDialogTitle: 'Create task',
|
factoryDialogTitle: 'Create task',
|
||||||
factoryJournal: 'Journal',
|
factoryJournal: 'Journal',
|
||||||
factoryJournalPlaceholder: 'Select a journal',
|
factoryJournalPlaceholder: 'Select a journal',
|
||||||
@@ -1243,6 +1382,7 @@ colTitle: 'Template title',
|
|||||||
,
|
,
|
||||||
autoPromotionLogs: {
|
autoPromotionLogs: {
|
||||||
detail: 'Auto Promotion Details',
|
detail: 'Auto Promotion Details',
|
||||||
|
pipelineHistory: 'PIPELINE HISTORY',
|
||||||
factoryTaskSelectPlaceholder: 'Select promotion task',
|
factoryTaskSelectPlaceholder: 'Select promotion task',
|
||||||
configured: 'Configured',
|
configured: 'Configured',
|
||||||
editConfig: 'Edit auto promotion configuration',
|
editConfig: 'Edit auto promotion configuration',
|
||||||
@@ -1310,6 +1450,7 @@ colTitle: 'Template title',
|
|||||||
taskLogState2: 'Failed',
|
taskLogState2: 'Failed',
|
||||||
taskLogState3: 'Bounced',
|
taskLogState3: 'Bounced',
|
||||||
taskLogState4: 'Cancelled',
|
taskLogState4: 'Cancelled',
|
||||||
|
logColIndex: 'No.',
|
||||||
logColExpert: 'Expert',
|
logColExpert: 'Expert',
|
||||||
logColSendTime: 'Sent at',
|
logColSendTime: 'Sent at',
|
||||||
logColPreparedAt: 'Prepared at',
|
logColPreparedAt: 'Prepared at',
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ const zh = {
|
|||||||
status: '状态',
|
status: '状态',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
deleteInfo: '您确定要删除该期刊分期吗?',
|
deleteInfo: '您确定要删除该期刊分期吗?',
|
||||||
|
plagiarismNotChecked: '未检测',
|
||||||
|
plagiarismChecking: '正在检测…',
|
||||||
|
plagiarismRecheck: '重新查重',
|
||||||
|
plagiarismCheckFailed: '查重任务启动失败。',
|
||||||
|
plagiarismStatusFailed: '获取查重状态失败。',
|
||||||
|
plagiarismNoReportUrl: '报告链接暂不可用。',
|
||||||
|
plagiarismReportDetailFailed: '获取稿件详情失败,请稍后重试。',
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
main: '个人中心',
|
main: '个人中心',
|
||||||
@@ -508,6 +515,23 @@ const zh = {
|
|||||||
deleteFail: '删除失败',
|
deleteFail: '删除失败',
|
||||||
previewTitle: '模板预览',
|
previewTitle: '模板预览',
|
||||||
previewClose: '关闭',
|
previewClose: '关闭',
|
||||||
|
batchImportBtn: '批量导入',
|
||||||
|
batchImportTitle: '批量导入邮件模板(JSON)',
|
||||||
|
batchImportHint:
|
||||||
|
'粘贴 JSON 数组,每条对应一次保存接口(新建不传 template_id;更新可带 template_id)。字段与编辑页一致:title、subject、scene、language(可用 lang)、version、body_html(可用 body)、variables_json(可用 variables)、is_active。',
|
||||||
|
batchImportCommonTip: '期刊 ID 在下方单独填写;若填写非空,会覆盖每条 JSON 中的 journal_id / journalId。',
|
||||||
|
batchImportJournalId: '期刊 ID',
|
||||||
|
batchImportJournalPlaceholder: '可与列表筛选一致,或手填',
|
||||||
|
batchImportRun: '开始导入',
|
||||||
|
batchImportBadJson: 'JSON 解析失败',
|
||||||
|
batchImportEmpty: '请至少包含一条对象',
|
||||||
|
batchImportMissingJournal: '第 {index} 条:缺少期刊 ID(请填写上方输入框或在 JSON 中提供 journal_id)',
|
||||||
|
batchImportMissingField: '第 {index} 条:缺少字段 {field}',
|
||||||
|
batchImportRowFail: '第 {index} 条保存失败:{msg}',
|
||||||
|
batchImportRowNetwork: '第 {index} 条请求异常',
|
||||||
|
batchImportDone: '完成:成功 {ok},失败 {fail}',
|
||||||
|
batchImportErrorsTitle: '失败明细(最多 8 条)',
|
||||||
|
batchImportSaveFail: '保存失败',
|
||||||
},
|
},
|
||||||
mailboxStyle: {
|
mailboxStyle: {
|
||||||
title: '邮件风格',
|
title: '邮件风格',
|
||||||
@@ -639,6 +663,68 @@ const zh = {
|
|||||||
printBtn: '打印',
|
printBtn: '打印',
|
||||||
previewNotSupported: '该文件格式无法在线预览',
|
previewNotSupported: '该文件格式无法在线预览',
|
||||||
downloadToView: '下载到本地查看',
|
downloadToView: '下载到本地查看',
|
||||||
|
registerAuthorBtn: '自动投稿',
|
||||||
|
registerAuthorConfirm:
|
||||||
|
'将使用推广后台「添加用户」接口创建账号:登录名「{account}」,显示名「{realname}」,邮箱「{email}」,初始密码 123456qwe(无需验证码)。是否继续?',
|
||||||
|
registerAuthorSuccess: '作者账号已创建。',
|
||||||
|
registerAuthorSuccessWithEmail: '已创建:{email},密码:123456qwe',
|
||||||
|
registerAuthorFail: '创建失败,请稍后重试或到用户管理中手动添加。',
|
||||||
|
registerAuthorExistsEmail: '该邮箱已被注册。',
|
||||||
|
registerAuthorExistsAccount: '该登录名已被占用,请人工处理或修改发件人显示名后重试。',
|
||||||
|
registerAuthorNeedEmail: '缺少发件人邮箱,无法创建账号。',
|
||||||
|
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: '关键词配置',
|
||||||
@@ -984,8 +1070,8 @@ const zh = {
|
|||||||
step3: '参考',
|
step3: '参考',
|
||||||
step: 'step',
|
step: 'step',
|
||||||
Information: 'Fill in information',
|
Information: 'Fill in information',
|
||||||
|
startPreAccept: '开始预接收流程',
|
||||||
|
startPreAcceptWithPayment: '开始预接收流程并完成支付',
|
||||||
},
|
},
|
||||||
Formula:{
|
Formula:{
|
||||||
FormulaTemplate:'公式模版'
|
FormulaTemplate:'公式模版'
|
||||||
@@ -1156,6 +1242,55 @@ const zh = {
|
|||||||
onlySaveConfig: '仅保存配置',
|
onlySaveConfig: '仅保存配置',
|
||||||
enableNowNextDay: '立即激活自动推广(次日开始自动推广)',
|
enableNowNextDay: '立即激活自动推广(次日开始自动推广)',
|
||||||
factoryCreateBtn: '创建自动化推广任务',
|
factoryCreateBtn: '创建自动化推广任务',
|
||||||
|
factoryBatchImportBtn: '临时批量导入',
|
||||||
|
factoryBatchImportTitle: '批量创建推广任务(JSON)',
|
||||||
|
factoryBatchImportHintShort: '数组提交;上方非空项会合并进每条请求,仍可直接改 JSON。',
|
||||||
|
factoryBatchImportHint:
|
||||||
|
'粘贴 JSON 数组…期刊用封面(getAllJournal)或手填 ID;模板/样式/推广领域 fetch_ids 在下方非空则覆盖 JSON。推广领域可「加载可选领域」勾选或手改逗号 ID;填期刊后可查邮箱 j_email_id(getAccounts)。expert_type 为 5 时须分区或国家。简写:zones、countries、email_id_list。',
|
||||||
|
factoryBatchImportCommonTip: '期刊以封面或下方 ID 为准;模板 ID、样式 ID、推广领域 fetch_ids 任一非空则覆盖每条 JSON 中对应字段后再请求接口。',
|
||||||
|
factoryBatchImportJournalId: '期刊 ID',
|
||||||
|
factoryBatchImportJournalPick: '选择期刊',
|
||||||
|
factoryBatchImportJournalEmpty: '未获取到期刊列表,请检查接口或稍后重试',
|
||||||
|
factoryBatchImportJournalManualPlaceholder: '点击上方封面自动填入,也可手改',
|
||||||
|
factoryBatchImportTemplateId: '模板 ID',
|
||||||
|
factoryBatchImportStyleId: '样式 ID',
|
||||||
|
factoryBatchImportJournalPlaceholder: '与 JSON 合并',
|
||||||
|
factoryBatchImportTemplatePlaceholder: '与 JSON 合并',
|
||||||
|
factoryBatchImportStylePlaceholder: '与 JSON 合并',
|
||||||
|
factoryBatchImportFetchIdsLabel: '推广领域 fetch_ids',
|
||||||
|
factoryBatchImportLoadFields: '加载可选领域',
|
||||||
|
factoryBatchImportFetchTip: '依赖当前期刊:先选期刊再加载。上方可搜索名称或 ID(多关键词用空格或逗号);有搜索时「全选」勾选当前筛选结果,无搜索时「全选」为全部。勾选与下方逗号文本同步,非空则覆盖每条 JSON 的 fetch_ids。',
|
||||||
|
factoryBatchImportFetchIdsManual: '合并用 ID(逗号分隔,可手改)',
|
||||||
|
factoryBatchImportFetchIdsPlaceholder: '例:1,2,3;或与勾选联动',
|
||||||
|
factoryBatchImportNeedJournalForFields: '请先选择或填写期刊 ID',
|
||||||
|
factoryBatchImportLoadAccounts: '查询该期刊邮箱',
|
||||||
|
factoryBatchImportAccountsApiTip: 'POST api/email_client/getAccounts,参数 journal_id',
|
||||||
|
factoryBatchImportColEmailId: 'j_email_id',
|
||||||
|
factoryBatchImportColAddress: '发件地址',
|
||||||
|
factoryBatchImportColQuota: '今日剩余 / 日上限',
|
||||||
|
factoryBatchImportCopyEmailIds: '复制 email_ids(逗号)',
|
||||||
|
factoryBatchImportNeedJournalForAccounts: '请先填写期刊 ID',
|
||||||
|
factoryBatchImportNoAccounts: '该期刊下暂无邮箱账号',
|
||||||
|
factoryBatchImportAccountsFail: '拉取邮箱列表失败',
|
||||||
|
factoryBatchImportCopyEmailIdsEmpty: '请先查询出账号列表',
|
||||||
|
factoryBatchImportIdsCopied: '已复制 j_email_id 列表到剪贴板',
|
||||||
|
factoryBatchImportCopyFail: '复制失败,请手动选中复制',
|
||||||
|
factoryBatchImportSyncToJson: '上方选项写入 JSON',
|
||||||
|
factoryBatchImportSyncFromJson: '首条 JSON 回显到上方',
|
||||||
|
factoryBatchImportSyncIncludeEmails: '写入时用当前邮箱列表覆盖每条 email_ids',
|
||||||
|
factoryBatchImportSyncTip: '写入后仍可单独改 JSON;提交时若上方输入框非空仍会再合并覆盖。',
|
||||||
|
factoryBatchImportJsonFromUiOk: '已根据上方选项更新 JSON',
|
||||||
|
factoryBatchImportUiFromJsonOk: '已用首条 JSON 更新上方表单',
|
||||||
|
factoryBatchImportRun: '开始批量创建',
|
||||||
|
factoryBatchImportBadJson: 'JSON 解析失败,请检查括号与引号',
|
||||||
|
factoryBatchImportEmpty: '请至少包含一条对象',
|
||||||
|
factoryBatchImportMissing: '第 {index} 条缺少字段:{field}',
|
||||||
|
factoryBatchImportNeedFetch: '第 {index} 条:专家库需填写 fetch_ids(推广领域)',
|
||||||
|
factoryBatchImportNeedZone: '第 {index} 条:专家库需至少填写分区或国家(target_partitions / target_country_ids)',
|
||||||
|
factoryBatchImportRowFail: '第 {index} 条创建失败:{msg}',
|
||||||
|
factoryBatchImportRowNetwork: '第 {index} 条请求异常',
|
||||||
|
factoryBatchImportDone: '完成:成功 {ok},失败 {fail}',
|
||||||
|
factoryBatchImportErrorsTitle: '失败明细(最多显示 8 条)',
|
||||||
factoryDialogTitle: '创建任务',
|
factoryDialogTitle: '创建任务',
|
||||||
factoryJournal: '期刊',
|
factoryJournal: '期刊',
|
||||||
factoryJournalPlaceholder: '请选择期刊',
|
factoryJournalPlaceholder: '请选择期刊',
|
||||||
@@ -1228,6 +1363,7 @@ const zh = {
|
|||||||
,
|
,
|
||||||
autoPromotionLogs: {
|
autoPromotionLogs: {
|
||||||
detail: '自动推广详情',
|
detail: '自动推广详情',
|
||||||
|
pipelineHistory: '流水线历史',
|
||||||
factoryTaskSelectPlaceholder: '选择推广任务',
|
factoryTaskSelectPlaceholder: '选择推广任务',
|
||||||
configured: '已配置',
|
configured: '已配置',
|
||||||
editConfig: '修改期刊自动推广配置',
|
editConfig: '修改期刊自动推广配置',
|
||||||
@@ -1295,6 +1431,7 @@ const zh = {
|
|||||||
taskLogState2: '失败',
|
taskLogState2: '失败',
|
||||||
taskLogState3: '退信',
|
taskLogState3: '退信',
|
||||||
taskLogState4: '取消',
|
taskLogState4: '取消',
|
||||||
|
logColIndex: '序号',
|
||||||
logColExpert: '专家信息',
|
logColExpert: '专家信息',
|
||||||
logColSendTime: '发送时间',
|
logColSendTime: '发送时间',
|
||||||
logColPreparedAt: '预处理完成时间',
|
logColPreparedAt: '预处理完成时间',
|
||||||
|
|||||||
@@ -27,14 +27,13 @@
|
|||||||
type="text"
|
type="text"
|
||||||
v-model="formData.engName"
|
v-model="formData.engName"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
required
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label><span class="required-star">*</span> Email (QQ mail is not allowed)</label>
|
<label><span class="required-star">*</span> Email (QQ mail is not allowed)</label>
|
||||||
<input type="email" v-model="formData.email" placeholder="" required autocomplete="off" />
|
<input type="email" v-model="formData.email" placeholder="" autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -43,7 +42,6 @@
|
|||||||
type="text"
|
type="text"
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
required
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,7 +315,22 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const email = (this.formData.email || '').toLowerCase();
|
const engName = (this.formData.engName || '').trim();
|
||||||
|
if (!engName) {
|
||||||
|
this.alertError('Please enter your English name.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRaw = (this.formData.email || '').trim();
|
||||||
|
if (!emailRaw) {
|
||||||
|
this.alertError('Please enter your email address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const email = emailRaw.toLowerCase();
|
||||||
|
if (!/^[-._A-Za-z0-9]+@[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+$/.test(email)) {
|
||||||
|
this.alertError('Please enter a valid email address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (email.endsWith('@qq.com')) {
|
if (email.endsWith('@qq.com')) {
|
||||||
this.alertError('Registration failed: QQ email addresses are not accepted.');
|
this.alertError('Registration failed: QQ email addresses are not accepted.');
|
||||||
return;
|
return;
|
||||||
@@ -340,8 +353,8 @@ export default {
|
|||||||
.post('api/Ucenter/submitApplyYboardForExpert', {
|
.post('api/Ucenter/submitApplyYboardForExpert', {
|
||||||
journal_id: this.journalId,
|
journal_id: this.journalId,
|
||||||
expert_id: this.expertId,
|
expert_id: this.expertId,
|
||||||
name: this.formData.engName,
|
name: engName,
|
||||||
email: this.formData.email,
|
email: emailRaw,
|
||||||
cv: this.uploadedCvPath,
|
cv: this.uploadedCvPath,
|
||||||
password: this.formData.password
|
password: this.formData.password
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="article-detail-editor-page">
|
||||||
<div class="crumbs">
|
<div class="crumbs">
|
||||||
<div class="art_state_message_id" style="padding-left: 18px">
|
<div class="art_state_message_id" style="padding-left: 18px">
|
||||||
<font
|
<font
|
||||||
@@ -915,9 +915,38 @@
|
|||||||
}}</b>
|
}}</b>
|
||||||
<!-- <el-button type="text" @click="testedit" icon="el-icon-edit">Change</el-button> -->
|
<!-- <el-button type="text" @click="testedit" icon="el-icon-edit">Change</el-button> -->
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="detail-plagiarism-row">
|
||||||
<span>Repetition : </span>
|
<span class="detail-plagiarism-main">
|
||||||
<b>{{ form.repetition }}%</b>
|
<span class="detail-plagiarism-lbl">Repetition :</span>
|
||||||
|
<template v-if="detailPlagiarismUiShowProcessing()">
|
||||||
|
<span class="detail-plagiarism-loading">
|
||||||
|
<i class="el-icon-loading"></i>
|
||||||
|
{{ $t('articleListEditor.plagiarismChecking') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="detailPlagiarismUiShowResult()">
|
||||||
|
<span
|
||||||
|
class="detail-plagiarism-pct-text"
|
||||||
|
:style="detailPlagiarismSimilarityStyle()"
|
||||||
|
@click.stop="detailOpenPlagiarismReport"
|
||||||
|
>
|
||||||
|
{{ detailPlagiarismSimilarityNumber() }}%
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="detail-plagiarism-not-checked-text" @click.stop="detailTriggerCrossrefPlagiarismCheck">
|
||||||
|
{{ $t('articleListEditor.plagiarismNotChecked') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-button
|
||||||
|
v-if="detailPlagiarismShowRecheck()"
|
||||||
|
type="text"
|
||||||
|
class="detail-plagiarism-recheck-btn detail-plagiarism-recheck-inline"
|
||||||
|
@click.stop="detailTriggerCrossrefPlagiarismCheck"
|
||||||
|
>
|
||||||
|
{{ $t('articleListEditor.plagiarismRecheck') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!--<br clear="both">
|
<!--<br clear="both">
|
||||||
<el-button type="primary" @click="showResubmit" style="margin: 15px 0 0 0;">Resubmit the manuscript
|
<el-button type="primary" @click="showResubmit" style="margin: 15px 0 0 0;">Resubmit the manuscript
|
||||||
@@ -937,9 +966,38 @@
|
|||||||
}}</b>
|
}}</b>
|
||||||
<el-button style="padding: 0" type="text" @click="testedit" icon="el-icon-edit">Change</el-button>
|
<el-button style="padding: 0" type="text" @click="testedit" icon="el-icon-edit">Change</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="detail-plagiarism-row detail-plagiarism-row--with-actions">
|
||||||
<span>Repetition : </span>
|
<span class="detail-plagiarism-main">
|
||||||
<b>{{ form.repetition }}%</b>
|
<span class="detail-plagiarism-lbl">Repetition :</span>
|
||||||
|
<template v-if="detailPlagiarismUiShowProcessing()">
|
||||||
|
<span class="detail-plagiarism-loading">
|
||||||
|
<i class="el-icon-loading"></i>
|
||||||
|
{{ $t('articleListEditor.plagiarismChecking') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="detailPlagiarismUiShowResult()">
|
||||||
|
<span
|
||||||
|
class="detail-plagiarism-pct-text"
|
||||||
|
:style="detailPlagiarismSimilarityStyle()"
|
||||||
|
@click.stop="detailOpenPlagiarismReport"
|
||||||
|
>
|
||||||
|
{{ detailPlagiarismSimilarityNumber() }}%
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="detail-plagiarism-not-checked-text" @click.stop="detailTriggerCrossrefPlagiarismCheck">
|
||||||
|
{{ $t('articleListEditor.plagiarismNotChecked') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-button
|
||||||
|
v-if="detailPlagiarismShowRecheck()"
|
||||||
|
type="text"
|
||||||
|
class="detail-plagiarism-recheck-btn detail-plagiarism-recheck-inline"
|
||||||
|
@click.stop="detailTriggerCrossrefPlagiarismCheck"
|
||||||
|
>
|
||||||
|
{{ $t('articleListEditor.plagiarismRecheck') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
<a :href="mediaUrl + form.repeurl" v-if="form.repeurl" class="zip_load" target="_blank">
|
<a :href="mediaUrl + form.repeurl" v-if="form.repeurl" class="zip_load" target="_blank">
|
||||||
<img src="../../assets/img/icon_0.png" />
|
<img src="../../assets/img/icon_0.png" />
|
||||||
<span>Duplicate check file</span>
|
<span>Duplicate check file</span>
|
||||||
@@ -1426,6 +1484,8 @@
|
|||||||
import timetalk from './time_talk';
|
import timetalk from './time_talk';
|
||||||
import reviewerDetail from '../../components/page/components/articleDetail/reviewerdetail.vue';
|
import reviewerDetail from '../../components/page/components/articleDetail/reviewerdetail.vue';
|
||||||
import FigureCopyright from '../../components/page/components/articleDetail/FigureCopyright.vue';
|
import FigureCopyright from '../../components/page/components/articleDetail/FigureCopyright.vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { getSimilarityStyle } from '../../utils/ithenticateSimilarityStyle';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
timetalk,
|
timetalk,
|
||||||
@@ -1549,6 +1609,9 @@ export default {
|
|||||||
approval_content: '',
|
approval_content: '',
|
||||||
is_figure_copyright: '',
|
is_figure_copyright: '',
|
||||||
repetition: '',
|
repetition: '',
|
||||||
|
plagiarism_similarity: '',
|
||||||
|
plagiarism_report_url: '',
|
||||||
|
plagiarism_job_state: '',
|
||||||
manuscirpt: '',
|
manuscirpt: '',
|
||||||
remarks: '',
|
remarks: '',
|
||||||
state: '',
|
state: '',
|
||||||
@@ -1743,7 +1806,9 @@ export default {
|
|||||||
underReview: ['1'],
|
underReview: ['1'],
|
||||||
finalDecision: ['1'],
|
finalDecision: ['1'],
|
||||||
is_figure_copyright: '',
|
is_figure_copyright: '',
|
||||||
figurecopyright_file: ''
|
figurecopyright_file: '',
|
||||||
|
plagiarismDetailPollTimer: null,
|
||||||
|
plagiarismDetailPending: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -2541,6 +2606,26 @@ export default {
|
|||||||
this.form.approval_file = res.article.approval_file;
|
this.form.approval_file = res.article.approval_file;
|
||||||
this.form.approval_content = res.article.approval_content;
|
this.form.approval_content = res.article.approval_content;
|
||||||
this.form.repetition = res.article.repetition;
|
this.form.repetition = res.article.repetition;
|
||||||
|
this.$set(
|
||||||
|
this.form,
|
||||||
|
'plagiarism_similarity',
|
||||||
|
res.article.plagiarism_similarity != null && res.article.plagiarism_similarity !== ''
|
||||||
|
? res.article.plagiarism_similarity
|
||||||
|
: res.article.crossref_similarity != null && res.article.crossref_similarity !== ''
|
||||||
|
? res.article.crossref_similarity
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
this.$set(
|
||||||
|
this.form,
|
||||||
|
'plagiarism_report_url',
|
||||||
|
res.article.plagiarism_report_url || res.article.crossref_report_url || ''
|
||||||
|
);
|
||||||
|
this.$set(
|
||||||
|
this.form,
|
||||||
|
'plagiarism_job_state',
|
||||||
|
res.article.plagiarism_job_state || res.article.crossref_status || ''
|
||||||
|
);
|
||||||
|
this.detailInitPlagiarismAfterLoad();
|
||||||
this.form.remarks = res.article.remarks;
|
this.form.remarks = res.article.remarks;
|
||||||
this.form.repeurl = res.article.repeurl;
|
this.form.repeurl = res.article.repeurl;
|
||||||
this.repeform.repefen = res.article.repetition;
|
this.repeform.repefen = res.article.repetition;
|
||||||
@@ -2856,11 +2941,188 @@ export default {
|
|||||||
// 关闭弹窗
|
// 关闭弹窗
|
||||||
closeResubmit() {
|
closeResubmit() {
|
||||||
(this.resubmitVisible = false), this.$refs['resubmitJournal'].resetFields();
|
(this.resubmitVisible = false), this.$refs['resubmitJournal'].resetFields();
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------- Crossref plagiarism(详情页右侧) ---------- */
|
||||||
|
detailPlagiarismArticleKey() {
|
||||||
|
return String(this.form.articleId || this.editform.articleId || this.$route.query.id || '');
|
||||||
|
},
|
||||||
|
detailNormalizePlagiarismStatusPayload(body) {
|
||||||
|
const root = body && typeof body === 'object' ? body : {};
|
||||||
|
if (root.code != null && Number(root.code) !== 0) {
|
||||||
|
return { status: 'api_error', similarity: null, reportUrl: '' };
|
||||||
|
}
|
||||||
|
let d = root.data != null ? root.data : root;
|
||||||
|
if (typeof d === 'string') {
|
||||||
|
try {
|
||||||
|
d = JSON.parse(d);
|
||||||
|
} catch (e) {
|
||||||
|
d = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!d || typeof d !== 'object') d = {};
|
||||||
|
const statusRaw = d.status || d.state || d.job_status || d.plagiarism_status || '';
|
||||||
|
const status = String(statusRaw).toLowerCase();
|
||||||
|
let sim =
|
||||||
|
d.similarity != null
|
||||||
|
? d.similarity
|
||||||
|
: d.percent != null
|
||||||
|
? d.percent
|
||||||
|
: d.similarity_percent != null
|
||||||
|
? d.similarity_percent
|
||||||
|
: d.crossref_similarity != null
|
||||||
|
? d.crossref_similarity
|
||||||
|
: null;
|
||||||
|
if (sim === '' || sim === undefined) sim = null;
|
||||||
|
const reportUrl = d.report_url || d.reportUrl || d.url || d.report_link || '';
|
||||||
|
return { status, similarity: sim, reportUrl: String(reportUrl || '') };
|
||||||
|
},
|
||||||
|
detailPlagiarismSimilarityRaw() {
|
||||||
|
const f = this.form;
|
||||||
|
if (!f) return null;
|
||||||
|
if (f.plagiarism_similarity != null && f.plagiarism_similarity !== '') return f.plagiarism_similarity;
|
||||||
|
if (f.crossref_similarity != null && f.crossref_similarity !== '') return f.crossref_similarity;
|
||||||
|
const st = String(f.plagiarism_job_state || f.crossref_status || '').toLowerCase();
|
||||||
|
if (['completed', 'done', 'success', 'complete'].includes(st) && (f.plagiarism_similarity === 0 || f.plagiarism_similarity === '0')) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const rep = f.repetition;
|
||||||
|
if (rep != null && rep !== '' && Number(rep) > 0) return rep;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
detailPlagiarismSimilarityNumber() {
|
||||||
|
const n = this.detailPlagiarismSimilarityRaw();
|
||||||
|
if (n == null || n === '') return 0;
|
||||||
|
const x = Number(n);
|
||||||
|
return isNaN(x) ? 0 : Math.round(x * 10) / 10;
|
||||||
|
},
|
||||||
|
detailPlagiarismUiShowProcessing() {
|
||||||
|
if (this.form._plagiarismLocalLoading) return true;
|
||||||
|
if (this.plagiarismDetailPending) return true;
|
||||||
|
const st = String(this.form.plagiarism_job_state || this.form.crossref_status || '').toLowerCase();
|
||||||
|
return ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st);
|
||||||
|
},
|
||||||
|
detailPlagiarismUiShowResult() {
|
||||||
|
if (this.detailPlagiarismUiShowProcessing()) return false;
|
||||||
|
const st = String(this.form.plagiarism_job_state || this.form.crossref_status || '').toLowerCase();
|
||||||
|
const done = ['completed', 'done', 'success', 'complete'].includes(st);
|
||||||
|
const raw = this.detailPlagiarismSimilarityRaw();
|
||||||
|
if (raw != null && raw !== '' && !isNaN(Number(raw))) {
|
||||||
|
if (Number(raw) === 0 && !done && !(this.form.plagiarism_report_url || this.form.crossref_report_url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (done && (this.form.plagiarism_similarity === 0 || this.form.plagiarism_similarity === '0')) return true;
|
||||||
|
return !!(this.form.plagiarism_report_url || this.form.crossref_report_url);
|
||||||
|
},
|
||||||
|
detailPlagiarismSimilarityStyle() {
|
||||||
|
const n = Number(this.detailPlagiarismSimilarityNumber());
|
||||||
|
return { color: getSimilarityStyle(n).color };
|
||||||
|
},
|
||||||
|
detailPlagiarismShowRecheck() {
|
||||||
|
if (this.detailPlagiarismUiShowProcessing()) return false;
|
||||||
|
return Number(this.form.state) === 6;
|
||||||
|
},
|
||||||
|
detailInitPlagiarismAfterLoad() {
|
||||||
|
const st = String(this.form.plagiarism_job_state || '').toLowerCase();
|
||||||
|
if (['pending', 'processing', 'queued', 'running', 'submitted'].includes(st)) {
|
||||||
|
this.plagiarismDetailPending = true;
|
||||||
|
this.detailEnsurePlagiarismPoll();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
detailEnsurePlagiarismPoll() {
|
||||||
|
if (!this.plagiarismDetailPending) {
|
||||||
|
this.detailStopPlagiarismPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.plagiarismDetailPollTimer) {
|
||||||
|
this.detailPollPlagiarismOnce();
|
||||||
|
this.plagiarismDetailPollTimer = setInterval(() => this.detailPollPlagiarismOnce(), 60000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
detailStopPlagiarismPolling() {
|
||||||
|
if (this.plagiarismDetailPollTimer) {
|
||||||
|
clearInterval(this.plagiarismDetailPollTimer);
|
||||||
|
this.plagiarismDetailPollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async detailPollPlagiarismOnce() {
|
||||||
|
const key = this.detailPlagiarismArticleKey();
|
||||||
|
if (!key || !this.plagiarismDetailPending) return;
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/plagiarism/status', { params: { article_id: key } });
|
||||||
|
const body = res && res.data;
|
||||||
|
const norm = this.detailNormalizePlagiarismStatusPayload(body);
|
||||||
|
if (norm.status === 'api_error') {
|
||||||
|
this.plagiarismDetailPending = false;
|
||||||
|
this.detailStopPlagiarismPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (norm.similarity != null && norm.similarity !== '') {
|
||||||
|
this.$set(this.form, 'plagiarism_similarity', norm.similarity);
|
||||||
|
}
|
||||||
|
if (norm.reportUrl) this.$set(this.form, 'plagiarism_report_url', norm.reportUrl);
|
||||||
|
if (norm.status) this.$set(this.form, 'plagiarism_job_state', norm.status);
|
||||||
|
const active = ['pending', 'processing', 'queued', 'running', 'submitted'];
|
||||||
|
const terminal = ['completed', 'done', 'success', 'complete', 'failed', 'error', 'fail', 'cancelled'];
|
||||||
|
const isActive = active.includes(norm.status);
|
||||||
|
let clearPending =
|
||||||
|
terminal.includes(norm.status) ||
|
||||||
|
norm.status === 'error' ||
|
||||||
|
(!isActive && norm.reportUrl && norm.similarity != null && norm.similarity !== '');
|
||||||
|
if (clearPending) {
|
||||||
|
this.plagiarismDetailPending = false;
|
||||||
|
}
|
||||||
|
this.detailEnsurePlagiarismPoll();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.$message.error(this.$t('articleListEditor.plagiarismStatusFailed'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async detailTriggerCrossrefPlagiarismCheck() {
|
||||||
|
const key = this.detailPlagiarismArticleKey();
|
||||||
|
if (!key) return;
|
||||||
|
if (this.detailPlagiarismUiShowProcessing()) return;
|
||||||
|
this.$set(this.form, '_plagiarismLocalLoading', true);
|
||||||
|
try {
|
||||||
|
const res = await this.$api.post('api/plagiarism/check', { article_id: key });
|
||||||
|
if (res && Number(res.code) === 0) {
|
||||||
|
this.plagiarismDetailPending = true;
|
||||||
|
this.$set(this.form, 'plagiarism_job_state', 'pending');
|
||||||
|
this.detailEnsurePlagiarismPoll();
|
||||||
|
await this.detailPollPlagiarismOnce();
|
||||||
|
} else {
|
||||||
|
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.$message.error(this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||||
|
} finally {
|
||||||
|
this.$set(this.form, '_plagiarismLocalLoading', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
detailOpenPlagiarismReport() {
|
||||||
|
const raw = (this.form.plagiarism_report_url || this.form.crossref_report_url || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let full = raw;
|
||||||
|
if (!/^https?:\/\//i.test(raw)) {
|
||||||
|
const base = (this.mediaUrl || '').replace(/\/+$/, '');
|
||||||
|
const path = raw.replace(/^\/+/, '');
|
||||||
|
full = base ? `${base}/${path}` : `/${path}`;
|
||||||
|
}
|
||||||
|
window.open(full, '_blank');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.opname = this.$route.query.mark;
|
this.opname = this.$route.query.mark;
|
||||||
this.resubmitJournal.manuscriptId = this.$route.query.id;
|
this.resubmitJournal.manuscriptId = this.$route.query.id;
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.detailStopPlagiarismPolling();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -3345,4 +3607,80 @@ td {
|
|||||||
.copyright-declaration-wrapper :deep(.el-radio.is-checked .el-radio__label) {
|
.copyright-declaration-wrapper :deep(.el-radio.is-checked .el-radio__label) {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 详情页:去掉全局 main.css 中 .art_caozuo_ 的浅蓝底;查重行无额外底色 */
|
||||||
|
.article-detail-editor-page .art_caozuo_ {
|
||||||
|
background-color: #fff !important;
|
||||||
|
border: 1px solid #ebeef5 !important;
|
||||||
|
}
|
||||||
|
.detail-plagiarism-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px 8px;
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 4px 0 8px 0;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
/* 合并「Repetition :」与数值,避免命中 .art_caozuo_ > div > span 的 min-width:65px 造成巨大空隙 */
|
||||||
|
.article-detail-editor-page .art_caozuo_ > div > span.detail-plagiarism-main {
|
||||||
|
min-width: 0 !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-right: 8px !important;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.article-detail-editor-page .art_caozuo_ .detail-plagiarism-lbl {
|
||||||
|
color: #777;
|
||||||
|
margin: 0 2px 0 0 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
width: auto !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.detail-plagiarism-loading {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.detail-plagiarism-loading .el-icon-loading {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.detail-plagiarism-pct-text {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.detail-plagiarism-pct-text:hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
.detail-plagiarism-not-checked-text {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
.detail-plagiarism-not-checked-text:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
.detail-plagiarism-recheck-btn {
|
||||||
|
margin-left: 4px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
/* 紧跟在百分比 / 未检测 文案后,避免继承整行大间距 */
|
||||||
|
.detail-plagiarism-main .detail-plagiarism-recheck-inline.el-button--text {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
padding: 0 2px !important;
|
||||||
|
vertical-align: baseline;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -107,11 +107,11 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
<span v-if="item.state == 6" style="text-decoration: none;margin-left: 20px;">
|
<span v-if="item.state == 6 && preAcceptButtonReady(item)" style="text-decoration: none;margin-left: 20px;">
|
||||||
|
|
||||||
<span @click="goPre_ingested(item.article_id)" class="preButton">
|
<span @click="goPre_ingested(item.article_id)" class="preButton">
|
||||||
<!-- <el-badge is-dot class="item" > -->
|
<!-- <el-badge is-dot class="item" > -->
|
||||||
<i class="el-icon-bank-card"></i>Start the pre-acceptance process and complete your payment
|
<i class="el-icon-bank-card"></i>{{ preAcceptEntryButtonText(item) }}
|
||||||
<!-- </el-badge> -->
|
<!-- </el-badge> -->
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -539,6 +539,61 @@
|
|||||||
this.getdate();
|
this.getdate();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/** 与 Complete_profile.vue getDetail 中订单/支付展示逻辑一致:preOrderDetail → is_buy、期刊费、折后费、isFree */
|
||||||
|
_feeNum(v) {
|
||||||
|
if (v == null || v === '') return 0;
|
||||||
|
const n = Number(String(v).replace(/,/g, ''));
|
||||||
|
return Number.isFinite(n) ? n : 0;
|
||||||
|
},
|
||||||
|
computePreacceptShortButton(articleInfo, journalInfo) {
|
||||||
|
if (!articleInfo || !journalInfo) return false;
|
||||||
|
const journalFee = journalInfo.fee;
|
||||||
|
const articleFee = articleInfo.fee;
|
||||||
|
const isBuy = Number(articleInfo.is_buy) === 1;
|
||||||
|
const tableFee = journalFee ? this._feeNum(articleFee) : 0;
|
||||||
|
const isFree = isBuy && tableFee === 0;
|
||||||
|
const noJournalFee = !journalFee || this._feeNum(journalFee) === 0 || String(journalFee) === '0.00';
|
||||||
|
// Complete_profile: active=1 当已付,或 未付但期刊无 APC
|
||||||
|
if (isFree) return true;
|
||||||
|
if (isBuy) return true;
|
||||||
|
if (!isBuy && noJournalFee) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
preAcceptButtonReady(item) {
|
||||||
|
const p = item._preacceptPay;
|
||||||
|
return !!(p && p.loading === false);
|
||||||
|
},
|
||||||
|
async hydratePreacceptPaymentForList(rows) {
|
||||||
|
if (!rows || !rows.length) return;
|
||||||
|
const targets = rows.filter((r) => Number(r.state) === 6);
|
||||||
|
await Promise.all(
|
||||||
|
targets.map(async (row) => {
|
||||||
|
try {
|
||||||
|
const res = await this.$api.post('api/Order/preOrderDetail', {
|
||||||
|
article_id: row.article_id
|
||||||
|
});
|
||||||
|
if (res && Number(res.code) === 0) {
|
||||||
|
const article = res.data.article_detail || {};
|
||||||
|
const journal = res.data.journal_detail || {};
|
||||||
|
const shortButton = this.computePreacceptShortButton(article, journal);
|
||||||
|
this.$set(row, '_preacceptPay', { loading: false, shortButton });
|
||||||
|
} else {
|
||||||
|
this.$set(row, '_preacceptPay', { loading: false, shortButton: false });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.$set(row, '_preacceptPay', { loading: false, shortButton: false });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
preAcceptEntryButtonText(item) {
|
||||||
|
const pay = item._preacceptPay;
|
||||||
|
if (!pay || pay.loading) return '';
|
||||||
|
return pay.shortButton
|
||||||
|
? this.$t('PreAccept.startPreAccept')
|
||||||
|
: this.$t('PreAccept.startPreAcceptWithPayment');
|
||||||
|
},
|
||||||
formatToHtml(val) {
|
formatToHtml(val) {
|
||||||
|
|
||||||
if (!val) return '';
|
if (!val) return '';
|
||||||
@@ -612,8 +667,12 @@ return processedText;
|
|||||||
1 + '-';
|
1 + '-';
|
||||||
let D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
let D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
||||||
res.data[i].ctime = Y + M + D;
|
res.data[i].ctime = Y + M + D;
|
||||||
|
if (Number(res.data[i].state) === 6) {
|
||||||
|
this.$set(res.data[i], '_preacceptPay', { loading: true, shortButton: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.tableData = res.data
|
this.tableData = res.data;
|
||||||
|
this.hydratePreacceptPaymentForList(res.data);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
|||||||
@@ -116,9 +116,36 @@
|
|||||||
<i class="el-icon-data-line"></i> Manuscript Tracking
|
<i class="el-icon-data-line"></i> Manuscript Tracking
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<span style="float: right">
|
<span style="float: right" class="plagiarism-header-bar">
|
||||||
<span class="labelTitle" style="font-weight: 500; font-size: 13px">Plagiarism Check :</span>
|
<span class="labelTitle" style="font-weight: 500; font-size: 13px">Plagiarism Check :</span>
|
||||||
<font style="margin-right: 16px; font-size: 13px; font-weight: 700"> {{ item.repetition }} % </font>
|
<template v-if="plagiarismUiShowProcessing(item)">
|
||||||
|
<span class="plagiarism-inline-loading plagiarism-on-blue">
|
||||||
|
<i class="el-icon-loading"></i>
|
||||||
|
<span>{{ $t('articleListEditor.plagiarismChecking') }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="plagiarismUiShowResult(item)">
|
||||||
|
<span
|
||||||
|
class="plagiarism-pct-text plagiarism-on-blue"
|
||||||
|
@click.stop="openPlagiarismReport(item)"
|
||||||
|
>
|
||||||
|
{{ plagiarismSimilarityNumber(item) }}%
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="plagiarism-not-checked-text plagiarism-on-blue" @click.stop="triggerCrossrefPlagiarismCheck(item)">
|
||||||
|
{{ $t('articleListEditor.plagiarismNotChecked') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-button
|
||||||
|
v-if="plagiarismShowRecheck(item)"
|
||||||
|
type="text"
|
||||||
|
size="mini"
|
||||||
|
class="plagiarism-recheck-btn"
|
||||||
|
@click.native.stop="triggerCrossrefPlagiarismCheck(item)"
|
||||||
|
>
|
||||||
|
{{ $t('articleListEditor.plagiarismRecheck') }}
|
||||||
|
</el-button>
|
||||||
</span>
|
</span>
|
||||||
<span style="margin: 0 10px; float: right">| </span>
|
<span style="margin: 0 10px; float: right">| </span>
|
||||||
|
|
||||||
@@ -1065,6 +1092,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
import { Loading } from 'element-ui';
|
import { Loading } from 'element-ui';
|
||||||
import timetalk from './time_talk';
|
import timetalk from './time_talk';
|
||||||
import commonRemarkList from './articleListEditor_A_list.vue';
|
import commonRemarkList from './articleListEditor_A_list.vue';
|
||||||
@@ -1432,12 +1460,18 @@ export default {
|
|||||||
editVisible1: false,
|
editVisible1: false,
|
||||||
bankVisible: false,
|
bankVisible: false,
|
||||||
majorData: {},
|
majorData: {},
|
||||||
googleSearchInfo: ''
|
googleSearchInfo: '',
|
||||||
|
/** Crossref / plagiarism async polling */
|
||||||
|
plagiarismPollTimer: null,
|
||||||
|
plagiarismPendingIds: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getdate();
|
this.getdate();
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopPlagiarismPolling();
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
upload_zip: function () {
|
upload_zip: function () {
|
||||||
return this.baseUrl + 'api/Article/up_file/type/repezip';
|
return this.baseUrl + 'api/Article/up_file/type/repezip';
|
||||||
@@ -2072,6 +2106,7 @@ export default {
|
|||||||
this.tableData[i].reportList = this.tableData[i].reportList.slice(0, 3);
|
this.tableData[i].reportList = this.tableData[i].reportList.slice(0, 3);
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
|
this.initPlagiarismFromList();
|
||||||
|
|
||||||
loading.close();
|
loading.close();
|
||||||
})
|
})
|
||||||
@@ -2658,6 +2693,234 @@ export default {
|
|||||||
return str;
|
return str;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* ---------- Crossref plagiarism (list header) ---------- */
|
||||||
|
_plagiarismArticleKey(item) {
|
||||||
|
return item && item.article_id != null ? String(item.article_id) : '';
|
||||||
|
},
|
||||||
|
_plagiarismPending(articleKey) {
|
||||||
|
return !!(articleKey && this.plagiarismPendingIds[articleKey]);
|
||||||
|
},
|
||||||
|
plagiarismSimilarityNumber(item) {
|
||||||
|
const n = this.plagiarismSimilarityRaw(item);
|
||||||
|
if (n == null || n === '') return 0;
|
||||||
|
const x = Number(n);
|
||||||
|
return isNaN(x) ? 0 : Math.round(x * 10) / 10;
|
||||||
|
},
|
||||||
|
plagiarismSimilarityRaw(item) {
|
||||||
|
if (!item) return null;
|
||||||
|
if (item.plagiarism_similarity != null && item.plagiarism_similarity !== '') return item.plagiarism_similarity;
|
||||||
|
if (item.crossref_similarity != null && item.crossref_similarity !== '') return item.crossref_similarity;
|
||||||
|
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
|
||||||
|
if (['completed', 'done', 'success', 'complete'].includes(st) && (item.plagiarism_similarity === 0 || item.plagiarism_similarity === '0')) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const rep = item.repetition;
|
||||||
|
if (rep != null && rep !== '' && Number(rep) > 0) return rep;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
plagiarismUiShowProcessing(item) {
|
||||||
|
const key = this._plagiarismArticleKey(item);
|
||||||
|
if (!key) return false;
|
||||||
|
if (item._plagiarismLocalLoading) return true;
|
||||||
|
if (this._plagiarismPending(key)) return true;
|
||||||
|
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
|
||||||
|
return ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st);
|
||||||
|
},
|
||||||
|
plagiarismUiShowResult(item) {
|
||||||
|
if (this.plagiarismUiShowProcessing(item)) return false;
|
||||||
|
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
|
||||||
|
const done = ['completed', 'done', 'success', 'complete'].includes(st);
|
||||||
|
const raw = this.plagiarismSimilarityRaw(item);
|
||||||
|
if (raw != null && raw !== '' && !isNaN(Number(raw))) {
|
||||||
|
if (Number(raw) === 0 && !done && !(item.plagiarism_report_url || item.crossref_report_url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (done && (item.plagiarism_similarity === 0 || item.plagiarism_similarity === '0')) return true;
|
||||||
|
return !!(item.plagiarism_report_url || item.crossref_report_url);
|
||||||
|
},
|
||||||
|
plagiarismShowRecheck(item) {
|
||||||
|
if (this.plagiarismUiShowProcessing(item)) return false;
|
||||||
|
return Number(item.state) === 6;
|
||||||
|
},
|
||||||
|
initPlagiarismFromList() {
|
||||||
|
if (!this.tableData || !this.tableData.length) {
|
||||||
|
this.reconcilePlagiarismPollTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tableData.forEach((row) => {
|
||||||
|
if (row.crossref_report_url && !row.plagiarism_report_url) {
|
||||||
|
this.$set(row, 'plagiarism_report_url', row.crossref_report_url);
|
||||||
|
}
|
||||||
|
if (row.crossref_similarity != null && row.plagiarism_similarity == null) {
|
||||||
|
this.$set(row, 'plagiarism_similarity', row.crossref_similarity);
|
||||||
|
}
|
||||||
|
if (row.crossref_status && !row.plagiarism_job_state) {
|
||||||
|
this.$set(row, 'plagiarism_job_state', row.crossref_status);
|
||||||
|
}
|
||||||
|
const key = this._plagiarismArticleKey(row);
|
||||||
|
const st = String(row.plagiarism_job_state || '').toLowerCase();
|
||||||
|
if (key && ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st)) {
|
||||||
|
this.$set(this.plagiarismPendingIds, key, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.reconcilePlagiarismPollTimer();
|
||||||
|
},
|
||||||
|
reconcilePlagiarismPollTimer() {
|
||||||
|
const hasPending = Object.keys(this.plagiarismPendingIds).some((k) => this.plagiarismPendingIds[k]);
|
||||||
|
if (hasPending) {
|
||||||
|
if (!this.plagiarismPollTimer) {
|
||||||
|
this.pollPlagiarismPendingOnce();
|
||||||
|
this.plagiarismPollTimer = setInterval(() => this.pollPlagiarismPendingOnce(), 60000);
|
||||||
|
}
|
||||||
|
} else if (this.plagiarismPollTimer) {
|
||||||
|
clearInterval(this.plagiarismPollTimer);
|
||||||
|
this.plagiarismPollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopPlagiarismPolling() {
|
||||||
|
if (this.plagiarismPollTimer) {
|
||||||
|
clearInterval(this.plagiarismPollTimer);
|
||||||
|
this.plagiarismPollTimer = null;
|
||||||
|
}
|
||||||
|
this.plagiarismPendingIds = {};
|
||||||
|
},
|
||||||
|
pollPlagiarismPendingOnce() {
|
||||||
|
Object.keys(this.plagiarismPendingIds).forEach((key) => {
|
||||||
|
if (this.plagiarismPendingIds[key]) this.fetchPlagiarismStatusByArticleId(key);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_normalizePlagiarismStatusPayload(body) {
|
||||||
|
const root = body && typeof body === 'object' ? body : {};
|
||||||
|
if (root.code != null && Number(root.code) !== 0) {
|
||||||
|
return { status: 'api_error', similarity: null, reportUrl: '' };
|
||||||
|
}
|
||||||
|
let d = root.data != null ? root.data : root;
|
||||||
|
if (typeof d === 'string') {
|
||||||
|
try {
|
||||||
|
d = JSON.parse(d);
|
||||||
|
} catch (e) {
|
||||||
|
d = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!d || typeof d !== 'object') d = {};
|
||||||
|
const statusRaw = d.status || d.state || d.job_status || d.plagiarism_status || '';
|
||||||
|
const status = String(statusRaw).toLowerCase();
|
||||||
|
let sim =
|
||||||
|
d.similarity != null
|
||||||
|
? d.similarity
|
||||||
|
: d.percent != null
|
||||||
|
? d.percent
|
||||||
|
: d.similarity_percent != null
|
||||||
|
? d.similarity_percent
|
||||||
|
: d.crossref_similarity != null
|
||||||
|
? d.crossref_similarity
|
||||||
|
: null;
|
||||||
|
if (sim === '' || sim === undefined) sim = null;
|
||||||
|
const reportUrl = d.report_url || d.reportUrl || d.url || d.report_link || '';
|
||||||
|
return { status, similarity: sim, reportUrl: String(reportUrl || '') };
|
||||||
|
},
|
||||||
|
findTableRowByArticleId(articleId) {
|
||||||
|
const id = String(articleId);
|
||||||
|
return (this.tableData || []).find((r) => String(r.article_id) === id) || null;
|
||||||
|
},
|
||||||
|
async fetchPlagiarismStatusByArticleId(articleId) {
|
||||||
|
const key = String(articleId);
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/plagiarism/status', { params: { article_id: key } });
|
||||||
|
const body = res && res.data;
|
||||||
|
const norm = this._normalizePlagiarismStatusPayload(body);
|
||||||
|
if (norm.status === 'api_error') {
|
||||||
|
this.$set(this.plagiarismPendingIds, key, false);
|
||||||
|
this.reconcilePlagiarismPollTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = this.findTableRowByArticleId(key);
|
||||||
|
if (row) {
|
||||||
|
if (norm.similarity != null && norm.similarity !== '') {
|
||||||
|
this.$set(row, 'plagiarism_similarity', norm.similarity);
|
||||||
|
}
|
||||||
|
if (norm.reportUrl) this.$set(row, 'plagiarism_report_url', norm.reportUrl);
|
||||||
|
if (norm.status) this.$set(row, 'plagiarism_job_state', norm.status);
|
||||||
|
}
|
||||||
|
const active = ['pending', 'processing', 'queued', 'running', 'submitted'];
|
||||||
|
const terminal = ['completed', 'done', 'success', 'complete', 'failed', 'error', 'fail', 'cancelled'];
|
||||||
|
const isActive = active.includes(norm.status);
|
||||||
|
let clearPending =
|
||||||
|
terminal.includes(norm.status) ||
|
||||||
|
norm.status === 'error' ||
|
||||||
|
(!isActive && norm.reportUrl && norm.similarity != null && norm.similarity !== '');
|
||||||
|
if (clearPending) {
|
||||||
|
this.$set(this.plagiarismPendingIds, key, false);
|
||||||
|
}
|
||||||
|
this.reconcilePlagiarismPollTimer();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.$message.error(this.$t('articleListEditor.plagiarismStatusFailed'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async triggerCrossrefPlagiarismCheck(item) {
|
||||||
|
const key = this._plagiarismArticleKey(item);
|
||||||
|
if (!key) return;
|
||||||
|
if (this.plagiarismUiShowProcessing(item)) return;
|
||||||
|
this.$set(item, '_plagiarismLocalLoading', true);
|
||||||
|
try {
|
||||||
|
const res = await this.$api.post('api/plagiarism/check', { article_id: key });
|
||||||
|
if (res && Number(res.code) === 0) {
|
||||||
|
this.$set(this.plagiarismPendingIds, key, true);
|
||||||
|
this.$set(item, 'plagiarism_job_state', 'pending');
|
||||||
|
this.reconcilePlagiarismPollTimer();
|
||||||
|
await this.fetchPlagiarismStatusByArticleId(key);
|
||||||
|
} else {
|
||||||
|
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.$message.error(this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||||
|
} finally {
|
||||||
|
this.$set(item, '_plagiarismLocalLoading', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openPlagiarismReport(item) {
|
||||||
|
const articleId = item && item.article_id;
|
||||||
|
if (!articleId) {
|
||||||
|
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let raw = '';
|
||||||
|
try {
|
||||||
|
const res = await this.$api.post('api/Article/getArticleDetail', {
|
||||||
|
articleId,
|
||||||
|
human: 'editor'
|
||||||
|
});
|
||||||
|
console.log("🚀 ~ openPlagiarismReport ~ res:", res);
|
||||||
|
|
||||||
|
const a = res && res.article;
|
||||||
|
if (a) {
|
||||||
|
raw = String(
|
||||||
|
a.repeurl || ''
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.$message.error(this.$t('articleListEditor.plagiarismReportDetailFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = String(raw).trim();
|
||||||
|
if (!url) {
|
||||||
|
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let full = url;
|
||||||
|
if (!/^https?:\/\//i.test(url)) {
|
||||||
|
const base = (this.mediaUrl || '').replace(/\/+$/, '');
|
||||||
|
const path = url.replace(/^\/+/, '');
|
||||||
|
full = base ? `${base}/${path}` : `/${path}`;
|
||||||
|
}
|
||||||
|
window.open(full, '_blank');
|
||||||
|
},
|
||||||
|
|
||||||
//文章类型
|
//文章类型
|
||||||
|
|
||||||
|
|
||||||
@@ -3426,4 +3689,65 @@ td {
|
|||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
/* box-shadow: 0 2px 8px rgba(0,0,0,0.1); */
|
/* box-shadow: 0 2px 8px rgba(0,0,0,0.1); */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Crossref plagiarism strip (blue header bar) */
|
||||||
|
.plagiarism-header-bar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 8px;
|
||||||
|
margin-right: 4px;
|
||||||
|
max-width: 48%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.plagiarism-on-blue {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.articleTopBaseInfo .plagiarism-inline-loading {
|
||||||
|
color: #e6f4ff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.articleTopBaseInfo .plagiarism-inline-loading .el-icon-loading {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.articleTopBaseInfo .plagiarism-pct-text {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #fff;
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.articleTopBaseInfo .plagiarism-pct-text:hover {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
.articleTopBaseInfo .plagiarism-not-checked-text {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e6f4ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
.articleTopBaseInfo .plagiarism-not-checked-text:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.articleTopBaseInfo .plagiarism-recheck-btn {
|
||||||
|
color: #e6f4ff !important;
|
||||||
|
padding: 0 4px !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.articleTopBaseInfo .plagiarism-recheck-btn:hover {
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,19 +19,46 @@
|
|||||||
<el-select
|
<el-select
|
||||||
v-if="config.initialized && selectedJournalId"
|
v-if="config.initialized && selectedJournalId"
|
||||||
v-model="headerPromotionFactoryId"
|
v-model="headerPromotionFactoryId"
|
||||||
class="header-factory-task-select"
|
class="header-factory-task-select custom-pipeline-select"
|
||||||
size="small"
|
size="small"
|
||||||
filterable
|
filterable
|
||||||
:loading="factoryTasksHeaderLoading"
|
:loading="factoryTasksHeaderLoading"
|
||||||
:placeholder="$t('autoPromotionLogs.factoryTaskSelectPlaceholder')"
|
:placeholder="$t('autoPromotionLogs.factoryTaskSelectPlaceholder')"
|
||||||
@change="onHeaderFactoryTaskChange"
|
@change="onHeaderFactoryTaskChange"
|
||||||
|
popper-class="pipeline-popper"
|
||||||
>
|
>
|
||||||
<el-option
|
|
||||||
v-for="opt in factoryTaskOptions"
|
|
||||||
:key="opt.value"
|
<el-option-group >
|
||||||
:label="opt.label"
|
|
||||||
:value="opt.value"
|
<el-option
|
||||||
/>
|
v-for="opt in factoryTaskOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
class="pipeline-option"
|
||||||
|
>
|
||||||
|
<div class="option-content">
|
||||||
|
<div class="row-top">
|
||||||
|
<span class="task-title">{{ mapFactoryTaskTypeLabel(opt.task.type) }}</span>
|
||||||
|
<el-tag :type="getStatusType(opt.task.start_promotion)" size="mini" effect="plain" class="status-tag">
|
||||||
|
{{ opt.task.start_promotion==1 ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
|
||||||
|
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="row-bottom">
|
||||||
|
<span class="meta-item database">{{ mapFactoryExpertTypeLabel(opt.task.expert_type) }}</span>
|
||||||
|
<template v-if="opt.task.expert_type==5">
|
||||||
|
<span class="separator">•</span>
|
||||||
|
<span class="meta-item region">{{ opt.task.country_scope_label }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span class="separator">•</span>
|
||||||
|
<span class="meta-item time">{{ opt.task.ctime_text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
</el-option-group>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-tag
|
<el-tag
|
||||||
v-if="config.initialized && selectedJournalId && headerFactoryTaskRunning !== null"
|
v-if="config.initialized && selectedJournalId && headerFactoryTaskRunning !== null"
|
||||||
@@ -45,19 +72,10 @@
|
|||||||
{{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
|
{{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
|
|
||||||
<!-- <template v-if="config.initialized"> -->
|
<el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
|
||||||
<!-- <el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
|
<i class="el-icon-edit"></i>
|
||||||
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.configured') }}
|
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
|
||||||
</el-tag> -->
|
</el-button>
|
||||||
<el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
|
|
||||||
<i class="el-icon-edit"></i>
|
|
||||||
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
|
|
||||||
</el-button>
|
|
||||||
<!-- </template> -->
|
|
||||||
|
|
||||||
<!-- <el-tag v-else type="info" size="small" effect="plain" style="margin-left: 10px">
|
|
||||||
<i class="el-icon-info"></i> {{ $t('autoPromotionLogs.notConfigured') }}
|
|
||||||
</el-tag> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="config.initialized && selectedJournalId && headerPromotionFactoryId" class="right">
|
<div v-if="config.initialized && selectedJournalId && headerPromotionFactoryId" class="right">
|
||||||
<el-button
|
<el-button
|
||||||
@@ -100,8 +118,6 @@
|
|||||||
|
|
||||||
<div v-else class="manage-mode">
|
<div v-else class="manage-mode">
|
||||||
<el-card shadow="never" class="list-card">
|
<el-card shadow="never" class="list-card">
|
||||||
|
|
||||||
|
|
||||||
<div class="filter-header-row">
|
<div class="filter-header-row">
|
||||||
<div class="tmr-capsule-group">
|
<div class="tmr-capsule-group">
|
||||||
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
|
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
|
||||||
@@ -135,7 +151,9 @@
|
|||||||
<div class="task-column">
|
<div class="task-column">
|
||||||
<div class="task-name">{{ scope.row.task_name || '-' }}</div>
|
<div class="task-name">{{ scope.row.task_name || '-' }}</div>
|
||||||
<div class="task-id-tags">
|
<div class="task-id-tags">
|
||||||
<span class="id-tag">{{ $t('autoPromotionLogs.templateIdLabel') }}: {{ scope.row.template_id }}</span>
|
<span class="id-tag"
|
||||||
|
>{{ $t('autoPromotionLogs.templateIdLabel') }}: {{ scope.row.template_id }}</span
|
||||||
|
>
|
||||||
<span class="id-tag">{{ $t('autoPromotionLogs.styleIdLabel') }}: {{ scope.row.style_id }}</span>
|
<span class="id-tag">{{ $t('autoPromotionLogs.styleIdLabel') }}: {{ scope.row.style_id }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,7 +242,11 @@
|
|||||||
:icon="String(scope.row.state) === '5' ? 'el-icon-edit-outline' : 'el-icon-view'"
|
:icon="String(scope.row.state) === '5' ? 'el-icon-edit-outline' : 'el-icon-view'"
|
||||||
@click="previewRow(scope.row)"
|
@click="previewRow(scope.row)"
|
||||||
>
|
>
|
||||||
{{ String(scope.row.state) === '5' ? $t('autoPromotionLogs.editAction') : $t('autoPromotionLogs.previewAction') }}
|
{{
|
||||||
|
String(scope.row.state) === '5'
|
||||||
|
? $t('autoPromotionLogs.editAction')
|
||||||
|
: $t('autoPromotionLogs.previewAction')
|
||||||
|
}}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -427,6 +449,36 @@ export default {
|
|||||||
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
|
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
|
||||||
if (opt && typeof opt.running === 'boolean') return opt.running;
|
if (opt && typeof opt.running === 'boolean') return opt.running;
|
||||||
return null;
|
return null;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 顶部 select 前缀展示用:与 headerPromotionFactoryId 对应的 option(含 task)。
|
||||||
|
* URL 回退插入的占位项可能没有 task,这里统一成可安全渲染的对象。
|
||||||
|
*/
|
||||||
|
currentSelectedTask() {
|
||||||
|
const emptyTask = { type: '', ctime_text: '' };
|
||||||
|
const id = String(this.headerPromotionFactoryId || '').trim();
|
||||||
|
if (!id || !Array.isArray(this.factoryTaskOptions) || !this.factoryTaskOptions.length) {
|
||||||
|
return { value: '', label: '', running: false, task: { ...emptyTask } };
|
||||||
|
}
|
||||||
|
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
|
||||||
|
if (!opt) {
|
||||||
|
return { value: id, label: id, running: false, task: { ...emptyTask } };
|
||||||
|
}
|
||||||
|
const raw = opt.task && typeof opt.task === 'object' ? opt.task : {};
|
||||||
|
const ctimeText =
|
||||||
|
raw.ctime_text != null && String(raw.ctime_text).trim() !== ''
|
||||||
|
? String(raw.ctime_text).trim()
|
||||||
|
: this.formatFactoryHeaderTaskCreateTime(raw);
|
||||||
|
return {
|
||||||
|
value: opt.value,
|
||||||
|
label: opt.label,
|
||||||
|
running: typeof opt.running === 'boolean' ? opt.running : this.isFactoryHeaderTaskRunning(raw),
|
||||||
|
task: {
|
||||||
|
...raw,
|
||||||
|
type: raw.type != null ? String(raw.type) : '',
|
||||||
|
ctime_text: ctimeText || ''
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -445,12 +497,33 @@ export default {
|
|||||||
this.initPage();
|
this.initPage();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
mapFactoryTaskTypeLabel(type) {
|
||||||
|
const t = String(type || '');
|
||||||
|
if (t === '1') return this.$t('autoPromotion.factoryScenarioSolicit');
|
||||||
|
if (t === '2') return this.$t('autoPromotion.factoryScenarioPromoteCitation');
|
||||||
|
if (t === '3') return this.$t('autoPromotion.factoryScenarioGeneralThanks');
|
||||||
|
if (t === '4') return this.$t('autoPromotion.autoSolicit');
|
||||||
|
return this.$t('autoPromotion.autoSolicit');
|
||||||
|
},
|
||||||
|
mapFactoryExpertTypeLabel(expertType) {
|
||||||
|
const t = String(expertType || '').trim();
|
||||||
|
if (t === '1') return this.$t('autoPromotion.factoryExpertChief');
|
||||||
|
if (t === '2') return this.$t('autoPromotion.factoryExpertBoard');
|
||||||
|
if (t === '3') return this.$t('autoPromotion.factoryExpertYoungBoard');
|
||||||
|
if (t === '4') return this.$t('autoPromotion.factoryExpertAuthor');
|
||||||
|
if (t === '5') return this.$t('autoPromotion.factoryExpertDb');
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
getStatusType(status) {
|
||||||
|
|
||||||
|
return status==1?'success':'info';
|
||||||
|
},
|
||||||
handleTabClick(tab) {
|
handleTabClick(tab) {
|
||||||
// tab.name 对应的就是原来的 value ("0", "1" 等)
|
// tab.name 对应的就是原来的 value ("0", "1" 等)
|
||||||
// 注意:el-tabs 的 v-model 绑定的是字符串
|
// 注意:el-tabs 的 v-model 绑定的是字符串
|
||||||
this.query.state = tab.name;
|
this.query.state = tab.name;
|
||||||
this.handleStateChange(); // 触发你原有的搜索逻辑
|
this.handleStateChange(); // 触发你原有的搜索逻辑
|
||||||
},
|
},
|
||||||
getPercent(row) {
|
getPercent(row) {
|
||||||
if (!row.total_count || row.total_count === 0) return 0;
|
if (!row.total_count || row.total_count === 0) return 0;
|
||||||
// 计算已发送占比
|
// 计算已发送占比
|
||||||
@@ -554,9 +627,7 @@ export default {
|
|||||||
this.hidePage = false;
|
this.hidePage = false;
|
||||||
var journal_id = (this.$route.query && this.$route.query.journal_id) || '';
|
var journal_id = (this.$route.query && this.$route.query.journal_id) || '';
|
||||||
var pfid =
|
var pfid =
|
||||||
(this.$route.query && this.$route.query.promotion_factory_id) ||
|
(this.$route.query && this.$route.query.promotion_factory_id) || (this.$route.query && this.$route.query.taskId) || '';
|
||||||
(this.$route.query && this.$route.query.taskId) ||
|
|
||||||
'';
|
|
||||||
this.routePromotionFactoryId = String(pfid || '');
|
this.routePromotionFactoryId = String(pfid || '');
|
||||||
this.headerPromotionFactoryId = this.routePromotionFactoryId;
|
this.headerPromotionFactoryId = this.routePromotionFactoryId;
|
||||||
this.selectedJournalId = String(journal_id);
|
this.selectedJournalId = String(journal_id);
|
||||||
@@ -662,7 +733,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (!dt || isNaN(dt.getTime())) return String(raw).trim();
|
if (!dt || isNaN(dt.getTime())) return String(raw).trim();
|
||||||
const pad = (v) => String(v).padStart(2, '0');
|
const pad = (v) => String(v).padStart(2, '0');
|
||||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
|
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`;
|
||||||
},
|
},
|
||||||
isFactoryHeaderTaskRunning(task) {
|
isFactoryHeaderTaskRunning(task) {
|
||||||
if (!task || typeof task !== 'object') return false;
|
if (!task || typeof task !== 'object') return false;
|
||||||
@@ -676,9 +747,12 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 下拉仅展示「类型 - 创建日期」,运行状态单独用 el-tag */
|
/** 下拉仅展示「类型 - 创建日期」,运行状态单独用 el-tag */
|
||||||
buildFactoryHeaderOptionMainLabel(task, pidFallback) {
|
buildFactoryHeaderOptionMainLabel(task, pidFallback) {
|
||||||
|
console.log("🚀 ~ buildFactoryHeaderOptionMainLabel ~ task:", task);
|
||||||
|
|
||||||
const typePart = this.getFactoryHeaderTaskTypeLabel(task) || String(pidFallback || '').trim() || '—';
|
const typePart = this.getFactoryHeaderTaskTypeLabel(task) || String(pidFallback || '').trim() || '—';
|
||||||
|
const expertTypePart = this.mapFactoryExpertTypeLabel(task.expert_type);
|
||||||
const datePart = this.formatFactoryHeaderTaskCreateTime(task);
|
const datePart = this.formatFactoryHeaderTaskCreateTime(task);
|
||||||
return datePart ? `${typePart} - ${datePart}` : typePart;
|
return datePart ? `${typePart} | ${expertTypePart}${task.expert_type==5 ? ` | ${task.country_scope_label} ` :''} | ${datePart}` : typePart;
|
||||||
},
|
},
|
||||||
replacePromotionFactoryIdInUrl(promotionFactoryId) {
|
replacePromotionFactoryIdInUrl(promotionFactoryId) {
|
||||||
try {
|
try {
|
||||||
@@ -704,18 +778,20 @@ export default {
|
|||||||
});
|
});
|
||||||
const payload = (res && res.data) || {};
|
const payload = (res && res.data) || {};
|
||||||
const list = this.findArray(payload) || this.findArray(res) || [];
|
const list = this.findArray(payload) || this.findArray(res) || [];
|
||||||
let opts = (Array.isArray(list) ? list : []).map((task, idx) => {
|
let opts = (Array.isArray(list) ? list : [])
|
||||||
const pid =
|
.map((task, idx) => {
|
||||||
task && task.promotion_factory_id != null
|
const pid =
|
||||||
? String(task.promotion_factory_id)
|
task && task.promotion_factory_id != null
|
||||||
: task && task.id != null
|
? String(task.promotion_factory_id)
|
||||||
? String(task.id)
|
: task && task.id != null
|
||||||
: '';
|
? String(task.id)
|
||||||
if (!pid) return null;
|
: '';
|
||||||
const label = this.buildFactoryHeaderOptionMainLabel(task, pid);
|
if (!pid) return null;
|
||||||
const running = this.isFactoryHeaderTaskRunning(task);
|
const label = this.buildFactoryHeaderOptionMainLabel(task, pid);
|
||||||
return { value: pid, label, running };
|
const running = this.isFactoryHeaderTaskRunning(task);
|
||||||
}).filter(Boolean);
|
return { value: pid, label, running,task:task }
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
let cur = String(this.routePromotionFactoryId || this.headerPromotionFactoryId || '').trim();
|
let cur = String(this.routePromotionFactoryId || this.headerPromotionFactoryId || '').trim();
|
||||||
const ids = new Set(opts.map((o) => o.value));
|
const ids = new Set(opts.map((o) => o.value));
|
||||||
@@ -799,10 +875,14 @@ export default {
|
|||||||
selectedPayload.country_fetch_ids != null
|
selectedPayload.country_fetch_ids != null
|
||||||
? selectedPayload.country_fetch_ids
|
? selectedPayload.country_fetch_ids
|
||||||
: selectedPayload.country_ids != null
|
: selectedPayload.country_ids != null
|
||||||
? selectedPayload.country_ids
|
? selectedPayload.country_ids
|
||||||
: '';
|
: '';
|
||||||
if (typeof raw === 'string' && raw.trim()) {
|
if (typeof raw === 'string' && raw.trim()) {
|
||||||
return raw.split(',').map((s) => s.trim()).filter(Boolean).map(String);
|
return raw
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(String);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
@@ -825,7 +905,7 @@ export default {
|
|||||||
let availableArr = this.findArray(availablePayload);
|
let availableArr = this.findArray(availablePayload);
|
||||||
if (!availableArr) availableArr = Array.isArray(availablePayload) ? availablePayload : [];
|
if (!availableArr) availableArr = Array.isArray(availablePayload) ? availablePayload : [];
|
||||||
this.availableFields = availableArr.map((item, idx) => {
|
this.availableFields = availableArr.map((item, idx) => {
|
||||||
const id = item.expert_fetch_id || item.fetch_id || item.id || item.field_id || (idx + 1);
|
const id = item.expert_fetch_id || item.fetch_id || item.id || item.field_id || idx + 1;
|
||||||
const label = item.field || item.title || item.name || item.label || String(id);
|
const label = item.field || item.title || item.name || item.label || String(id);
|
||||||
return { id: String(id), label };
|
return { id: String(id), label };
|
||||||
});
|
});
|
||||||
@@ -898,10 +978,10 @@ export default {
|
|||||||
matched.promotion_factory_id != null
|
matched.promotion_factory_id != null
|
||||||
? String(matched.promotion_factory_id)
|
? String(matched.promotion_factory_id)
|
||||||
: matched.id != null
|
: matched.id != null
|
||||||
? String(matched.id)
|
? String(matched.id)
|
||||||
: matched.task_id != null
|
: matched.task_id != null
|
||||||
? String(matched.task_id)
|
? String(matched.task_id)
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// promotion_factory/getDetail 必须使用地址栏 promotion_factory_id,避免列表首行 id 与路由不一致
|
// promotion_factory/getDetail 必须使用地址栏 promotion_factory_id,避免列表首行 id 与路由不一致
|
||||||
@@ -1048,11 +1128,7 @@ export default {
|
|||||||
const runAt = item.run_at || item.run_time || item.plan_time || item.execute_time || item.send_date || '';
|
const runAt = item.run_at || item.run_time || item.plan_time || item.execute_time || item.send_date || '';
|
||||||
const state = String(item.state != null ? item.state : '');
|
const state = String(item.state != null ? item.state : '');
|
||||||
const promotionFactoryId =
|
const promotionFactoryId =
|
||||||
item.promotion_factory_id != null
|
item.promotion_factory_id != null ? item.promotion_factory_id : item.id != null ? item.id : item.task_id;
|
||||||
? item.promotion_factory_id
|
|
||||||
: item.id != null
|
|
||||||
? item.id
|
|
||||||
: item.task_id;
|
|
||||||
return {
|
return {
|
||||||
id: item.id || item.task_id || `task_${idx + 1}`,
|
id: item.id || item.task_id || `task_${idx + 1}`,
|
||||||
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
|
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
|
||||||
@@ -1206,16 +1282,16 @@ export default {
|
|||||||
return Object.prototype.hasOwnProperty.call(stateTextMap, key) ? stateTextMap[key] : '-';
|
return Object.prototype.hasOwnProperty.call(stateTextMap, key) ? stateTextMap[key] : '-';
|
||||||
},
|
},
|
||||||
getTaskStatusClass(state) {
|
getTaskStatusClass(state) {
|
||||||
const stateClassMap = {
|
const stateClassMap = {
|
||||||
0: 'status-draft', // Draft - 浅灰色
|
0: 'status-draft', // Draft - 浅灰色
|
||||||
5: 'status-preparing', // Preparing - 橘色
|
5: 'status-preparing', // Preparing - 橘色
|
||||||
1: 'status-running', // Running - 深蓝色
|
1: 'status-running', // Running - 深蓝色
|
||||||
2: 'status-paused', // Paused - 深灰色
|
2: 'status-paused', // Paused - 深灰色
|
||||||
3: 'status-completed', // Completed - 绿色
|
3: 'status-completed', // Completed - 绿色
|
||||||
4: 'status-cancelled' // Cancelled - 浅红色
|
4: 'status-cancelled' // Cancelled - 浅红色
|
||||||
};
|
};
|
||||||
return stateClassMap[Number(state)] || '';
|
return stateClassMap[Number(state)] || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -1229,11 +1305,13 @@ export default {
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
.config-bar {
|
.config-bar {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.config-bar .left {
|
.config-bar .left {
|
||||||
|
width: calc(100% - 100px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1758,96 +1836,96 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-header-row {
|
.filter-header-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center; /* 垂直居中对齐 */
|
align-items: center; /* 垂直居中对齐 */
|
||||||
gap: 16px; /* 胶囊和Search按钮之间的间距 */
|
gap: 16px; /* 胶囊和Search按钮之间的间距 */
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background-color: transparent; /* 容器透明,不厚重 */
|
background-color: transparent; /* 容器透明,不厚重 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. 彻底重置 el-tabs 的原生样式 (最丑的地方) */
|
/* 2. 彻底重置 el-tabs 的原生样式 (最丑的地方) */
|
||||||
.tmr-capsule-group {
|
.tmr-capsule-group {
|
||||||
flex: 1; /* 占据左侧空间 */
|
flex: 1; /* 占据左侧空间 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 强制隐藏默认灰色横线和卡片灰边 */
|
/* 强制隐藏默认灰色横线和卡片灰边 */
|
||||||
.tmr-capsule-group .el-tabs__header {
|
.tmr-capsule-group .el-tabs__header {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tmr-capsule-group .el-tabs__nav {
|
.tmr-capsule-group .el-tabs__nav {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3. 重新定义每个 Tab 的样式 (让其变成按钮) */
|
/* 3. 重新定义每个 Tab 的样式 (让其变成按钮) */
|
||||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
|
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
|
||||||
height: 32px !important; /* 紧凑高度 */
|
height: 32px !important; /* 紧凑高度 */
|
||||||
line-height: 32px !important;
|
line-height: 32px !important;
|
||||||
font-size: 13px; /* 紧凑字体 */
|
font-size: 13px; /* 紧凑字体 */
|
||||||
border: none !important; /* 彻底隐藏原生卡片边框 */
|
border: none !important; /* 彻底隐藏原生卡片边框 */
|
||||||
background-color: transparent; /* 默认状态下透明背景 */
|
background-color: transparent; /* 默认状态下透明背景 */
|
||||||
color: #515a6e; /* 默认状态深灰文字,更专业 */
|
color: #515a6e; /* 默认状态深灰文字,更专业 */
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
padding: 0 16px !important; /* 适当内边距 */
|
padding: 0 16px !important; /* 适当内边距 */
|
||||||
margin-right: 8px; /* 每个 Tab 之间的间距 */
|
margin-right: 8px; /* 每个 Tab 之间的间距 */
|
||||||
border-radius: 6px !important; /* 先统一圆角 */
|
border-radius: 6px !important; /* 先统一圆角 */
|
||||||
overflow: visible; /* 确保选中的阴影显示完全 */
|
overflow: visible; /* 确保选中的阴影显示完全 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 首尾 Tab 的圆角处理 (形成整体感) */
|
/* 首尾 Tab 的圆角处理 (形成整体感) */
|
||||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:first-child {
|
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:first-child {
|
||||||
border-top-left-radius: 6px;
|
border-top-left-radius: 6px;
|
||||||
border-bottom-left-radius: 6px;
|
border-bottom-left-radius: 6px;
|
||||||
}
|
}
|
||||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
|
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
|
||||||
border-top-right-radius: 6px;
|
border-top-right-radius: 6px;
|
||||||
border-bottom-right-radius: 6px;
|
border-bottom-right-radius: 6px;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 中间 Tab 的处理 (去掉左右圆角,紧凑对齐) */
|
/* 中间 Tab 的处理 (去掉左右圆角,紧凑对齐) */
|
||||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:first-child):not(:last-child) {
|
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:first-child):not(:last-child) {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 4. **选中效果 (Active) - 胶囊浮动核心** */
|
/* 4. **选中效果 (Active) - 胶囊浮动核心** */
|
||||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
|
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
|
||||||
background-color: #ffffff !important; /* 白色底色 */
|
background-color: #ffffff !important; /* 白色底色 */
|
||||||
color: #409eff !important; /* 主题蓝色文字 */
|
color: #409eff !important; /* 主题蓝色文字 */
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; /* **关键:增加轻微阴影,浮动感** */
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; /* **关键:增加轻微阴影,浮动感** */
|
||||||
border-radius: 6px; /* 选中时恢复圆角 */
|
border-radius: 6px; /* 选中时恢复圆角 */
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2; /* 确保选中态在最前面,不被覆盖线破坏 */
|
z-index: 2; /* 确保选中态在最前面,不被覆盖线破坏 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 5. 处理 Tab 之间的断层 (像素级微调) */
|
/* 5. 处理 Tab 之间的断层 (像素级微调) */
|
||||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
|
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 首尾 Tab 之间的像素级处理 */
|
/* 首尾 Tab 之间的像素级处理 */
|
||||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:last-child)::after {
|
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:last-child)::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -5px; /* 让 Tab 之间紧密无缝 */
|
right: -5px; /* 让 Tab 之间紧密无缝 */
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: transparent; /* 移除灰线,不丑 */
|
background: transparent; /* 移除灰线,不丑 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 6. 搜索按钮对齐微调 */
|
/* 6. 搜索按钮对齐微调 */
|
||||||
.filter-actions .el-button {
|
.filter-actions .el-button {
|
||||||
height: 32px; /* 与 Tab 高度一致 */
|
height: 32px; /* 与 Tab 高度一致 */
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-actions {
|
.filter-actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
/* 基础 Badge 样式 */
|
/* 基础 Badge 样式 */
|
||||||
.status-badge {
|
.status-badge {
|
||||||
@@ -1894,21 +1972,25 @@ export default {
|
|||||||
background-color: #f1f5f9;
|
background-color: #f1f5f9;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
.status-draft::before { background-color: #94a3b8; }
|
.status-draft::before {
|
||||||
|
background-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
/* 5: Preparing - 橘色 */
|
/* 5: Preparing - 橘色 */
|
||||||
.status-preparing {
|
.status-preparing {
|
||||||
background-color: #fff7ed;
|
background-color: #fff7ed;
|
||||||
color: #c2410c;
|
color: #c2410c;
|
||||||
}
|
}
|
||||||
.status-preparing::before { background-color: #f97316; }
|
.status-preparing::before {
|
||||||
|
background-color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
/* 1: Running - 深蓝色 */
|
/* 1: Running - 深蓝色 */
|
||||||
.status-running {
|
.status-running {
|
||||||
background-color: #eff6ff;
|
background-color: #eff6ff;
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
}
|
}
|
||||||
.status-running::before {
|
.status-running::before {
|
||||||
background-color: #3b82f6;
|
background-color: #3b82f6;
|
||||||
box-shadow: 0 0 4px rgba(59, 130, 246, 0.5); /* 模拟运行中的发光感 */
|
box-shadow: 0 0 4px rgba(59, 130, 246, 0.5); /* 模拟运行中的发光感 */
|
||||||
}
|
}
|
||||||
@@ -1919,19 +2001,104 @@ export default {
|
|||||||
color: #334155;
|
color: #334155;
|
||||||
border: 1px solid #e2e8f0; /* 停用状态加个微边框增加分量感 */
|
border: 1px solid #e2e8f0; /* 停用状态加个微边框增加分量感 */
|
||||||
}
|
}
|
||||||
.status-paused::before { background-color: #475569; }
|
.status-paused::before {
|
||||||
|
background-color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
/* 3: Completed - 绿色 */
|
/* 3: Completed - 绿色 */
|
||||||
.status-completed {
|
.status-completed {
|
||||||
background-color: #f0fdf4;
|
background-color: #f0fdf4;
|
||||||
color: #15803d;
|
color: #15803d;
|
||||||
}
|
}
|
||||||
.status-completed::before { background-color: #22c55e; }
|
.status-completed::before {
|
||||||
|
background-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
/* 4: Cancelled - 浅红色 */
|
/* 4: Cancelled - 浅红色 */
|
||||||
.status-cancelled {
|
.status-cancelled {
|
||||||
background-color: #fef2f2;
|
background-color: #fef2f2;
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
.status-cancelled::before { background-color: #ef4444; }
|
.status-cancelled::before {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
/* 基础 Select 宽度 */
|
||||||
|
.custom-pipeline-select {
|
||||||
|
width: 580px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分组标题(el-option-group,避免在 ul 内放 div) */
|
||||||
|
.pipeline-popper .el-select-group__title {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #909399;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单个选项容器 */
|
||||||
|
.pipeline-popper .el-select-dropdown__item {
|
||||||
|
height: auto; /* 允许高度自适应 */
|
||||||
|
padding: 12px 20px;
|
||||||
|
line-height: normal;
|
||||||
|
border-left: 3px solid transparent; /* 预留边框位置防止抖动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中状态 */
|
||||||
|
.pipeline-popper .el-select-dropdown__item.selected {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-left: 3px solid #409eff; /* 选中时的左侧蓝条 */
|
||||||
|
color: #606266; /* 覆盖 Element 默认高亮色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选项内容布局 */
|
||||||
|
.pipeline-popper .option-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 第一行:标题 + 状态标签 */
|
||||||
|
.pipeline-popper .option-content .row-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-popper .option-content .row-top .task-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-popper .option-content .row-top .status-tag {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 第二行:元数据信息 */
|
||||||
|
.pipeline-popper .option-content .row-bottom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-popper .option-content .row-bottom .meta-item.database {
|
||||||
|
color: #5856d6;
|
||||||
|
font-weight: bold;
|
||||||
|
/* text-transform: uppercase; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-popper .option-content .row-bottom .separator {
|
||||||
|
margin: 0 6px;
|
||||||
|
color: #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -59,20 +59,22 @@
|
|||||||
|
|
||||||
<div class="drawer-body" v-loading="loading">
|
<div class="drawer-body" v-loading="loading">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
|
<div class="col-index">{{ $t('autoPromotionLogs.logColIndex') }}</div>
|
||||||
<div class="col-info">{{ $t('autoPromotionLogs.logColExpert') }}</div>
|
<div class="col-info">{{ $t('autoPromotionLogs.logColExpert') }}</div>
|
||||||
<div class="col-send" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColSendTime') }}</div>
|
<div class="col-send" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColSendTime') }}</div>
|
||||||
<div class="col-prepared" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColPreparedAt') }}</div>
|
<div class="col-prepared" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColPreparedAt') }}</div>
|
||||||
<div class="col-status">{{ $t('autoPromotionLogs.logColStatus') }}</div>
|
<div class="col-status">{{ $t('autoPromotionLogs.logColStatus') }}</div>
|
||||||
<div class="col-action">{{ $t('autoPromotionLogs.logColAction') }}</div>
|
<div class="col-action"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-wrapper">
|
<div class="list-wrapper">
|
||||||
<div
|
<div
|
||||||
v-for="item in fullData"
|
v-for="(item, rowIndex) in fullData"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="log-row"
|
class="log-row"
|
||||||
:class="{ 'row-error': item.isErrorRow }"
|
:class="{ 'row-error': item.isErrorRow }"
|
||||||
>
|
>
|
||||||
|
<div class="col-index">{{ (currentPage - 1) * pageSize + rowIndex + 1 }}</div>
|
||||||
<div class="col-info">
|
<div class="col-info">
|
||||||
<div class="expert-main">
|
<div class="expert-main">
|
||||||
<span class="name">{{ item.expertName }}</span>
|
<span class="name">{{ item.expertName }}</span>
|
||||||
@@ -782,6 +784,14 @@ export default {
|
|||||||
color: #475569;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col-index {
|
||||||
|
flex: 0 0 52px;
|
||||||
|
width: 52px;
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
.col-info {
|
.col-info {
|
||||||
flex: 1.8;
|
flex: 1.8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,29 @@
|
|||||||
<h1 class="mail-subject-top">{{ $t('mailboxCollect.subject') }}:{{ mailData.subject }}</h1>
|
<h1 class="mail-subject-top">{{ $t('mailboxCollect.subject') }}:{{ mailData.subject }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
|
<el-button
|
||||||
|
v-if="hasWordAttachment"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
plain
|
||||||
|
icon="el-icon-upload2"
|
||||||
|
:loading="registerAuthorLoading"
|
||||||
|
class="register-author-btn"
|
||||||
|
@click="registerAuthorFromMail"
|
||||||
|
>
|
||||||
|
{{ $t('mailboxCollect.registerAuthorBtn') }}
|
||||||
|
</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>
|
||||||
@@ -74,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mail-body-content" v-html="mailData.content_html"></div>
|
<div class="mail-body-content" v-html="mailBodyHtml"></div>
|
||||||
|
|
||||||
<div v-if="mailData.attachments && mailData.attachments.length" class="attachment-section">
|
<div v-if="mailData.attachments && mailData.attachments.length" class="attachment-section">
|
||||||
<div class="attachment-header">
|
<div class="attachment-header">
|
||||||
@@ -118,14 +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 { 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
|
||||||
@@ -144,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() {
|
||||||
@@ -151,17 +272,580 @@ export default {
|
|||||||
mediaUrl: Common.mediaUrl,
|
mediaUrl: Common.mediaUrl,
|
||||||
isDetailExpanded: false,
|
isDetailExpanded: false,
|
||||||
downloadingIndex: -1,
|
downloadingIndex: -1,
|
||||||
packingAll: false
|
packingAll: false,
|
||||||
|
registerAuthorLoading: false,
|
||||||
|
autoSubmitDialogVisible: false,
|
||||||
|
autoSubmitLoading: false,
|
||||||
|
/** 建稿成功后在同一弹窗内展示摘要,仅保留关闭 */
|
||||||
|
autoSubmitSuccessMode: false,
|
||||||
|
autoSubmitSuccessBodyHtml: '',
|
||||||
|
autoSubmitForm: {
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
/** 弹窗内本机选择的 .docx,优先于邮件附件下载 */
|
||||||
|
autoSubmitLocalDocxFile: null,
|
||||||
|
/** 因「邮箱/账号已存在」打开建稿弹窗时展示提示条 */
|
||||||
|
autoSubmitPrefillFromRegisterConflict: false,
|
||||||
|
/** 本地上传组件强制重建 key,超限选新文件时替换 */
|
||||||
|
autoSubmitUploadKey: 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
/** 存在 Word 附件(.doc / .docx)时显示「创建作者账号」 */
|
||||||
|
hasWordAttachment() {
|
||||||
|
const att = (this.mailData && this.mailData.attachments) || [];
|
||||||
|
if (!att.length) return false;
|
||||||
|
return att.some((f) => {
|
||||||
|
const n = (f && f.name) || '';
|
||||||
|
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 */
|
||||||
|
mailBodyHtml() {
|
||||||
|
const m = this.mailData || {};
|
||||||
|
let raw = m.content_html || m.body_html || m.html || m.body || m.content || '';
|
||||||
|
raw = normalizeEmailHtmlForInlineDisplay(raw);
|
||||||
|
if (raw) return raw;
|
||||||
|
const text = m.content_text;
|
||||||
|
if (text != null && String(text).trim() !== '') {
|
||||||
|
return `<pre class="mail-plain-pre">${this.escapeHtml(String(text))}</pre>`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
stripHtml(html) {
|
||||||
|
if (html == null || html === '') return '';
|
||||||
|
const s = String(html);
|
||||||
|
const d = typeof document !== 'undefined' ? document.createElement('div') : null;
|
||||||
|
if (d) {
|
||||||
|
d.innerHTML = s;
|
||||||
|
return (d.textContent || d.innerText || '').trim();
|
||||||
|
}
|
||||||
|
return s.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
},
|
||||||
|
/** 与 partyList 添加用户一致:从正文尝试提取手机号(国内 11 位或含 + 的国际号) */
|
||||||
|
extractPhoneFromMailBody() {
|
||||||
|
const m = this.mailData || {};
|
||||||
|
let blob = '';
|
||||||
|
['content_text', 'content', 'body', 'content_html', 'html', 'body_html'].forEach((k) => {
|
||||||
|
const v = m[k];
|
||||||
|
if (v != null && String(v).trim() !== '') blob += `\n${String(v)}`;
|
||||||
|
});
|
||||||
|
const text = this.stripHtml(blob);
|
||||||
|
const cn = text.match(/(?:^|\D)(1[3-9]\d{9})(?:\D|$)/);
|
||||||
|
if (cn) return cn[1];
|
||||||
|
const intl = text.match(/\+\d{1,3}[\s\-]?\d[\d\s\-]{8,18}\d/);
|
||||||
|
if (intl) return intl[0].replace(/\s+/g, ' ').trim().slice(0, 32);
|
||||||
|
const labeled = text.match(/(?:Tel|Phone|Mobile|MW)[\s::]*([+()\d][\d\s\-().]{10,40})/i);
|
||||||
|
if (labeled) return labeled[1].trim().slice(0, 32);
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
/** 发件人显示名(用于 realname),与 partyList 校验规则一致 */
|
||||||
|
deriveRealnameFromMail() {
|
||||||
|
const m = this.mailData || {};
|
||||||
|
const fromEmail = String(m.from_email || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const localPart = fromEmail.split('@')[0] || 'user';
|
||||||
|
let rawName = String(m.from_name || '').trim();
|
||||||
|
let realname = rawName || localPart;
|
||||||
|
if (this.$validateString && !this.$validateString(realname)) {
|
||||||
|
realname = localPart;
|
||||||
|
if (this.$validateString && !this.$validateString(realname)) {
|
||||||
|
realname = 'Author';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return realname;
|
||||||
|
},
|
||||||
|
/** 当前邮件发件人邮箱(来稿真实地址),用于注册作者与通知收件人 */
|
||||||
|
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 (!acc || acc.length < 2) {
|
||||||
|
acc = 'author_' + String(Date.now()).slice(-8);
|
||||||
|
}
|
||||||
|
if (acc.length > 32) acc = acc.slice(0, 32);
|
||||||
|
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() {
|
||||||
|
if (!this.hasWordAttachment) return;
|
||||||
|
const realname = this.deriveRealnameFromMail();
|
||||||
|
const email2 = this.getPrimaryMailSenderEmail();
|
||||||
|
if (!email2) {
|
||||||
|
this.$message.error(this.$t('mailboxCollect.registerAuthorNeedEmail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const account2 = this.deriveLoginAccountFromEmail(email2);
|
||||||
|
this.registerAuthorLoading = true;
|
||||||
|
try {
|
||||||
|
const r1 = await this.$api.post('api/User/checkUserByEmail', { email: email2, account: account2 });
|
||||||
|
const r2 = await this.$api.post('api/User/checkUserByAccount', { email: email2, account: account2 });
|
||||||
|
if (!(r1 && r2 && Number(r1.code) === 0 && Number(r2.code) === 0)) {
|
||||||
|
this.$message.error((r1 && r1.msg) || (r2 && r2.msg) || this.$t('mailboxCollect.registerAuthorFail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r1.data && Number(r1.data.has) !== 0) {
|
||||||
|
this.openAutoSubmitDialogWithCredentialsPrefill({ usernameHint: email2 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r2.data && Number(r2.data.has) !== 0) {
|
||||||
|
this.openAutoSubmitDialogWithCredentialsPrefill({ usernameHint: account2 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.$message.error(this.$t('mailboxCollect.registerAuthorFail'));
|
||||||
|
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 = {
|
||||||
|
email: email2,
|
||||||
|
account: account2,
|
||||||
|
password: pwd,
|
||||||
|
repassword: pwd,
|
||||||
|
realname,
|
||||||
|
phone: phone2 || ''
|
||||||
|
};
|
||||||
|
const res = await this.$api.post('api/User/addUser', addForm);
|
||||||
|
if (res && Number(res.code) === 0) {
|
||||||
|
this.autoSubmitForm.username = account2;
|
||||||
|
this.autoSubmitForm.password = pwd;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.resolvedJournalId != null) {
|
||||||
|
this.openAutoSubmitDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
|
console.error(err);
|
||||||
|
this.$message.error(this.$t('mailboxCollect.registerAuthorFail'));
|
||||||
|
} finally {
|
||||||
|
this.registerAuthorLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
escapeHtml(text) {
|
||||||
|
if (text == null) return '';
|
||||||
|
return String(text)
|
||||||
|
.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');
|
||||||
@@ -213,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;
|
||||||
@@ -221,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);
|
||||||
@@ -240,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),
|
||||||
@@ -248,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 必须是公网可访问的!
|
||||||
@@ -266,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();
|
||||||
@@ -362,6 +1062,72 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.register-author-btn {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.auto-submit-email-readonly /deep/ .el-input__inner {
|
||||||
|
color: #606266;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -441,6 +1207,15 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* v-html 注入的正文无 scoped 属性,用 deep 命中内部的 pre */
|
||||||
|
.mail-body-content >>> .mail-plain-pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 附件卡片 */
|
/* 附件卡片 */
|
||||||
.attachment-section {
|
.attachment-section {
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
@@ -635,7 +1410,9 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
|||||||
}
|
}
|
||||||
.attachment-brief-bar {
|
.attachment-brief-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px 12px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
@@ -644,14 +1421,17 @@ 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;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
.jump-link {
|
.jump-link {
|
||||||
margin-left: 15px;
|
margin-left: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.attachment-section {
|
.attachment-section {
|
||||||
|
|||||||
@@ -109,7 +109,14 @@
|
|||||||
<div class="mail-content-panel" v-if="selectedAccount" v-loading="detailLoading">
|
<div class="mail-content-panel" v-if="selectedAccount" v-loading="detailLoading">
|
||||||
<template v-if="activeMailId && !detailLoading">
|
<template v-if="activeMailId && !detailLoading">
|
||||||
<div class="mail-page">
|
<div class="mail-page">
|
||||||
<mail-detail v-if="detailMail" :mailData="detailMail" @close="closeDetail" />
|
<mail-detail
|
||||||
|
v-if="detailMail && String(detailMail.inbox_id || '') !== ''"
|
||||||
|
:mailData="detailMail"
|
||||||
|
:journal-id="mailDetailJournalId"
|
||||||
|
:journal-title="mailDetailJournalTitle"
|
||||||
|
:j-email-id="mailDetailJEmailId"
|
||||||
|
@close="closeDetail"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -168,9 +175,11 @@ const API = {
|
|||||||
getAllJournal: 'api/Article/getJournal'
|
getAllJournal: 'api/Article/getJournal'
|
||||||
};
|
};
|
||||||
import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
||||||
|
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
baseUrl: this.Common.baseUrl,
|
||||||
currentFolder: 'inbox',
|
currentFolder: 'inbox',
|
||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
syncLoading: false,
|
syncLoading: false,
|
||||||
@@ -214,6 +223,39 @@ import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
|||||||
},
|
},
|
||||||
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() {
|
||||||
@@ -292,8 +334,14 @@ import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
|||||||
this.$api
|
this.$api
|
||||||
.post(API.getOneEmail, { j_email_id: jEmailId })
|
.post(API.getOneEmail, { j_email_id: jEmailId })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const email = res && res.data ? res.data.email : null;
|
const raw = res && res.data != null ? res.data : null;
|
||||||
if (res && res.code === 0 && email) {
|
const email =
|
||||||
|
raw && typeof raw === 'object' && raw.email && typeof raw.email === 'object'
|
||||||
|
? raw.email
|
||||||
|
: raw && typeof raw === 'object' && (raw.j_email_id != null || raw.smtp_user)
|
||||||
|
? raw
|
||||||
|
: null;
|
||||||
|
if (res && Number(res.code) === 0 && email) {
|
||||||
this.selectedAccount = email;
|
this.selectedAccount = email;
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
this.startInboxSse();
|
this.startInboxSse();
|
||||||
@@ -488,28 +536,103 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
|||||||
this.currentFolder = f;
|
this.currentFolder = f;
|
||||||
this.activeMailId = null;
|
this.activeMailId = null;
|
||||||
},
|
},
|
||||||
selectMail(item,index) {
|
/** 接口 code 可能是数字 0 或字符串 "0" */
|
||||||
|
isEmailApiSuccess(res) {
|
||||||
|
if (!res || res.code === undefined || res.code === null) return false;
|
||||||
|
return Number(res.code) === 0;
|
||||||
|
},
|
||||||
|
escapeHtml(text) {
|
||||||
|
if (text == null) return '';
|
||||||
|
return String(text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
},
|
||||||
|
/** 解析 getEmailDetail 返回体,统一 content 字段;失败时用列表行兜底 */
|
||||||
|
buildDetailMailFromResponse(res, item, inboxId) {
|
||||||
|
const ok = this.isEmailApiSuccess(res);
|
||||||
|
let d = res && res.data != null ? res.data : null;
|
||||||
|
if (d && typeof d === 'string') {
|
||||||
|
try {
|
||||||
|
d = JSON.parse(d);
|
||||||
|
} catch (e) {
|
||||||
|
d = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(d) && d.length) {
|
||||||
|
d = d[0];
|
||||||
|
}
|
||||||
|
if (ok && d && typeof d === 'object' && d.data && typeof d.data === 'object' && !d.content_html && !d.content_text) {
|
||||||
|
d = { ...d, ...d.data };
|
||||||
|
}
|
||||||
|
if (ok && (!d || typeof d !== 'object') && res && (res.content_html != null || res.content_text != null || res.subject != null)) {
|
||||||
|
d = { ...res };
|
||||||
|
delete d.code;
|
||||||
|
delete d.msg;
|
||||||
|
if (Object.prototype.hasOwnProperty.call(d, 'data')) delete d.data;
|
||||||
|
}
|
||||||
|
const inboxKey = String(inboxId);
|
||||||
|
const listSnippet = (item && item.content) || '';
|
||||||
|
if (!ok || !d || typeof d !== 'object') {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
subject: (item && item.subject) || '',
|
||||||
|
from_name: (item && item.from_name) || '',
|
||||||
|
from_email: (item && item.email) || '',
|
||||||
|
email_date: item && item.email_date,
|
||||||
|
content_html: listSnippet,
|
||||||
|
inbox_id: inboxKey,
|
||||||
|
attachments: [],
|
||||||
|
has_attachment: (item && item.has_attachment) || 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const html = d.content_html || d.body_html || d.html || d.body || '';
|
||||||
|
const text = d.content_text || '';
|
||||||
|
let content_html =
|
||||||
|
html ||
|
||||||
|
(text ? `<pre class="mail-plain-pre">${this.escapeHtml(text)}</pre>` : '') ||
|
||||||
|
listSnippet;
|
||||||
|
if (content_html && typeof content_html === 'string' && /^<!DOCTYPE|^<\s*html[\s>]/i.test(content_html.trim())) {
|
||||||
|
const normalized = normalizeEmailHtmlForInlineDisplay(content_html);
|
||||||
|
if (normalized && normalized.trim()) content_html = normalized;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...d,
|
||||||
|
content_html,
|
||||||
|
inbox_id: inboxKey,
|
||||||
|
attachments: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
selectMail(item, index) {
|
||||||
this.activeMailId = item.id;
|
this.activeMailId = item.id;
|
||||||
this.detailLoading = true;
|
this.detailLoading = true;
|
||||||
|
this.detailMail = {};
|
||||||
const inboxId = item.inbox_id || item.id;
|
const inboxId = item.inbox_id || item.id;
|
||||||
this.$api
|
this.$api
|
||||||
.post(API.getEmailDetail, { inbox_id: inboxId })
|
.post(API.getEmailDetail, { inbox_id: inboxId })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const d = res && res.data;
|
try {
|
||||||
if (res && res.code === 0 && d) {
|
const merged = this.buildDetailMailFromResponse(res, item, inboxId);
|
||||||
this.detailMail = { ...d, inbox_id: String(inboxId), attachments: [] };
|
this.detailMail = merged;
|
||||||
item.state = 0;
|
if (item) item.state = 0;
|
||||||
this.displayList[index].is_read = d.is_read;
|
const row = this.displayList[index];
|
||||||
if (Number(d.has_attachment) === 1) {
|
if (row && merged && merged.is_read !== undefined && merged.is_read !== null) {
|
||||||
this.fetchAttachments(String(inboxId));
|
this.$set(row, 'is_read', merged.is_read);
|
||||||
} else {
|
|
||||||
this.detailLoading = false;
|
|
||||||
}
|
}
|
||||||
} else {
|
if (Number(merged.has_attachment) === 1) {
|
||||||
this.detailLoading = false;
|
this.fetchAttachments(String(inboxId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.detailMail = this.buildDetailMailFromResponse(null, item, inboxId);
|
||||||
}
|
}
|
||||||
|
this.detailLoading = false;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
this.detailMail = this.buildDetailMailFromResponse(null, item, inboxId);
|
||||||
this.detailLoading = false;
|
this.detailLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -645,7 +768,7 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
|||||||
},
|
},
|
||||||
buildSseUrl(jEmailId) {
|
buildSseUrl(jEmailId) {
|
||||||
// 与现有 axios baseURL=/api + 相对路径 规则一致;GET + query 传参
|
// 与现有 axios baseURL=/api + 相对路径 规则一致;GET + query 传参
|
||||||
const base = `/api/${API.inboxSse}`;
|
const base = `${this.baseUrl}${API.inboxSse}`;
|
||||||
const q = new URLSearchParams({ j_email_id: String(jEmailId) }).toString();
|
const q = new URLSearchParams({ j_email_id: String(jEmailId) }).toString();
|
||||||
return `${base}?${q}`;
|
return `${base}?${q}`;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,6 +50,9 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<div class="right-actions">
|
<div class="right-actions">
|
||||||
|
<el-button type="warning" plain icon="el-icon-upload2" @click="openTemplateBatchImportDialog">{{
|
||||||
|
$t('mailboxMould.batchImportBtn')
|
||||||
|
}}</el-button>
|
||||||
<el-button type="primary" plain icon="el-icon-plus" @click="handleCreateTemplate">{{ $t('mailboxMould.createTemplate') }}</el-button>
|
<el-button type="primary" plain icon="el-icon-plus" @click="handleCreateTemplate">{{ $t('mailboxMould.createTemplate') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,6 +139,36 @@
|
|||||||
<el-button @click="previewVisible = false">{{ $t('mailboxMould.previewClose') }}</el-button>
|
<el-button @click="previewVisible = false">{{ $t('mailboxMould.previewClose') }}</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
:title="$t('mailboxMould.batchImportTitle')"
|
||||||
|
:visible.sync="batchTplImportVisible"
|
||||||
|
width="720px"
|
||||||
|
append-to-body
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
custom-class="mailbox-mould-batch-import-dialog"
|
||||||
|
@closed="batchTplImporting = false"
|
||||||
|
>
|
||||||
|
<p class="batch-tpl-hint">{{ $t('mailboxMould.batchImportHint') }}</p>
|
||||||
|
<p class="batch-tpl-tip">{{ $t('mailboxMould.batchImportCommonTip') }}</p>
|
||||||
|
<div class="batch-tpl-journal-row">
|
||||||
|
<span class="batch-tpl-label">{{ $t('mailboxMould.batchImportJournalId') }}</span>
|
||||||
|
<el-input
|
||||||
|
v-model="batchTplImportJournalId"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
:placeholder="$t('mailboxMould.batchImportJournalPlaceholder')"
|
||||||
|
class="batch-tpl-journal-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-input v-model="batchTplImportText" type="textarea" :rows="14" class="batch-tpl-textarea" spellcheck="false" />
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button size="small" @click="batchTplImportVisible = false">{{ $t('mailboxMould.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" size="small" :loading="batchTplImporting" @click="runTemplateBatchImport">{{
|
||||||
|
$t('mailboxMould.batchImportRun')
|
||||||
|
}}</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -145,7 +178,8 @@ const API = {
|
|||||||
listStyles: 'api/mail_template/listStyles',
|
listStyles: 'api/mail_template/listStyles',
|
||||||
getAllJournal: 'api/Article/getJournal',
|
getAllJournal: 'api/Article/getJournal',
|
||||||
deleteTemplate: 'api/mail_template/deleteTemplate',
|
deleteTemplate: 'api/mail_template/deleteTemplate',
|
||||||
deleteStyle: 'api/mail_template/deleteStyle'
|
deleteStyle: 'api/mail_template/deleteStyle',
|
||||||
|
saveTemplate: 'api/mail_template/saveTemplate'
|
||||||
};
|
};
|
||||||
// 仅在当前 SPA 会话内记忆筛选(刷新页面即重置)
|
// 仅在当前 SPA 会话内记忆筛选(刷新页面即重置)
|
||||||
const mailboxMouldSessionMemory = {
|
const mailboxMouldSessionMemory = {
|
||||||
@@ -177,7 +211,13 @@ export default {
|
|||||||
|
|
||||||
// --- 共用预览 ---
|
// --- 共用预览 ---
|
||||||
previewVisible: false,
|
previewVisible: false,
|
||||||
previewContent: ''
|
previewContent: '',
|
||||||
|
|
||||||
|
/** 邮件模板 JSON 批量导入(期刊 ID 单独输入,与每条合并后调 saveTemplate) */
|
||||||
|
batchTplImportVisible: false,
|
||||||
|
batchTplImportText: '',
|
||||||
|
batchTplImportJournalId: '',
|
||||||
|
batchTplImporting: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -281,6 +321,147 @@ export default {
|
|||||||
const journalId = this.tplFilters && this.tplFilters.journalId ? String(this.tplFilters.journalId) : '';
|
const journalId = this.tplFilters && this.tplFilters.journalId ? String(this.tplFilters.journalId) : '';
|
||||||
this.$router.push({ path: '/mailboxMouldDetail', query: journalId ? { journal_id: journalId } : {} });
|
this.$router.push({ path: '/mailboxMouldDetail', query: journalId ? { journal_id: journalId } : {} });
|
||||||
},
|
},
|
||||||
|
openTemplateBatchImportDialog() {
|
||||||
|
const cur = String((this.tplFilters && this.tplFilters.journalId) || '').trim();
|
||||||
|
if (cur) this.batchTplImportJournalId = cur;
|
||||||
|
if (!this.batchTplImportText || !String(this.batchTplImportText).trim()) {
|
||||||
|
this.batchTplImportText = this.defaultTemplateBatchImportSample();
|
||||||
|
}
|
||||||
|
this.batchTplImportVisible = true;
|
||||||
|
},
|
||||||
|
defaultTemplateBatchImportSample() {
|
||||||
|
return (
|
||||||
|
'[\n' +
|
||||||
|
' {\n' +
|
||||||
|
' "title": "示例标题",\n' +
|
||||||
|
' "subject": "示例主题",\n' +
|
||||||
|
' "scene": "invite_submission",\n' +
|
||||||
|
' "language": "en",\n' +
|
||||||
|
' "version": "1.0.0",\n' +
|
||||||
|
' "body_html": "<p>正文 HTML</p>",\n' +
|
||||||
|
' "variables_json": "",\n' +
|
||||||
|
' "is_active": 1\n' +
|
||||||
|
' }\n' +
|
||||||
|
']\n'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
/** 与 mailboxMouldDetail._doSave 提交 saveTemplate 字段对齐;支持 body、lang、variables 别名 */
|
||||||
|
normalizeMailTemplateSavePayload(row) {
|
||||||
|
if (!row || typeof row !== 'object') return {};
|
||||||
|
const bodyHtml =
|
||||||
|
row.body_html != null
|
||||||
|
? String(row.body_html)
|
||||||
|
: row.body != null
|
||||||
|
? String(row.body)
|
||||||
|
: '';
|
||||||
|
const bodyTextRaw = row.body_text != null ? String(row.body_text) : '';
|
||||||
|
const bodyText =
|
||||||
|
bodyTextRaw.trim() ||
|
||||||
|
bodyHtml
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
const lang = String(row.language != null ? row.language : row.lang != null ? row.lang : 'en').toLowerCase();
|
||||||
|
let isActive = '1';
|
||||||
|
if (row.is_active === 0 || row.is_active === '0' || row.is_active === false) isActive = '0';
|
||||||
|
const out = {
|
||||||
|
journal_id: String(row.journal_id != null ? row.journal_id : row.journalId != null ? row.journalId : '').trim(),
|
||||||
|
scene: String(row.scene || 'invite_submission'),
|
||||||
|
language: lang,
|
||||||
|
title: String(row.title || ''),
|
||||||
|
subject: String(row.subject || ''),
|
||||||
|
body_html: bodyHtml,
|
||||||
|
body_text: bodyText,
|
||||||
|
variables_json: String(
|
||||||
|
row.variables_json != null ? row.variables_json : row.variables != null ? row.variables : ''
|
||||||
|
),
|
||||||
|
version: String(row.version != null ? row.version : '1.0.0'),
|
||||||
|
is_active: isActive
|
||||||
|
};
|
||||||
|
const tid = row.template_id != null ? row.template_id : row.id;
|
||||||
|
if (tid != null && String(tid).trim() !== '') out.template_id = String(tid).trim();
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
applyTemplateBatchImportJournal(payload) {
|
||||||
|
const jid = String(this.batchTplImportJournalId || '').trim();
|
||||||
|
if (jid) payload.journal_id = jid;
|
||||||
|
},
|
||||||
|
validateMailTemplateSavePayload(p, index) {
|
||||||
|
if (!p.journal_id || !String(p.journal_id).trim()) {
|
||||||
|
return this.$t('mailboxMould.batchImportMissingJournal', { index: index + 1 });
|
||||||
|
}
|
||||||
|
if (!p.title || !String(p.title).trim()) {
|
||||||
|
return this.$t('mailboxMould.batchImportMissingField', { index: index + 1, field: 'title' });
|
||||||
|
}
|
||||||
|
if (!p.subject || !String(p.subject).trim()) {
|
||||||
|
return this.$t('mailboxMould.batchImportMissingField', { index: index + 1, field: 'subject' });
|
||||||
|
}
|
||||||
|
if (!p.body_html || !String(p.body_html).trim()) {
|
||||||
|
return this.$t('mailboxMould.batchImportMissingField', { index: index + 1, field: 'body_html' });
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
async runTemplateBatchImport() {
|
||||||
|
let rows;
|
||||||
|
try {
|
||||||
|
rows = JSON.parse(this.batchTplImportText || '[]');
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(this.$t('mailboxMould.batchImportBadJson'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(rows) || !rows.length) {
|
||||||
|
this.$message.warning(this.$t('mailboxMould.batchImportEmpty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.batchTplImporting = true;
|
||||||
|
let ok = 0;
|
||||||
|
let fail = 0;
|
||||||
|
const errLines = [];
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const payload = this.normalizeMailTemplateSavePayload(rows[i]);
|
||||||
|
this.applyTemplateBatchImportJournal(payload);
|
||||||
|
const ve = this.validateMailTemplateSavePayload(payload, i);
|
||||||
|
if (ve) {
|
||||||
|
fail++;
|
||||||
|
errLines.push(ve);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await this.$api.post(API.saveTemplate, payload);
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
ok++;
|
||||||
|
} else {
|
||||||
|
fail++;
|
||||||
|
errLines.push(
|
||||||
|
this.$t('mailboxMould.batchImportRowFail', {
|
||||||
|
index: i + 1,
|
||||||
|
msg: (res && res.msg) || this.$t('mailboxMould.batchImportSaveFail')
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
fail++;
|
||||||
|
errLines.push(this.$t('mailboxMould.batchImportRowNetwork', { index: i + 1 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$message.success(this.$t('mailboxMould.batchImportDone', { ok, fail }));
|
||||||
|
if (errLines.length) {
|
||||||
|
this.$notify({
|
||||||
|
title: this.$t('mailboxMould.batchImportErrorsTitle'),
|
||||||
|
message: errLines.slice(0, 8).join('\n'),
|
||||||
|
type: fail && !ok ? 'error' : 'warning',
|
||||||
|
duration: 12000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.batchTplImportVisible = false;
|
||||||
|
this.syncTplFilterMemory();
|
||||||
|
this.fetchTemplates();
|
||||||
|
} finally {
|
||||||
|
this.batchTplImporting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
handleEditTemplate(row) {
|
handleEditTemplate(row) {
|
||||||
this.syncTplFilterMemory();
|
this.syncTplFilterMemory();
|
||||||
const templateId = row && (row.template_id || row.id);
|
const templateId = row && (row.template_id || row.id);
|
||||||
@@ -419,4 +600,39 @@ export default {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.batch-tpl-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #606266;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
.batch-tpl-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #909399;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
.batch-tpl-journal-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.batch-tpl-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
|
min-width: 72px;
|
||||||
|
}
|
||||||
|
.batch-tpl-journal-input {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
.mailbox-mould-batch-import-dialog .batch-tpl-textarea >>> textarea {
|
||||||
|
font-family: Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
26
src/utils/emailHtmlView.js
Normal file
26
src/utils/emailHtmlView.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 将完整 HTML 邮件转为适合 div[v-html] 的片段:
|
||||||
|
* 取 body.innerHTML,并前置 head 内 <style>,避免版式与改版前不一致。
|
||||||
|
*/
|
||||||
|
export function normalizeEmailHtmlForInlineDisplay(html) {
|
||||||
|
if (!html || typeof html !== 'string') return '';
|
||||||
|
const t = html.trim();
|
||||||
|
if (!/^<!DOCTYPE|^<\s*html[\s>]/i.test(t)) return html;
|
||||||
|
try {
|
||||||
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
|
const body = doc && doc.body;
|
||||||
|
if (!body) return html;
|
||||||
|
const inner = body.innerHTML;
|
||||||
|
if (!inner || !inner.trim()) return html;
|
||||||
|
let headInject = '';
|
||||||
|
const head = doc.head;
|
||||||
|
if (head) {
|
||||||
|
head.querySelectorAll('style').forEach((node) => {
|
||||||
|
headInject += node.outerHTML;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return headInject + inner;
|
||||||
|
} catch (e) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/utils/ithenticateSimilarityStyle.js
Normal file
15
src/utils/ithenticateSimilarityStyle.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 基于 iThenticate 标准的颜色映射(与编辑端常见分档一致)
|
||||||
|
* @param {number|string} score 相似度百分比(可为小数,内部会规范化)
|
||||||
|
* @returns {{ color: string, label: string }}
|
||||||
|
*/
|
||||||
|
export function getSimilarityStyle(score) {
|
||||||
|
let n = Number(score);
|
||||||
|
if (!Number.isFinite(n) || n < 0) n = 0;
|
||||||
|
const s = Math.round(n);
|
||||||
|
if (s === 0) return { color: '#0000FF', label: 'Blue' };
|
||||||
|
if (s <= 24) return { color: '#008000', label: 'Green' };
|
||||||
|
if (s <= 49) return { color: '#EDBD3E', label: 'Yellow' };
|
||||||
|
if (s <= 74) return { color: '#FF8C00', label: 'Orange' };
|
||||||
|
return { color: '#FF0000', label: 'Red' };
|
||||||
|
}
|
||||||
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