Files
tougao_web/src/components/page/YouthEditorialBoardRegistration.vue

613 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>