613 lines
19 KiB
Vue
613 lines
19 KiB
Vue
<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>
|