567e4aabdb
- 新增 updateSelfUser 方法,用于修改当前登录用户的个人资料 - 个人中心调用接口由原 updateUser 切换为 updateSelfUser,避免学生账号权限不足导致的保存失败 - 更新了头像保存、个人信息保存等相关调用代码,添加异常捕获和提示 - 修正环境变量 .env.development 中 API URL 配置为生产地址 - 优化审核记录排序逻辑,新增获取最新审核记录和编辑入口显示判断函数 - 调整审核状态标签使用统一排序规则,防止显示和编辑状态不一致 - 新增社团活动、社团编辑、志愿时长录入、学分记录编辑、社会实践学分申报、青马学员编辑等多个新增组件及表单功能 - 各新增功能支持表单验证、操作反馈和数据加载优化
313 lines
8.7 KiB
Vue
313 lines
8.7 KiB
Vue
<template>
|
|
<div class="page">
|
|
<div class="ele-body">
|
|
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
|
<div class="summary-bar">
|
|
<div class="summary-item">
|
|
<span class="summary-label">已修学分</span>
|
|
<span class="summary-value">{{ summary.totalEarned ?? 0 }}</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label">应修学分</span>
|
|
<span class="summary-value">{{ summary.totalRequired ?? 0 }}</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label">还差</span>
|
|
<span class="summary-value warning">{{ summary.totalGap ?? 0 }}</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-label">完成率</span>
|
|
<span class="summary-value success">{{ summary.completionRate ?? 0 }}%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<ele-pro-table
|
|
ref="tableRef"
|
|
row-key="id"
|
|
:columns="columns"
|
|
:datasource="datasource"
|
|
:scroll="{ x: 1200 }"
|
|
tool-class="ele-toolbar-form"
|
|
>
|
|
<template #toolbar>
|
|
<a-space :size="10" style="flex-wrap: wrap">
|
|
<a-select
|
|
v-model:value="category"
|
|
allow-clear
|
|
style="width: 160px"
|
|
placeholder="学分类别"
|
|
:options="categoryOptions"
|
|
@change="reload"
|
|
/>
|
|
<a-select
|
|
v-model:value="status"
|
|
allow-clear
|
|
style="width: 130px"
|
|
placeholder="状态"
|
|
:options="statusOptions"
|
|
@change="reload"
|
|
/>
|
|
<a-input-search
|
|
v-model:value="searchText"
|
|
allow-clear
|
|
style="width: 240px"
|
|
placeholder="项目名称/姓名/学号"
|
|
@search="reload"
|
|
@pressEnter="reload"
|
|
/>
|
|
<a-button type="primary" @click="reload">查询</a-button>
|
|
<a-button @click="resetSearch">重置</a-button>
|
|
<a-button
|
|
v-if="hasPermission('gxmu:credit:import')"
|
|
class="ele-btn-icon"
|
|
@click="showImport = true"
|
|
>
|
|
<template #icon><upload-outlined /></template>
|
|
导入
|
|
</a-button>
|
|
</a-space>
|
|
</template>
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.key === 'score'">
|
|
<span class="score-text">{{ record.score }}</span>
|
|
</template>
|
|
<template v-else-if="column.key === 'status'">
|
|
<a-tag :color="record.status === 1 ? 'green' : 'default'">
|
|
{{ record.status === 1 ? '有效' : '作废' }}
|
|
</a-tag>
|
|
</template>
|
|
<template v-else-if="column.key === 'source'">
|
|
<a-tag>{{ sourceLabel(record.source) }}</a-tag>
|
|
</template>
|
|
<template v-else-if="column.key === 'action'">
|
|
<a-space>
|
|
<a
|
|
v-if="hasPermission('gxmu:credit:update')"
|
|
@click="openEdit(record)"
|
|
>修改</a>
|
|
<a-divider type="vertical" />
|
|
<a-popconfirm
|
|
v-if="hasPermission('gxmu:credit:remove')"
|
|
title="确定要删除这条学分记录吗?"
|
|
@confirm="remove(record)"
|
|
>
|
|
<a class="ele-text-danger">删除</a>
|
|
</a-popconfirm>
|
|
</a-space>
|
|
</template>
|
|
</template>
|
|
</ele-pro-table>
|
|
</a-card>
|
|
|
|
<CreditImport
|
|
v-model:visible="showImport"
|
|
@done="onImported"
|
|
/>
|
|
|
|
<CreditEdit
|
|
v-model:visible="showEdit"
|
|
:data="current"
|
|
@done="reload"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { message } from 'ant-design-vue';
|
|
import { UploadOutlined } from '@ant-design/icons-vue';
|
|
import type { EleProTable } from 'ele-admin-pro';
|
|
import { toDateString } from 'ele-admin-pro';
|
|
import type {
|
|
DatasourceFunction,
|
|
ColumnItem
|
|
} from 'ele-admin-pro/es/ele-pro-table/types';
|
|
import CreditImport from './components/import.vue';
|
|
import CreditEdit from './components/edit.vue';
|
|
import {
|
|
pageCreditRecords,
|
|
removeCreditRecord,
|
|
getMyCreditSummary,
|
|
listGraduateRequirements
|
|
} from '@/api/growth/credit';
|
|
import type { CreditRecord, CreditSummary } from '@/api/growth/credit';
|
|
import { hasPermission } from '@/utils/permission';
|
|
|
|
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
|
const summary = ref<CreditSummary>({
|
|
userId: 0,
|
|
totalEarned: 0,
|
|
totalRequired: 0,
|
|
totalGap: 0,
|
|
completionRate: 0,
|
|
categories: []
|
|
});
|
|
const category = ref<string>();
|
|
const status = ref<number>();
|
|
const searchText = ref('');
|
|
const showImport = ref(false);
|
|
const showEdit = ref(false);
|
|
const current = ref<CreditRecord | null>(null);
|
|
const requirementCategories = ref<string[]>([]);
|
|
|
|
const categoryOptions = computed(() =>
|
|
requirementCategories.value.map((item) => ({ label: item, value: item }))
|
|
);
|
|
const statusOptions = [
|
|
{ label: '有效', value: 1 },
|
|
{ label: '作废', value: 0 }
|
|
];
|
|
|
|
const columns = ref<ColumnItem[]>([
|
|
{ title: '姓名', dataIndex: 'studentName', key: 'studentName', width: 110 },
|
|
{ title: '学号', dataIndex: 'studentNo', key: 'studentNo', width: 140 },
|
|
{ title: '学分类别', dataIndex: 'category', key: 'category', width: 150 },
|
|
{ title: '项目名称', dataIndex: 'itemName', key: 'itemName', ellipsis: true },
|
|
{ title: '学分', key: 'score', width: 90, align: 'center' },
|
|
{
|
|
title: '获得时间',
|
|
dataIndex: 'acquireTime',
|
|
key: 'acquireTime',
|
|
width: 120,
|
|
align: 'center'
|
|
},
|
|
{ title: '来源', key: 'source', width: 110, align: 'center' },
|
|
{ title: '状态', key: 'status', width: 90, align: 'center' },
|
|
{
|
|
title: '录入时间',
|
|
dataIndex: 'createTime',
|
|
key: 'createTime',
|
|
width: 160,
|
|
align: 'center',
|
|
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
width: 130,
|
|
fixed: 'right',
|
|
align: 'center',
|
|
hideInSetting: true
|
|
}
|
|
]);
|
|
|
|
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
|
return pageCreditRecords({
|
|
...where,
|
|
...orders,
|
|
page,
|
|
limit,
|
|
backendAccess: true,
|
|
category: category.value,
|
|
status: status.value,
|
|
keywords: searchText.value || undefined
|
|
});
|
|
};
|
|
|
|
const reload = () => {
|
|
tableRef.value?.reload?.({ page: 1 });
|
|
};
|
|
|
|
const resetSearch = () => {
|
|
category.value = undefined;
|
|
status.value = undefined;
|
|
searchText.value = '';
|
|
reload();
|
|
};
|
|
|
|
const openEdit = (row: CreditRecord) => {
|
|
current.value = row;
|
|
showEdit.value = true;
|
|
};
|
|
|
|
const remove = async (row: CreditRecord) => {
|
|
if (!row.id) return;
|
|
try {
|
|
const msg = await removeCreditRecord(row.id);
|
|
message.success(msg);
|
|
reload();
|
|
await loadSummary();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '删除失败');
|
|
}
|
|
};
|
|
|
|
const sourceLabel = (value?: string) => {
|
|
const map: Record<string, string> = {
|
|
import: '导入',
|
|
second_class: '第二课堂',
|
|
practice: '社会实践',
|
|
activity: '活动',
|
|
manual: '手工录入'
|
|
};
|
|
return map[value ?? ''] ?? value ?? '-';
|
|
};
|
|
|
|
const loadSummary = async () => {
|
|
try {
|
|
summary.value = await getMyCreditSummary();
|
|
} catch (e: any) {
|
|
// 汇总失败不阻塞列表
|
|
}
|
|
};
|
|
|
|
const loadCategories = async () => {
|
|
try {
|
|
const list = await listGraduateRequirements();
|
|
requirementCategories.value = list.map((item) => item.category).filter((v): v is string => !!v);
|
|
} catch (e: any) {
|
|
// 忽略
|
|
}
|
|
};
|
|
|
|
const onImported = () => {
|
|
reload();
|
|
loadSummary();
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadSummary();
|
|
loadCategories();
|
|
});
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.summary-bar {
|
|
display: flex;
|
|
gap: 40px;
|
|
flex-wrap: wrap;
|
|
padding: 4px 4px 18px;
|
|
}
|
|
|
|
.summary-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.summary-label {
|
|
font-size: 13px;
|
|
color: #8c8c8c;
|
|
}
|
|
|
|
.summary-value {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #262626;
|
|
}
|
|
|
|
.summary-value.warning {
|
|
color: #faad14;
|
|
}
|
|
|
|
.summary-value.success {
|
|
color: #52c41a;
|
|
}
|
|
|
|
.score-text {
|
|
font-weight: 600;
|
|
color: #1677ff;
|
|
}
|
|
</style>
|