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

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>