kindergarten_java/reading-platform-frontend/src/views/profile/ProfileView.vue
En 6f64723428 feat: 教师端数据看板与学校端课程统计功能
教师端数据看板:
- 新增 TeacherDashboardResponse/TeacherLessonVO/TeacherLessonTrendVO
- 新增 TeacherWeeklyStatsResponse 周统计响应
- 新增 TeacherActivityLevel 枚举和 TeacherActivityRankResponse 活跃度排行
- 实现教师端课程统计、任务完成详情、任务反馈接口

学校端课程统计:
- 新增 CourseUsageVO/CourseUsageStatsVO/CoursePackageVO
- 新增 SchoolCourseResponse 和学校端课程使用查询接口
- 实现学校端统计数据和课程趋势接口

用户资料功能:
- 新增 UpdateProfileRequest/UpdateProfileResponse
- 实现用户资料更新接口

前后端对齐:
- 更新 OpenAPI 规范和前端 API 类型生成
- 优化 DashboardView 组件和 API 调用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 12:45:56 +08:00

335 lines
8.6 KiB
Vue

<template>
<div class="profile-view">
<div class="page-header">
<h1>
<UserOutlined /> 个人信息
</h1>
<p>查看和修改您的账户信息</p>
</div>
<a-spin :spinning="loading">
<div class="profile-content" v-if="profile">
<div class="profile-card">
<div class="avatar-section">
<a-avatar :size="96" class="profile-avatar">
<img v-if="avatarUrl" :src="avatarUrl" alt="头像" />
<UserOutlined v-else />
</a-avatar>
</div>
<div class="info-section">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="姓名">
{{ profile.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="账号">
{{ profile.username || '-' }}
</a-descriptions-item>
<a-descriptions-item label="角色">
{{ roleLabel }}
</a-descriptions-item>
<a-descriptions-item label="手机号">
{{ profile.phone || '-' }}
</a-descriptions-item>
<a-descriptions-item label="邮箱">
{{ profile.email || '-' }}
</a-descriptions-item>
</a-descriptions>
</div>
<div class="action-section">
<a-button type="primary" @click="enterEditMode">
<EditOutlined /> 编辑资料
</a-button>
<a-button @click="showChangePasswordModal" style="margin-left: 12px">
<LockOutlined /> 修改密码
</a-button>
</div>
</div>
</div>
<a-empty v-else-if="!loading" description="加载失败,请刷新重试" />
</a-spin>
<!-- 编辑资料弹窗 -->
<a-modal
v-model:open="editModalOpen"
title="编辑个人信息"
@ok="submitEdit"
:confirmLoading="editLoading"
width="480px"
>
<a-form
:model="editForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item
label="姓名"
name="name"
:rules="[
{ required: true, message: '请输入姓名' },
{ pattern: /^[\u4e00-\u9fa5a-zA-Z\s]{2,20}$/, message: '姓名长度为 2-20 位,只能包含中文或英文' }
]"
>
<a-input v-model:value="editForm.name" placeholder="请输入姓名" />
</a-form-item>
<a-form-item
label="手机号"
name="phone"
:rules="[
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
]"
>
<a-input v-model:value="editForm.phone" placeholder="请输入手机号" />
</a-form-item>
<a-form-item
label="邮箱"
name="email"
:rules="[
{ type: 'email', message: '请输入正确的邮箱格式' }
]"
>
<a-input v-model:value="editForm.email" placeholder="请输入邮箱" />
</a-form-item>
</a-form>
</a-modal>
<!-- 修改密码弹窗 -->
<a-modal
v-model:open="passwordModalOpen"
title="修改密码"
@ok="submitChangePassword"
:confirmLoading="passwordLoading"
width="480px"
>
<a-form
:model="passwordForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item
label="旧密码"
name="oldPassword"
:rules="[{ required: true, message: '请输入旧密码' }]"
>
<a-input-password v-model:value="passwordForm.oldPassword" placeholder="请输入旧密码" />
</a-form-item>
<a-form-item
label="新密码"
name="newPassword"
:rules="[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码长度不能少于 6 位' }
]"
>
<a-input-password v-model:value="passwordForm.newPassword" placeholder="请输入新密码" />
</a-form-item>
<a-form-item
label="确认密码"
name="confirmPassword"
:rules="[
{ required: true, message: '请确认新密码' },
{ validator: validateConfirmPassword }
]"
>
<a-input-password v-model:value="passwordForm.confirmPassword" placeholder="请再次输入新密码" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { UserOutlined, EditOutlined, LockOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { getProfile, type UserProfile, updateProfile, changePassword, type UpdateProfileDto } from '@/api/auth';
import { useRouter } from 'vue-router';
const router = useRouter();
const loading = ref(false);
const profile = ref<UserProfile | null>(null);
// 编辑资料相关
const editModalOpen = ref(false);
const editLoading = ref(false);
const editForm = ref<UpdateProfileDto>({
name: '',
phone: '',
email: '',
});
// 修改密码相关
const passwordModalOpen = ref(false);
const passwordLoading = ref(false);
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
const avatarUrl = computed(() => {
const p = profile.value;
if (!p) return '';
return (p.avatarUrl || p.avatar || '') as string;
});
const roleLabel = computed(() => {
const role = profile.value?.role;
const map: Record<string, string> = {
admin: '超管',
school: '学校管理员',
teacher: '教师',
parent: '家长',
};
return role ? map[role] || role : '-';
});
const loadProfile = async () => {
loading.value = true;
try {
const data = await getProfile();
profile.value = data as UserProfile;
} catch (error: any) {
console.error('获取个人信息失败', error);
} finally {
loading.value = false;
}
};
// 进入编辑模式
const enterEditMode = () => {
if (profile.value) {
editForm.value = {
name: profile.value.name || '',
phone: profile.value.phone || '',
email: profile.value.email || '',
};
}
editModalOpen.value = true;
};
// 提交编辑
const submitEdit = async () => {
try {
editLoading.value = true;
await updateProfile(editForm.value);
message.success('修改成功');
editModalOpen.value = false;
await loadProfile();
} catch (error: any) {
console.error('修改失败', error);
message.error(error.message || '修改失败,请重试');
} finally {
editLoading.value = false;
}
};
// 显示修改密码弹窗
const showChangePasswordModal = () => {
passwordForm.value = {
oldPassword: '',
newPassword: '',
confirmPassword: '',
};
passwordModalOpen.value = true;
};
// 验证确认密码
const validateConfirmPassword = async (_rule: any, value: string) => {
if (value !== passwordForm.value.newPassword) {
throw new Error('两次输入的密码不一致');
}
};
// 提交修改密码
const submitChangePassword = async () => {
try {
passwordLoading.value = true;
await changePassword(passwordForm.value.oldPassword, passwordForm.value.newPassword);
message.success('密码修改成功,请重新登录');
passwordModalOpen.value = false;
// 清除本地存储并跳转到登录页
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
setTimeout(() => {
router.push('/login');
}, 1000);
} catch (error: any) {
console.error('修改密码失败', error);
message.error(error.message || '修改密码失败,请重试');
} finally {
passwordLoading.value = false;
}
};
onMounted(() => {
loadProfile();
});
</script>
<style scoped>
.profile-view {
padding: 24px;
min-height: 100vh;
background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 12px;
}
.page-header p {
font-size: 14px;
color: #666;
margin: 0;
}
.profile-card {
background: white;
border-radius: 16px;
padding: 32px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
max-width: 560px;
}
.avatar-section {
text-align: center;
margin-bottom: 24px;
}
.profile-avatar {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
color: white;
}
.info-section :deep(.ant-descriptions-item-label) {
width: 100px;
font-weight: 500;
color: #666;
}
.action-section {
margin-top: 24px;
display: flex;
justify-content: center;
gap: 12px;
}
</style>