Merge branch 'master' of https://git.nuttyreading.com/wangjinlei/tougao_web into Editorial-Board
This commit is contained in:
@@ -859,8 +859,8 @@
|
||||
<el-form-item label="Account :" prop="account">
|
||||
<span>{{ coreForm.account }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="Email :">
|
||||
<span>{{ coreForm.email }}</span>
|
||||
<el-form-item label="Email :" prop="email">
|
||||
<el-input type="text" placeholder="Please enter email..." v-model="coreForm.email" style="width: 320px" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Real name :" prop="realname">
|
||||
@@ -1610,6 +1610,14 @@ this.applyCvitaTable = data;
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.coreTable = res.data.baseInfo;
|
||||
try {
|
||||
if (this.coreTable && this.coreTable.email != null && this.coreTable.email !== undefined) {
|
||||
localStorage.setItem('U_email', String(this.coreTable.email));
|
||||
}
|
||||
if (this.coreTable && this.coreTable.realname != null && this.coreTable.realname !== undefined) {
|
||||
localStorage.setItem('U_relname', String(this.coreTable.realname));
|
||||
}
|
||||
} catch (e) {}
|
||||
this.majorsList=res.data.baseInfo.majors;
|
||||
this.cvitaForm.user_id = res.data.baseInfo.user_id;
|
||||
this.reviewForm.user_id = res.data.baseInfo.user_id;
|
||||
@@ -1901,6 +1909,14 @@ this.applyCvitaTable = data;
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.$message.success('Personal information modified successfully!');
|
||||
try {
|
||||
if (this.coreForm.email != null && this.coreForm.email !== undefined) {
|
||||
localStorage.setItem('U_email', String(this.coreForm.email));
|
||||
}
|
||||
if (this.coreForm.realname != null && this.coreForm.realname !== undefined) {
|
||||
localStorage.setItem('U_relname', String(this.coreForm.realname));
|
||||
}
|
||||
} catch (e) {}
|
||||
this.coreVisible = false;
|
||||
this.tipVisible = false;
|
||||
this.getPersonData();
|
||||
|
||||
@@ -3320,13 +3320,13 @@ export default {
|
||||
},
|
||||
|
||||
async onDrop(event, dataId) {
|
||||
const loading = this.$loading({
|
||||
if (event.dataTransfer.getData('image')) {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: 'Loading...',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
if (event.dataTransfer.getData('image')) {
|
||||
const draggedImage = JSON.parse(event.dataTransfer.getData('image'));
|
||||
const draggedImageIndex = JSON.parse(event.dataTransfer.getData('imageIndex'));
|
||||
this.$nextTick(async () => {
|
||||
@@ -3353,7 +3353,13 @@ export default {
|
||||
this.$message.error(err.msg);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
} else if(event.dataTransfer.getData('table')) {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: 'Loading...',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
const draggedtable = JSON.parse(event.dataTransfer.getData('table'));
|
||||
|
||||
this.$nextTick(async () => {
|
||||
@@ -3380,6 +3386,8 @@ export default {
|
||||
this.$message.error(err.msg);
|
||||
});
|
||||
});
|
||||
}else{
|
||||
|
||||
}
|
||||
},
|
||||
getCommentList() {
|
||||
|
||||
@@ -263,7 +263,39 @@
|
||||
<i v-else class="el-icon-plus avatar-uploader-icon" style="line-height: 120px"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
</el-form-item>
|
||||
<el-form-item label="Contact Editor WeChat QR Code :" prop="wechat_yboard_qrcode">
|
||||
<div class="wechat-name-split__right">
|
||||
<div class="yboard-qrcode-side__label"></div>
|
||||
<div class="portrait WeChatCode">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
ref="upIconIMgYboard"
|
||||
:action="baseUrl + 'api/Journal/uploadYboardQrcode'"
|
||||
:show-file-list="false"
|
||||
name="qrcode_url"
|
||||
:on-success="handleAvatarSuccessYboard"
|
||||
:on-error="handleAvatarErrorYboard"
|
||||
:before-upload="beforeAvatarUpload2"
|
||||
>
|
||||
<img
|
||||
v-if="detailForm.wechat_yboard_qrcode"
|
||||
:src="
|
||||
/^https?:\/\//.test(detailForm.wechat_yboard_qrcode)
|
||||
? detailForm.wechat_yboard_qrcode
|
||||
: mediaUrl + 'journalyboardqrcode/' + detailForm.wechat_yboard_qrcode
|
||||
"
|
||||
class="avatar"
|
||||
accept=".png,.jpg"
|
||||
/>
|
||||
<i v-else class="el-icon-plus avatar-uploader-icon" style="line-height: 120px"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Journal Topic :" prop="ResearchAreas">
|
||||
<el-button @click="addArea" size="mini" type="primary" plain style="position: absolute; right: 0; margin-left: 10px"
|
||||
>+ Add</el-button
|
||||
@@ -283,8 +315,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Wechat Name :" prop="wechat_name">
|
||||
<el-input v-model="detailForm.wechat_name" placeholder="eg:公众号名称"></el-input>
|
||||
<el-form-item label="Wechat Name :" prop="wechat_name" class="wechat-name-split">
|
||||
<div class="wechat-name-split__inner">
|
||||
<div class="wechat-name-split__left">
|
||||
<el-input v-model="detailForm.wechat_name" placeholder="eg:公众号名称"></el-input>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Wechat APP ID :" prop="wechat_app_id">
|
||||
<el-input v-model="detailForm.wechat_app_id" placeholder="eg:公众号app_id"></el-input>
|
||||
@@ -657,6 +694,17 @@ export default {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
},
|
||||
async handleAvatarSuccessYboard(res, file) {
|
||||
if (res.code == 0) {
|
||||
this.detailForm.wechat_yboard_qrcode = res.upurl;
|
||||
this.$forceUpdate();
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
},
|
||||
handleAvatarErrorYboard(res, file) {
|
||||
// no-op
|
||||
},
|
||||
handleAvatarError2(res, file) {
|
||||
// this.$message.error(res);
|
||||
},
|
||||
@@ -883,7 +931,9 @@ export default {
|
||||
epassword: data.epassword,
|
||||
kfen: data.kfen,
|
||||
fee: data.fee,
|
||||
databases: data.databases || data.database_inclusion || '',
|
||||
editor_qrcode: data.editor_qrcode,
|
||||
wechat_yboard_qrcode: data.wechat_yboard_qrcode || data.yboard_qrcode,
|
||||
scope: data.scope,
|
||||
abstract_chinese: data.abstract_chinese,
|
||||
publish_author: data.publish_author,
|
||||
@@ -1369,6 +1419,29 @@ export default {
|
||||
border-radius: 110px;
|
||||
}
|
||||
|
||||
/* Wechat Name + Apply Youth Board QR:两列对齐到右侧 */
|
||||
.wechat-name-split__inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.wechat-name-split__left {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.wechat-name-split__right {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.yboard-qrcode-side__label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.portrait .ptmark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -215,6 +215,8 @@
|
||||
localStorage.setItem('U_role', 'superadmin');
|
||||
localStorage.setItem('U_name', res.userinfo.account);
|
||||
localStorage.setItem('U_id', res.userinfo.user_id);
|
||||
localStorage.setItem('U_email', '');
|
||||
|
||||
|
||||
this.$router.push('/');
|
||||
} else if (res.data.roles.includes('editor')) {
|
||||
@@ -222,6 +224,7 @@
|
||||
localStorage.setItem('U_role', res.data.roles);
|
||||
localStorage.setItem('U_name', res.data.userinfo.account);
|
||||
localStorage.setItem('U_id', res.data.userinfo.user_id);
|
||||
localStorage.setItem('U_email', res.data.userinfo.email);
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
localStorage.setItem('U_status', '2'); //其余的身份
|
||||
@@ -229,6 +232,7 @@
|
||||
localStorage.setItem('U_name', res.data.userinfo.account);
|
||||
localStorage.setItem('U_id', res.data.userinfo.user_id);
|
||||
localStorage.setItem('U_relname', res.data.userinfo.realname);
|
||||
localStorage.setItem('U_email', res.data.userinfo.email);
|
||||
this.$router.push('/');
|
||||
// this.roleVisible = true;
|
||||
// this.user_cap = res.data.roles;
|
||||
|
||||
238
src/components/page/YouthBoardSubmitSuccess.vue
Normal file
238
src/components/page/YouthBoardSubmitSuccess.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="submission-page">
|
||||
<div class="container">
|
||||
<div class="success-icon">✔</div>
|
||||
<h2 class="success-title">Application submitted successfully!</h2>
|
||||
<p class="success-desc" v-if="isFromChina">
|
||||
We have a WeChat group for academic discussions. Please feel free to scan the QR code below to join.
|
||||
</p>
|
||||
<p class="success-desc" v-else>
|
||||
Your application is currently under review. We appreciate your patience, and our team will notify you of the final decision via email as soon as possible.
|
||||
</p>
|
||||
|
||||
<div v-if="isFromChina" class="qr-section">
|
||||
<div v-if="qrLoading" class="qr-loading">Loading...</div>
|
||||
<div v-else class="qr-code-box" v-if="qrCodeUrl">
|
||||
<img :src="qrCodeUrl" alt="WeChat Group QR" />
|
||||
</div>
|
||||
<!-- <p class="remark-tip">Please use the following format for group remark:</p>
|
||||
<div class="remark-box">Name - Research Field - Affiliation</div> -->
|
||||
</div>
|
||||
|
||||
<router-link to="/login" replace class="back-btn"> Login to the Submission System Now </router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'YouthBoardSubmitSuccess',
|
||||
data() {
|
||||
return {
|
||||
qrCodeUrl: '',
|
||||
qrLoading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFromChina() {
|
||||
const s = String(this.$route.query.country || '').trim();
|
||||
if (!s) return false;
|
||||
// 兼容:1 / China / 中国 / CN / CHN
|
||||
if (s === '1') return true;
|
||||
if (s === 'China' || s === 'CN' || s === 'CHN') return true;
|
||||
if (s === '中国') return true;
|
||||
if (s === 'US' || s === 'United States' || s === '美国') return false;
|
||||
return s === 'China';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.isFromChina) this.loadApplyBaseInfo();
|
||||
},
|
||||
methods: {
|
||||
normalizeCountry(v) {
|
||||
const s = String(v || '').trim();
|
||||
if (!s) return '';
|
||||
if (s === '中国' || s === 'CN' || s === 'CHN') return 'China';
|
||||
if (s === '美国' || s === 'USA' || s === 'US') return 'United States';
|
||||
if (s === '英国' || s === 'UK' || s === 'GBR') return 'United Kingdom';
|
||||
return s;
|
||||
},
|
||||
buildMediaUrl(raw) {
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) return raw;
|
||||
const mediaBase = (this.Common && this.Common.mediaUrl ? this.Common.mediaUrl : '').replace(/\/+$/, '');
|
||||
if (!mediaBase) return raw;
|
||||
const cleanPath = String(raw).replace(/^\/+/, '');
|
||||
// 与后台上传目录保持一致(JournalManagement/common.vue)
|
||||
return `${mediaBase}/journalyboardqrcode/${cleanPath}`;
|
||||
},
|
||||
loadApplyBaseInfo() {
|
||||
const journal_id = this.$route.query.journal_id || this.$route.query.journalId;
|
||||
const expert_id = this.$route.query.expert_id || this.$route.query.expertId;
|
||||
if (!journal_id || !expert_id) {
|
||||
return;
|
||||
}
|
||||
this.qrLoading = true;
|
||||
|
||||
this.$api
|
||||
.post('api/Ucenter/getApplyYboardForExpertBaseInfo', {
|
||||
journal_id,
|
||||
expert_id
|
||||
})
|
||||
.then((res) => {
|
||||
const data = (res && res.data) || {};
|
||||
const journal = data.journal || data.journal_info || data.journalInfo || null;
|
||||
const raw =
|
||||
(journal && (journal.wechat_yboard_qrcode || journal.yboard_qrcode || journal.qrcode_url)) || '';
|
||||
this.qrCodeUrl = this.buildMediaUrl(raw);
|
||||
})
|
||||
.catch(() => {
|
||||
this.qrCodeUrl = '';
|
||||
})
|
||||
.finally(() => {
|
||||
this.qrLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.submission-page {
|
||||
--primary-blue: #3a91d9;
|
||||
--text-main: #2c3e50;
|
||||
--bg-light: #f7f9fc;
|
||||
--success-green: #48bb78;
|
||||
--danger: #e53e3e;
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: var(--bg-light);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
height: fit-content;
|
||||
padding: 60px 40px;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.04);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 60px;
|
||||
color: var(--success-green);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 24px;
|
||||
color: var(--text-main);
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.success-desc {
|
||||
font-size: 16px;
|
||||
color: #718096;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
background-color: #f0f7ff;
|
||||
border: 1px solid #c3dafe;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.qr-header {
|
||||
color: #2b6cb0;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.qr-code-box {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #fff;
|
||||
margin: 15px auto;
|
||||
border: 1px solid #eee;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qr-code-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.remark-tip {
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.remark-box {
|
||||
background: #fff;
|
||||
border: 1px solid #feb2b2;
|
||||
color: var(--danger);
|
||||
font-weight: bold;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: inline-block;
|
||||
margin-top: 40px;
|
||||
color: #006699;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #fff;
|
||||
margin: 15px auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
612
src/components/page/YouthEditorialBoardRegistration.vue
Normal file
612
src/components/page/YouthEditorialBoardRegistration.vue
Normal file
@@ -0,0 +1,612 @@
|
||||
<template>
|
||||
<div class="registration-container">
|
||||
<div class="card">
|
||||
<h1>Youth Editorial Board Registration</h1>
|
||||
<div v-if="journalInfoLoaded" class="journal-hero">
|
||||
<div class="journal-cover-wrap" v-if="journalCoverUrl">
|
||||
<img :src="journalCoverUrl" :alt="journalTitle || 'Journal Cover'" class="journal-cover" />
|
||||
</div>
|
||||
<div class="journal-title">{{ journalTitle || 'Journal Information' }}</div>
|
||||
</div>
|
||||
<!-- <p v-if="journalId && expertId" class="link-meta">
|
||||
Journal ID: <strong>{{ journalId }}</strong> · Expert ID: <strong>{{ expertId }}</strong>
|
||||
</p> -->
|
||||
<!-- <p v-else class="link-meta link-meta--warn">
|
||||
Missing <code>journal_id</code> and/or <code>expert_id</code> in the URL. Please use the full invitation link.
|
||||
</p> -->
|
||||
|
||||
<div v-if="submitSuccess" class="success-panel">
|
||||
<h2>Thank you</h2>
|
||||
<p>Your registration has been submitted. We will contact you by email.</p>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="handleSubmit" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label><span class="required-star">*</span> English Name</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.engName"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><span class="required-star">*</span> Email (QQ mail is not allowed)</label>
|
||||
<input type="email" v-model="formData.email" placeholder="" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><span class="required-star">*</span> Password</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.password"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><span class="required-star">*</span> Upload CV</label>
|
||||
<div v-if="!selectedFile" class="upload-trigger" @click="$refs.fileInput.click()">
|
||||
<div class="icon">📤</div>
|
||||
<div class="hint">Click to upload PDF CV</div>
|
||||
<input type="file" ref="fileInput" hidden accept=".pdf" @change="onFileChange" />
|
||||
</div>
|
||||
<div v-else class="file-display-box">
|
||||
<span class="file-icon">📄</span>
|
||||
<span class="file-name">{{ selectedFile.name }}</span>
|
||||
<span class="remove-btn" @click.prevent="removeFile">×</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <transition name="fade">
|
||||
<div v-if="formData.country === 'China'" class="qr-area">
|
||||
<div class="qr-title">Mandatory for Youth Scientists in China</div>
|
||||
<div v-if="wechatQrUrl" class="qr-box">
|
||||
<img :src="wechatQrUrl" alt="Group QR Code" @error="onQrImgError" />
|
||||
</div>
|
||||
<p v-else class="qr-fallback">Please add image file <code>public/youth-board-wechat-qr.png</code> for the group QR code.</p>
|
||||
<p class="qr-hint">Please use the following format for the group remark:</p>
|
||||
<div class="format-tag">Name - Research Field - Affiliation</div>
|
||||
</div>
|
||||
</transition> -->
|
||||
|
||||
<button type="submit" class="submit-btn" :disabled="submitting">
|
||||
{{ submitting ? 'Submitting...' : 'Register Now' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
/** 公开邀请注册:后端实现 multipart 接口后填写此路径(与 Dashboard 青年编委申请字段对齐可再改) */
|
||||
const SUBMIT_URL = '/api/User/youthBoardInviteRegister';
|
||||
|
||||
export default {
|
||||
name: 'YouthEditorialBoardRegistration',
|
||||
data() {
|
||||
return {
|
||||
baseUrl: this.Common.baseUrl,
|
||||
formData: {
|
||||
engName: '',
|
||||
email: '',
|
||||
country: '',
|
||||
password: ''
|
||||
},
|
||||
countryList: [],
|
||||
selectedFile: null,
|
||||
submitting: false,
|
||||
submitSuccess: false,
|
||||
wechatQrUrl: '',
|
||||
journalInfoLoaded: false,
|
||||
journalDetail: null,
|
||||
uploadedCvPath: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
journalId() {
|
||||
const q = this.$route.query || {};
|
||||
return String(q.journal_id || q.journalId || '').trim();
|
||||
},
|
||||
expertId() {
|
||||
const q = this.$route.query || {};
|
||||
return String(q.expert_id || q.expertId || '').trim();
|
||||
},
|
||||
journalTitle() {
|
||||
const j = this.journalDetail || {};
|
||||
return j.full_name || j.journal_name || j.title || j.abbr || '';
|
||||
},
|
||||
journalCoverUrl() {
|
||||
const j = this.journalDetail || {};
|
||||
const raw =
|
||||
j.journal_icon ||
|
||||
j.cover ||
|
||||
j.img ||
|
||||
j.image ||
|
||||
j.icon ||
|
||||
j.logo ||
|
||||
j.thumb ||
|
||||
j.picture ||
|
||||
j.photo ||
|
||||
j.journal_cover ||
|
||||
j.journal_img ||
|
||||
'';
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) return raw;
|
||||
const mediaBase = (this.Common.mediaUrl || '').replace(/\/+$/, '');
|
||||
const cleanPath = String(raw).replace(/^\/+/, '');
|
||||
if (!mediaBase) return `/${cleanPath}`;
|
||||
if (/^journal\//i.test(cleanPath) || /^reviewer\//i.test(cleanPath)) {
|
||||
return `${mediaBase}/${cleanPath}`;
|
||||
}
|
||||
return `${mediaBase}/journal/${cleanPath}`;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const base = process.env.BASE_URL || '/';
|
||||
const prefix = base.endsWith('/') ? base : `${base}/`;
|
||||
this.wechatQrUrl = `${prefix}youth-board-wechat-qr.png`;
|
||||
this.fetchCountries();
|
||||
this.fetchApplyBaseInfo();
|
||||
},
|
||||
methods: {
|
||||
countryOptionKey(item) {
|
||||
return item.name || item.id || item.country || JSON.stringify(item);
|
||||
},
|
||||
countryOptionValue(item) {
|
||||
return item.name != null ? item.name : item.country || item.title || '';
|
||||
},
|
||||
countryOptionLabel(item) {
|
||||
return item.name != null ? item.name : item.country || item.title || '';
|
||||
},
|
||||
fetchCountries() {
|
||||
// this.$api
|
||||
// .post('api/Reviewer/getCountrys')
|
||||
// .then((res) => {
|
||||
// const list = res.countrys || res.data || [];
|
||||
// this.countryList = Array.isArray(list) ? list : [];
|
||||
// })
|
||||
// .catch(() => {
|
||||
// this.countryList = [{ name: 'China' }, { name: 'United States' }, { name: 'United Kingdom' }];
|
||||
// });
|
||||
},
|
||||
fetchApplyBaseInfo() {
|
||||
if (!this.journalId || !this.expertId) return;
|
||||
this.$api
|
||||
.post('api/Ucenter/getApplyYboardForExpertBaseInfo', {
|
||||
journal_id: this.journalId,
|
||||
expert_id: this.expertId
|
||||
})
|
||||
.then((res) => {
|
||||
const data = (res && res.data) || {};
|
||||
const expertCandidate =
|
||||
data.expert_info ||{}
|
||||
|
||||
const journal =
|
||||
data.journal ||
|
||||
data.journal_info ||
|
||||
data.journalInfo ||
|
||||
data.journal_detail ||
|
||||
data.journalDetail ||
|
||||
null;
|
||||
|
||||
// 为了兼容后端返回字段名,这里做多字段兜底取值
|
||||
const pickFirst = (obj, keys) => {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const k = keys[i];
|
||||
if (obj && obj[k] !== undefined && obj[k] !== null && String(obj[k]).trim() !== '') {
|
||||
return obj[k];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const pickFromCandidateOrData = (keys) => {
|
||||
const v1 = pickFirst(expertCandidate, keys);
|
||||
if (v1) return v1;
|
||||
return pickFirst(data, keys);
|
||||
};
|
||||
|
||||
const normalizeCountry = (v) => {
|
||||
const s = String(v || '').trim();
|
||||
if (!s) return '';
|
||||
// 常见中文/英文归一化(确保和下拉 options.value 匹配)
|
||||
if (s === '中国' || s === 'CN' || s === 'CHN') return 'China';
|
||||
if (s === '美国' || s === 'USA' || s === 'US') return 'United States';
|
||||
if (s === '英国' || s === 'UK' || s === 'GBR') return 'United Kingdom';
|
||||
return s;
|
||||
};
|
||||
|
||||
this.formData.engName = pickFromCandidateOrData([
|
||||
'eng_name',
|
||||
'english_name',
|
||||
'u_eng_name',
|
||||
'U_eng_name',
|
||||
'U_engName',
|
||||
'U_name_en',
|
||||
'realname',
|
||||
'u_relname',
|
||||
'U_relname',
|
||||
'name',
|
||||
'U_name'
|
||||
]);
|
||||
|
||||
this.formData.email = pickFromCandidateOrData([
|
||||
'email',
|
||||
'u_email',
|
||||
'U_email',
|
||||
'U_email_address',
|
||||
'user_email',
|
||||
'username',
|
||||
'U_username'
|
||||
]);
|
||||
|
||||
this.formData.country = normalizeCountry(
|
||||
pickFromCandidateOrData([
|
||||
'country',
|
||||
'country_name',
|
||||
'u_country',
|
||||
'U_country',
|
||||
'countryName',
|
||||
'U_country_name'
|
||||
])
|
||||
);
|
||||
|
||||
if (journal) {
|
||||
this.journalDetail = journal;
|
||||
this.journalInfoLoaded = true;
|
||||
}
|
||||
if (!journal) {
|
||||
this.journalDetail = null;
|
||||
this.journalInfoLoaded = true;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.journalDetail = null;
|
||||
this.journalInfoLoaded = true;
|
||||
});
|
||||
},
|
||||
async onFileChange(e) {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (file && file.type === 'application/pdf') {
|
||||
this.selectedFile = file;
|
||||
const fd = new FormData();
|
||||
fd.append('reviewerCV', file);
|
||||
try {
|
||||
const resp = await axios.post(`${this.baseUrl}api/Ucenter/up_cv_file`, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
const body = resp && resp.data ? resp.data : {};
|
||||
if (Number(body.code) === 0 && body.upurl) {
|
||||
this.uploadedCvPath = body.upurl;
|
||||
} else {
|
||||
this.selectedFile = null;
|
||||
this.uploadedCvPath = '';
|
||||
this.alertError((body && body.msg) || 'CV upload failed.');
|
||||
}
|
||||
} catch (err) {
|
||||
this.selectedFile = null;
|
||||
this.uploadedCvPath = '';
|
||||
this.alertError('CV upload failed. Please try again.');
|
||||
}
|
||||
} else if (file) {
|
||||
this.alertError('Please upload a valid PDF file.');
|
||||
}
|
||||
},
|
||||
removeFile() {
|
||||
this.selectedFile = null;
|
||||
this.uploadedCvPath = '';
|
||||
if (this.$refs.fileInput) this.$refs.fileInput.value = '';
|
||||
},
|
||||
onQrImgError() {
|
||||
this.wechatQrUrl = '';
|
||||
},
|
||||
alertError(msg) {
|
||||
this.$message.error(msg);
|
||||
},
|
||||
async handleSubmit() {
|
||||
if (!this.journalId || !this.expertId) {
|
||||
this.alertError('Invalid link: journal_id and expert_id are required in the URL.');
|
||||
return;
|
||||
}
|
||||
|
||||
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')) {
|
||||
this.alertError('Registration failed: QQ email addresses are not accepted.');
|
||||
return;
|
||||
}
|
||||
if (!this.formData.password) {
|
||||
this.alertError('Please enter your password.');
|
||||
return;
|
||||
}
|
||||
if (!this.selectedFile) {
|
||||
this.alertError('Please upload your CV.');
|
||||
return;
|
||||
}
|
||||
if (!this.uploadedCvPath) {
|
||||
this.alertError('CV upload is not complete. Please upload again.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
this.$api
|
||||
.post('api/Ucenter/submitApplyYboardForExpert', {
|
||||
journal_id: this.journalId,
|
||||
expert_id: this.expertId,
|
||||
name: engName,
|
||||
email: emailRaw,
|
||||
cv: this.uploadedCvPath,
|
||||
password: this.formData.password
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.code == 0) {
|
||||
this.$router.replace({
|
||||
path: '/youthBoardSubmitSuccess',
|
||||
query: {
|
||||
country: this.formData.country == 'China' ? '1' : '0',
|
||||
journal_id: this.journalId,
|
||||
expert_id: this.expertId
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.alertError((res && res.msg) || 'Submission failed.');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.alertError((err && err.msg) || 'Submission failed.');
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.registration-container {
|
||||
--primary: #3a91d9;
|
||||
--text: #2c3e50;
|
||||
--border: #e2e8f0;
|
||||
--danger: #e53e3e;
|
||||
--bg: #f7f9fc;
|
||||
|
||||
min-height: 100vh;
|
||||
background-color: var(--bg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
padding: 40px;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.journal-hero {
|
||||
text-align: center;
|
||||
margin: 6px 0 18px;
|
||||
}
|
||||
|
||||
.journal-cover-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 112px;
|
||||
height: 150px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.journal-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.journal-title {
|
||||
margin-top: 10px;
|
||||
color: #1d4f8c;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: var(--text);
|
||||
font-size: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.link-meta {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.link-meta--warn {
|
||||
color: #c05621;
|
||||
}
|
||||
|
||||
.success-panel {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.success-panel h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.required-star {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
border: 1px dashed #cbd5e0;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
}
|
||||
.upload-trigger:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.upload-trigger .icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
.upload-trigger .hint {
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.file-display-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.remove-btn {
|
||||
color: var(--danger);
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.qr-area {
|
||||
background-color: #f0f7ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 16px;
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.qr-title {
|
||||
color: #2b6cb0;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.qr-box {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: #fff;
|
||||
margin: 0 auto 15px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.qr-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.qr-fallback {
|
||||
font-size: 13px;
|
||||
color: #718096;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.qr-hint {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.format-tag {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #feb2b2;
|
||||
border-radius: 8px;
|
||||
color: var(--danger);
|
||||
font-weight: bold;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #4da1e6 0%, #3588d1 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -810,9 +810,37 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="articleAddQqEmailSubmitBlocked"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="max-width: 720px; margin: 24px auto 0"
|
||||
>
|
||||
<template slot="title">
|
||||
<div class="qq-email-alert-body">
|
||||
<div class="qq-email-alert-line1">{{ $t('articleAdd.qqEmailAlertLine1') }}</div>
|
||||
<div class="qq-email-alert-line2">
|
||||
<span>{{ $t('articleAdd.qqEmailAlertLine2Before') }}</span>
|
||||
<router-link to="/dashboard" class="qq-email-dashboard-link">
|
||||
<i class="el-icon-user-solid"></i>
|
||||
{{ $t('articleAdd.qqEmailDashboardLink') }}
|
||||
</router-link>
|
||||
<span>{{ $t('articleAdd.qqEmailAlertLine2After') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<div style="text-align: center; margin: 40px 0 0 0">
|
||||
<el-button type="warning" @click="onStagingSave(4)" class="pro_stage">Save as draft </el-button>
|
||||
<el-button type="primary" @click="onSubmit(1)" class="pro_ceed">Confirm Submit </el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="onSubmit(1)"
|
||||
class="pro_ceed"
|
||||
:disabled="articleAddQqEmailSubmitBlocked"
|
||||
>
|
||||
Confirm Submit
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1612,6 +1640,17 @@ export default {
|
||||
},
|
||||
upload_qualifications: function () {
|
||||
return this.baseUrl + 'api/Admin/up_file';
|
||||
},
|
||||
/** 投稿页第四步:登录邮箱为 QQ 邮箱时不允许正式提交 */
|
||||
articleAddQqEmailSubmitBlocked() {
|
||||
try {
|
||||
const raw = localStorage.getItem('U_email') || '';
|
||||
const email = String(raw).trim().toLowerCase();
|
||||
if (!email || !email.includes('@')) return false;
|
||||
return /@qq\.com$/i.test(email) || /@vip\.qq\.com$/i.test(email);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -1907,6 +1946,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async onSubmit() {
|
||||
if (this.articleAddQqEmailSubmitBlocked) {
|
||||
this.$message.warning(this.$t('articleAdd.qqEmailSubmitBlockedMsg'));
|
||||
return false;
|
||||
}
|
||||
// 1. 校验协议勾选
|
||||
if (!this.agreechecked) {
|
||||
this.$message.error(
|
||||
@@ -4063,6 +4106,28 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.qq-email-alert-body {
|
||||
line-height: 1.55;
|
||||
}
|
||||
.qq-email-alert-line1 {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.qq-email-alert-line2 {
|
||||
display: block;
|
||||
}
|
||||
.qq-email-dashboard-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 0 4px;
|
||||
font-weight: 600;
|
||||
color: #cf9236;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.qq-email-dashboard-link:hover {
|
||||
color: #b8822f;
|
||||
}
|
||||
/deep/.apc_content a{
|
||||
color: rgb(81, 127, 213) !important; cursor: pointer !important; text-decoration: underline !important;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,20 +16,59 @@
|
||||
<div class="left">
|
||||
<span class="label">{{ $t('autoPromotion.journal') }} : </span>
|
||||
{{ currentJournalName }}
|
||||
<el-select
|
||||
v-if="config.initialized && selectedJournalId"
|
||||
v-model="headerPromotionFactoryId"
|
||||
class="header-factory-task-select"
|
||||
size="small"
|
||||
filterable
|
||||
:loading="factoryTasksHeaderLoading"
|
||||
:placeholder="$t('autoPromotionLogs.factoryTaskSelectPlaceholder')"
|
||||
@change="onHeaderFactoryTaskChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in factoryTaskOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tag
|
||||
v-if="config.initialized && selectedJournalId && headerFactoryTaskRunning !== null"
|
||||
:type="headerFactoryTaskRunning ? 'success' : 'info'"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="header-factory-status-tag"
|
||||
>
|
||||
<i v-if="headerFactoryTaskRunning" class="el-icon-circle-check"></i>
|
||||
<span v-else class="header-factory-status-dot"></span>
|
||||
{{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
|
||||
</el-tag>
|
||||
|
||||
<template v-if="config.initialized">
|
||||
<el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
|
||||
<!-- <template v-if="config.initialized"> -->
|
||||
<!-- <el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
|
||||
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.configured') }}
|
||||
</el-tag>
|
||||
<el-button type="text" size="small" style="margin-left: 10px" @click="openWizardDialog">
|
||||
</el-tag> -->
|
||||
<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>
|
||||
<!-- </template> -->
|
||||
|
||||
<el-tag v-else type="info" size="small" effect="plain" style="margin-left: 10px">
|
||||
<!-- <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>
|
||||
</el-tag> -->
|
||||
</div>
|
||||
<div v-if="config.initialized && selectedJournalId && headerPromotionFactoryId" class="right">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="el-icon-plus"
|
||||
:loading="createTaskLoading"
|
||||
@click="handleCreateEmailClientTask"
|
||||
>
|
||||
{{ $t('autoPromotion.emailClientCreateTaskBtn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -41,7 +80,9 @@
|
||||
:config="config"
|
||||
:wizardStartDate.sync="wizardStartDate"
|
||||
:selectedFieldIds.sync="selectedFieldIds"
|
||||
:selectedCountryIds.sync="selectedCountryIds"
|
||||
:availableFields="availableFields"
|
||||
:availableCountries="availableCountries"
|
||||
:fieldsLoading="fieldsLoading"
|
||||
:fieldsSaving="fieldsSaving"
|
||||
:currentJournalName="currentJournalName"
|
||||
@@ -52,6 +93,7 @@
|
||||
:title="$t('autoPromotion.title')"
|
||||
@open-template-selector="showTemplateDialog = true"
|
||||
@confirm-fields="savePromotionFieldsNow"
|
||||
@confirm-countries="savePromotionCountriesNow"
|
||||
@confirm="completeInitialization"
|
||||
/>
|
||||
</el-card>
|
||||
@@ -61,22 +103,24 @@
|
||||
|
||||
|
||||
<div class="filter-header-row">
|
||||
<div class="tmr-capsule-group">
|
||||
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.statusAll')" name="all"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state0')" name="0"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state5')" name="5"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state1')" name="1"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state3')" name="3"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state2')" name="2"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state4')" name="4"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="tmr-capsule-group">
|
||||
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.statusAll')" name="all"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state0')" name="0"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state5')" name="5"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state1')" name="1"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state3')" name="3"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state2')" name="2"></el-tab-pane>
|
||||
<el-tab-pane :label="$t('autoPromotionLogs.state4')" name="4"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- <div class="filter-actions">
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleSearch">{{ $t('autoPromotionLogs.searchBtn') }}</el-button>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<el-button type="primary" plain icon="el-icon-refresh" :loading="loading" @click="handleSearch">
|
||||
{{ $t('autoPromotionLogs.logRefresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" border stripe size="small" class="custom-table exquisite-log-table">
|
||||
<el-table-column
|
||||
@@ -210,7 +254,9 @@
|
||||
:config="config"
|
||||
:wizardStartDate.sync="wizardStartDate"
|
||||
:selectedFieldIds.sync="selectedFieldIds"
|
||||
:selectedCountryIds.sync="selectedCountryIds"
|
||||
:availableFields="availableFields"
|
||||
:availableCountries="availableCountries"
|
||||
:fieldsLoading="fieldsLoading"
|
||||
:fieldsSaving="fieldsSaving"
|
||||
:currentJournalName="currentJournalName"
|
||||
@@ -221,6 +267,7 @@
|
||||
:title="$t('autoPromotion.title')"
|
||||
@open-template-selector="showTemplateDialog = true"
|
||||
@confirm-fields="savePromotionFieldsNow"
|
||||
@confirm-countries="savePromotionCountriesNow"
|
||||
@cancel="showWizardDialog = false"
|
||||
@confirm="completeInitialization"
|
||||
/>
|
||||
@@ -236,6 +283,16 @@
|
||||
@confirm="handleTemplateApply"
|
||||
@close-all-dialogs="closeAllDialogs"
|
||||
/>
|
||||
<promotion-factory-task-dialog
|
||||
:visible.sync="showFactoryTaskDialog"
|
||||
:initial-journal-id="factoryDialogInitialJournalId"
|
||||
:initial-task="factoryDialogInitialTask"
|
||||
@success="
|
||||
fetchFactoryTasksForHeader();
|
||||
fetchList();
|
||||
fetchJournalDetail();
|
||||
"
|
||||
/>
|
||||
<el-dialog :title="$t('autoPromotionLogs.previewEditTitle')" :visible.sync="showPreviewDialog" width="1200px" top="5vh">
|
||||
<div class="mail-edit-wrapper" v-if="previewForm">
|
||||
<el-form label-width="120px" size="small">
|
||||
@@ -281,6 +338,7 @@
|
||||
import CkeditorMail from '@/components/page/components/email/CkeditorMail.vue';
|
||||
import TemplateSelectorDialog from '@/components/page/components/email/TemplateSelectorDialog.vue';
|
||||
import AutoPromotionWizard from '@/components/page/components/autoPromotion/AutoPromotionWizard.vue';
|
||||
import PromotionFactoryTaskDialog from '@/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue';
|
||||
import PromotionDetailDrawer from '@/components/page/components/autoPromotion/PromotionDetailDrawer.vue';
|
||||
// 这里假设你已经定义了 API 地址
|
||||
const API = {
|
||||
@@ -294,7 +352,7 @@ const API = {
|
||||
|
||||
export default {
|
||||
name: 'autoPromotion',
|
||||
components: { TemplateSelectorDialog, AutoPromotionWizard, CkeditorMail, PromotionDetailDrawer },
|
||||
components: { TemplateSelectorDialog, AutoPromotionWizard, PromotionFactoryTaskDialog, CkeditorMail, PromotionDetailDrawer },
|
||||
data() {
|
||||
return {
|
||||
handleRefreshList: [],
|
||||
@@ -323,7 +381,7 @@ export default {
|
||||
selectedStyleName: '',
|
||||
|
||||
// 列表数据
|
||||
query: { keyword: '', state: 'all', pageIndex: 1, pageSize: 15 },
|
||||
query: { keyword: '', state: 'all', pageIndex: 1, pageSize: 10 },
|
||||
list: [],
|
||||
total: 0,
|
||||
|
||||
@@ -339,9 +397,19 @@ export default {
|
||||
templateDialogInitialTemplateId: '',
|
||||
togglingTaskId: '',
|
||||
selectedFieldIds: [],
|
||||
selectedCountryIds: [],
|
||||
availableFields: [],
|
||||
availableCountries: [],
|
||||
fieldsLoading: false,
|
||||
fieldsSaving: false,
|
||||
showFactoryTaskDialog: false,
|
||||
factoryDialogInitialJournalId: '',
|
||||
factoryDialogInitialTask: null,
|
||||
routePromotionFactoryId: '',
|
||||
headerPromotionFactoryId: '',
|
||||
factoryTaskOptions: [],
|
||||
factoryTasksHeaderLoading: false,
|
||||
createTaskLoading: false,
|
||||
previewForm: {
|
||||
id: '',
|
||||
email: '',
|
||||
@@ -351,7 +419,28 @@ export default {
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
computed: {
|
||||
/** 当前选中工厂任务是否运行中(与下拉 options 中 running 一致) */
|
||||
headerFactoryTaskRunning() {
|
||||
const id = String(this.headerPromotionFactoryId || '').trim();
|
||||
if (!id || !this.factoryTaskOptions || !this.factoryTaskOptions.length) return null;
|
||||
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
|
||||
if (opt && typeof opt.running === 'boolean') return opt.running;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.query.promotion_factory_id'(val) {
|
||||
const s = String(val != null ? val : '').trim();
|
||||
if (s === String(this.headerPromotionFactoryId || '').trim()) return;
|
||||
this.headerPromotionFactoryId = s;
|
||||
this.routePromotionFactoryId = s;
|
||||
if (this.config.initialized && this.selectedJournalId) {
|
||||
this.query.pageIndex = 1;
|
||||
this.fetchList();
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initPage();
|
||||
},
|
||||
@@ -370,6 +459,7 @@ export default {
|
||||
closeAllDialogs() {
|
||||
// 点击“去新增模板”后:关闭当前页面所有可能的弹窗
|
||||
this.showWizardDialog = false;
|
||||
this.showFactoryTaskDialog = false;
|
||||
this.showTemplateDialog = false;
|
||||
this.showPreviewDialog = false;
|
||||
this.currentRow = null;
|
||||
@@ -463,6 +553,12 @@ export default {
|
||||
async initPage() {
|
||||
this.hidePage = false;
|
||||
var journal_id = (this.$route.query && this.$route.query.journal_id) || '';
|
||||
var pfid =
|
||||
(this.$route.query && this.$route.query.promotion_factory_id) ||
|
||||
(this.$route.query && this.$route.query.taskId) ||
|
||||
'';
|
||||
this.routePromotionFactoryId = String(pfid || '');
|
||||
this.headerPromotionFactoryId = this.routePromotionFactoryId;
|
||||
this.selectedJournalId = String(journal_id);
|
||||
this.loading = true;
|
||||
try {
|
||||
@@ -472,6 +568,7 @@ export default {
|
||||
}
|
||||
if (this.config.initialized) {
|
||||
await this.fetchTemplates();
|
||||
await this.fetchFactoryTasksForHeader();
|
||||
await this.fetchList();
|
||||
}
|
||||
} finally {
|
||||
@@ -540,6 +637,150 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/** 下拉项:场景类型文案(优先 scene,否则按 type 映射) */
|
||||
getFactoryHeaderTaskTypeLabel(task) {
|
||||
if (!task || typeof task !== 'object') return '';
|
||||
const scene = String(task.scene || task.scene_name || '').trim();
|
||||
if (scene) return scene;
|
||||
const type = String(task.type != null ? task.type : '').trim();
|
||||
if (type === '1') return this.$t('autoPromotion.factoryScenarioSolicit');
|
||||
if (type === '2') return this.$t('autoPromotion.factoryScenarioPromoteCitation');
|
||||
if (type === '3') return this.$t('autoPromotion.factoryScenarioGeneralThanks');
|
||||
if (type === '4') return this.$t('autoPromotion.autoSolicit');
|
||||
return String(task.task_name || task.name || '').trim();
|
||||
},
|
||||
formatFactoryHeaderTaskCreateTime(task) {
|
||||
if (!task || typeof task !== 'object') return '';
|
||||
const raw = task.ctime || task.create_time || task.created_at || task.time || '';
|
||||
if (raw == null || String(raw).trim() === '') return '';
|
||||
const n = Number(raw);
|
||||
let dt = null;
|
||||
if (!isNaN(n) && n > 0) {
|
||||
dt = new Date(n > 1e12 ? n : n * 1000);
|
||||
} else {
|
||||
dt = new Date(String(raw));
|
||||
}
|
||||
if (!dt || isNaN(dt.getTime())) return String(raw).trim();
|
||||
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())}`;
|
||||
},
|
||||
isFactoryHeaderTaskRunning(task) {
|
||||
if (!task || typeof task !== 'object') return false;
|
||||
if (task.start_promotion != null && String(task.start_promotion).trim() !== '') {
|
||||
return String(task.start_promotion) === '1';
|
||||
}
|
||||
if (task.state != null && String(task.state).trim() !== '') {
|
||||
return String(task.state) === '1';
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/** 下拉仅展示「类型 - 创建日期」,运行状态单独用 el-tag */
|
||||
buildFactoryHeaderOptionMainLabel(task, pidFallback) {
|
||||
const typePart = this.getFactoryHeaderTaskTypeLabel(task) || String(pidFallback || '').trim() || '—';
|
||||
const datePart = this.formatFactoryHeaderTaskCreateTime(task);
|
||||
return datePart ? `${typePart} - ${datePart}` : typePart;
|
||||
},
|
||||
replacePromotionFactoryIdInUrl(promotionFactoryId) {
|
||||
try {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('promotion_factory_id', String(promotionFactoryId));
|
||||
window.history.replaceState({}, document.title, url.toString());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
async fetchFactoryTasksForHeader() {
|
||||
if (!this.selectedJournalId || !this.config.initialized) {
|
||||
this.factoryTaskOptions = [];
|
||||
return;
|
||||
}
|
||||
this.factoryTasksHeaderLoading = true;
|
||||
try {
|
||||
const userId = localStorage.getItem('U_id') || '';
|
||||
const res = await this.$api.post('api/promotion_factory/getList', {
|
||||
journal_id: String(this.selectedJournalId),
|
||||
user_id: String(userId),
|
||||
state: '-1'
|
||||
});
|
||||
const payload = (res && res.data) || {};
|
||||
const list = this.findArray(payload) || this.findArray(res) || [];
|
||||
let opts = (Array.isArray(list) ? list : []).map((task, idx) => {
|
||||
const pid =
|
||||
task && task.promotion_factory_id != null
|
||||
? String(task.promotion_factory_id)
|
||||
: task && task.id != null
|
||||
? String(task.id)
|
||||
: '';
|
||||
if (!pid) return null;
|
||||
const label = this.buildFactoryHeaderOptionMainLabel(task, pid);
|
||||
const running = this.isFactoryHeaderTaskRunning(task);
|
||||
return { value: pid, label, running };
|
||||
}).filter(Boolean);
|
||||
|
||||
let cur = String(this.routePromotionFactoryId || this.headerPromotionFactoryId || '').trim();
|
||||
const ids = new Set(opts.map((o) => o.value));
|
||||
if (cur && !ids.has(cur)) {
|
||||
opts = [{ value: cur, label: cur, running: false }].concat(opts);
|
||||
}
|
||||
this.factoryTaskOptions = opts;
|
||||
|
||||
if (!cur && opts.length) {
|
||||
cur = opts[0].value;
|
||||
}
|
||||
if (cur) {
|
||||
this.headerPromotionFactoryId = cur;
|
||||
this.routePromotionFactoryId = cur;
|
||||
const routePid = String((this.$route.query && this.$route.query.promotion_factory_id) || '').trim();
|
||||
if (cur !== routePid) {
|
||||
this.replacePromotionFactoryIdInUrl(cur);
|
||||
}
|
||||
} else {
|
||||
this.headerPromotionFactoryId = '';
|
||||
}
|
||||
} catch (e) {
|
||||
this.factoryTaskOptions = [];
|
||||
} finally {
|
||||
this.factoryTasksHeaderLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
onHeaderFactoryTaskChange(id) {
|
||||
const next = String(id != null ? id : '').trim();
|
||||
if (!next) return;
|
||||
this.routePromotionFactoryId = next;
|
||||
this.headerPromotionFactoryId = next;
|
||||
this.query.pageIndex = 1;
|
||||
this.replacePromotionFactoryIdInUrl(next);
|
||||
this.fetchList();
|
||||
},
|
||||
async handleCreateEmailClientTask() {
|
||||
const pid = String(this.headerPromotionFactoryId || this.routePromotionFactoryId || '').trim();
|
||||
if (!pid) {
|
||||
this.$message.warning(this.$t('autoPromotion.emailClientCreateTaskNeedFactory'));
|
||||
return;
|
||||
}
|
||||
this.createTaskLoading = true;
|
||||
try {
|
||||
const res = await this.$api.post('api/email_client/createTask', { promotion_factory_id: pid });
|
||||
if (res && Number(res.code) === 0) {
|
||||
const taskId = String(res.task_id || (res.data && res.data.task_id) || '').trim();
|
||||
if (taskId) {
|
||||
// Fire-and-forget: prepare recipient list in background.
|
||||
this.$api.post('api/email_client/prepareTask', { task_id: taskId }).catch(() => {});
|
||||
}
|
||||
this.$message.success(this.$t('autoPromotion.emailClientCreateTaskPreparingHint'));
|
||||
this.query.pageIndex = 1;
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.$message.error((res && res.msg) || this.$t('autoPromotion.emailClientCreateTaskFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('autoPromotion.emailClientCreateTaskFailed'));
|
||||
} finally {
|
||||
this.createTaskLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 打开向导弹窗:用于“修改期刊自动推广配置”
|
||||
findArray(obj) {
|
||||
if (Array.isArray(obj)) return obj;
|
||||
@@ -552,10 +793,32 @@ export default {
|
||||
if (values.length && Array.isArray(values[0])) return values[0];
|
||||
return null;
|
||||
},
|
||||
parseCountryIdsFromPromotionPayload(selectedPayload) {
|
||||
if (!selectedPayload || typeof selectedPayload !== 'object') return [];
|
||||
const raw =
|
||||
selectedPayload.country_fetch_ids != null
|
||||
? selectedPayload.country_fetch_ids
|
||||
: selectedPayload.country_ids != null
|
||||
? selectedPayload.country_ids
|
||||
: '';
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
return raw.split(',').map((s) => s.trim()).filter(Boolean).map(String);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
journalPromotionFieldsPayload(journalId) {
|
||||
return {
|
||||
journal_id: String(journalId),
|
||||
fetch_ids: (this.selectedFieldIds || []).join(','),
|
||||
country_fetch_ids: (this.selectedCountryIds || []).join(',')
|
||||
};
|
||||
},
|
||||
async loadPromotionFields(journalId) {
|
||||
this.fieldsLoading = true;
|
||||
this.availableFields = [];
|
||||
this.availableCountries = [];
|
||||
this.selectedFieldIds = [];
|
||||
this.selectedCountryIds = [];
|
||||
try {
|
||||
const availableRes = await this.$api.post('api/email_client/getAvailableFields', { journal_id: String(journalId) });
|
||||
const availablePayload = (availableRes && availableRes.data) || availableRes || {};
|
||||
@@ -566,33 +829,22 @@ export default {
|
||||
const label = item.field || item.title || item.name || item.label || String(id);
|
||||
return { id: String(id), label };
|
||||
});
|
||||
this.availableCountries = this.availableFields.map((x) => ({ id: String(x.id), label: x.label }));
|
||||
} catch (e) {
|
||||
this.availableFields = [];
|
||||
this.availableCountries = [];
|
||||
}
|
||||
try {
|
||||
const selectedRes = await this.$api.post('api/email_client/getJournalPromotionFields', { journal_id: String(journalId) });
|
||||
const selectedPayload = (selectedRes && selectedRes.data) || selectedRes || {};
|
||||
let selectedArr = this.findArray(selectedPayload);
|
||||
if (selectedArr) {
|
||||
this.selectedFieldIds = selectedArr.map((it) => String(it.expert_fetch_id || it.fetch_id || it.id || it.field_id || it));
|
||||
} else if (typeof selectedPayload === 'string') {
|
||||
this.selectedFieldIds = selectedPayload.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
} else if (typeof selectedPayload.fetch_ids === 'string') {
|
||||
this.selectedFieldIds = selectedPayload.fetch_ids.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
} catch (e) {
|
||||
this.selectedFieldIds = [];
|
||||
}
|
||||
// 日志页不请求 getJournalPromotionFields(该接口在此场景不可用);已选字段/国家由向导内操作或他处回显
|
||||
this.fieldsLoading = false;
|
||||
},
|
||||
async savePromotionFieldsNow() {
|
||||
if (!this.selectedJournalId) return;
|
||||
this.fieldsSaving = true;
|
||||
try {
|
||||
await this.$api.post('api/email_client/setJournalPromotionFields', {
|
||||
journal_id: String(this.selectedJournalId),
|
||||
fetch_ids: (this.selectedFieldIds || []).join(',')
|
||||
});
|
||||
await this.$api.post(
|
||||
'api/email_client/setJournalPromotionFields',
|
||||
this.journalPromotionFieldsPayload(this.selectedJournalId)
|
||||
);
|
||||
this.$message.success(this.$t('autoPromotion.fieldsSaved'));
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('autoPromotion.saveFailed'));
|
||||
@@ -600,6 +852,21 @@ export default {
|
||||
this.fieldsSaving = false;
|
||||
}
|
||||
},
|
||||
async savePromotionCountriesNow() {
|
||||
if (!this.selectedJournalId) return;
|
||||
this.fieldsSaving = true;
|
||||
try {
|
||||
await this.$api.post(
|
||||
'api/email_client/setJournalPromotionFields',
|
||||
this.journalPromotionFieldsPayload(this.selectedJournalId)
|
||||
);
|
||||
this.$message.success(this.$t('autoPromotion.countriesSaved'));
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('autoPromotion.saveFailed'));
|
||||
} finally {
|
||||
this.fieldsSaving = false;
|
||||
}
|
||||
},
|
||||
async openWizardDialog() {
|
||||
this.wizardStep = 0;
|
||||
if (this.config && this.config.start_date) {
|
||||
@@ -610,6 +877,41 @@ export default {
|
||||
}
|
||||
this.showWizardDialog = true;
|
||||
},
|
||||
openFactoryTaskDialogFromLogs() {
|
||||
this.factoryDialogInitialJournalId = this.selectedJournalId ? String(this.selectedJournalId) : '';
|
||||
const routePid = String(this.routePromotionFactoryId || '').trim();
|
||||
const first = this.list && this.list.length ? this.list[0] : null;
|
||||
const matched = routePid
|
||||
? (this.list || []).find((row) => {
|
||||
const pid = row && row.promotion_factory_id != null ? String(row.promotion_factory_id) : '';
|
||||
const rid = row && row.id != null ? String(row.id) : '';
|
||||
const tid = row && row.task_id != null ? String(row.task_id) : '';
|
||||
return pid === routePid || rid === routePid || tid === routePid;
|
||||
}) || first
|
||||
: first;
|
||||
|
||||
let task = null;
|
||||
if (matched) {
|
||||
task = { ...matched };
|
||||
if (!routePid) {
|
||||
task.promotion_factory_id =
|
||||
matched.promotion_factory_id != null
|
||||
? String(matched.promotion_factory_id)
|
||||
: matched.id != null
|
||||
? String(matched.id)
|
||||
: matched.task_id != null
|
||||
? String(matched.task_id)
|
||||
: '';
|
||||
}
|
||||
}
|
||||
// promotion_factory/getDetail 必须使用地址栏 promotion_factory_id,避免列表首行 id 与路由不一致
|
||||
if (routePid) {
|
||||
task = { ...(task || {}), promotion_factory_id: routePid };
|
||||
}
|
||||
|
||||
this.factoryDialogInitialTask = task && Object.keys(task).length ? task : null;
|
||||
this.showFactoryTaskDialog = true;
|
||||
},
|
||||
|
||||
// 切换期刊逻辑
|
||||
async handleJournalChange() {
|
||||
@@ -702,13 +1004,14 @@ export default {
|
||||
};
|
||||
this.config.initialized = true;
|
||||
this.showWizardDialog = false;
|
||||
this.fetchList();
|
||||
await this.$api.post(API.saveConfig, payload);
|
||||
await this.$api.post('api/email_client/setJournalPromotionFields', {
|
||||
journal_id: String(this.selectedJournalId || ''),
|
||||
fetch_ids: (this.selectedFieldIds || []).join(',')
|
||||
});
|
||||
await this.$api.post(
|
||||
'api/email_client/setJournalPromotionFields',
|
||||
this.journalPromotionFieldsPayload(this.selectedJournalId || '')
|
||||
);
|
||||
this.$message.success(this.$t('autoPromotionLogs.configUpdated'));
|
||||
await this.fetchFactoryTasksForHeader();
|
||||
await this.fetchList();
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
@@ -730,8 +1033,9 @@ export default {
|
||||
try {
|
||||
const params = {
|
||||
journal_id: String(this.selectedJournalId || ''),
|
||||
factory_id: String(this.routePromotionFactoryId || ''),
|
||||
page: Number(this.query.pageIndex || 1),
|
||||
per_page: Number(this.query.pageSize || 15)
|
||||
per_page: Number(this.query.pageSize || 10)
|
||||
};
|
||||
if (this.query.state !== 'all' && this.query.state !== '' && this.query.state != null) {
|
||||
params.state = String(this.query.state);
|
||||
@@ -743,8 +1047,15 @@ export default {
|
||||
this.list = rawList.map((item, idx) => {
|
||||
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 promotionFactoryId =
|
||||
item.promotion_factory_id != null
|
||||
? item.promotion_factory_id
|
||||
: item.id != null
|
||||
? item.id
|
||||
: item.task_id;
|
||||
return {
|
||||
id: item.id || item.task_id || `task_${idx + 1}`,
|
||||
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
|
||||
task_id: String(item.task_id != null ? item.task_id : item.id || ''),
|
||||
task_name: item.task_name || item.name || '',
|
||||
scene: item.scene || '',
|
||||
@@ -922,6 +1233,36 @@ export default {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.config-bar .left {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 10px;
|
||||
}
|
||||
.config-bar .right {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
.header-factory-task-select {
|
||||
width: min(380px, 46vw);
|
||||
min-width: 200px;
|
||||
}
|
||||
.header-factory-status-tag {
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.header-factory-status-tag .el-icon-circle-check {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.header-factory-status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #909399;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 向导样式 */
|
||||
.wizard-card {
|
||||
@@ -1504,6 +1845,10 @@ export default {
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
/* 基础 Badge 样式 */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -198,8 +198,8 @@
|
||||
<el-form-item label="Account :" prop="account">
|
||||
<span>{{coreForm.account}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="Email :">
|
||||
<span>{{coreForm.email}}</span>
|
||||
<el-form-item label="Email :" prop="email">
|
||||
<el-input type="text" placeholder="Please enter email..." v-model="coreForm.email" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="head portrait :">
|
||||
<el-upload class="avatar-uploader" :action="baseUrl+'master/Journal/up_topic_file'"
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
:selectedTemplateName="selectedTemplateName"
|
||||
:selectedStyleName="selectedStyleName"
|
||||
:availableFields="availableFields"
|
||||
:availableCountries="availableCountries"
|
||||
:fieldsLoading="fieldsLoading"
|
||||
:fieldsSaving="fieldsSaving"
|
||||
:selectedFieldIds.sync="selectedFieldIdsProxy"
|
||||
:selectedCountryIds.sync="selectedCountryIdsProxy"
|
||||
@open-template-selector="emitOpenTemplateSelector"
|
||||
@confirm-fields="emitConfirmFields"
|
||||
@confirm-countries="emitConfirmCountries"
|
||||
@update:wizardStartDate="onWizardStartDateUpdate"
|
||||
/>
|
||||
|
||||
@@ -49,11 +52,14 @@
|
||||
:selectedTemplateName="selectedTemplateName"
|
||||
:selectedStyleName="selectedStyleName"
|
||||
:availableFields="availableFields"
|
||||
:availableCountries="availableCountries"
|
||||
:fieldsLoading="fieldsLoading"
|
||||
:fieldsSaving="fieldsSaving"
|
||||
:selectedFieldIds.sync="selectedFieldIdsProxy"
|
||||
:selectedCountryIds.sync="selectedCountryIdsProxy"
|
||||
@open-template-selector="emitOpenTemplateSelector"
|
||||
@confirm-fields="emitConfirmFields"
|
||||
@confirm-countries="emitConfirmCountries"
|
||||
@update:wizardStartDate="onWizardStartDateUpdate"
|
||||
/>
|
||||
<div class="dialog-footer">
|
||||
@@ -89,9 +95,11 @@ export default {
|
||||
selectedStyleName: { type: String, default: '' },
|
||||
saving: { type: Boolean, default: false },
|
||||
availableFields: { type: Array, default: () => [] },
|
||||
availableCountries: { type: Array, default: () => [] },
|
||||
fieldsLoading: { type: Boolean, default: false },
|
||||
fieldsSaving: { type: Boolean, default: false },
|
||||
selectedFieldIds: { type: Array, default: () => [] }
|
||||
selectedFieldIds: { type: Array, default: () => [] },
|
||||
selectedCountryIds: { type: Array, default: () => [] }
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
@@ -118,6 +126,14 @@ export default {
|
||||
this.$emit('update:selectedFieldIds', val);
|
||||
}
|
||||
},
|
||||
selectedCountryIdsProxy: {
|
||||
get() {
|
||||
return this.selectedCountryIds;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:selectedCountryIds', val);
|
||||
}
|
||||
},
|
||||
canConfirm() {
|
||||
const id = this.config && this.config.defaultTemplateId != null ? String(this.config.defaultTemplateId) : '';
|
||||
return id !== '' && id !== '0';
|
||||
@@ -130,6 +146,9 @@ export default {
|
||||
emitConfirmFields() {
|
||||
this.$emit('confirm-fields');
|
||||
},
|
||||
emitConfirmCountries() {
|
||||
this.$emit('confirm-countries');
|
||||
},
|
||||
onWizardStartDateUpdate(val) {
|
||||
// 由内容组件回传日期,继续走父组件的 .sync 链路
|
||||
this.wizardStartDateProxy = val;
|
||||
|
||||
@@ -103,7 +103,44 @@
|
||||
|
||||
<section class="form-section">
|
||||
<h4 class="section-title">
|
||||
<i class="el-icon-finished"></i> 3. {{ $t('autoPromotion.confirmAndEnable') }}
|
||||
<i class="el-icon-location-outline"></i> 3. {{ $t('autoPromotion.selectPromotionCountry') }}
|
||||
<!-- <span class="selected-count">
|
||||
{{ $t('autoPromotion.selectedCount', { count: selectedCountryIdsProxy.length }) }}
|
||||
</span> -->
|
||||
<!-- <el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-edit-outline"
|
||||
class="section-action-btn"
|
||||
@click="countryDialogVisible = true"
|
||||
>
|
||||
{{ $t('autoPromotion.choosePromotionCountry') }}
|
||||
</el-button> -->
|
||||
</h4>
|
||||
<div class="status-confirm-box">
|
||||
<div class="country-quick-checks">
|
||||
<div class="field-tip" style="margin-bottom: 10px;">{{ $t('autoPromotion.selectPromotionCountryTip') }}</div>
|
||||
<el-checkbox-group v-model="selectedCountryIdsProxy" size="small">
|
||||
<el-checkbox label="Partition1">{{ $t('autoPromotion.countryQuickZone1') }}</el-checkbox>
|
||||
<el-checkbox label="Partition2">{{ $t('autoPromotion.countryQuickZone2') }}</el-checkbox>
|
||||
<el-checkbox label="Partition3">{{ $t('autoPromotion.countryQuickZone3') }}</el-checkbox>
|
||||
<el-checkbox label="country_china" value="239">{{ $t('autoPromotion.countryQuickChina') }}</el-checkbox>
|
||||
<el-checkbox label="country_india" value="228">{{ $t('autoPromotion.countryQuickIndia') }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<!-- <div v-if="selectedCountryTagRows.length" class="selected-tags">
|
||||
<el-tag v-for="row in selectedCountryTagRows" :key="'c-' + row.id" size="mini" type="info" effect="plain">{{ row.text }}</el-tag>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<section class="form-section">
|
||||
<h4 class="section-title">
|
||||
<i class="el-icon-finished"></i> 4. {{ $t('autoPromotion.confirmAndEnable') }}
|
||||
</h4>
|
||||
|
||||
<div class="status-confirm-box">
|
||||
@@ -150,6 +187,41 @@
|
||||
<el-button size="small" type="primary" :loading="fieldsSaving" @click="emitConfirmFields">{{ $t('autoPromotion.confirm') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('autoPromotion.selectPromotionCountry')"
|
||||
:visible.sync="countryDialogVisible"
|
||||
width="1200px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="field-dialog-toolbar">
|
||||
<el-input
|
||||
v-model="countrySearchText"
|
||||
size="small"
|
||||
clearable
|
||||
class="field-search-input"
|
||||
prefix-icon="el-icon-search"
|
||||
:placeholder="$t('autoPromotion.countrySearchPlaceholder')"
|
||||
/>
|
||||
<el-button size="mini" @click="selectAllCountries">{{ $t('autoPromotion.selectAll') }}</el-button>
|
||||
<el-button size="mini" @click="clearAllCountries">{{ $t('autoPromotion.clearAll') }}</el-button>
|
||||
</div>
|
||||
<div class="field-dialog-body" v-loading="fieldsLoading">
|
||||
<el-checkbox-group v-model="selectedCountryIdsProxy" class="field-check-group">
|
||||
<el-checkbox v-for="c in sortedFilteredCountries" :key="'country-' + String(c.id)" :label="String(c.id)">
|
||||
{{ c.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<div v-if="!fieldsLoading && sortedFilteredCountries.length === 0" class="field-empty-tip">
|
||||
{{ $t('autoPromotion.noCountryMatch') }}
|
||||
</div>
|
||||
</div>
|
||||
<span slot="footer">
|
||||
<el-button size="small" @click="countryDialogVisible = false">{{ $t('autoPromotion.cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" :loading="fieldsSaving" @click="emitConfirmCountries">{{ $t('autoPromotion.confirm') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -159,7 +231,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
fieldSearchText: '',
|
||||
fieldDialogVisible: false
|
||||
fieldDialogVisible: false,
|
||||
countrySearchText: '',
|
||||
countryDialogVisible: false
|
||||
};
|
||||
},
|
||||
props: {
|
||||
@@ -170,9 +244,11 @@ export default {
|
||||
selectedTemplateName: { type: String, default: '' },
|
||||
selectedStyleName: { type: String, default: '' },
|
||||
availableFields: { type: Array, default: () => [] },
|
||||
availableCountries: { type: Array, default: () => [] },
|
||||
fieldsLoading: { type: Boolean, default: false },
|
||||
fieldsSaving: { type: Boolean, default: false },
|
||||
selectedFieldIds: { type: Array, default: () => [] }
|
||||
selectedFieldIds: { type: Array, default: () => [] },
|
||||
selectedCountryIds: { type: Array, default: () => [] }
|
||||
},
|
||||
computed: {
|
||||
hasSelectedTemplate() {
|
||||
@@ -190,8 +266,37 @@ export default {
|
||||
this.$emit('update:selectedFieldIds', val);
|
||||
}
|
||||
},
|
||||
selectedCountryIdsProxy: {
|
||||
get() {
|
||||
return this.selectedCountryIds;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:selectedCountryIds', val);
|
||||
}
|
||||
},
|
||||
sortedFilteredFields() {
|
||||
const kwRaw = String(this.fieldSearchText || '');
|
||||
const kwRaw = String(this.fieldSearchText || '');
|
||||
const normalize = (s) =>
|
||||
String(s || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.toLowerCase();
|
||||
const tokens = kwRaw
|
||||
? kwRaw
|
||||
.split(/[\r\n,,;;]+/g)
|
||||
.map((s) => normalize(s))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const list = (this.availableFields || []).filter((item) => {
|
||||
if (!tokens.length) return true;
|
||||
const label = normalize(item.label || '');
|
||||
// 严格匹配:必须与字段名完全一致(忽略大小写与空白差异)
|
||||
return tokens.some((t) => t === label);
|
||||
});
|
||||
return list.slice().sort((a, b) => String(a.label || '').localeCompare(String(b.label || '')));
|
||||
},
|
||||
sortedFilteredCountries() {
|
||||
const kwRaw = String(this.countrySearchText || '');
|
||||
const normalize = (s) =>
|
||||
String(s || '')
|
||||
.trim()
|
||||
@@ -203,10 +308,9 @@ export default {
|
||||
.map((s) => normalize(s))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const list = (this.availableFields || []).filter((item) => {
|
||||
const list = (this.availableCountries || []).filter((item) => {
|
||||
if (!tokens.length) return true;
|
||||
const label = normalize(item.label || '');
|
||||
// 严格匹配:必须与字段名完全一致(忽略大小写与空白差异)
|
||||
return tokens.some((t) => t === label);
|
||||
});
|
||||
return list.slice().sort((a, b) => String(a.label || '').localeCompare(String(b.label || '')));
|
||||
@@ -217,6 +321,30 @@ export default {
|
||||
return (this.selectedFieldIdsProxy || [])
|
||||
.map((id) => map[String(id)])
|
||||
.filter(Boolean);
|
||||
},
|
||||
selectedCountryTagRows() {
|
||||
const map = {};
|
||||
(this.availableCountries || []).forEach((i) => {
|
||||
map[String(i.id)] = i.label;
|
||||
});
|
||||
const quick = {
|
||||
zone_1: this.$t('autoPromotion.countryQuickZone1'),
|
||||
zone_2: this.$t('autoPromotion.countryQuickZone2'),
|
||||
zone_3: this.$t('autoPromotion.countryQuickZone3'),
|
||||
country_china: this.$t('autoPromotion.countryQuickChina'),
|
||||
country_india: this.$t('autoPromotion.countryQuickIndia')
|
||||
};
|
||||
return (this.selectedCountryIdsProxy || []).map((id) => {
|
||||
const sid = String(id);
|
||||
const fromList = map[sid];
|
||||
const text =
|
||||
fromList != null && fromList !== ''
|
||||
? fromList
|
||||
: quick[sid] != null
|
||||
? quick[sid]
|
||||
: sid;
|
||||
return { id: sid, text };
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -232,6 +360,16 @@ export default {
|
||||
emitConfirmFields() {
|
||||
this.$emit('confirm-fields');
|
||||
this.fieldDialogVisible = false;
|
||||
},
|
||||
selectAllCountries() {
|
||||
this.selectedCountryIdsProxy = (this.availableCountries || []).map((c) => String(c.id));
|
||||
},
|
||||
clearAllCountries() {
|
||||
this.selectedCountryIdsProxy = [];
|
||||
},
|
||||
emitConfirmCountries() {
|
||||
this.$emit('confirm-countries');
|
||||
this.countryDialogVisible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -580,5 +718,17 @@ export default {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.country-quick-checks {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.country-quick-checks >>> .el-checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px 20px;
|
||||
}
|
||||
.country-quick-checks >>> .el-checkbox {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
:close-on-click-modal="false"
|
||||
width="90%"
|
||||
top="5vh"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:before-close="handleClose"
|
||||
custom-class="template-modal"
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" @click="openPreview(false)" class="preview-trigger-btn">
|
||||
<!-- <button type="button" @click="openPreview(false)" class="preview-trigger-btn">
|
||||
<i class="icon-eye"></i> {{ $t('tmrEmailEditor.preview') }}
|
||||
</button>
|
||||
</button> -->
|
||||
<button type="button" @click="openPreview(true)" class="preview-trigger-btn preview-with-vars-btn">
|
||||
<i class="icon-eye"></i> {{ $t('tmrEmailEditor.previewWithVariables') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
<!-- <textarea
|
||||
ref="editorRef"
|
||||
class="tmr-textarea"
|
||||
:value="plainText"
|
||||
@input="handleInput"
|
||||
:placeholder="resolvedPlaceholder"
|
||||
></textarea>
|
||||
></textarea> -->
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="showModal" class="tmr-modal-mask" @click.self="closePreviewModal">
|
||||
@@ -62,6 +62,10 @@ export default {
|
||||
|
||||
default: ''
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -87,13 +91,29 @@ export default {
|
||||
expert_name: "John Doe", // 专家姓名
|
||||
expert_field: "Biomedical Engineering", // 专家研究领域
|
||||
representative_work_title: "Advanced Applications of AI in Medical Imaging", // 专家代表作标题
|
||||
// ai_content_analysis: "", // AI 约稿理由分析
|
||||
ai_content_analysis: "【AI分析文章,一句话总结】", // AI solicitation rationale
|
||||
ai_advised_topics: "Based on your research expertise, we would particularly welcome submissions on topics such as 【这里是AI针对学者领域给特定约稿主题】, or other closely related areas that align with your work.", // AI suggested directions
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isZhLanguage() {
|
||||
return String(this.language || '').toLowerCase() === 'zh';
|
||||
},
|
||||
localizedAiMockData() {
|
||||
if (this.isZhLanguage) {
|
||||
return {
|
||||
ai_content_analysis: '【AI分析这篇文章,一句话总结】。【我们希望也关注个领域】',
|
||||
ai_advised_topics: '我们尤其关注如【方向/题目建议1】、【方向/题目建议2】以及【方向/题目建议3】等相关议题的研究进展。'
|
||||
};
|
||||
}
|
||||
return {
|
||||
ai_content_analysis: '【AI分析文章,一句话总结】',
|
||||
ai_advised_topics: 'Based on your research expertise, we would particularly welcome submissions on topics such as 【这里是AI针对学者领域给特定约稿主题】, or other closely related areas that align with your work.'
|
||||
};
|
||||
},
|
||||
resolvedPlaceholder() {
|
||||
return this.placeholder || (this.$t && this.$t('tmrEmailEditor.placeholder')) || '请输入邮件内容...';
|
||||
},
|
||||
@@ -141,6 +161,7 @@ const deadlineStr = oneMonthLater.toISOString().split('T')[0];
|
||||
|
||||
const map = {
|
||||
...this.variableMockData,
|
||||
...this.localizedAiMockData,
|
||||
journal_abbr: journal_info.jabbr, // 期刊缩写
|
||||
journal_name: journal_info.title,// 期刊全称
|
||||
journal_url: journal_info.website, // 期刊官网链接
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
:form="baseQuestionform"
|
||||
@update="(e) => (questionform = e)"
|
||||
></newForm>
|
||||
|
||||
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -134,7 +131,6 @@ export default {
|
||||
recommend: [{ required: true, message: 'please select', trigger: 'blur' }]
|
||||
};
|
||||
}
|
||||
|
||||
},
|
||||
questionSubmit() {
|
||||
if (this.questionform.is_anonymous == '' && this.questionform.is_anonymous != '0') {
|
||||
@@ -161,30 +157,29 @@ export default {
|
||||
}
|
||||
let Char_Cter = null;
|
||||
// 验证相加的字数
|
||||
if (this.isNewForm) {
|
||||
Char_Cter = [
|
||||
this.questionform.qu5contents,
|
||||
this.questionform.qu6contents,
|
||||
this.questionform.qu7contents,
|
||||
this.questionform.qu8contents,
|
||||
this.questionform.qu9contents,
|
||||
this.questionform.qu10contents,
|
||||
this.questionform.qu11contents,
|
||||
this.questionform.qu12contents,
|
||||
this.questionform.qu13contents,
|
||||
this.questionform.qu14contents,
|
||||
this.questionform.comment
|
||||
].join(' ');
|
||||
} else {
|
||||
Char_Cter = [
|
||||
this.questionform.qu9contents,
|
||||
this.questionform.qu10contents,
|
||||
this.questionform.qu11contents,
|
||||
this.questionform.qu12contents,
|
||||
this.questionform.qu13contents,
|
||||
this.questionform.comment
|
||||
].join(' ');
|
||||
}
|
||||
const contents = this.isNewForm
|
||||
? [
|
||||
this.questionform.qu5contents,
|
||||
this.questionform.qu6contents,
|
||||
this.questionform.qu7contents,
|
||||
this.questionform.qu8contents,
|
||||
this.questionform.qu9contents,
|
||||
this.questionform.qu10contents,
|
||||
this.questionform.qu11contents,
|
||||
this.questionform.qu12contents,
|
||||
this.questionform.qu13contents,
|
||||
this.questionform.qu14contents,
|
||||
this.questionform.comment
|
||||
]
|
||||
: [
|
||||
this.questionform.qu9contents,
|
||||
this.questionform.qu10contents,
|
||||
this.questionform.qu11contents,
|
||||
this.questionform.qu12contents,
|
||||
this.questionform.qu13contents,
|
||||
this.questionform.comment
|
||||
];
|
||||
Char_Cter = this.$commonJS.getCleanTextForCount(contents.join(' '));
|
||||
|
||||
if (new RegExp('[\\u4E00-\\u9FFF]+', 'g').test(Char_Cter)) {
|
||||
//中文
|
||||
|
||||
@@ -342,6 +342,7 @@ export default {
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
async questionSubmit() {
|
||||
|
||||
if (this.questionform.is_anonymous == '' && this.questionform.is_anonymous != '0') {
|
||||
@@ -368,30 +369,29 @@ export default {
|
||||
}
|
||||
let Char_Cter = null;
|
||||
// 验证相加的字数
|
||||
if (this.isNewForm) {
|
||||
Char_Cter = [
|
||||
this.questionform.qu5contents,
|
||||
this.questionform.qu6contents,
|
||||
this.questionform.qu7contents,
|
||||
this.questionform.qu8contents,
|
||||
this.questionform.qu9contents,
|
||||
this.questionform.qu10contents,
|
||||
this.questionform.qu11contents,
|
||||
this.questionform.qu12contents,
|
||||
this.questionform.qu13contents,
|
||||
this.questionform.qu14contents,
|
||||
this.questionform.comment
|
||||
].join(' ');
|
||||
} else {
|
||||
Char_Cter = [
|
||||
this.questionform.qu9contents,
|
||||
this.questionform.qu10contents,
|
||||
this.questionform.qu11contents,
|
||||
this.questionform.qu12contents,
|
||||
this.questionform.qu13contents,
|
||||
this.questionform.comment
|
||||
].join(' ');
|
||||
}
|
||||
const contents = this.isNewForm ? [
|
||||
this.questionform.qu5contents,
|
||||
this.questionform.qu6contents,
|
||||
this.questionform.qu7contents,
|
||||
this.questionform.qu8contents,
|
||||
this.questionform.qu9contents,
|
||||
this.questionform.qu10contents,
|
||||
this.questionform.qu11contents,
|
||||
this.questionform.qu12contents,
|
||||
this.questionform.qu13contents,
|
||||
this.questionform.qu14contents,
|
||||
this.questionform.comment
|
||||
] : [
|
||||
this.questionform.qu9contents,
|
||||
this.questionform.qu10contents,
|
||||
this.questionform.qu11contents,
|
||||
this.questionform.qu12contents,
|
||||
this.questionform.qu13contents,
|
||||
this.questionform.comment
|
||||
];
|
||||
Char_Cter = this.$commonJS.getCleanTextForCount(contents.join(' '));
|
||||
|
||||
|
||||
|
||||
if (new RegExp('[\\u4E00-\\u9FFF]+', 'g').test(Char_Cter)) {
|
||||
//中文
|
||||
|
||||
@@ -1345,10 +1345,16 @@ export default {
|
||||
hasChange: false,
|
||||
hasInit: false,
|
||||
selectedIds: [],
|
||||
isMouseSelecting: false,
|
||||
_selectionSyncToCheckboxesTimer: null,
|
||||
_onDocumentSelectionChange: null,
|
||||
_onDocumentMouseUp: null,
|
||||
_onManuscriptMouseDown: null,
|
||||
|
||||
displayList: [],
|
||||
currentTypeText: '',
|
||||
tinymceId: this.id || 'vue-tinymce-' + +new Date()
|
||||
|
||||
};
|
||||
},
|
||||
// this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(newVal));
|
||||
@@ -1505,51 +1511,60 @@ export default {
|
||||
});
|
||||
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
|
||||
|
||||
document.addEventListener('selectionchange', () => {
|
||||
if(this.isPreview)return;
|
||||
const selection = window.getSelection();
|
||||
if (selection.rangeCount === 0) return;
|
||||
// document.addEventListener('selectionchange', () => {
|
||||
// if(this.isPreview)return;
|
||||
// const selection = window.getSelection();
|
||||
// if (selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
// 依然保留 trim() 后的文本判断,用来决定是否显示气泡
|
||||
const plainText = selection.toString().trim();
|
||||
// const range = selection.getRangeAt(0);
|
||||
// // 依然保留 trim() 后的文本判断,用来决定是否显示气泡
|
||||
// const plainText = selection.toString().trim();
|
||||
|
||||
if (plainText !== '' && selection.rangeCount > 0) {
|
||||
// --- 1. 获取包含标签的 HTML 内容 ---
|
||||
const fragment = range.cloneContents();
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.appendChild(fragment);
|
||||
// 关键点:这个 label 变量现在包含了完整的 HTML 结构(如 <myfigure>)
|
||||
const htmlLabel = tempDiv.innerHTML;
|
||||
const allPMainElements = this.getInvolvedPMain(range);
|
||||
const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))];
|
||||
if (allIds.length > 0) {
|
||||
this.updateBubblePosition(range);
|
||||
const rootItem = this.wordList.find((item) => item.am_id == allIds[0]);
|
||||
// if (plainText !== '' && selection.rangeCount > 0) {
|
||||
// // --- 1. 获取包含标签的 HTML 内容 ---
|
||||
// const fragment = range.cloneContents();
|
||||
// const tempDiv = document.createElement('div');
|
||||
// tempDiv.appendChild(fragment);
|
||||
// // 关键点:这个 label 变量现在包含了完整的 HTML 结构(如 <myfigure>)
|
||||
// const htmlLabel = tempDiv.innerHTML;
|
||||
// const allPMainElements = this.getInvolvedPMain(range);
|
||||
// const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))];
|
||||
// if (allIds.length > 0) {
|
||||
// this.updateBubblePosition(range);
|
||||
// const rootItem = this.wordList.find((item) => item.am_id == allIds[0]);
|
||||
|
||||
this.currentSelection = {
|
||||
// 将 label 设置为包含标签的 HTML 字符串
|
||||
label: htmlLabel,
|
||||
mainId: allIds[0],
|
||||
index: this.wordList.indexOf(rootItem),
|
||||
// this.currentSelection = {
|
||||
// // 将 label 设置为包含标签的 HTML 字符串
|
||||
// label: htmlLabel,
|
||||
// mainId: allIds[0],
|
||||
// index: this.wordList.indexOf(rootItem),
|
||||
|
||||
content: rootItem ? rootItem.content : ''
|
||||
};
|
||||
// content: rootItem ? rootItem.content : ''
|
||||
// };
|
||||
|
||||
this.currentId = allIds[0];
|
||||
this.currentData = rootItem;
|
||||
}
|
||||
} else {
|
||||
this.currentTag = '';
|
||||
this.currentTagData = {};
|
||||
this.currentSelection = {
|
||||
label: '',
|
||||
mainId: '',
|
||||
index: 0,
|
||||
content: {}
|
||||
};
|
||||
}
|
||||
});
|
||||
// this.currentId = allIds[0];
|
||||
// this.currentData = rootItem;
|
||||
// }
|
||||
// } else {
|
||||
// this.currentTag = '';
|
||||
// this.currentTagData = {};
|
||||
// this.currentSelection = {
|
||||
// label: '',
|
||||
// mainId: '',
|
||||
// index: 0,
|
||||
// content: {}
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
this._onDocumentSelectionChange = this.handleDocumentSelectionChange.bind(this);
|
||||
this._onDocumentMouseUp = this.handleDocumentMouseUp.bind(this);
|
||||
this._onManuscriptMouseDown = this.handleManuscriptMouseDown.bind(this);
|
||||
document.addEventListener('selectionchange', this._onDocumentSelectionChange);
|
||||
document.addEventListener('mouseup', this._onDocumentMouseUp);
|
||||
if (this.$refs.scroll) {
|
||||
this.$refs.scroll.addEventListener('mousedown', this._onManuscriptMouseDown);
|
||||
}
|
||||
|
||||
},
|
||||
activated() {
|
||||
// 主动触发 MathJax 渲染
|
||||
@@ -1702,6 +1717,115 @@ renderCiteLabels(html) {
|
||||
|
||||
return rangePs;
|
||||
},
|
||||
handleManuscriptMouseDown(event) {
|
||||
if (this.isPreview) return;
|
||||
const root = this.$refs && this.$refs.scroll;
|
||||
if (!root) return;
|
||||
if (root.contains(event.target)) {
|
||||
this.isMouseSelecting = true;
|
||||
}
|
||||
},
|
||||
handleDocumentMouseUp() {
|
||||
this.isMouseSelecting = false;
|
||||
},
|
||||
handleDocumentSelectionChange() {
|
||||
if (this.isPreview) return;
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const plainText = selection.toString().trim();
|
||||
|
||||
if (plainText !== '' && selection.rangeCount > 0) {
|
||||
const fragment = range.cloneContents();
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.appendChild(fragment);
|
||||
const htmlLabel = tempDiv.innerHTML;
|
||||
const allPMainElements = this.getInvolvedPMain(range);
|
||||
const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))];
|
||||
if (allIds.length > 0) {
|
||||
this.updateBubblePosition(range);
|
||||
const rootItem = this.wordList.find((item) => item.am_id == allIds[0]);
|
||||
|
||||
this.currentSelection = {
|
||||
label: htmlLabel,
|
||||
mainId: allIds[0],
|
||||
index: this.wordList.indexOf(rootItem),
|
||||
content: rootItem ? rootItem.content : ''
|
||||
};
|
||||
|
||||
this.currentId = allIds[0];
|
||||
this.currentData = rootItem;
|
||||
}
|
||||
} else {
|
||||
this.currentTag = '';
|
||||
this.currentTagData = {};
|
||||
this.currentSelection = {
|
||||
label: '',
|
||||
mainId: '',
|
||||
index: 0,
|
||||
content: {}
|
||||
};
|
||||
}
|
||||
this.scheduleSyncSelectedIdsFromRange();
|
||||
},
|
||||
scheduleSyncSelectedIdsFromRange() {
|
||||
if (this._selectionSyncToCheckboxesTimer) {
|
||||
clearTimeout(this._selectionSyncToCheckboxesTimer);
|
||||
}
|
||||
this._selectionSyncToCheckboxesTimer = setTimeout(() => {
|
||||
this.syncSelectedIdsFromRangeInternal();
|
||||
}, 80);
|
||||
},
|
||||
syncSelectedIdsFromRangeInternal() {
|
||||
if (this.isPreview || this.isInternalAction) return;
|
||||
const scrollRoot = this.$refs && this.$refs.scroll;
|
||||
if (!scrollRoot) return;
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;
|
||||
const range = selection.getRangeAt(0);
|
||||
const common = range.commonAncestorContainer;
|
||||
const commonElement = common && common.nodeType === 1 ? common : common && common.parentElement;
|
||||
if (!commonElement || !scrollRoot.contains(commonElement)) return;
|
||||
|
||||
const nodes = Array.from(scrollRoot.querySelectorAll('.drop-target[main-id]'));
|
||||
const touchedMainIds = [];
|
||||
nodes.forEach((node) => {
|
||||
const id = node.getAttribute('main-id');
|
||||
if (!id || id === 'References') return;
|
||||
try {
|
||||
if (range.intersectsNode(node)) {
|
||||
touchedMainIds.push(id);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
if (!touchedMainIds.length) return;
|
||||
|
||||
const indexMap = {};
|
||||
this.wordList.forEach((item, idx) => {
|
||||
if (item && item.am_id != null) {
|
||||
indexMap[String(item.am_id)] = idx;
|
||||
}
|
||||
});
|
||||
const indices = [...new Set(touchedMainIds.map((id) => indexMap[String(id)]).filter((v) => Number.isInteger(v)))];
|
||||
if (!indices.length) return;
|
||||
const lo = Math.min(...indices);
|
||||
const hi = Math.max(...indices);
|
||||
const rangeIds = this.wordList
|
||||
.slice(lo, hi + 1)
|
||||
.map((item) => (item && item.am_id != null ? item.am_id : null))
|
||||
.filter((id) => id != null);
|
||||
if (!rangeIds.length) return;
|
||||
|
||||
const existingSet = new Set(this.selectedIds || []);
|
||||
rangeIds.forEach((id) => existingSet.add(id));
|
||||
const ordered = this.wordList
|
||||
.map((item) => (item && item.am_id != null ? item.am_id : null))
|
||||
.filter((id) => id != null && existingSet.has(id));
|
||||
this.selectedIds = ordered;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
handleUnbindLink(type) {
|
||||
const rootItem = this.wordList.find((item) => item.am_id == this.currentTagData.main_id);
|
||||
|
||||
932
src/components/page/countryManagement.vue
Normal file
932
src/components/page/countryManagement.vue
Normal file
@@ -0,0 +1,932 @@
|
||||
<template>
|
||||
<div class="country-manage">
|
||||
<div class="crumbs">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item> <i class="el-icon-place"></i> {{ $t('countryManagement.title') }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-form :inline="true" :model="query" size="small">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
:placeholder="$t('countryManagement.keywordPlaceholder')"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="query.partition"
|
||||
clearable
|
||||
:placeholder="$t('countryManagement.partitionAll')"
|
||||
style="width: 140px"
|
||||
@change="onPartitionChange"
|
||||
>
|
||||
<el-option :label="$t('countryManagement.partitionAll')" value="" />
|
||||
<el-option :label="$t('countryManagement.partition1')" value="1" />
|
||||
<el-option :label="$t('countryManagement.partition2')" value="2" />
|
||||
<el-option :label="$t('countryManagement.partition3')" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleSearch">
|
||||
{{ $t('countryManagement.searchBtn') }}
|
||||
</el-button>
|
||||
<el-button @click="handleReset">{{ $t('countryManagement.resetBtn') }}</el-button>
|
||||
<!-- 批量修改分区按钮 -->
|
||||
<!-- <el-button type="warning" plain icon="el-icon-upload2" @click="openBatchPartitionDialog">
|
||||
|
||||
{{ $t('countryManagement.batchPartitionBtn') }}
|
||||
</el-button> -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="table-card">
|
||||
<el-table :data="list" border stripe v-loading="loading" header-row-class-name="dark-table-header">
|
||||
<el-table-column type="index" :label="$t('countryManagement.table.no')" width="70" align="center" />
|
||||
<el-table-column prop="zh_name" :label="$t('countryManagement.table.zhName')" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="en_name" :label="$t('countryManagement.table.enName')" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="code" :label="$t('countryManagement.table.code')" width="100" align="center" />
|
||||
<el-table-column prop="partition" :label="$t('countryManagement.table.partition')" width="100" align="center" />
|
||||
<el-table-column :label="$t('countryManagement.table.actions')" width="220" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<div class="table-row-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
class="action-btn action-btn--edit"
|
||||
@click="openEdit(scope.row)"
|
||||
>
|
||||
{{ $t('countryManagement.edit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
class="action-btn action-btn--delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
{{ $t('countryManagement.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="query.page"
|
||||
:page-size="query.per_page"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('countryManagement.editTitle')"
|
||||
:visible.sync="editVisible"
|
||||
width="580px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="editForm" :model="form" :rules="rules" label-width="150px" size="small" class="country-edit-form">
|
||||
<el-form-item :label="$t('countryManagement.form.zhName')" prop="zh_name">
|
||||
<el-input v-model="form.zh_name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('countryManagement.form.enName')" prop="en_name">
|
||||
<el-input v-model="form.en_name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('countryManagement.form.code')" prop="code">
|
||||
<el-input v-model="form.code" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('countryManagement.form.partition')" prop="partition">
|
||||
<el-select v-model="form.partition" style="width: 100%">
|
||||
<el-option :label="$t('countryManagement.partition1')" value="1" />
|
||||
<el-option :label="$t('countryManagement.partition2')" value="2" />
|
||||
<el-option :label="$t('countryManagement.partition3')" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="editVisible = false">{{ $t('countryManagement.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveLoading" @click="submitEdit">{{ $t('countryManagement.save') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('countryManagement.batchPartitionTitle')"
|
||||
:visible.sync="batchPartitionVisible"
|
||||
width="80vw"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="resetBatchPartitionDialog"
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div class="batch-partition-content" style="width: 100%">
|
||||
<el-form label-width="120px" size="small" class="batch-partition-target-form">
|
||||
<el-form-item :label="$t('countryManagement.batchPartitionTargetLabel')">
|
||||
<el-select
|
||||
v-model="batchPartitionTargetPartition"
|
||||
style="width: 220px"
|
||||
@change="handleBatchPartitionDraftChange"
|
||||
>
|
||||
<el-option :label="$t('countryManagement.partition1')" value="1" />
|
||||
<el-option :label="$t('countryManagement.partition2')" value="2" />
|
||||
<el-option :label="$t('countryManagement.partition3')" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<p class="batch-partition-help">{{ $t('countryManagement.batchPartitionHelp') }}</p>
|
||||
<el-input
|
||||
v-model="batchPartitionInput"
|
||||
type="textarea"
|
||||
:rows="20"
|
||||
:placeholder="$t('countryManagement.batchPartitionPlaceholder')"
|
||||
@input="handleBatchPartitionDraftChange"
|
||||
/>
|
||||
<div class="batch-partition-actions">
|
||||
<el-button type="primary" plain :loading="batchPartitionPreviewLoading" @click="previewBatchPartition">
|
||||
{{ $t('countryManagement.batchPartitionPreview') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="batchPartitionToApplyBaseRows.length > 0"
|
||||
type="success"
|
||||
:loading="batchPartitionApplyLoading"
|
||||
:disabled="!batchPartitionPreviewRows.length"
|
||||
@click="applyBatchPartition"
|
||||
>
|
||||
{{ $t('countryManagement.batchPartitionApply') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="batchPartitionStats.input > 0" class="batch-partition-summary-container" style="margin-left: 20px">
|
||||
<div v-if="batchPartitionStats.input > 0" class="batch-partition-summary">
|
||||
{{
|
||||
$t('countryManagement.batchPartitionSummaryLine', {
|
||||
input: String(batchPartitionStats.input),
|
||||
matched: String(batchPartitionStats.matched),
|
||||
miss: String(batchPartitionStats.miss),
|
||||
same: String(batchPartitionStats.skipSame),
|
||||
will: String(batchPartitionStats.willUpdate)
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<p v-if="batchPartitionPreviewRows.length" class="batch-partition-selection-hint">
|
||||
{{ $t('countryManagement.batchPartitionSelectionHint') }}
|
||||
</p>
|
||||
|
||||
<el-table
|
||||
v-if="batchPartitionPreviewRows.length"
|
||||
:data="batchPartitionFilteredPreviewRows"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
max-height="400"
|
||||
class="batch-partition-table"
|
||||
:row-class-name="batchPartitionRowClassName"
|
||||
>
|
||||
<el-table-column type="index" :label="$t('countryManagement.table.no')" width="64" align="center" />
|
||||
|
||||
<el-table-column
|
||||
prop="key"
|
||||
:label="$t('countryManagement.batchPartitionColKey')"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column prop="country_id" :label="$t('countryManagement.batchPartitionColId')" width="90" align="center" />
|
||||
<el-table-column
|
||||
prop="en_name"
|
||||
:label="$t('countryManagement.batchPartitionColName')"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column :label="$t('countryManagement.batchPartitionColCurrentPartition')" width="140" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :class="['partition-chip', scope.row.status === 'diff' ? 'partition-chip--warn' : '']">
|
||||
{{ scope.row.currentPartition || '—' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('countryManagement.batchPartitionColMatch')" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
:class="[
|
||||
'match-chip',
|
||||
scope.row.status === 'not_found'
|
||||
? 'match-chip--danger'
|
||||
: scope.row.status === 'diff'
|
||||
? 'match-chip--warn'
|
||||
: 'match-chip--ok'
|
||||
]"
|
||||
>
|
||||
{{ scope.row.matchLabel }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'countryManagement',
|
||||
data() {
|
||||
return {
|
||||
query: {
|
||||
keyword: '',
|
||||
partition: '',
|
||||
page: 1,
|
||||
per_page: 20
|
||||
},
|
||||
list: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
editVisible: false,
|
||||
saveLoading: false,
|
||||
form: {
|
||||
country_id: '',
|
||||
zh_name: '',
|
||||
en_name: '',
|
||||
code: '',
|
||||
partition: '1'
|
||||
},
|
||||
rules: {},
|
||||
batchPartitionVisible: false,
|
||||
batchPartitionTargetPartition: '1',
|
||||
batchPartitionInput: '',
|
||||
batchPartitionPreviewRows: [],
|
||||
batchPartitionPreviewLoading: false,
|
||||
batchPartitionApplyLoading: false,
|
||||
batchPartitionAllRows: [],
|
||||
/** 预览后的统计:录入、匹配、将更新等 */
|
||||
batchPartitionStats: {
|
||||
input: 0,
|
||||
matched: 0,
|
||||
miss: 0,
|
||||
skipSame: 0,
|
||||
willUpdate: 0
|
||||
},
|
||||
/** 预览表筛选关键词(分号、逗号、换行分隔多词) */
|
||||
batchPartitionTableFilter: '',
|
||||
/** none:未勾选视为提交全部可更新行;explicit:仅提交已勾选 */
|
||||
batchPartitionSelectionMode: 'none',
|
||||
batchPartitionSelectedIds: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
batchPartitionFilteredPreviewRows() {
|
||||
const rows = this.batchPartitionPreviewRows || [];
|
||||
const tokens = this.batchPartitionFilterTokens(this.batchPartitionTableFilter);
|
||||
if (!tokens.length) return rows;
|
||||
return rows.filter((r) => this.batchPartitionRowMatchesTokens(r, tokens));
|
||||
},
|
||||
batchPartitionToApplyBaseRows() {
|
||||
return (this.batchPartitionPreviewRows || []).filter(
|
||||
(r) => r._row && String(r._row.partition != null ? r._row.partition : '') !== String(r.partition)
|
||||
);
|
||||
},
|
||||
batchPartitionEffectiveToApplyRows() {
|
||||
const base = this.batchPartitionToApplyBaseRows;
|
||||
if (this.batchPartitionSelectionMode === 'none') return base;
|
||||
const ids = new Set((this.batchPartitionSelectedIds || []).map(String));
|
||||
return base.filter((r) => ids.has(this.countryRowId(r._row)));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.rules = {
|
||||
zh_name: [{ required: true, message: this.$t('countryManagement.ruleZhName'), trigger: 'blur' }],
|
||||
en_name: [{ required: true, message: this.$t('countryManagement.ruleEnName'), trigger: 'blur' }],
|
||||
code: [{ required: true, message: this.$t('countryManagement.ruleCode'), trigger: 'blur' }],
|
||||
partition: [{ required: true, message: this.$t('countryManagement.rulePartition'), trigger: 'change' }]
|
||||
};
|
||||
this.fetchList();
|
||||
},
|
||||
methods: {
|
||||
countryRowId(row) {
|
||||
if (!row) return '';
|
||||
return row.country_id != null ? String(row.country_id) : row.id != null ? String(row.id) : '';
|
||||
},
|
||||
normalizeListResponse(res) {
|
||||
const d = res && res.data;
|
||||
if (!d) return { list: [], total: 0 };
|
||||
const list = d.list || d.data || d.rows || [];
|
||||
const total = Number(d.total != null ? d.total : d.count != null ? d.count : list.length) || 0;
|
||||
return { list: Array.isArray(list) ? list : [], total };
|
||||
},
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = {
|
||||
keyword: this.query.keyword || '',
|
||||
partition: this.query.partition === '' || this.query.partition == null ? '' : String(this.query.partition),
|
||||
page: this.query.page,
|
||||
per_page: this.query.per_page
|
||||
};
|
||||
const res = await this.$api.post('api/Country/getList', params);
|
||||
if (res && res.code === 0) {
|
||||
const { list, total } = this.normalizeListResponse(res);
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
} else {
|
||||
this.list = [];
|
||||
this.total = 0;
|
||||
if (res && res.msg) this.$message.warning(res.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
this.list = [];
|
||||
this.total = 0;
|
||||
this.$message.error(this.$t('countryManagement.loadFailed'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.query.page = 1;
|
||||
this.fetchList();
|
||||
},
|
||||
/** 切换分区即请求列表,无需再点搜索 */
|
||||
onPartitionChange() {
|
||||
this.query.page = 1;
|
||||
this.fetchList();
|
||||
},
|
||||
handleReset() {
|
||||
this.query = {
|
||||
keyword: '',
|
||||
partition: '',
|
||||
page: 1,
|
||||
per_page: 20
|
||||
};
|
||||
this.fetchList();
|
||||
},
|
||||
handleSizeChange(size) {
|
||||
this.query.per_page = size;
|
||||
this.query.page = 1;
|
||||
this.fetchList();
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.query.page = page;
|
||||
this.fetchList();
|
||||
},
|
||||
openEdit(row) {
|
||||
const id = this.countryRowId(row);
|
||||
this.form = {
|
||||
country_id: id,
|
||||
zh_name: (row && row.zh_name) || '',
|
||||
en_name: (row && row.en_name) || '',
|
||||
code: (row && row.code) || '',
|
||||
partition: row && row.partition != null && row.partition !== '' ? String(row.partition) : '1'
|
||||
};
|
||||
this.editVisible = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.editForm) this.$refs.editForm.clearValidate();
|
||||
});
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
country_id: '',
|
||||
zh_name: '',
|
||||
en_name: '',
|
||||
code: '',
|
||||
partition: '1'
|
||||
};
|
||||
},
|
||||
submitEdit() {
|
||||
this.$refs.editForm.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
this.saveLoading = true;
|
||||
try {
|
||||
const params = {
|
||||
country_id: String(this.form.country_id),
|
||||
zh_name: this.form.zh_name,
|
||||
en_name: this.form.en_name,
|
||||
code: this.form.code,
|
||||
partition: String(this.form.partition)
|
||||
};
|
||||
const res = await this.$api.post('api/Country/edit', params);
|
||||
if (res && res.code === 0) {
|
||||
this.$message.success(this.$t('countryManagement.saveSuccess'));
|
||||
this.editVisible = false;
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.$message.error((res && res.msg) || this.$t('countryManagement.opFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('countryManagement.opFailed'));
|
||||
} finally {
|
||||
this.saveLoading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
handleDelete(row) {
|
||||
const id = this.countryRowId(row);
|
||||
if (!id) {
|
||||
this.$message.warning(this.$t('countryManagement.missingId'));
|
||||
return;
|
||||
}
|
||||
this.$confirm(this.$t('countryManagement.deleteConfirm'), this.$t('countryManagement.deleteTitle'), {
|
||||
type: 'warning',
|
||||
confirmButtonText: this.$t('countryManagement.confirm'),
|
||||
cancelButtonText: this.$t('countryManagement.cancel')
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await this.$api.post('api/Country/delete', { country_id: String(id) });
|
||||
if (res && res.code === 0) {
|
||||
this.$message.success(this.$t('countryManagement.deleteSuccess'));
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.$message.error((res && res.msg) || this.$t('countryManagement.opFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('countryManagement.opFailed'));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
openBatchPartitionDialog() {
|
||||
this.batchPartitionVisible = true;
|
||||
},
|
||||
resetBatchPartitionDialog() {
|
||||
this.batchPartitionTargetPartition = '1';
|
||||
this.batchPartitionInput = '';
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.batchPartitionAllRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
this.batchPartitionTableFilter = '';
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
this.batchPartitionSelectedIds = [];
|
||||
},
|
||||
handleBatchPartitionDraftChange() {
|
||||
if (!this.batchPartitionPreviewRows.length && !(this.batchPartitionStats && this.batchPartitionStats.input > 0)) return;
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.batchPartitionAllRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
this.batchPartitionTableFilter = '';
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
this.batchPartitionSelectedIds = [];
|
||||
},
|
||||
batchPartitionFilterTokens(text) {
|
||||
return String(text || '')
|
||||
.toLowerCase()
|
||||
.split(/[;\n,,]+/g)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
batchPartitionRowMatchesTokens(pr, tokens) {
|
||||
if (!tokens || !tokens.length) return true;
|
||||
const parts = [
|
||||
pr.key,
|
||||
pr.en_name,
|
||||
pr.country_id,
|
||||
pr.matchLabel,
|
||||
pr._row && pr._row.code,
|
||||
pr._row && pr._row.zh_name,
|
||||
pr._row && pr._row.en_name
|
||||
]
|
||||
.filter((x) => x != null && String(x).trim() !== '')
|
||||
.map((x) => String(x).toLowerCase());
|
||||
const hay = parts.join(' ');
|
||||
return tokens.some((t) => hay.indexOf(t) !== -1);
|
||||
},
|
||||
isBatchPartitionRowUpdatable(pr) {
|
||||
if (!pr || !pr._row) return false;
|
||||
return String(pr._row.partition != null ? pr._row.partition : '') !== String(pr.partition);
|
||||
},
|
||||
isBatchPartitionRowSelected(pr) {
|
||||
if (!pr._row || this.batchPartitionSelectionMode === 'none') return false;
|
||||
const id = this.countryRowId(pr._row);
|
||||
return (this.batchPartitionSelectedIds || []).map(String).includes(String(id));
|
||||
},
|
||||
onBatchPartitionNativeCheckboxChange(pr, ev) {
|
||||
const checked = !!(ev && ev.target && ev.target.checked);
|
||||
const id = pr._row ? this.countryRowId(pr._row) : '';
|
||||
if (!id || !this.isBatchPartitionRowUpdatable(pr)) return;
|
||||
let ids = (this.batchPartitionSelectedIds || []).map(String);
|
||||
if (this.batchPartitionSelectionMode === 'none') {
|
||||
if (checked) {
|
||||
this.batchPartitionSelectionMode = 'explicit';
|
||||
ids = [String(id)];
|
||||
}
|
||||
} else {
|
||||
if (checked) {
|
||||
if (!ids.includes(String(id))) ids.push(String(id));
|
||||
} else {
|
||||
ids = ids.filter((x) => x !== String(id));
|
||||
}
|
||||
}
|
||||
if (this.batchPartitionSelectionMode === 'explicit' && ids.length === 0) {
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
}
|
||||
this.batchPartitionSelectedIds = ids;
|
||||
},
|
||||
selectAllFilteredBatchPartitionRows() {
|
||||
const ids = [];
|
||||
(this.batchPartitionFilteredPreviewRows || []).forEach((r) => {
|
||||
if (!this.isBatchPartitionRowUpdatable(r)) return;
|
||||
const id = this.countryRowId(r._row);
|
||||
if (id) ids.push(String(id));
|
||||
});
|
||||
if (!ids.length) {
|
||||
this.$message.info(this.$t('countryManagement.batchPartitionSkipSame'));
|
||||
return;
|
||||
}
|
||||
this.batchPartitionSelectionMode = 'explicit';
|
||||
this.batchPartitionSelectedIds = [...new Set(ids)];
|
||||
},
|
||||
clearBatchPartitionRowSelection() {
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
this.batchPartitionSelectedIds = [];
|
||||
},
|
||||
batchPartitionRowClassName({ row }) {
|
||||
if (!row) return '';
|
||||
if (row.status === 'not_found') return 'batch-row-not-found';
|
||||
if (row.status === 'diff') return 'batch-row-mismatch';
|
||||
return '';
|
||||
},
|
||||
resetBatchPartitionStats() {
|
||||
this.batchPartitionStats = {
|
||||
input: 0,
|
||||
matched: 0,
|
||||
miss: 0,
|
||||
skipSame: 0,
|
||||
willUpdate: 0
|
||||
};
|
||||
},
|
||||
recomputeBatchPartitionStats() {
|
||||
const rows = this.batchPartitionPreviewRows || [];
|
||||
let matched = 0;
|
||||
let miss = 0;
|
||||
let skipSame = 0;
|
||||
let willUpdate = 0;
|
||||
rows.forEach((r) => {
|
||||
if (!r._row) {
|
||||
miss += 1;
|
||||
return;
|
||||
}
|
||||
matched += 1;
|
||||
const cur = String(r._row.partition != null ? r._row.partition : '');
|
||||
if (cur === String(r.partition)) {
|
||||
skipSame += 1;
|
||||
} else {
|
||||
willUpdate += 1;
|
||||
}
|
||||
});
|
||||
this.batchPartitionStats = {
|
||||
input: rows.length,
|
||||
matched,
|
||||
miss,
|
||||
skipSame,
|
||||
willUpdate
|
||||
};
|
||||
},
|
||||
parseBatchPartitionLines(text) {
|
||||
const lines = String(text || '')
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && !l.startsWith('#'));
|
||||
return lines.map((key) => ({ key }));
|
||||
},
|
||||
findCountryRowForBatch(rows, keyRaw) {
|
||||
const key = String(keyRaw || '').trim();
|
||||
if (!key || !rows || !rows.length) return null;
|
||||
const upper = key.toUpperCase();
|
||||
if (upper.length === 3) {
|
||||
const byCode = rows.find((r) => String(r.code || '').toUpperCase() === upper);
|
||||
if (byCode) return byCode;
|
||||
}
|
||||
const lower = key.toLowerCase();
|
||||
return (
|
||||
rows.find(
|
||||
(r) =>
|
||||
String(r.en_name || '')
|
||||
.trim()
|
||||
.toLowerCase() === lower
|
||||
) ||
|
||||
rows.find((r) => String(r.zh_name || '').trim() === key) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
async fetchAllCountryRowsForBatch() {
|
||||
const all = [];
|
||||
let page = 1;
|
||||
const per_page = 300;
|
||||
while (true) {
|
||||
const res = await this.$api.post('api/Country/getList', {
|
||||
keyword: '',
|
||||
partition: '',
|
||||
page,
|
||||
per_page
|
||||
});
|
||||
if (!res || res.code !== 0) {
|
||||
throw new Error('getList');
|
||||
}
|
||||
const { list, total } = this.normalizeListResponse(res);
|
||||
if (!Array.isArray(list) || list.length === 0) break;
|
||||
all.push(...list);
|
||||
if (all.length >= total) break;
|
||||
page += 1;
|
||||
if (page > 500) break;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
async previewBatchPartition() {
|
||||
const targetPartition = String(this.batchPartitionTargetPartition || '').trim();
|
||||
if (!['1', '2', '3'].includes(targetPartition)) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionTargetRequired'));
|
||||
return;
|
||||
}
|
||||
const parsed = this.parseBatchPartitionLines(this.batchPartitionInput);
|
||||
if (!parsed.length) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionEmpty'));
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
return;
|
||||
}
|
||||
this.batchPartitionPreviewLoading = true;
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
try {
|
||||
this.batchPartitionAllRows = await this.fetchAllCountryRowsForBatch();
|
||||
} catch (e) {
|
||||
this.batchPartitionAllRows = [];
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
this.$message.error(this.$t('countryManagement.batchPartitionLoadListFailed'));
|
||||
this.batchPartitionPreviewLoading = false;
|
||||
return;
|
||||
}
|
||||
const rows = this.batchPartitionAllRows;
|
||||
this.batchPartitionPreviewRows = parsed.map((item) => {
|
||||
const row = this.findCountryRowForBatch(rows, item.key);
|
||||
const id = row ? this.countryRowId(row) : '';
|
||||
const curPart = row && row.partition != null && row.partition !== '' ? String(row.partition) : '';
|
||||
let matchLabel = this.$t('countryManagement.batchPartitionMissing');
|
||||
let status = 'not_found';
|
||||
if (row) {
|
||||
if (curPart === targetPartition) {
|
||||
matchLabel = this.$t('countryManagement.batchPartitionSkipSame');
|
||||
status = 'same';
|
||||
} else {
|
||||
matchLabel = this.$t('countryManagement.batchPartitionMismatch');
|
||||
status = 'diff';
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: item.key,
|
||||
partition: targetPartition,
|
||||
currentPartition: curPart || '—',
|
||||
matchLabel,
|
||||
status,
|
||||
country_id: id || '—',
|
||||
en_name: (row && row.en_name) || '—',
|
||||
_row: row
|
||||
};
|
||||
});
|
||||
if (!this.batchPartitionPreviewRows.length) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionPreviewEmpty'));
|
||||
}
|
||||
this.recomputeBatchPartitionStats();
|
||||
this.batchPartitionTableFilter = '';
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
this.batchPartitionSelectedIds = [];
|
||||
this.batchPartitionPreviewLoading = false;
|
||||
},
|
||||
async applyBatchPartition() {
|
||||
if (!this.batchPartitionPreviewRows.length) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionPreviewEmpty'));
|
||||
return;
|
||||
}
|
||||
const toApply = this.batchPartitionEffectiveToApplyRows.slice();
|
||||
if (this.batchPartitionSelectionMode === 'explicit' && toApply.length === 0) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionNoSelection'));
|
||||
return;
|
||||
}
|
||||
if (!toApply.length) {
|
||||
this.$message.info(this.$t('countryManagement.batchPartitionSkipSame'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$confirm(
|
||||
this.$t('countryManagement.batchPartitionApplyConfirm', { n: String(toApply.length) }),
|
||||
this.$t('countryManagement.batchPartitionTitle'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: this.$t('countryManagement.confirm'),
|
||||
cancelButtonText: this.$t('countryManagement.cancel')
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
this.batchPartitionApplyLoading = true;
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
for (let i = 0; i < toApply.length; i++) {
|
||||
const pr = toApply[i];
|
||||
const row = pr._row;
|
||||
const params = {
|
||||
country_id: String(this.countryRowId(row)),
|
||||
zh_name: (row && row.zh_name) || '',
|
||||
en_name: (row && row.en_name) || '',
|
||||
code: (row && row.code) || '',
|
||||
partition: String(pr.partition)
|
||||
};
|
||||
try {
|
||||
const res = await this.$api.post('api/Country/edit', params);
|
||||
if (res && res.code === 0) ok += 1;
|
||||
else fail += 1;
|
||||
} catch (e) {
|
||||
fail += 1;
|
||||
}
|
||||
}
|
||||
const miss = this.batchPartitionPreviewRows.filter((r) => !r._row).length;
|
||||
const inputN = String(this.batchPartitionStats.input || this.batchPartitionPreviewRows.length);
|
||||
this.$message.success(
|
||||
this.$t('countryManagement.batchPartitionDone', {
|
||||
ok: String(ok),
|
||||
fail: String(fail),
|
||||
miss: String(miss),
|
||||
input: inputN
|
||||
})
|
||||
);
|
||||
this.batchPartitionApplyLoading = false;
|
||||
this.batchPartitionVisible = false;
|
||||
this.fetchList();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.country-manage {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.crumbs {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.toolbar {
|
||||
/* margin-bottom: 15px; */
|
||||
margin-top: 20px;
|
||||
}
|
||||
.table-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
.pagination {
|
||||
margin-top: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
::v-deep .dark-table-header th {
|
||||
background-color: #f5f7fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
.table-row-actions {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-row-actions .action-btn {
|
||||
margin: 0;
|
||||
padding: 7px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.table-row-actions .action-btn--edit {
|
||||
background-color: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
}
|
||||
.table-row-actions .action-btn--edit:hover {
|
||||
background-color: #d9ecff;
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
.table-row-actions .action-btn--delete {
|
||||
background-color: #fef0f0;
|
||||
border-color: #fbc4c4;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.table-row-actions .action-btn--delete:hover {
|
||||
background-color: #fde2e2;
|
||||
border-color: #f56c6c;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.batch-partition-help {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
white-space: pre-line;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.batch-partition-target-form {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.batch-partition-actions {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.batch-partition-summary {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
line-height: 1.55;
|
||||
margin: 0 0 10px;
|
||||
padding: 10px 12px;
|
||||
background: #f4f6f9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
.batch-partition-selection-hint {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.55;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.batch-partition-table-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.batch-partition-filter-input {
|
||||
width: min(320px, 100%);
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.batch-partition-filter-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.batch-partition-checkbox-placeholder {
|
||||
color: #dcdfe6;
|
||||
font-size: 12px;
|
||||
}
|
||||
.batch-partition-native-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
accent-color: #409eff;
|
||||
}
|
||||
.batch-partition-table {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.partition-chip,
|
||||
.match-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 62px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.partition-chip--warn,
|
||||
.match-chip--warn {
|
||||
color: #8a5700;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
}
|
||||
.match-chip--danger {
|
||||
color: #a8071a;
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffa39e;
|
||||
}
|
||||
.match-chip--ok {
|
||||
color: #237804;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
::v-deep .batch-row-not-found td {
|
||||
background: #fff5f5 !important;
|
||||
}
|
||||
::v-deep .batch-row-mismatch td {
|
||||
background: #fffdf0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -771,6 +771,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../common/bus'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -1188,6 +1189,43 @@ export default {
|
||||
this.getjour();
|
||||
},
|
||||
methods: {
|
||||
readPromotionFactoryJumpJournalId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('promotionFactoryJump');
|
||||
if (!raw) return '';
|
||||
const obj = JSON.parse(raw || '{}');
|
||||
if (!obj || obj.from !== 'promotionFactory') return '';
|
||||
if (obj.targetPath && String(obj.targetPath) !== String(this.$route.path)) return '';
|
||||
const id = obj.journal_id != null ? String(obj.journal_id) : '';
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
return id;
|
||||
} catch (e) {
|
||||
try {
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
} catch (e2) {}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
forceClearJumpQuery() {
|
||||
const q = (this.$route && this.$route.query) || {};
|
||||
const has = q && (q._ap_from || q.journal_id || q.journalId || q.journalIdd);
|
||||
if (!has) return;
|
||||
const oldFullPath = this.$route && this.$route.fullPath ? this.$route.fullPath : '';
|
||||
const nextQuery = Object.assign({}, q);
|
||||
delete nextQuery._ap_from;
|
||||
delete nextQuery.journal_id;
|
||||
delete nextQuery.journalId;
|
||||
delete nextQuery.journalIdd;
|
||||
// 关闭“中间态 fullPath”对应的标签页,避免残留在顶部 Tags
|
||||
if (oldFullPath) {
|
||||
bus.$emit('close_tag_by_path', { path: oldFullPath, silent: true });
|
||||
}
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => {});
|
||||
try {
|
||||
const cleanUrl = window.location.origin + this.$route.path;
|
||||
window.history.replaceState({}, '', cleanUrl);
|
||||
} catch (e) {}
|
||||
},
|
||||
unplIcon() {
|
||||
this.$refs['upIconIMg'].$refs['upload-inner'].handleClick();
|
||||
},
|
||||
@@ -1314,9 +1352,28 @@ export default {
|
||||
if (res.code == 0) {
|
||||
this.df_jour = res.data.journals;
|
||||
this.add_jour = res.data.journals;
|
||||
this.query.journal_id = this.df_jour[0].journal_id;
|
||||
this.query.journal_title = this.df_jour[0].title;
|
||||
const fromQueryId = this.$route && this.$route.query ? this.$route.query.journal_id : '';
|
||||
const fromSessionId = this.readPromotionFactoryJumpJournalId();
|
||||
const pickedId = fromQueryId || fromSessionId;
|
||||
const matched = pickedId ? this.df_jour.find((j) => String(j.journal_id) === String(pickedId)) : null;
|
||||
const first = this.df_jour[0];
|
||||
const picked = matched || first;
|
||||
this.query.journal_id = picked ? picked.journal_id : 0;
|
||||
this.query.journal_title = picked ? picked.title : '';
|
||||
this.getgroup();
|
||||
|
||||
// 仅首次接收参数:应用后立刻清掉 URL,避免刷新仍然固定某一本期刊
|
||||
if (fromQueryId) {
|
||||
const nextQuery = Object.assign({}, (this.$route && this.$route.query) || {});
|
||||
delete nextQuery.journal_id;
|
||||
delete nextQuery.journalId;
|
||||
delete nextQuery._ap_from;
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => {});
|
||||
// 双保险:路由稳定后再强制清参一次,避免地址栏残留“中间态”
|
||||
setTimeout(() => {
|
||||
this.forceClearJumpQuery();
|
||||
}, 0);
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -68,16 +68,42 @@
|
||||
</p>
|
||||
<p class="info-row" style="margin-top: 10px; font-size: 12px">
|
||||
<span class="label">{{ $t('expertDatabase.fields.acquisitionTimeLabel') }}</span>
|
||||
<span class="value time">{{ scope.row.ctime_text ? scope.row.ctime_text : '-' }}</span>
|
||||
<span class="value time">{{ scope.row.ctime_text ? scope.row.ctime_text : $t('expertDatabase.emptyMark') }}</span>
|
||||
</p>
|
||||
|
||||
<span class="custom-tag">{{ scope.row.state_text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="affiliation" :label="$t('expertDatabase.columns.affiliation')" min-width="260" />
|
||||
<el-table-column prop="fieldDisplay" :label="$t('expertDatabase.columns.researchAreas')" min-width="200">
|
||||
<el-table-column prop="country" :label="$t('expertDatabase.columns.country')" min-width="100" />
|
||||
<el-table-column prop="affiliation" :label="$t('expertDatabase.columns.affiliation')" min-width="460" />
|
||||
<el-table-column :label="$t('expertDatabase.columns.unsubscribeStatus')" width="180" align="center">
|
||||
<template slot-scope="scope">
|
||||
<div class="unsubscribe-cell">
|
||||
<el-tag
|
||||
size="mini"
|
||||
:type="getUnsubscribeValue(scope.row) === 1 ? 'warning' : 'success'"
|
||||
effect="plain"
|
||||
class="unsubscribe-tag"
|
||||
>
|
||||
{{
|
||||
getUnsubscribeValue(scope.row) === 1
|
||||
? $t('expertDatabase.unsubscribeUnsubscribed')
|
||||
: $t('expertDatabase.unsubscribeNormal')
|
||||
}}
|
||||
</el-tag>
|
||||
<el-switch
|
||||
class="unsubscribe-switch"
|
||||
:value="getUnsubscribeValue(scope.row) === 0"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#dcdfe6"
|
||||
:disabled="switchingExpertId === getExpertId(scope.row)"
|
||||
@change="(val) => handleUnsubscribeSwitch(scope.row, val)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fieldDisplay" :label="$t('expertDatabase.columns.researchAreas')" min-width="260">
|
||||
<template slot-scope="scope">
|
||||
<div v-for="(field, index) in scope.row.fields" :key="index">
|
||||
<span>
|
||||
@@ -85,10 +111,43 @@
|
||||
{{ field.field }}
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="scope.row.fields && scope.row.fields.length"
|
||||
type="text"
|
||||
size="small"
|
||||
class="view-all-btn"
|
||||
@click.stop="openFieldDetail(scope.row)"
|
||||
>
|
||||
{{ $t('expertDatabase.viewAllInfo') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('expertDatabase.detailDialogTitle')"
|
||||
:visible.sync="fieldDetailVisible"
|
||||
width="1200px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="field-detail-dialog"
|
||||
>
|
||||
<p v-if="fieldDetailExpert" class="field-detail-name">
|
||||
<span class="label">{{ $t('expertDatabase.fields.nameLabel') }}</span>
|
||||
<span class="value bold">{{ fieldDetailExpert.name }}</span>
|
||||
</p>
|
||||
<el-table v-if="fieldDetailRows.length" :data="fieldDetailRows" border stripe size="small" max-height="420">
|
||||
<el-table-column type="index" :label="$t('expertDatabase.table.no')" width="56" align="center" />
|
||||
<el-table-column prop="field" :label="$t('expertDatabase.detailColField')" min-width="140" />
|
||||
<el-table-column prop="paper_title" :label="$t('expertDatabase.detailColPaper')" min-width="220" />
|
||||
<el-table-column prop="paper_journal" :label="$t('expertDatabase.detailColJournal')" min-width="140" />
|
||||
</el-table>
|
||||
<p v-else class="field-detail-empty">{{ $t('expertDatabase.noFieldDetail') }}</p>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="fieldDetailVisible = false">{{ $t('expertDatabase.detailClose') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
background
|
||||
@@ -124,7 +183,11 @@ export default {
|
||||
list: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
exportLoading: false
|
||||
exportLoading: false,
|
||||
switchingExpertId: '',
|
||||
fieldDetailVisible: false,
|
||||
fieldDetailExpert: null,
|
||||
fieldDetailRows: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -187,8 +250,12 @@ export default {
|
||||
this.list = rawList.map((item) => {
|
||||
const fieldArray = item.fields || [];
|
||||
const fieldNames = fieldArray.map((f) => f.field).join(', ');
|
||||
const unsubscribed = this.normalizeUnsubscribeValue(item);
|
||||
return {
|
||||
...item,
|
||||
// 以 unsubscribed 作为页面主判断字段
|
||||
unsubscribed,
|
||||
unsubscribe: unsubscribed,
|
||||
fieldDisplay: fieldNames
|
||||
};
|
||||
});
|
||||
@@ -204,6 +271,57 @@ export default {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
getExpertId(row) {
|
||||
if (!row) return '';
|
||||
return String(row.expert_id || row.id || row.user_id || row.uid || '').trim();
|
||||
},
|
||||
normalizeUnsubscribeValue(row) {
|
||||
const raw =
|
||||
row && row.unsubscribed != null
|
||||
? row.unsubscribed
|
||||
: row && row.unsubscribe != null
|
||||
? row.unsubscribe
|
||||
: row && row.is_unsubscribe != null
|
||||
? row.is_unsubscribe
|
||||
: 0;
|
||||
return Number(raw) === 1 ? 1 : 0;
|
||||
},
|
||||
getUnsubscribeValue(row) {
|
||||
return this.normalizeUnsubscribeValue(row);
|
||||
},
|
||||
async handleUnsubscribeSwitch(row, checked) {
|
||||
const expertId = this.getExpertId(row);
|
||||
if (!expertId) {
|
||||
this.$message.warning(this.$t('expertDatabase.unsubscribeMissingId'));
|
||||
return;
|
||||
}
|
||||
const oldValue = this.getUnsubscribeValue(row);
|
||||
// switch: ON(checked)=正常(0), OFF=退订(1)
|
||||
const nextValue = checked ? 0 : 1;
|
||||
if (oldValue === nextValue) return;
|
||||
row.unsubscribed = nextValue;
|
||||
row.unsubscribe = nextValue;
|
||||
this.switchingExpertId = expertId;
|
||||
try {
|
||||
const apiUrl = nextValue === 1 ? 'api/expert_manage/unsubscribe' : 'api/expert_manage/resubscribe';
|
||||
const res = await this.$api.post(apiUrl, {
|
||||
expert_ids: String(expertId)
|
||||
});
|
||||
if (res && Number(res.code) === 0) {
|
||||
this.$message.success(this.$t('expertDatabase.unsubscribeUpdateSuccess'));
|
||||
} else {
|
||||
row.unsubscribed = oldValue;
|
||||
row.unsubscribe = oldValue;
|
||||
this.$message.error((res && res.msg) || this.$t('expertDatabase.unsubscribeUpdateFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
row.unsubscribed = oldValue;
|
||||
row.unsubscribe = oldValue;
|
||||
this.$message.error(this.$t('expertDatabase.unsubscribeUpdateFailed'));
|
||||
} finally {
|
||||
this.switchingExpertId = '';
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.query.pageIndex = 1;
|
||||
this.fetchList();
|
||||
@@ -228,6 +346,17 @@ export default {
|
||||
this.query.pageIndex = page;
|
||||
this.fetchList();
|
||||
},
|
||||
openFieldDetail(row) {
|
||||
const empty = this.$t('expertDatabase.detailCellEmpty');
|
||||
this.fieldDetailExpert = row || null;
|
||||
const fields = (row && row.fields) || [];
|
||||
this.fieldDetailRows = fields.map((f) => ({
|
||||
field: (f && f.field) || empty,
|
||||
paper_title: (f && (f.paper_title || f.title)) || empty,
|
||||
paper_journal: (f && (f.paper_journal || f.journal)) || empty
|
||||
}));
|
||||
this.fieldDetailVisible = true;
|
||||
},
|
||||
async handleExport() {
|
||||
if (!this.query.major_id && !this.query.keyword && !this.query.field) {
|
||||
this.$message.warning(this.$t('expertDatabase.exportWarn'));
|
||||
@@ -330,5 +459,37 @@ export default {
|
||||
.value.time {
|
||||
color: #888;
|
||||
}
|
||||
.view-all-btn {
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.field-detail-name {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.field-detail-empty {
|
||||
margin: 16px 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
.unsubscribe-cell {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.unsubscribe-tag {
|
||||
min-width: 56px;
|
||||
height: 22px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.unsubscribe-switch {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -171,6 +171,7 @@ import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
baseUrl: this.Common.baseUrl,
|
||||
currentFolder: 'inbox',
|
||||
searchKeyword: '',
|
||||
syncLoading: false,
|
||||
@@ -645,7 +646,7 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
},
|
||||
buildSseUrl(jEmailId) {
|
||||
// 与现有 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();
|
||||
return `${base}?${q}`;
|
||||
},
|
||||
|
||||
@@ -147,6 +147,12 @@ const API = {
|
||||
deleteTemplate: 'api/mail_template/deleteTemplate',
|
||||
deleteStyle: 'api/mail_template/deleteStyle'
|
||||
};
|
||||
// 仅在当前 SPA 会话内记忆筛选(刷新页面即重置)
|
||||
const mailboxMouldSessionMemory = {
|
||||
journalId: '',
|
||||
scene: '',
|
||||
language: ''
|
||||
};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -203,8 +209,15 @@ export default {
|
||||
}));
|
||||
this.journalList = mapped;
|
||||
if (mapped.length > 0) {
|
||||
this.tplFilters.journalId = String(mapped[0].journal_id);
|
||||
const rememberedJournalId = String(mailboxMouldSessionMemory.journalId || '').trim();
|
||||
const hasRemembered = rememberedJournalId && mapped.some(j => String(j.journal_id) === rememberedJournalId);
|
||||
this.tplFilters.journalId = hasRemembered
|
||||
? rememberedJournalId
|
||||
: String(mapped[0].journal_id);
|
||||
}
|
||||
this.tplFilters.scene = String(mailboxMouldSessionMemory.scene || '');
|
||||
this.tplFilters.language = String(mailboxMouldSessionMemory.language || '');
|
||||
this.syncTplFilterMemory();
|
||||
if (this.activeTab === 'styles') {
|
||||
this.fetchStyles();
|
||||
} else {
|
||||
@@ -226,6 +239,11 @@ export default {
|
||||
},
|
||||
|
||||
// ========== Templates ==========
|
||||
syncTplFilterMemory() {
|
||||
mailboxMouldSessionMemory.journalId = String((this.tplFilters && this.tplFilters.journalId) || '');
|
||||
mailboxMouldSessionMemory.scene = String((this.tplFilters && this.tplFilters.scene) || '');
|
||||
mailboxMouldSessionMemory.language = String((this.tplFilters && this.tplFilters.language) || '');
|
||||
},
|
||||
fetchTemplates() {
|
||||
this.tplLoading = true;
|
||||
const params = {
|
||||
@@ -233,6 +251,7 @@ export default {
|
||||
scene: this.tplFilters.scene || '',
|
||||
language: this.tplFilters.language || ''
|
||||
};
|
||||
this.syncTplFilterMemory();
|
||||
this.$api
|
||||
.post(API.listTemplates, params)
|
||||
.then(res => {
|
||||
@@ -257,11 +276,13 @@ export default {
|
||||
});
|
||||
},
|
||||
handleCreateTemplate() {
|
||||
this.syncTplFilterMemory();
|
||||
// 传入当前模板列表选中的期刊,详情页用于默认回填
|
||||
const journalId = this.tplFilters && this.tplFilters.journalId ? String(this.tplFilters.journalId) : '';
|
||||
this.$router.push({ path: '/mailboxMouldDetail', query: journalId ? { journal_id: journalId } : {} });
|
||||
},
|
||||
handleEditTemplate(row) {
|
||||
this.syncTplFilterMemory();
|
||||
const templateId = row && (row.template_id || row.id);
|
||||
const journalId = this.tplFilters && this.tplFilters.journalId ? String(this.tplFilters.journalId) : '';
|
||||
const query = templateId ? { template_id: String(templateId) } : {};
|
||||
@@ -274,6 +295,7 @@ export default {
|
||||
this.previewVisible = true;
|
||||
},
|
||||
handleDeleteTemplate(row) {
|
||||
this.syncTplFilterMemory();
|
||||
const templateId = row && (row.template_id || row.id);
|
||||
if (!templateId) return;
|
||||
this.$confirm(this.$t('mailboxMould.deleteConfirm'), this.$t('mailboxMould.colActions'), {
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
v-model="form.body"
|
||||
:journalList="journalList"
|
||||
:journalId="form.journalId"
|
||||
:language="form.lang"
|
||||
placeholder=""
|
||||
/> -->
|
||||
<CkeditorMail v-model="form.body" />
|
||||
|
||||
@@ -424,6 +424,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../common/bus'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -435,8 +436,7 @@ export default {
|
||||
baseUrl: this.Common.baseUrl,
|
||||
mediaUrl: this.Common.mediaUrl,
|
||||
query: {
|
||||
// journal_id: '',
|
||||
// year: 0,
|
||||
journal_id: 0,
|
||||
keywords: '',
|
||||
page: 1,
|
||||
limit: 15
|
||||
@@ -472,6 +472,23 @@ export default {
|
||||
this.initMajor();
|
||||
},
|
||||
methods: {
|
||||
readPromotionFactoryJumpJournalId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('promotionFactoryJump');
|
||||
if (!raw) return '';
|
||||
const obj = JSON.parse(raw || '{}');
|
||||
if (!obj || obj.from !== 'promotionFactory') return '';
|
||||
if (obj.targetPath && String(obj.targetPath) !== String(this.$route.path)) return '';
|
||||
const id = obj.journal_id != null ? String(obj.journal_id) : '';
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
return id;
|
||||
} catch (e) {
|
||||
try {
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
} catch (e2) {}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
goDetail(row) {
|
||||
console.log('row at line 460:', row);
|
||||
this.$router.push({
|
||||
@@ -518,8 +535,31 @@ export default {
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.jourList = res.data.journals;
|
||||
this.query.journal_id = this.jourList[0].journal_id;
|
||||
const fromQueryId = this.$route && this.$route.query ? this.$route.query.journal_id : '';
|
||||
const fromSessionId = this.readPromotionFactoryJumpJournalId();
|
||||
const pickedId = fromQueryId || fromSessionId;
|
||||
const matched = pickedId ? this.jourList.find((j) => String(j.journal_id) === String(pickedId)) : null;
|
||||
this.query.journal_id = matched ? matched.journal_id : this.jourList[0].journal_id;
|
||||
this.getData();
|
||||
|
||||
// 仅首次接收参数:应用后立刻清掉 URL,避免刷新仍然固定某一本期刊
|
||||
if (fromQueryId) {
|
||||
const oldFullPath = this.$route && this.$route.fullPath ? this.$route.fullPath : '';
|
||||
const nextQuery = Object.assign({}, (this.$route && this.$route.query) || {});
|
||||
delete nextQuery.journal_id;
|
||||
delete nextQuery.journalId;
|
||||
delete nextQuery._ap_from;
|
||||
if (oldFullPath) {
|
||||
bus.$emit('close_tag_by_path', { path: oldFullPath, silent: true });
|
||||
}
|
||||
this.$router.replace({ query: nextQuery }).catch(() => {});
|
||||
// 兜底:某些场景 router.replace 不更新地址栏,这里强制清掉 query
|
||||
try {
|
||||
const u = new URL(window.location.href);
|
||||
['_ap_from', 'journal_id', 'journalId', 'journalIdd'].forEach((k) => u.searchParams.delete(k));
|
||||
window.history.replaceState({}, '', u.toString());
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -478,6 +478,7 @@ const currentYear = new Date().getFullYear();
|
||||
var ReviewTime = `(${currentYear - 2}–${currentYear})`;
|
||||
import commonReviewer from '../page/components/reviewerList/add.vue';
|
||||
import commonMajorTableList from '../page/components/major/tableList.vue';
|
||||
import bus from '../common/bus'
|
||||
export default {
|
||||
components: {
|
||||
commonReviewer,
|
||||
@@ -657,11 +658,27 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.getDate();
|
||||
this.getContent();
|
||||
this.loadFields();
|
||||
// this.initMajor()
|
||||
},
|
||||
methods: {
|
||||
readPromotionFactoryJumpJournalId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('promotionFactoryJump');
|
||||
if (!raw) return '';
|
||||
const obj = JSON.parse(raw || '{}');
|
||||
if (!obj || obj.from !== 'promotionFactory') return '';
|
||||
if (obj.targetPath && String(obj.targetPath) !== String(this.$route.path)) return '';
|
||||
const id = obj.journal_id != null ? String(obj.journal_id) : '';
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
return id;
|
||||
} catch (e) {
|
||||
try {
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
} catch (e2) {}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
getProps() {
|
||||
return {
|
||||
value: 'value',
|
||||
@@ -716,6 +733,32 @@ export default {
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.df_jour = res.data.journals;
|
||||
const fromQueryId = (this.$route && this.$route.query && (this.$route.query.journalId || this.$route.query.journal_id)) || '';
|
||||
const fromSessionId = this.readPromotionFactoryJumpJournalId();
|
||||
const pickedId = fromQueryId || fromSessionId;
|
||||
if (pickedId) {
|
||||
this.query.journalId = Number(pickedId) || 0;
|
||||
}
|
||||
this.getContent();
|
||||
|
||||
// 仅首次接收参数:应用后立刻清掉 URL,避免刷新仍然固定某一本期刊
|
||||
if (fromQueryId) {
|
||||
const oldFullPath = this.$route && this.$route.fullPath ? this.$route.fullPath : '';
|
||||
const nextQuery = Object.assign({}, (this.$route && this.$route.query) || {});
|
||||
delete nextQuery.journal_id;
|
||||
delete nextQuery.journalId;
|
||||
delete nextQuery._ap_from;
|
||||
if (oldFullPath) {
|
||||
bus.$emit('close_tag_by_path', { path: oldFullPath, silent: true });
|
||||
}
|
||||
this.$router.replace({ query: nextQuery }).catch(() => {});
|
||||
// 兜底:某些场景 router.replace 不更新地址栏,这里强制清掉 query
|
||||
try {
|
||||
const u = new URL(window.location.href);
|
||||
['_ap_from', 'journal_id', 'journalId', 'journalIdd'].forEach((k) => u.searchParams.delete(k));
|
||||
window.history.replaceState({}, '', u.toString());
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
|
||||
<script>
|
||||
import commonCv from '../common/cv.vue';
|
||||
import bus from '../common/bus';
|
||||
export default {
|
||||
components: {
|
||||
commonCv
|
||||
@@ -362,6 +363,7 @@ export default {
|
||||
for (var i = 0; i < this.tableData.length; i++) {
|
||||
this.getScoreData(i, this.tableData[i].score);
|
||||
}
|
||||
bus.$emit('apply-badge-refresh');
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -248,6 +248,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../common/bus'
|
||||
export default {
|
||||
name: 'youthList',
|
||||
data() {
|
||||
@@ -326,6 +327,21 @@
|
||||
this.yearData()
|
||||
},
|
||||
methods: {
|
||||
readPromotionFactoryJumpJournalId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('promotionFactoryJump')
|
||||
if (!raw) return ''
|
||||
const obj = JSON.parse(raw || '{}')
|
||||
if (!obj || obj.from !== 'promotionFactory') return ''
|
||||
if (obj.targetPath && String(obj.targetPath) !== String(this.$route.path)) return ''
|
||||
const id = obj.journal_id != null ? String(obj.journal_id) : ''
|
||||
sessionStorage.removeItem('promotionFactoryJump')
|
||||
return id
|
||||
} catch (e) {
|
||||
try { sessionStorage.removeItem('promotionFactoryJump') } catch (e2) {}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
// 获取当年时间
|
||||
yearData() {
|
||||
this.YearThis = new Date().getFullYear()
|
||||
@@ -347,8 +363,33 @@
|
||||
.then(res => {
|
||||
if (res.code == 0) {
|
||||
this.df_jour = res.data.journals;
|
||||
this.query.journal_id = this.df_jour[0].journal_id
|
||||
const fromQueryId =
|
||||
(this.$route && this.$route.query && (this.$route.query.journal_id || this.$route.query.journalId)) || ''
|
||||
const fromSessionId = this.readPromotionFactoryJumpJournalId()
|
||||
const pickedId = fromQueryId || fromSessionId
|
||||
const matched = pickedId ? this.df_jour.find(j => String(j.journal_id) === String(pickedId)) : null
|
||||
this.query.journal_id = matched ? matched.journal_id : this.df_jour[0].journal_id
|
||||
this.getDate();
|
||||
|
||||
// 仅首次接收参数:应用后立刻清掉 URL,避免刷新仍然固定某一本期刊
|
||||
if (fromQueryId) {
|
||||
const oldFullPath = this.$route && this.$route.fullPath ? this.$route.fullPath : ''
|
||||
const nextQuery = Object.assign({}, (this.$route && this.$route.query) || {})
|
||||
delete nextQuery.journal_id
|
||||
delete nextQuery.journalId
|
||||
delete nextQuery.journalIdd
|
||||
delete nextQuery._ap_from
|
||||
if (oldFullPath) {
|
||||
bus.$emit('close_tag_by_path', { path: oldFullPath, silent: true })
|
||||
}
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => {})
|
||||
// 兜底:某些场景 router.replace 不更新地址栏,这里强制清掉 query
|
||||
try {
|
||||
const u = new URL(window.location.href)
|
||||
;['_ap_from', 'journal_id', 'journalId', 'journalIdd'].forEach(k => u.searchParams.delete(k))
|
||||
window.history.replaceState({}, '', u.toString())
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user