2.3.0-rc2

This commit is contained in:
2026-08-04 10:48:08 +08:00 Unverified
parent 6735a6d3fc
commit 10b51eb08a
19 changed files with 7292 additions and 7 deletions
+169
View File
@@ -0,0 +1,169 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Check, EditPen, Refresh } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
const loading = ref(true)
const saving = ref(false)
const editing = ref(false)
const profile = reactive<Record<string, any>>({})
const form = reactive<Record<string, any>>({})
const genderLabels: Record<string, string> = {
Unknown: '未设置', Male: '男', Female: '女',
}
const statusLabels: Record<string, string> = {
Active: '在籍', Suspended: '休学', Graduated: '毕业', Withdrawn: '退学',
}
const editableFields = [
'gender', 'dateOfBirth', 'englishName', 'idCardNumber', 'nationality',
'ethnicity', 'politicalStatus', 'nativePlace', 'householdAddress',
'currentAddress', 'postalCode', 'phone', 'email', 'qq', 'weChat',
'emergencyContactName', 'emergencyContactRelationship',
'emergencyContactPhone', 'specialTags', 'specialNeeds', 'biography',
]
function copyToForm() {
for (const key of editableFields) form[key] = profile[key] ?? ''
}
async function load() {
loading.value = true
try {
Object.assign(profile, (await http.get('/student/profile')).data)
copyToForm()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
}
function startEdit() {
copyToForm()
editing.value = true
}
function cancelEdit() {
copyToForm()
editing.value = false
}
async function save() {
saving.value = true
try {
await http.put('/student/profile', form)
ElMessage.success('个人信息已保存,无需审核。')
editing.value = false
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<template>
<div class="profile-page" v-loading="loading">
<section class="profile-hero">
<div>
<span>STUDENT PROFILE</span>
<h2>我的个人信息</h2>
<p>完善个人资料联系方式与需要任课教师关注的特殊情况</p>
</div>
<div class="profile-actions">
<el-button :icon="Refresh" @click="load">刷新</el-button>
<el-button v-if="!editing" type="primary" :icon="EditPen" @click="startEdit">修改信息</el-button>
<template v-else>
<el-button @click="cancelEdit">取消</el-button>
<el-button type="primary" :icon="Check" :loading="saving" @click="save">直接保存</el-button>
</template>
</div>
</section>
<el-alert
title="联系电话、电子邮箱、微信、紧急联系人以及特殊标记/说明会向你的任课教师展示,并写入教学班名单导出文件。"
type="info"
:closable="false"
show-icon
/>
<section class="identity-strip">
<div><span>学号</span><b>{{ profile.studentNumber }}</b><small>不可自行修改</small></div>
<div><span>姓名</span><b>{{ profile.name }}</b><small>不可自行修改</small></div>
<div><span>学院 / 专业</span><b>{{ profile.collegeName }}</b><small>{{ profile.majorName }}</small></div>
<div><span>行政班 / 学籍</span><b>{{ profile.className }}</b><small>{{ statusLabels[profile.status] }} · {{ profile.enrollmentYear }} </small></div>
</section>
<el-form :model="form" label-position="top" class="profile-form" :disabled="!editing">
<article class="profile-card">
<header><span>01</span><div><h3>身份与背景</h3><p>除学号和姓名外可直接维护常用身份信息</p></div></header>
<div class="form-grid">
<el-form-item label="英文姓名"><el-input v-model="form.englishName" maxlength="100" /></el-form-item>
<el-form-item label="性别"><el-select v-model="form.gender"><el-option v-for="(label, value) in genderLabels" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item label="出生日期"><el-date-picker v-model="form.dateOfBirth" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="证件号码"><el-input v-model="form.idCardNumber" maxlength="30" /></el-form-item>
<el-form-item label="国籍"><el-input v-model="form.nationality" maxlength="50" /></el-form-item>
<el-form-item label="民族"><el-input v-model="form.ethnicity" maxlength="50" /></el-form-item>
<el-form-item label="政治面貌"><el-input v-model="form.politicalStatus" maxlength="50" /></el-form-item>
<el-form-item label="籍贯"><el-input v-model="form.nativePlace" maxlength="100" /></el-form-item>
</div>
</article>
<article class="profile-card">
<header><span>02</span><div><h3>联系方式与地址</h3><p>任课教师仅能看到本区的电话邮箱和微信</p></div></header>
<div class="form-grid">
<el-form-item label="联系电话"><el-input v-model="form.phone" maxlength="30" /></el-form-item>
<el-form-item label="电子邮箱"><el-input v-model="form.email" maxlength="100" /></el-form-item>
<el-form-item label="微信"><el-input v-model="form.weChat" maxlength="60" /></el-form-item>
<el-form-item label="QQ"><el-input v-model="form.qq" maxlength="30" /></el-form-item>
<el-form-item label="邮政编码"><el-input v-model="form.postalCode" maxlength="20" /></el-form-item>
<el-form-item label="户籍地址" class="span-2"><el-input v-model="form.householdAddress" maxlength="300" /></el-form-item>
<el-form-item label="现居住地址" class="span-2"><el-input v-model="form.currentAddress" maxlength="300" /></el-form-item>
</div>
</article>
<article class="profile-card attention-card">
<header><span>03</span><div><h3>紧急联系与特殊标记</h3><p>本区内容会向任课教师展示请填写真实且必要的信息</p></div></header>
<div class="form-grid">
<el-form-item label="紧急联系人"><el-input v-model="form.emergencyContactName" maxlength="50" /></el-form-item>
<el-form-item label="与本人关系"><el-input v-model="form.emergencyContactRelationship" maxlength="30" /></el-form-item>
<el-form-item label="紧急联系电话"><el-input v-model="form.emergencyContactPhone" maxlength="30" /></el-form-item>
<el-form-item label="特殊标记"><el-input v-model="form.specialTags" maxlength="300" placeholder="多个标记可用逗号分隔" /></el-form-item>
<el-form-item label="特殊情况说明" class="span-2"><el-input v-model="form.specialNeeds" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="例如学习支持、健康与安全方面需教师留意的事项" /></el-form-item>
<el-form-item label="个人简介" class="span-2"><el-input v-model="form.biography" type="textarea" :rows="4" maxlength="1000" show-word-limit /></el-form-item>
</div>
</article>
</el-form>
</div>
</template>
<style scoped>
.profile-page { display: grid; gap: 18px; }
.profile-hero { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 26px 30px; border-radius: 18px; color: white; background: linear-gradient(125deg, #173a50, #176b87 62%, #2e9b91); box-shadow: 0 18px 45px rgba(23, 58, 80, .16); }
.profile-hero span { color: #8ee4d8; font-size: 11px; font-weight: 800; letter-spacing: .12em; }
.profile-hero h2 { margin: 7px 0 5px; font-size: 30px; font-family: "STZhongsong", "Songti SC", serif; }
.profile-hero p { margin: 0; color: rgba(255,255,255,.75); }
.profile-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; }
.identity-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--line); background: white; }
.identity-strip > div { display: grid; gap: 5px; padding: 18px 20px; border-right: 1px solid var(--line); }
.identity-strip > div:last-child { border-right: 0; }
.identity-strip span, .identity-strip small { color: var(--muted); font-size: 12px; }
.identity-strip b { color: #263445; }
.profile-form { display: grid; gap: 18px; }
.profile-card { padding: 24px; border: 1px solid var(--line); border-radius: 14px; background: white; box-shadow: 0 10px 28px rgba(35,57,78,.05); }
.profile-card > header { display: flex; gap: 14px; margin-bottom: 22px; padding-bottom: 16px; border-bottom: 1px solid #edf0f3; }
.profile-card > header > span { color: var(--teal); font: 800 12px/1 Consolas, monospace; }
.profile-card h3 { margin: 0 0 5px; color: #263445; }
.profile-card p { margin: 0; color: var(--muted); font-size: 12px; }
.attention-card { border-top: 3px solid #d89b36; }
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 18px; }
.span-2 { grid-column: span 2; }
:deep(.el-form.is-disabled .el-input__wrapper), :deep(.el-form.is-disabled .el-textarea__inner) { background: #f8fafb; color: #364655; }
@media (max-width: 900px) { .identity-strip { grid-template-columns: repeat(2, 1fr); } .form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px) { .profile-hero { align-items: flex-start; flex-direction: column; padding: 22px; } .profile-actions { justify-content: flex-start; } .identity-strip, .form-grid { grid-template-columns: 1fr; } .identity-strip > div { border-right: 0; border-bottom: 1px solid var(--line); } .span-2 { grid-column: auto; } .profile-card { padding: 18px; } }
</style>