2.3.0-rc2
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "2.3.0-rc1",
|
||||
"version": "2.3.0-rc2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -212,6 +212,10 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
||||
key: 'student-status',
|
||||
label: '学籍管理',
|
||||
items: [
|
||||
...whenVisible(
|
||||
isStudent.value,
|
||||
{ path: '/my-profile', label: '个人信息' },
|
||||
),
|
||||
...whenVisible(
|
||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student']),
|
||||
{ path: '/student-status-changes', label: isStudent.value ? '学籍异动' : '异动审核' },
|
||||
|
||||
@@ -58,6 +58,12 @@ const router = createRouter({
|
||||
name: 'account',
|
||||
component: () => import('../views/AccountView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'my-profile',
|
||||
name: 'my-profile',
|
||||
component: () => import('../views/StudentProfileView.vue'),
|
||||
meta: { roles: ['Student'] },
|
||||
},
|
||||
{
|
||||
path: 'base-data',
|
||||
redirect: '/base-data/organization',
|
||||
|
||||
@@ -133,8 +133,25 @@ function resetForm(row?: any) {
|
||||
enrollmentDate: `${currentYear}-09-01`,
|
||||
status: 'Active',
|
||||
dateOfBirth: '',
|
||||
englishName: '',
|
||||
idCardNumber: '',
|
||||
nationality: '',
|
||||
ethnicity: '',
|
||||
politicalStatus: '',
|
||||
nativePlace: '',
|
||||
householdAddress: '',
|
||||
currentAddress: '',
|
||||
postalCode: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
qq: '',
|
||||
weChat: '',
|
||||
emergencyContactName: '',
|
||||
emergencyContactRelationship: '',
|
||||
emergencyContactPhone: '',
|
||||
specialTags: '',
|
||||
specialNeeds: '',
|
||||
biography: '',
|
||||
notes: '',
|
||||
}, row ?? {})
|
||||
}
|
||||
@@ -516,7 +533,7 @@ watch(active, async () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${pageTitle}`" width="620px">
|
||||
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${pageTitle}`" width="min(860px, 94vw)">
|
||||
<el-form label-position="top" class="entity-form">
|
||||
<template v-if="active === 'teachers'">
|
||||
<div class="form-grid">
|
||||
@@ -580,6 +597,38 @@ watch(active, async () => {
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="出生日期"><el-date-picker v-model="form.dateOfBirth" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-divider content-position="left">个人资料</el-divider>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="英文姓名"><el-input v-model="form.englishName" /></el-form-item>
|
||||
<el-form-item label="证件号码"><el-input v-model="form.idCardNumber" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="国籍"><el-input v-model="form.nationality" /></el-form-item>
|
||||
<el-form-item label="民族"><el-input v-model="form.ethnicity" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="政治面貌"><el-input v-model="form.politicalStatus" /></el-form-item>
|
||||
<el-form-item label="籍贯"><el-input v-model="form.nativePlace" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="户籍地址"><el-input v-model="form.householdAddress" /></el-form-item>
|
||||
<el-form-item label="现居住地址"><el-input v-model="form.currentAddress" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="邮政编码"><el-input v-model="form.postalCode" /></el-form-item>
|
||||
<el-form-item label="微信"><el-input v-model="form.weChat" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="QQ"><el-input v-model="form.qq" /></el-form-item>
|
||||
<el-form-item label="紧急联系人"><el-input v-model="form.emergencyContactName" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="与本人关系"><el-input v-model="form.emergencyContactRelationship" /></el-form-item>
|
||||
<el-form-item label="紧急联系电话"><el-input v-model="form.emergencyContactPhone" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="特殊标记"><el-input v-model="form.specialTags" placeholder="多个标记可用逗号分隔" /></el-form-item>
|
||||
<el-form-item label="特殊情况说明"><el-input v-model="form.specialNeeds" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item label="个人简介"><el-input v-model="form.biography" type="textarea" :rows="3" /></el-form-item>
|
||||
</template>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="联系电话"><el-input v-model="form.phone" /></el-form-item>
|
||||
|
||||
@@ -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>
|
||||
@@ -118,6 +118,31 @@ onMounted(async () => {
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="className" label="班级" min-width="130" />
|
||||
<el-table-column prop="majorName" label="专业" min-width="150" />
|
||||
<el-table-column label="联系方式" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div class="contact-cell">
|
||||
<span>{{ row.phone || '未填写电话' }}</span>
|
||||
<small>{{ row.email || row.weChat ? [row.email, row.weChat && `微信 ${row.weChat}`].filter(Boolean).join(' · ') : '未填写邮箱或微信' }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="紧急联系人" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="contact-cell">
|
||||
<span>{{ row.emergencyContactName || '—' }}<template v-if="row.emergencyContactRelationship">({{ row.emergencyContactRelationship }})</template></span>
|
||||
<small>{{ row.emergencyContactPhone || '未填写联系电话' }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="特殊标记" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div class="special-cell">
|
||||
<el-tag v-if="row.specialTags" type="warning" effect="light">{{ row.specialTags }}</el-tag>
|
||||
<span v-else>—</span>
|
||||
<small v-if="row.specialNeeds">{{ row.specialNeeds }}</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名单来源" width="210">
|
||||
<template #default="{ row }">{{ row.enrolledAt ? `选课 · ${new Date(row.enrolledAt).toLocaleString('zh-CN')}` : '行政班关联' }}</template>
|
||||
</el-table-column>
|
||||
@@ -156,6 +181,9 @@ onMounted(async () => {
|
||||
.roster-head h3 { margin: 5px 0 4px; font-size: 18px; }
|
||||
.roster-head p { color: var(--muted); font-size: 12px; margin: 0; }
|
||||
.roster-head span { color: var(--teal); font-size: 10px; font-weight: 700; }
|
||||
.contact-cell, .special-cell { display: grid; gap: 4px; line-height: 1.35; }
|
||||
.contact-cell small, .special-cell small { color: var(--muted); white-space: normal; }
|
||||
.special-cell .el-tag { width: fit-content; max-width: 100%; white-space: normal; height: auto; padding-block: 3px; }
|
||||
@media (max-width: 760px) {
|
||||
.roster-workspace { grid-template-columns: 1fr; }
|
||||
.roster-task-list { max-height: 220px; border-right: none; border-bottom: 1px solid var(--line); }
|
||||
|
||||
Reference in New Issue
Block a user