Files
tuanwei-admin/src/views/gxmu/qmgcStudent/index.vue
T
gxwebsoft 567e4aabdb feat(user): 支持修改当前用户资料接口并更新个人中心调用
- 新增 updateSelfUser 方法,用于修改当前登录用户的个人资料
- 个人中心调用接口由原 updateUser 切换为 updateSelfUser,避免学生账号权限不足导致的保存失败
- 更新了头像保存、个人信息保存等相关调用代码,添加异常捕获和提示
- 修正环境变量 .env.development 中 API URL 配置为生产地址
- 优化审核记录排序逻辑,新增获取最新审核记录和编辑入口显示判断函数
- 调整审核状态标签使用统一排序规则,防止显示和编辑状态不一致
- 新增社团活动、社团编辑、志愿时长录入、学分记录编辑、社会实践学分申报、青马学员编辑等多个新增组件及表单功能
- 各新增功能支持表单验证、操作反馈和数据加载优化
2026-11-05 19:03:40 +08:00

405 lines
14 KiB
Vue

<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:scroll="{ x: 1300 }"
tool-class="ele-toolbar-form"
>
<template #toolbar>
<a-space :size="10" style="flex-wrap: wrap">
<a-button
v-if="hasPermission('gxmu:qmgcStudent:save')"
type="primary"
@click="openEdit()"
>
<template #icon><plus-outlined /></template>
新增学员
</a-button>
<a-input-search
v-model:value="searchText"
allow-clear
style="width: 240px"
placeholder="姓名/学号/学员编号"
@search="reload"
@pressEnter="reload"
/>
<a-input
v-model:value="batch"
allow-clear
style="width: 180px"
placeholder="期次"
@pressEnter="reload"
/>
<a-select
v-model:value="stage"
allow-clear
style="width: 150px"
placeholder="阶段"
:options="stageOptions"
@change="reload"
/>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="resetSearch">重置</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'stage'">
<a-tag color="blue">{{ stageLabel(record.stage) }}</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'green' : record.status === 2 ? 'blue' : 'default'">
{{ record.status === 1 ? '在培' : record.status === 2 ? '结业' : '退出' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="openDetail(record)">档案</a>
<a-divider type="vertical" />
<a v-if="hasPermission('gxmu:qmgcStudent:update')" @click="openEdit(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm
v-if="hasPermission('gxmu:qmgcStudent:remove')"
title="确定要删除这个学员档案吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<QmgcStudentEdit v-model:visible="showEdit" :data="current" @done="reload" />
<a-drawer v-model:visible="showDetail" title="青马培养档案" width="880">
<template v-if="detail">
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="姓名">{{ student?.studentName }}</a-descriptions-item>
<a-descriptions-item label="学号">{{ student?.studentNo }}</a-descriptions-item>
<a-descriptions-item label="期次">{{ student?.batch || '-' }}</a-descriptions-item>
<a-descriptions-item label="学员编号">{{ student?.candidateNo || '-' }}</a-descriptions-item>
<a-descriptions-item label="当前阶段">{{ stageLabel(student?.stage) }}</a-descriptions-item>
<a-descriptions-item label="累计得分">{{ student?.totalScore ?? 0 }}</a-descriptions-item>
</a-descriptions>
<a-tabs style="margin-top: 16px">
<a-tab-pane key="material" tab="培养材料">
<a-space style="margin-bottom: 12px">
<a-button size="small" type="primary" @click="showMaterial = true">
提交材料
</a-button>
</a-space>
<a-empty v-if="!materials.length" description="暂无材料" />
<a-list v-else :data-source="materials" item-layout="vertical">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="item.title" :description="item.scoreComment || item.stage" />
<template #actions>
<a-tag :color="item.status === 1 ? 'green' : 'orange'">
{{ item.status === 1 ? `已评分 ${item.score ?? 0}` : '待评分' }}
</a-tag>
<a @click="openScore(item)">评分</a>
</template>
</a-list-item>
</template>
</a-list>
</a-tab-pane>
<a-tab-pane key="task" tab="任务进度">
<a-empty v-if="!tasks.length" description="暂无任务" />
<a-table
v-else
row-key="taskId"
size="small"
:data-source="tasks"
:columns="taskColumns"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.assignStatus === 1 ? 'green' : 'orange'">
{{ record.assignStatus === 1 ? '已完成' : '未完成' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a v-if="record.assignStatus !== 1" @click="completeTask(record)">标记完成</a>
<span v-else>-</span>
</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="score" tab="考核评分">
<a-table
row-key="id"
size="small"
:data-source="scores"
:columns="scoreColumns"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a @click="openScore(record)">编辑</a>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</template>
</a-drawer>
<a-modal
v-model:visible="showMaterial"
title="提交培养材料"
:confirm-loading="submitting"
width="560px"
@ok="submitMaterial"
>
<a-form layout="vertical">
<a-form-item label="材料标题" required>
<a-input v-model:value="materialForm.title" allow-clear placeholder="请输入标题" />
</a-form-item>
<a-form-item label="阶段">
<a-select
v-model:value="materialForm.stage"
allow-clear
:options="stageOptions"
placeholder="请选择阶段"
/>
</a-form-item>
<a-form-item label="内容">
<a-textarea v-model:value="materialForm.content" :rows="4" placeholder="请输入材料内容" />
</a-form-item>
<a-form-item label="附件">
<MultiFileUpload v-model="materialFile" :max-count="3" />
</a-form-item>
</a-form>
</a-modal>
<a-modal
v-model:visible="showScoreModal"
title="评分"
:confirm-loading="submitting"
width="480px"
@ok="submitScore"
>
<a-form layout="vertical">
<a-form-item label="得分" required>
<a-input-number v-model:value="scoreValue" :min="0" :max="100" class="ele-fluid" />
</a-form-item>
<a-form-item label="评语">
<a-textarea v-model:value="scoreComment" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
import QmgcStudentEdit from './components/edit.vue';
import MultiFileUpload from '@/components/MultiFileUpload/index.vue';
import {
pageQmgcStudents,
removeQmgcStudent,
getQmgcStudentDetail,
submitQmgcMaterial,
scoreQmgcMaterial,
completeQmgcTask
} from '@/api/growth/qmgc';
import type { QmgcStudent, QmgcMaterial, QmgcScore } from '@/api/growth/qmgc';
import { hasPermission } from '@/utils/permission';
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const searchText = ref('');
const batch = ref('');
const stage = ref<string>();
const showEdit = ref(false);
const showDetail = ref(false);
const showMaterial = ref(false);
const showScoreModal = ref(false);
const current = ref<QmgcStudent | null>(null);
const detail = ref<Record<string, any> | null>(null);
const materialForm = ref<Partial<QmgcMaterial>>({});
const materialFile = ref('');
const submitting = ref(false);
const scoreValue = ref(0);
const scoreComment = ref('');
const scoreTargetId = ref<number>();
const stageOptions = [
{ label: '选拔', value: 'selection' },
{ label: '学习', value: 'study' },
{ label: '实践', value: 'practice' },
{ label: '考核', value: 'exam' },
{ label: '结业', value: 'graduated' }
];
const student = computed(() => detail.value?.student || null);
const materials = computed<QmgcMaterial[]>(() => detail.value?.materials || []);
const tasks = computed(() => detail.value?.tasks || []);
const scores = computed<QmgcScore[]>(() => detail.value?.scores || []);
const columns = ref<ColumnItem[]>([
{ title: '姓名', dataIndex: 'studentName', key: 'studentName', width: 110 },
{ title: '学号', dataIndex: 'studentNo', key: 'studentNo', width: 140 },
{ title: '学院', dataIndex: 'collegeName', key: 'collegeName', width: 160 },
{ title: '期次', dataIndex: 'batch', key: 'batch', width: 160 },
{ title: '年度', dataIndex: 'year', key: 'year', width: 90, align: 'center' },
{ title: '阶段', key: 'stage', width: 100, align: 'center' },
{ title: '累计得分', dataIndex: 'totalScore', key: 'totalScore', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 90, align: 'center' },
{ title: '操作', key: 'action', width: 180, fixed: 'right', align: 'center', hideInSetting: true }
]);
const taskColumns: ColumnItem[] = [
{ title: '任务', key: 'title', ellipsis: true },
{ title: '类型', key: 'type', width: 90 },
{ title: '截止', key: 'deadline', width: 170 },
{ title: '状态', key: 'status', width: 90 },
{ title: '操作', key: 'action', width: 100 }
];
const scoreColumns: ColumnItem[] = [
{ title: '维度', dataIndex: 'dimension', key: 'dimension', width: 140 },
{ title: '评分项', dataIndex: 'item', key: 'item', width: 180 },
{ title: '得分', dataIndex: 'score', key: 'score', width: 90 },
{ title: '评语', dataIndex: 'comment', key: 'comment', ellipsis: true },
{ title: '操作', key: 'action', width: 80 }
];
const datasource = ({ page, limit, where, orders }: any) => {
return pageQmgcStudents({
...where,
...orders,
page,
limit,
batch: batch.value || undefined,
stage: stage.value,
keywords: searchText.value || undefined
});
};
const reload = () => tableRef.value?.reload?.({ page: 1 });
const resetSearch = () => {
searchText.value = '';
batch.value = '';
stage.value = undefined;
reload();
};
const openEdit = (row?: QmgcStudent) => {
current.value = row || null;
showEdit.value = true;
};
const openDetail = async (row: QmgcStudent) => {
current.value = row;
if (!row.id) return;
try {
detail.value = await getQmgcStudentDetail(row.id);
showDetail.value = true;
} catch (e: any) {
message.error(e?.message || '加载档案失败');
}
};
const remove = async (row: QmgcStudent) => {
if (!row.id) return;
try {
const msg = await removeQmgcStudent(row.id);
message.success(msg);
reload();
} catch (e: any) {
message.error(e?.message || '删除失败');
}
};
const reloadDetail = async () => {
if (!current.value?.id) return;
detail.value = await getQmgcStudentDetail(current.value.id);
};
const submitMaterial = async () => {
if (!materialForm.value.title?.trim()) {
message.warning('请填写材料标题');
return;
}
submitting.value = true;
try {
await submitQmgcMaterial({
studentId: current.value?.id,
title: materialForm.value.title,
stage: materialForm.value.stage,
content: materialForm.value.content,
fileUrl: materialFile.value || undefined
});
message.success('提交成功');
showMaterial.value = false;
materialForm.value = {};
materialFile.value = '';
await reloadDetail();
} catch (e: any) {
message.error(e?.message || '提交失败');
} finally {
submitting.value = false;
}
};
const openScore = (item: any) => {
scoreTargetId.value = item.id;
scoreValue.value = item.score ?? 0;
scoreComment.value = item.scoreComment || item.comment || '';
showScoreModal.value = true;
};
const submitScore = async () => {
if (!scoreTargetId.value) return;
submitting.value = true;
try {
await scoreQmgcMaterial(scoreTargetId.value, Number(scoreValue.value), scoreComment.value);
message.success('评分成功');
showScoreModal.value = false;
await reloadDetail();
} catch (e: any) {
message.error(e?.message || '评分失败');
} finally {
submitting.value = false;
}
};
const completeTask = async (record: any) => {
if (!current.value?.id) return;
try {
const msg = await completeQmgcTask(current.value.id, record.task?.id);
message.success(msg);
await reloadDetail();
} catch (e: any) {
message.error(e?.message || '操作失败');
}
};
const stageLabel = (value?: string) => {
const map: Record<string, string> = {
selection: '选拔',
study: '学习',
practice: '实践',
exam: '考核',
graduated: '结业'
};
return map[value ?? ''] ?? value ?? '-';
};
</script>