feat(user): 支持修改当前用户资料接口并更新个人中心调用

- 新增 updateSelfUser 方法,用于修改当前登录用户的个人资料
- 个人中心调用接口由原 updateUser 切换为 updateSelfUser,避免学生账号权限不足导致的保存失败
- 更新了头像保存、个人信息保存等相关调用代码,添加异常捕获和提示
- 修正环境变量 .env.development 中 API URL 配置为生产地址
- 优化审核记录排序逻辑,新增获取最新审核记录和编辑入口显示判断函数
- 调整审核状态标签使用统一排序规则,防止显示和编辑状态不一致
- 新增社团活动、社团编辑、志愿时长录入、学分记录编辑、社会实践学分申报、青马学员编辑等多个新增组件及表单功能
- 各新增功能支持表单验证、操作反馈和数据加载优化
This commit is contained in:
2026-11-05 19:03:40 +08:00
parent 1c3ea8ec77
commit 567e4aabdb
56 changed files with 8941 additions and 20 deletions
@@ -0,0 +1,173 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<a-alert
type="info"
show-icon
style="margin-bottom: 16px"
message="年度志愿服务证明按「已通过审核」的志愿时长自动汇总生成。证书编号由后端按年度顺序生成,模板与电子章在 P1 阶段接入。"
/>
<a-space style="margin-bottom: 16px" wrap>
<a-select
v-model:value="year"
style="width: 140px"
:options="yearOptions"
@change="loadData"
/>
<a-input
v-model:value="studentNo"
allow-clear
style="width: 180px"
placeholder="学号(管理员)"
/>
<a-button type="primary" :loading="loading" @click="loadData">查询</a-button>
<a-button :disabled="!summary.totalHours" :loading="generating" @click="generate">
生成证明
</a-button>
</a-space>
<a-spin :spinning="loading">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="8">
<div class="metric-card">
<div class="metric-label">年度累计时长</div>
<div class="metric-value">{{ summary.totalHours ?? 0 }}</div>
<div class="metric-foot">仅统计已审核通过的记录</div>
</div>
</a-col>
<a-col :xs="24" :sm="8">
<div class="metric-card accent">
<div class="metric-label">志愿项目数</div>
<div class="metric-value">{{ projects.length }}</div>
<div class="metric-foot">按项目名称去重</div>
</div>
</a-col>
<a-col :xs="24" :sm="8">
<div class="metric-card warm">
<div class="metric-label">记录条数</div>
<div class="metric-value">{{ summary.approvedCount ?? 0 }}</div>
<div class="metric-foot">已通过的志愿记录</div>
</div>
</a-col>
</a-row>
<a-card :bordered="false" class="project-card" title="项目明细">
<a-empty v-if="!projects.length" description="该年度暂无可生成证明的志愿记录" />
<a-table
v-else
row-key="projectName"
size="small"
:data-source="projects"
:columns="projectColumns"
:pagination="false"
/>
</a-card>
</a-spin>
</a-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
import {
getMyVolunteerSummary,
getMyVolunteerProjectStats,
getMyVolunteerTotal
} from '@/api/growth/volunteer';
const loading = ref(false);
const generating = ref(false);
const year = ref(dayjs().year());
const studentNo = ref('');
const summary = ref<Record<string, any>>({});
const projects = ref<Record<string, any>[]>([]);
const yearOptions = computed(() => {
const current = dayjs().year();
return Array.from({ length: 6 }, (_, index) => {
const value = current - index;
return { label: `${value} 年`, value };
});
});
const projectColumns: ColumnItem[] = [
{ title: '志愿项目', dataIndex: 'projectName', key: 'projectName' },
{ title: '组织单位', dataIndex: 'orgName', key: 'orgName', width: 260 },
{ title: '时长', dataIndex: 'hours', key: 'hours', width: 100, align: 'center' },
{ title: '记录数', dataIndex: 'count', key: 'count', width: 100, align: 'center' }
];
const loadData = async () => {
loading.value = true;
try {
const [summaryData, projectData, total] = await Promise.all([
getMyVolunteerSummary(),
getMyVolunteerProjectStats(year.value),
getMyVolunteerTotal(year.value)
]);
summary.value = { ...summaryData, totalHours: total ?? 0 };
projects.value = projectData || [];
} catch (e: any) {
message.error(e?.message || '加载志愿数据失败');
} finally {
loading.value = false;
}
};
const generate = () => {
generating.value = true;
// 证明文件由后端模板渲染,P0 先给出数据核对结果,文件下载在模板接入后开放
setTimeout(() => {
generating.value = false;
message.info(
`已汇总 ${year.value} 年 ${summary.value.totalHours ?? 0} 小时志愿时长,证明材料模板接入后可在此下载 PDF`
);
}, 300);
};
onMounted(loadData);
</script>
<style lang="less" scoped>
.metric-card {
padding: 18px 20px;
border-radius: 10px;
background: #fff;
border-left: 4px solid #1677ff;
}
.metric-card.accent {
border-left-color: #13c2c2;
}
.metric-card.warm {
border-left-color: #faad14;
}
.metric-label {
font-size: 13px;
color: #8c8c8c;
}
.metric-value {
margin: 6px 0 4px;
font-size: 26px;
font-weight: 600;
color: #262626;
}
.metric-foot {
font-size: 12px;
color: #bfbfbf;
}
.project-card {
margin-top: 16px;
}
</style>