567e4aabdb
- 新增 updateSelfUser 方法,用于修改当前登录用户的个人资料 - 个人中心调用接口由原 updateUser 切换为 updateSelfUser,避免学生账号权限不足导致的保存失败 - 更新了头像保存、个人信息保存等相关调用代码,添加异常捕获和提示 - 修正环境变量 .env.development 中 API URL 配置为生产地址 - 优化审核记录排序逻辑,新增获取最新审核记录和编辑入口显示判断函数 - 调整审核状态标签使用统一排序规则,防止显示和编辑状态不一致 - 新增社团活动、社团编辑、志愿时长录入、学分记录编辑、社会实践学分申报、青马学员编辑等多个新增组件及表单功能 - 各新增功能支持表单验证、操作反馈和数据加载优化
405 lines
14 KiB
Vue
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>
|