提交代码

This commit is contained in:
zlwu committed 2025-08-23 16:20:50 +08:00
1 parent e46971e383
commit dee854ab07
29 files changed
-13711

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 MiB

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because it is too large. Load diff
@@ -1,85 +0,0 @@
<template>
<div class="batch-actions">
<el-dropdown @command="handleCommand" trigger="click">
<el-button type="primary" size="small">
批次操作
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="batch.batchStatus === 'draft'" command="start" icon="VideoPlay"> 开始批次 </el-dropdown-item>
<el-dropdown-item v-if="batch.batchStatus === 'active'" command="pause" icon="VideoPause"> 暂停批次 </el-dropdown-item>
<el-dropdown-item v-if="batch.batchStatus === 'paused'" command="resume" icon="VideoPlay"> 恢复批次 </el-dropdown-item>
<el-dropdown-item v-if="['active', 'paused'].includes(batch.batchStatus)" command="end" icon="VideoCamera"> 结束批次 </el-dropdown-item>
<el-dropdown-item divided />
<el-dropdown-item command="copy" icon="CopyDocument"> 复制批次 </el-dropdown-item>
<el-dropdown-item command="statistics" icon="TrendCharts"> 统计分析 </el-dropdown-item>
<el-dropdown-item command="permission" icon="Lock"> 权限配置 </el-dropdown-item>
<el-dropdown-item divided />
<el-dropdown-item command="export" icon="Download"> 导出数据 </el-dropdown-item>
<el-dropdown-item command="delete" icon="Delete" style="color: #f56c6c"> 删除批次 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup lang="ts">
import { CollectionBatchVO } from '@/api/xg-xx/types';
interface Props {
batch: CollectionBatchVO;
}
const props = defineProps<Props>();
const emit = defineEmits<{
start: [batch: CollectionBatchVO];
pause: [batch: CollectionBatchVO];
resume: [batch: CollectionBatchVO];
end: [batch: CollectionBatchVO];
copy: [batch: CollectionBatchVO];
statistics: [batch: CollectionBatchVO];
permission: [batch: CollectionBatchVO];
export: [batch: CollectionBatchVO];
delete: [batch: CollectionBatchVO];
}>();
const handleCommand = (command: string) => {
switch (command) {
case 'start':
emit('start', props.batch);
break;
case 'pause':
emit('pause', props.batch);
break;
case 'resume':
emit('resume', props.batch);
break;
case 'end':
emit('end', props.batch);
break;
case 'copy':
emit('copy', props.batch);
break;
case 'statistics':
emit('statistics', props.batch);
break;
case 'permission':
emit('permission', props.batch);
break;
case 'export':
emit('export', props.batch);
break;
case 'delete':
emit('delete', props.batch);
break;
}
};
</script>
<style scoped>
.batch-actions {
display: inline-block;
}
</style>
@@ -1,65 +0,0 @@
<template>
<el-tag :type="tagType" :effect="effect">
<el-icon v-if="showIcon" class="mr-1">
<component :is="iconComponent" />
</el-icon>
{{ typeText }}
</el-tag>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
type: string;
showIcon?: boolean;
effect?: 'dark' | 'light' | 'plain';
}
const props = withDefaults(defineProps<Props>(), {
showIcon: true,
effect: 'light'
});
// 类型映射配置
const typeConfig = {
student_info: {
text: '学生信息采集',
type: 'primary',
icon: 'User'
},
scholarship: {
text: '奖助学金申请',
type: 'success',
icon: 'Trophy'
},
evaluation: {
text: '综合测评',
type: 'warning',
icon: 'DataAnalysis'
},
other: {
text: '其他',
type: 'info',
icon: 'More'
}
};
const tagType = computed(() => {
return (typeConfig[props.type as keyof typeof typeConfig]?.type as 'primary' | 'success' | 'warning' | 'info' | 'danger') || 'info';
});
const typeText = computed(() => {
return typeConfig[props.type as keyof typeof typeConfig]?.text || props.type;
});
const iconComponent = computed(() => {
return typeConfig[props.type as keyof typeof typeConfig]?.icon || 'QuestionFilled';
});
</script>
<style scoped>
.mr-1 {
margin-right: 0.25rem;
}
</style>
@@ -1,75 +0,0 @@
<template>
<div class="progress-container">
<el-progress :percentage="percentage" :stroke-width="strokeWidth" :color="progressColor" :show-text="showText" :format="format" />
<div v-if="showDetail" class="progress-detail">
<span class="detail-text">{{ current }}/{{ total }}</span>
<span v-if="showRate" class="rate-text">({{ percentage.toFixed(1) }}%)</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
current: number;
total: number;
strokeWidth?: number;
showText?: boolean;
showDetail?: boolean;
showRate?: boolean;
colorThresholds?: {
low: number;
medium: number;
};
}
const props = withDefaults(defineProps<Props>(), {
strokeWidth: 12,
showText: true,
showDetail: true,
showRate: true,
colorThresholds: () => ({
low: 30,
medium: 70
})
});
const percentage = computed(() => {
if (props.total === 0) return 0;
return Math.round((props.current / props.total) * 100);
});
const progressColor = computed(() => {
const rate = percentage.value;
if (rate < props.colorThresholds.low) return '#f56c6c';
if (rate < props.colorThresholds.medium) return '#e6a23c';
return '#67c23a';
});
const format = (percentage: number) => {
return `${percentage}%`;
};
</script>
<style scoped>
.progress-container {
width: 100%;
}
.progress-detail {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
font-size: 12px;
}
.detail-text {
color: #606266;
}
.rate-text {
color: #909399;
}
</style>
@@ -1,125 +0,0 @@
<template>
<el-card shadow="hover">
<el-form ref="searchFormRef" :model="searchForm" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="searchForm.batchName" placeholder="请输入批次名称" clearable @keyup.enter="handleSearch" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="批次编码" prop="batchCode">
<el-input v-model="searchForm.batchCode" placeholder="请输入批次编码" clearable @keyup.enter="handleSearch" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="批次类型" prop="batchType">
<el-select v-model="searchForm.batchType" placeholder="请选择批次类型" clearable style="width: 100%">
<el-option label="学生信息采集" value="student_info" />
<el-option label="奖助学金申请" value="scholarship" />
<el-option label="综合测评" value="evaluation" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="批次状态" prop="batchStatus">
<el-select v-model="searchForm.batchStatus" placeholder="请选择批次状态" clearable style="width: 100%">
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="进行中" value="active" />
<el-option label="已暂停" value="paused" />
<el-option label="已结束" value="ended" />
<el-option label="已归档" value="archived" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学年学期" prop="academicYear">
<el-input v-model="searchForm.academicYear" placeholder="请输入学年学期" clearable @keyup.enter="handleSearch" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="目标届别" prop="targetGrades">
<el-input v-model="searchForm.targetGrades" placeholder="请输入目标届别" clearable @keyup.enter="handleSearch" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="searchForm.createTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { CollectionBatchQuery } from '@/api/xg-xx/types';
const emit = defineEmits<{
search: [query: CollectionBatchQuery];
reset: [];
}>();
const searchFormRef = ref<ElFormInstance>();
const searchForm = reactive<CollectionBatchQuery>({
batchName: '',
batchCode: '',
batchType: '',
batchStatus: '',
academicYear: '',
targetGrades: '',
createTime: []
});
const handleSearch = () => {
// 处理时间范围
const query = { ...searchForm };
if (query.createTime && query.createTime.length === 2) {
query.publishTimeStart = query.createTime[0];
query.publishTimeEnd = query.createTime[1];
}
delete query.createTime;
emit('search', query);
};
const handleReset = () => {
searchFormRef.value?.resetFields();
Object.assign(searchForm, {
batchName: '',
batchCode: '',
batchType: '',
batchStatus: '',
academicYear: '',
targetGrades: '',
createTime: []
});
emit('reset');
};
</script>
<style scoped>
/* 样式可以根据需要添加 */
</style>
@@ -1,563 +0,0 @@
<template>
<div class="p-2 page-container">
<el-card shadow="never" class="mb-4">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg font-semibold">批次详情</span>
<el-button @click="goBack" icon="Back">返回</el-button>
</div>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- 基本信息 -->
<el-card shadow="hover" class="info-card">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-blue-500"><InfoFilled /></el-icon>
<span>基本信息</span>
</div>
</template>
<div class="space-y-3">
<div class="info-item">
<label>批次编码:</label>
<span>{{ batchInfo.batchCode }}</span>
</div>
<div class="info-item">
<label>批次名称:</label>
<span>{{ batchInfo.batchName }}</span>
</div>
<div class="info-item">
<label>批次类型:</label>
<el-tag v-if="batchInfo.batchType === 'student_info'" type="primary">学生信息采集</el-tag>
<el-tag v-else-if="batchInfo.batchType === 'scholarship'" type="success">奖助学金申请</el-tag>
<el-tag v-else-if="batchInfo.batchType === 'evaluation'" type="warning">综合测评</el-tag>
<el-tag v-else type="info">其他</el-tag>
</div>
<div class="info-item">
<label>学年学期:</label>
<span>{{ batchInfo.academicYear }}</span>
</div>
<div class="info-item">
<label>批次状态:</label>
<el-tag v-if="batchInfo.batchStatus === 'draft'" type="info">草稿</el-tag>
<el-tag v-else-if="batchInfo.batchStatus === 'published'" type="warning">已发布</el-tag>
<el-tag v-else-if="batchInfo.batchStatus === 'active'" type="success">进行中</el-tag>
<el-tag v-else-if="batchInfo.batchStatus === 'paused'" type="warning">已暂停</el-tag>
<el-tag v-else-if="batchInfo.batchStatus === 'ended'" type="danger">已结束</el-tag>
<el-tag v-else-if="batchInfo.batchStatus === 'archived'" type="info">已归档</el-tag>
</div>
</div>
</el-card>
<!-- 时间信息 -->
<el-card shadow="hover" class="info-card">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-green-500"><Clock /></el-icon>
<span>时间信息</span>
</div>
</template>
<div class="space-y-3">
<div class="info-item">
<label>发布时间:</label>
<span>{{ parseTime(batchInfo.publishTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</div>
<div class="info-item">
<label>开始时间:</label>
<span>{{ parseTime(batchInfo.startTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</div>
<div class="info-item">
<label>结束时间:</label>
<span>{{ parseTime(batchInfo.endTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</div>
<div class="info-item">
<label>归档时间:</label>
<span>{{ parseTime(batchInfo.archiveTime, '{y}-{m}-{d} {h}:{i}') || '未归档' }}</span>
</div>
<div class="info-item">
<label>剩余时间:</label>
<span :class="getRemainingTimeClass()">{{ getRemainingTime() }}</span>
</div>
</div>
</el-card>
<!-- 统计信息 -->
<el-card shadow="hover" class="info-card">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-purple-500"><DataAnalysis /></el-icon>
<span>统计信息</span>
</div>
</template>
<div class="space-y-3">
<div class="info-item">
<label>目标学生:</label>
<span class="text-blue-600 font-semibold">{{ batchInfo.totalStudents || 0 }}</span>
</div>
<div class="info-item">
<label>已提交:</label>
<span class="text-green-600 font-semibold">{{ batchInfo.submittedCount || 0 }}</span>
</div>
<div class="info-item">
<label>已审核:</label>
<span class="text-purple-600 font-semibold">{{ batchInfo.approvedCount || 0 }}</span>
</div>
<div class="info-item">
<label>被驳回:</label>
<span class="text-red-600 font-semibold">{{ batchInfo.rejectedCount || 0 }}</span>
</div>
<div class="info-item">
<label>完成率:</label>
<div class="flex items-center">
<el-progress
:percentage="batchInfo.completionRate || 0"
:stroke-width="8"
:color="getProgressColor(batchInfo.completionRate || 0)"
class="flex-1 mr-2"
/>
<span class="text-sm">{{ (batchInfo.completionRate || 0).toFixed(1) }}%</span>
</div>
</div>
</div>
</el-card>
</div>
<!-- 目标设置 -->
<el-card shadow="hover" class="mt-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-orange-500"><Target /></el-icon>
<span>目标设置</span>
</div>
</template>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="info-item">
<label>目标届别:</label>
<span>{{ batchInfo.targetGrades || '全部' }}</span>
</div>
<div class="info-item">
<label>目标学院:</label>
<span>{{ batchInfo.targetColleges || '全部' }}</span>
</div>
<div class="info-item">
<label>目标专业:</label>
<span>{{ batchInfo.targetMajors || '全部' }}</span>
</div>
</div>
</el-card>
<!-- 配置信息 -->
<el-card shadow="hover" class="mt-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-cyan-500"><Setting /></el-icon>
<span>配置信息</span>
</div>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="info-item">
<label>开始前提醒:</label>
<span>{{ batchInfo.remindBeforeStart || 0 }} 小时</span>
</div>
<div class="info-item">
<label>结束前提醒:</label>
<span>{{ batchInfo.remindBeforeEnd || 0 }} 小时</span>
</div>
<div class="info-item">
<label>自动关闭:</label>
<el-tag :type="batchInfo.autoClose ? 'success' : 'danger'">
{{ batchInfo.autoClose ? '是' : '否' }}
</el-tag>
</div>
<div class="info-item">
<label>通知发送:</label>
<el-tag :type="batchInfo.notificationSent ? 'success' : 'warning'">
{{ batchInfo.notificationSent ? '已发送' : '未发送' }}
</el-tag>
</div>
</div>
</el-card>
<!-- 批次描述 -->
<el-card shadow="hover" class="mt-6" v-if="batchInfo.batchDescription">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-gray-500"><Document /></el-icon>
<span>批次描述</span>
</div>
</template>
<div class="whitespace-pre-wrap text-gray-700">
{{ batchInfo.batchDescription }}
</div>
</el-card>
<!-- 操作按钮 -->
<div class="mt-6 flex justify-center space-x-4">
<el-button
v-if="batchInfo.batchStatus === 'draft'"
type="primary"
icon="VideoPlay"
@click="handleStart"
v-hasPermi="['xg:collectionBatch:edit']"
>
开始批次
</el-button>
<el-button
v-if="batchInfo.batchStatus === 'active'"
type="warning"
icon="VideoPause"
@click="handlePause"
v-hasPermi="['xg:collectionBatch:edit']"
>
暂停批次
</el-button>
<el-button
v-if="batchInfo.batchStatus === 'paused'"
type="success"
icon="VideoPlay"
@click="handleResume"
v-hasPermi="['xg:collectionBatch:edit']"
>
恢复批次
</el-button>
<el-button
v-if="['active', 'paused'].includes(batchInfo.batchStatus)"
type="danger"
icon="VideoCamera"
@click="handleEnd"
v-hasPermi="['xg:collectionBatch:edit']"
>
结束批次
</el-button>
<el-button type="info" icon="Edit" @click="handleEdit" v-hasPermi="['xg:collectionBatch:edit']"> 编辑批次 </el-button>
<el-button type="success" icon="TrendCharts" @click="handleStatistics" v-hasPermi="['xg:collectionBatch:statistics']"> 统计分析 </el-button>
<el-button type="warning" icon="CopyDocument" @click="handleCopy" v-hasPermi="['xg:collectionBatch:add']"> 复制批次 </el-button>
</div>
</el-card>
</div>
</template>
<script setup name="CollectionBatchDetail" lang="ts">
import { getCollectionBatch, startBatch, pauseBatch, resumeBatch, endBatch } from '@/api/xg-xx/collectionBatch';
import { CollectionBatchVO } from '@/api/xg-xx/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const batchInfo = ref<CollectionBatchVO>({});
const loading = ref(true);
/** 获取批次详情 */
const getBatchDetail = async () => {
const batchId = route.query.id as string;
if (!batchId) {
proxy?.$modal.msgError('批次ID不能为空');
return;
}
try {
loading.value = true;
const res = await getCollectionBatch(batchId);
batchInfo.value = res.data;
} catch (error) {
proxy?.$modal.msgError('获取批次详情失败');
} finally {
loading.value = false;
}
};
/** 返回列表 */
const goBack = () => {
router.push('/xg-xx/collectionBatch');
};
/** 开始批次 */
const handleStart = async () => {
try {
await startBatch(batchInfo.value.id!);
proxy?.$modal.msgSuccess('批次已开始');
await getBatchDetail();
} catch (error) {
proxy?.$modal.msgError('开始批次失败');
}
};
/** 暂停批次 */
const handlePause = async () => {
try {
await pauseBatch(batchInfo.value.id!);
proxy?.$modal.msgSuccess('批次已暂停');
await getBatchDetail();
} catch (error) {
proxy?.$modal.msgError('暂停批次失败');
}
};
/** 恢复批次 */
const handleResume = async () => {
try {
await resumeBatch(batchInfo.value.id!);
proxy?.$modal.msgSuccess('批次已恢复');
await getBatchDetail();
} catch (error) {
proxy?.$modal.msgError('恢复批次失败');
}
};
/** 结束批次 */
const handleEnd = async () => {
try {
await proxy?.$modal.confirm('确认要结束此批次吗?结束后将无法恢复。');
await endBatch(batchInfo.value.id!);
proxy?.$modal.msgSuccess('批次已结束');
await getBatchDetail();
} catch (error) {
if (error !== 'cancel') {
proxy?.$modal.msgError('结束批次失败');
}
}
};
/** 编辑批次 */
const handleEdit = () => {
router.push({ path: '/xg-xx/collectionBatch/edit', query: { id: batchInfo.value.id } });
};
/** 统计分析 */
const handleStatistics = () => {
router.push({ path: '/xg-xx/collectionBatch/statistics', query: { batchId: batchInfo.value.id } });
};
/** 复制批次 */
const handleCopy = () => {
router.push({ path: '/xg-xx/collectionBatch/copy', query: { id: batchInfo.value.id } });
};
/** 获取剩余时间 */
const getRemainingTime = () => {
if (!batchInfo.value.endTime || batchInfo.value.batchStatus === 'ended') {
return '已结束';
}
const now = new Date();
const endTime = new Date(batchInfo.value.endTime);
const diff = endTime.getTime() - now.getTime();
if (diff <= 0) {
return '已超时';
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
if (days > 0) {
return `${days}天${hours}小时`;
} else if (hours > 0) {
return `${hours}小时${minutes}分钟`;
} else {
return `${minutes}分钟`;
}
};
/** 获取剩余时间样式类 */
const getRemainingTimeClass = () => {
if (!batchInfo.value.endTime || batchInfo.value.batchStatus === 'ended') {
return 'text-gray-500';
}
const now = new Date();
const endTime = new Date(batchInfo.value.endTime);
const diff = endTime.getTime() - now.getTime();
if (diff <= 0) {
return 'text-red-600 font-semibold';
}
const hours = diff / (1000 * 60 * 60);
if (hours < 24) {
return 'text-red-600 font-semibold';
} else if (hours < 72) {
return 'text-orange-600 font-semibold';
} else {
return 'text-green-600';
}
};
/** 获取进度条颜色 */
const getProgressColor = (percentage: number) => {
if (percentage < 30) return '#f56c6c';
if (percentage < 70) return '#e6a23c';
return '#67c23a';
};
onMounted(() => {
getBatchDetail();
});
</script>
<style scoped>
.page-container {
background-color: #f5f7fa;
}
.info-card {
height: 100%;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.info-item label {
font-weight: 500;
color: #606266;
min-width: 80px;
margin-right: 8px;
}
.info-item span {
color: #303133;
flex: 1;
}
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.gap-4 {
gap: 1rem;
}
.gap-6 {
gap: 1.5rem;
}
.space-y-3 > * + * {
margin-top: 0.75rem;
}
.space-x-4 > * + * {
margin-left: 1rem;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.text-lg {
font-size: 1.125rem;
}
.font-semibold {
font-weight: 600;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.text-blue-500 {
color: #3b82f6;
}
.text-green-500 {
color: #10b981;
}
.text-purple-500 {
color: #8b5cf6;
}
.text-orange-500 {
color: #f59e0b;
}
.text-cyan-500 {
color: #06b6d4;
}
.text-gray-500 {
color: #6b7280;
}
.text-blue-600 {
color: #2563eb;
}
.text-green-600 {
color: #059669;
}
.text-purple-600 {
color: #7c3aed;
}
.text-red-600 {
color: #dc2626;
}
.text-orange-600 {
color: #ea580c;
}
.text-gray-700 {
color: #374151;
}
.whitespace-pre-wrap {
white-space: pre-wrap;
}
@media (min-width: 768px) {
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
</style>
@@ -1,478 +0,0 @@
<template>
<div class="p-2 page-container">
<el-card shadow="never">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg font-semibold">{{ isEdit ? '编辑批次' : '新增批次' }}</span>
<el-button @click="goBack" icon="Back">返回</el-button>
</div>
</template>
<el-form ref="batchFormRef" :model="form" :rules="rules" label-width="120px" class="max-w-4xl">
<!-- 基本信息 -->
<el-card shadow="hover" class="mb-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-blue-500"><InfoFilled /></el-icon>
<span>基本信息</span>
</div>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="批次编码" prop="batchCode">
<el-input v-model="form.batchCode" placeholder="请输入批次编码" :disabled="isEdit" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="form.batchName" placeholder="请输入批次名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="批次类型" prop="batchType">
<el-select v-model="form.batchType" placeholder="请选择批次类型" style="width: 100%">
<el-option label="学生信息采集" value="student_info" />
<el-option label="奖助学金申请" value="scholarship" />
<el-option label="综合测评" value="evaluation" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学年学期" prop="academicYear">
<el-input v-model="form.academicYear" placeholder="请输入学年学期,如:2024-2025第一学期" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学期" prop="semester">
<el-select v-model="form.semester" placeholder="请选择学期" style="width: 100%">
<el-option label="第一学期" value="1" />
<el-option label="第二学期" value="2" />
<el-option label="第三学期" value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="批次状态" prop="batchStatus">
<el-select v-model="form.batchStatus" placeholder="请选择批次状态" style="width: 100%">
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="进行中" value="active" />
<el-option label="已暂停" value="paused" />
<el-option label="已结束" value="ended" />
<el-option label="已归档" value="archived" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 时间设置 -->
<el-card shadow="hover" class="mb-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-green-500"><Clock /></el-icon>
<span>时间设置</span>
</div>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发布时间" prop="publishTime">
<el-date-picker
v-model="form.publishTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="datetime"
placeholder="选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-model="form.endTime"
type="datetime"
placeholder="选择结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归档时间" prop="archiveTime">
<el-date-picker
v-model="form.archiveTime"
type="datetime"
placeholder="选择归档时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 目标设置 -->
<el-card shadow="hover" class="mb-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-orange-500"><Target /></el-icon>
<span>目标设置</span>
</div>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="目标学院" prop="targetColleges">
<el-input v-model="form.targetColleges" placeholder="请输入目标学院,多个用逗号分隔" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标届别" prop="targetGrades">
<el-input v-model="form.targetGrades" placeholder="请输入目标届别,如:2021,2022,2023" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="目标专业" prop="targetMajors">
<el-input v-model="form.targetMajors" placeholder="请输入目标专业,多个用逗号分隔" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 字段配置 -->
<el-card shadow="hover" class="mb-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-purple-500"><Setting /></el-icon>
<span>字段配置</span>
</div>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="必填字段" prop="requiredFields">
<el-input v-model="form.requiredFields" type="textarea" :rows="3" placeholder="请输入必填字段,多个用逗号分隔" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="可选字段" prop="optionalFields">
<el-input v-model="form.optionalFields" type="textarea" :rows="3" placeholder="请输入可选字段,多个用逗号分隔" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="审核层级" prop="auditLevels">
<el-input v-model="form.auditLevels" placeholder="请输入审核层级,如:辅导员,学院,学生处" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 权限配置 -->
<el-card shadow="hover" class="mb-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-cyan-500"><Lock /></el-icon>
<span>权限配置</span>
</div>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="编辑权限" prop="editPermissions">
<el-input v-model="form.editPermissions" placeholder="请输入编辑权限配置" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审核权限" prop="auditPermissions">
<el-input v-model="form.auditPermissions" placeholder="请输入审核权限配置" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="查看权限" prop="viewPermissions">
<el-input v-model="form.viewPermissions" placeholder="请输入查看权限配置" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 提醒设置 -->
<el-card shadow="hover" class="mb-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-yellow-500"><Bell /></el-icon>
<span>提醒设置</span>
</div>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="自动提醒" prop="autoReminder">
<el-input-number v-model="form.autoReminder" :min="0" :max="720" placeholder="小时" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="提醒频率" prop="reminderFrequency">
<el-input-number v-model="form.reminderFrequency" :min="0" :max="720" placeholder="小时" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="允许修改" prop="allowModifyAfterSubmit">
<el-switch v-model="form.allowModifyAfterSubmit" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 批次描述 -->
<el-card shadow="hover" class="mb-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-gray-500"><Document /></el-icon>
<span>批次描述</span>
</div>
</template>
<el-form-item label="批次描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="6" placeholder="请输入批次描述" />
</el-form-item>
</el-card>
<!-- 操作按钮 -->
<div class="flex justify-center space-x-4 mt-6">
<el-button @click="goBack">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="loading">保存</el-button>
<el-button type="success" @click="handleSaveAndPublish" :loading="loading" v-if="!isEdit">保存并发布</el-button>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup name="CollectionBatchForm" lang="ts">
import { getCollectionBatch, addCollectionBatch, updateCollectionBatch } from '@/api/xg-xx/collectionBatch';
import { CollectionBatchForm } from '@/api/xg-xx/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const isEdit = ref(false);
const batchFormRef = ref<ElFormInstance>();
const form = ref<CollectionBatchForm>({
id: undefined,
batchCode: '',
batchName: '',
batchType: '',
academicYear: '',
semester: '',
batchStatus: 'draft',
publishTime: '',
startTime: '',
endTime: '',
archiveTime: '',
targetColleges: '',
targetGrades: '',
targetMajors: '',
requiredFields: '',
optionalFields: '',
auditLevels: '',
editPermissions: '',
auditPermissions: '',
viewPermissions: '',
autoReminder: 24,
reminderFrequency: 24,
allowModifyAfterSubmit: 1,
description: ''
});
const rules = ref({
batchCode: [
{ required: true, message: '批次编码不能为空', trigger: 'blur' },
{ min: 2, max: 50, message: '批次编码长度在 2 到 50 个字符', trigger: 'blur' }
],
batchName: [
{ required: true, message: '批次名称不能为空', trigger: 'blur' },
{ min: 2, max: 100, message: '批次名称长度在 2 到 100 个字符', trigger: 'blur' }
],
batchType: [{ required: true, message: '请选择批次类型', trigger: 'change' }],
academicYear: [{ required: true, message: '学年学期不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }]
});
/** 获取批次详情 */
const getBatchDetail = async () => {
const batchId = route.query.id as string;
if (!batchId) return;
try {
loading.value = true;
const res = await getCollectionBatch(batchId);
Object.assign(form.value, res.data);
isEdit.value = true;
} catch (error) {
proxy?.$modal.msgError('获取批次详情失败');
} finally {
loading.value = false;
}
};
/** 保存 */
const handleSave = () => {
batchFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
try {
loading.value = true;
if (isEdit.value) {
await updateCollectionBatch(form.value);
proxy?.$modal.msgSuccess('更新成功');
} else {
await addCollectionBatch(form.value);
proxy?.$modal.msgSuccess('新增成功');
}
goBack();
} catch (error) {
proxy?.$modal.msgError(isEdit.value ? '更新失败' : '新增失败');
} finally {
loading.value = false;
}
}
});
};
/** 保存并发布 */
const handleSaveAndPublish = () => {
form.value.batchStatus = 'published';
handleSave();
};
/** 返回 */
const goBack = () => {
router.push('/xg-xx/collectionBatch');
};
onMounted(() => {
if (route.query.id) {
getBatchDetail();
}
});
</script>
<style scoped>
.page-container {
background-color: #f5f7fa;
}
.max-w-4xl {
max-width: 56rem;
}
.space-x-4 > * + * {
margin-left: 1rem;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.text-lg {
font-size: 1.125rem;
}
.font-semibold {
font-weight: 600;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.text-blue-500 {
color: #3b82f6;
}
.text-green-500 {
color: #10b981;
}
.text-orange-500 {
color: #f59e0b;
}
.text-purple-500 {
color: #8b5cf6;
}
.text-cyan-500 {
color: #06b6d4;
}
.text-yellow-500 {
color: #eab308;
}
.text-gray-500 {
color: #6b7280;
}
</style>
@@ -1,980 +0,0 @@
<template>
<div class="page-container">
<!-- 统计卡片 -->
<el-row :gutter="10" class="mb-8">
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<el-card class="stat-card stat-card-primary" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Collection /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.totalBatches }}</div>
<div class="stat-label">总批次数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<el-card class="stat-card stat-card-success" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Select /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.activeBatches }}</div>
<div class="stat-label">进行中批次</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<el-card class="stat-card stat-card-warning" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.totalStudents }}</div>
<div class="stat-label">参与学生数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<el-card class="stat-card stat-card-info" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><DocumentCopy /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.completedBatches }}</div>
<div class="stat-label">已完成批次</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 查询表单 -->
<el-card class="search-card" shadow="never">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="queryParams.batchName" placeholder="请输入批次名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="批次类型" prop="batchType">
<el-select v-model="queryParams.batchType" placeholder="请选择批次类型" clearable>
<el-option label="年度采集" value="annual" />
<el-option label="学期采集" value="semester" />
<el-option label="月度采集" value="monthly" />
<el-option label="自定义" value="custom" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="批次状态" prop="batchStatus">
<el-select v-model="queryParams.batchStatus" placeholder="请选择批次状态" clearable>
<el-option label="草稿" value="1" />
<el-option label="发布" value="2" />
<el-option label="进行中" value="3" />
<el-option label="已结束" value="4" />
<el-option label="已归档" value="5" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<div class="search-btn-container">
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetQuery">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作按钮 -->
<el-card class="button-card" shadow="never">
<template #header>
<div class="card-header">
<span class="toolbar-title">信息采集批次管理</span>
<div class="toolbar-actions">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增批次
</el-button>
<el-button type="success" @click="handleExport">
<el-icon><Download /></el-icon>
导出数据
</el-button>
</div>
</div>
</template>
</el-card>
<!-- 数据表格 -->
<el-card class="table-card" shadow="never">
<el-table v-loading="loading" :data="batchList" class="scroll-table" @selection-change="handleSelectionChange" stripe height="600">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="批次名称" prop="batchName" min-width="200" show-overflow-tooltip />
<el-table-column label="批次类型" prop="batchType" width="120" align="center">
<template #default="scope">
<el-tag :type="getBatchTypeTagType(scope.row.batchType)" size="small">
{{ getBatchTypeText(scope.row.batchType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="批次状态" prop="batchStatus" width="120" align="center">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.batchStatus)" size="small">
{{ getStatusText(scope.row.batchStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="开始时间" prop="startTime" width="180" align="center" />
<el-table-column label="结束时间" prop="endTime" width="180" align="center" />
<el-table-column label="参与学生数" prop="totalStudents" width="120" align="center">
<template #default="scope">
<span class="student-count">{{ scope.row.totalStudents || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="完成率" prop="completionRate" width="120" align="center">
<template #default="scope">
<el-progress
:percentage="scope.row.completionRate || 0"
:stroke-width="6"
:show-text="false"
:color="getProgressColor(scope.row.completionRate)"
/>
<div class="progress-text">{{ scope.row.completionRate || 0 }}%</div>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="180" align="center" />
<el-table-column label="操作" width="200" align="center" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="查看详情" placement="top">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)" />
</el-tooltip>
<el-tooltip content="编辑" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" />
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-show="total > 0"
:current-page="queryParams.pageNum"
:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="open" :title="title" width="800px" class="batch-dialog" append-to-body>
<el-form ref="batchFormRef" :model="form" :rules="rules" label-width="100px" class="batch-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="批次编码" prop="batchCode">
<el-input v-model="form.batchCode" placeholder="请输入批次编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="批次名称" prop="batchName">
<el-input v-model="form.batchName" placeholder="请输入批次名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="批次类型" prop="batchType">
<el-select v-model="form.batchType" placeholder="请选择批次类型">
<el-option label="年度采集" value="annual" />
<el-option label="学期采集" value="semester" />
<el-option label="月度采集" value="monthly" />
<el-option label="自定义" value="custom" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学年" prop="academicYear">
<el-input v-model="form.academicYear" placeholder="请输入学年,如:2024-2025" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学期" prop="semester">
<el-select v-model="form.semester" placeholder="请选择学期">
<el-option label="春季" value="spring" />
<el-option label="秋季" value="autumn" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="datetime"
placeholder="请选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-model="form.endTime"
type="datetime"
placeholder="请选择结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="批次描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="4" placeholder="请输入批次描述" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, toRefs, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
listCollectionBatch,
getCollectionBatch,
delCollectionBatch,
exportCollectionBatch,
addCollectionBatch,
updateCollectionBatch
} from '@/api/xg-xx/collectionBatch';
import { Collection, Select, User, DocumentCopy, Plus, Download, Search, Refresh, View, Edit, Delete } from '@element-plus/icons-vue';
// 响应式数据
const batchList = ref([]);
const loading = ref(false);
const total = ref(0);
const open = ref(false);
const title = ref('');
const queryFormRef = ref();
const batchFormRef = ref();
// 统计数据
const statistics = ref({
totalBatches: 0,
activeBatches: 0,
totalStudents: 0,
completedBatches: 0
});
// 表单数据
const initFormData = {
id: undefined,
batchCode: '',
batchName: '',
batchType: '',
academicYear: '',
semester: '',
startTime: '',
endTime: '',
description: ''
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
batchName: '',
batchType: '',
batchStatus: '',
createTime: []
},
rules: {
batchCode: [{ required: true, message: '批次编码不能为空', trigger: 'blur' }],
batchName: [{ required: true, message: '批次名称不能为空', trigger: 'blur' }],
batchType: [{ required: true, message: '批次类型不能为空', trigger: 'change' }],
academicYear: [{ required: true, message: '学年不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
// 工具方法
const getBatchTypeTagType = (type: string) => {
const typeMap = {
annual: 'primary',
semester: 'success',
monthly: 'warning',
custom: 'info'
};
return typeMap[type] || 'info';
};
const getBatchTypeText = (type: string) => {
const typeMap = {
annual: '年度采集',
semester: '学期采集',
monthly: '月度采集',
custom: '自定义'
};
return typeMap[type] || type;
};
const getStatusTagType = (status: string) => {
const statusMap = {
'1': 'info', // 草稿
'2': 'warning', // 发布
'3': 'success', // 进行中
'4': 'primary', // 已结束
'5': 'info' // 已归档
};
return statusMap[status] || 'info';
};
const getStatusText = (status: string) => {
const statusMap = {
'1': '草稿',
'2': '发布',
'3': '进行中',
'4': '已结束',
'5': '已归档'
};
return statusMap[status] || status;
};
const getProgressColor = (percentage: number) => {
if (percentage >= 80) return '#67c23a';
if (percentage >= 60) return '#e6a23c';
return '#f56c6c';
};
// 查询方法
const getList = async () => {
loading.value = true;
try {
console.log('开始获取批次列表...');
const response = await listCollectionBatch(queryParams.value);
console.log('API响应:', response);
// 兼容不同的响应格式
batchList.value = response.rows || response.data || [];
total.value = response.total || 0;
// 更新统计数据
updateStatistics();
ElMessage.success('数据加载成功');
} catch (error) {
console.error('获取批次列表失败:', error);
ElMessage.error('数据加载失败');
batchList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// 更新统计数据
const updateStatistics = () => {
statistics.value = {
totalBatches: batchList.value.length,
activeBatches: batchList.value.filter((item) => item.batchStatus === '3').length, // 进行中
totalStudents: batchList.value.reduce((sum, item) => sum + (item.totalStudents || 0), 0),
completedBatches: batchList.value.filter((item) => item.batchStatus === '4').length // 已结束
};
};
// 查询按钮
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
// 重置按钮
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
// 新增按钮
const handleAdd = () => {
reset();
open.value = true;
title.value = '新增批次';
};
// 修改按钮
const handleUpdate = (row: any) => {
reset();
form.value = { ...row };
open.value = true;
title.value = '修改批次';
};
// 详情按钮
const handleDetail = (row: any) => {
ElMessage.info(`查看批次详情: ${row.batchName}`);
};
// 删除按钮
const handleDelete = (row: any) => {
ElMessageBox.confirm(`确定删除批次"${row.batchName}"吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
ElMessage.success('删除成功');
getList();
})
.catch(() => {
ElMessage.info('已取消删除');
});
};
// 导出按钮
const handleExport = () => {
ElMessage.info('导出功能开发中...');
};
// 表格选择
const handleSelectionChange = (selection: any[]) => {
console.log('选择的行:', selection);
};
// 分页
const handleSizeChange = (size: number) => {
queryParams.value.pageSize = size;
getList();
};
const handleCurrentChange = (page: number) => {
queryParams.value.pageNum = page;
getList();
};
// 表单提交
const submitForm = async () => {
if (!batchFormRef.value) return;
try {
const valid = await batchFormRef.value.validate();
if (valid) {
const formData = { ...form.value };
if (form.value.id) {
// 编辑模式
await updateCollectionBatch(formData);
ElMessage.success('修改成功');
} else {
// 新增模式
await addCollectionBatch(formData);
ElMessage.success('新增成功');
}
open.value = false;
await getList();
}
} catch (error: any) {
console.error('保存失败:', error);
// 显示详细的错误信息
if (error.response && error.response.data && error.response.data.msg) {
ElMessage.error(error.response.data.msg);
} else if (error.message) {
ElMessage.error(error.message);
} else {
ElMessage.error('保存失败,请重试');
}
}
};
// 取消
const cancel = () => {
open.value = false;
reset();
};
// 重置表单
const reset = () => {
form.value = { ...initFormData };
batchFormRef.value?.resetFields();
};
// 初始化
onMounted(() => {
console.log('页面初始化...');
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 统计卡片样式 - 参考后勤管理模块 */
.stat-card {
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 90px;
border: none;
overflow: hidden;
position: relative;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
padding: 20px;
height: 100%;
box-sizing: border-box;
position: relative;
z-index: 1;
.stat-icon {
font-size: 28px;
margin-right: 16px;
opacity: 0.9;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}
.stat-info {
flex: 1;
.stat-value {
font-size: 24px;
font-weight: 700;
line-height: 1;
margin-bottom: 6px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.stat-label {
font-size: 14px;
opacity: 0.9;
line-height: 1;
font-weight: 500;
}
}
}
/* 渐变背景 - 参考后勤管理模块 */
&.stat-card-primary {
background: linear-gradient(135deg, rgb(93, 135, 255) 0%, rgb(73, 190, 255) 100%);
color: white;
}
&.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
&.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
&.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
/* 添加光泽效果 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
}
}
/* 搜索卡片样式 */
.search-card {
margin-bottom: 10px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-card__body) {
padding: 20px;
}
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
}
/* 按钮卡片样式 */
.button-card {
margin-bottom: 10px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
&::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-radius: 2px;
margin-right: 12px;
}
}
.toolbar-actions {
display: flex;
gap: 12px;
}
}
}
/* 表格卡片样式 */
.table-card {
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
}
/* 表格样式 - 参考后勤管理模块 */
.scroll-table {
border-radius: 8px;
overflow: hidden;
:deep(.el-table__header-wrapper) {
.el-table__header {
th {
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
}
}
}
:deep(.el-table__body-wrapper) {
.el-table__row {
transition: all 0.3s ease;
&:hover {
background-color: rgba(93, 135, 255, 0.04) !important;
}
}
}
/* 表格横向滚动条样式 */
:deep(.el-table__body-wrapper)::-webkit-scrollbar {
height: 8px;
width: 8px;
}
:deep(.el-table__body-wrapper)::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
:deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb {
background: rgb(93, 135, 255);
border-radius: 4px;
transition: all 0.3s ease;
}
:deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover {
background: rgb(73, 190, 255);
}
}
/* 学生数量样式 */
.student-count {
font-weight: 600;
color: rgb(93, 135, 255);
}
/* 进度条样式 */
.progress-text {
font-size: 12px;
color: #606266;
margin-top: 4px;
text-align: center;
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
white-space: nowrap;
}
.action-buttons .el-button {
width: 27px !important;
height: 27px !important;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 20px 0 0 0;
margin-top: 20px;
border-top: 1px solid rgba(93, 135, 255, 0.1);
:deep(.el-pagination) {
.el-pager li {
border-radius: 6px;
margin: 0 2px;
transition: all 0.3s ease;
&:hover {
background: rgba(93, 135, 255, 0.1);
}
&.is-active {
background: rgb(93, 135, 255);
color: white;
}
}
.btn-prev,
.btn-next {
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
background: rgba(93, 135, 255, 0.1);
}
}
}
}
/* 对话框样式 */
.batch-dialog {
:deep(.el-dialog) {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
:deep(.el-dialog__header) {
background: linear-gradient(135deg, rgb(93, 135, 255) 0%, rgb(73, 190, 255) 100%);
color: white;
padding: 20px 24px;
margin: 0;
.el-dialog__title {
color: white;
font-size: 18px;
font-weight: 600;
}
.el-dialog__headerbtn {
.el-dialog__close {
color: white;
font-size: 20px;
transition: all 0.3s ease;
&:hover {
color: rgba(255, 255, 255, 0.8);
transform: rotate(90deg);
}
}
}
}
:deep(.el-dialog__body) {
padding: 24px;
background: #ffffff;
}
:deep(.el-dialog__footer) {
padding: 16px 24px 24px;
background: #ffffff;
border-top: 1px solid rgba(93, 135, 255, 0.1);
}
}
/* 表单样式 */
.batch-form {
:deep(.el-form-item__label) {
font-weight: 600;
color: #1e293b;
}
:deep(.el-input__wrapper) {
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
border-color: rgba(93, 135, 255, 0.6);
}
}
:deep(.el-input__wrapper.is-focus) {
border-color: rgb(93, 135, 255);
box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.2);
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-textarea__inner) {
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
border-color: rgba(93, 135, 255, 0.6);
}
&:focus {
border-color: rgb(93, 135, 255);
box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.2);
}
}
}
/* 对话框底部按钮 */
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
.el-button {
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
}
/* 标签样式优化 */
:deep(.el-tag) {
border-radius: 6px;
font-weight: 500;
border: none;
padding: 4px 12px;
}
/* 进度条颜色优化 */
:deep(.el-progress-bar__outer) {
border-radius: 10px;
background-color: rgba(93, 135, 255, 0.1);
}
:deep(.el-progress-bar__inner) {
border-radius: 10px;
}
</style>
@@ -1,542 +0,0 @@
<template>
<div class="p-2 page-container">
<el-card shadow="never">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg font-semibold">批次权限配置</span>
<el-button @click="goBack" icon="Back">返回</el-button>
</div>
</template>
<!-- 批次选择 -->
<div class="mb-4">
<el-form :model="queryParams" inline>
<el-form-item label="选择批次">
<el-select v-model="queryParams.batchId" placeholder="请选择批次" @change="handleBatchChange" style="width: 300px">
<el-option v-for="batch in batchList" :key="batch.id" :label="`${batch.batchName} (${batch.batchCode})`" :value="batch.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button type="success" @click="handleBatchPermission" :disabled="!queryParams.batchId">批量配置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 权限配置表格 -->
<el-table v-loading="loading" :data="permissionList" stripe>
<el-table-column prop="roleType" label="角色类型" align="center" width="120">
<template #default="scope">
<el-tag v-if="scope.row.roleType === 'student'" type="primary">学生</el-tag>
<el-tag v-else-if="scope.row.roleType === 'counselor'" type="success">辅导员</el-tag>
<el-tag v-else-if="scope.row.roleType === 'class_teacher'" type="warning">班主任</el-tag>
<el-tag v-else-if="scope.row.roleType === 'college_admin'" type="info">学院管理员</el-tag>
<el-tag v-else-if="scope.row.roleType === 'student_affairs'" type="danger">学生处</el-tag>
<el-tag v-else type="default">其他</el-tag>
</template>
</el-table-column>
<el-table-column prop="roleName" label="角色名称" align="center" min-width="150" />
<el-table-column prop="targetScope" label="管理范围" align="center" min-width="200" show-overflow-tooltip />
<el-table-column label="查看权限" align="center" width="100">
<template #default="scope">
<el-switch v-model="scope.row.canView" @change="handlePermissionChange(scope.row, 'canView')" />
</template>
</el-table-column>
<el-table-column label="编辑权限" align="center" width="100">
<template #default="scope">
<el-switch v-model="scope.row.canEdit" @change="handlePermissionChange(scope.row, 'canEdit')" />
</template>
</el-table-column>
<el-table-column label="审核权限" align="center" width="100">
<template #default="scope">
<el-switch v-model="scope.row.canAudit" @change="handlePermissionChange(scope.row, 'canAudit')" />
</template>
</el-table-column>
<el-table-column label="统计权限" align="center" width="100">
<template #default="scope">
<el-switch v-model="scope.row.canStatistics" @change="handlePermissionChange(scope.row, 'canStatistics')" />
</template>
</el-table-column>
<el-table-column label="导出权限" align="center" width="100">
<template #default="scope">
<el-switch v-model="scope.row.canExport" @change="handlePermissionChange(scope.row, 'canExport')" />
</template>
</el-table-column>
<el-table-column label="有效期" align="center" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.validFrom, '{y}-{m}-{d}') }} 至 {{ parseTime(scope.row.validTo, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 权限配置对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
<el-form ref="permissionFormRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="角色类型" prop="roleType">
<el-select v-model="form.roleType" placeholder="请选择角色类型" style="width: 100%">
<el-option label="学生" value="student" />
<el-option label="辅导员" value="counselor" />
<el-option label="班主任" value="class_teacher" />
<el-option label="学院管理员" value="college_admin" />
<el-option label="学生处" value="student_affairs" />
</el-select>
</el-form-item>
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="管理范围" prop="targetScope">
<el-input v-model="form.targetScope" type="textarea" :rows="3" placeholder="请输入管理范围描述" />
</el-form-item>
<el-form-item label="权限设置">
<div class="permission-grid">
<el-checkbox v-model="form.canView">查看权限</el-checkbox>
<el-checkbox v-model="form.canEdit">编辑权限</el-checkbox>
<el-checkbox v-model="form.canAudit">审核权限</el-checkbox>
<el-checkbox v-model="form.canStatistics">统计权限</el-checkbox>
<el-checkbox v-model="form.canExport">导出权限</el-checkbox>
</div>
</el-form-item>
<el-form-item label="有效期" prop="validPeriod">
<el-date-picker
v-model="form.validPeriod"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio value="active">启用</el-radio>
<el-radio value="inactive">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</template>
</el-dialog>
<!-- 批量配置对话框 -->
<el-dialog title="批量权限配置" v-model="batchDialog.visible" width="800px" append-to-body>
<div class="mb-4">
<el-alert
title="批量配置说明"
description="选择要配置的角色类型,系统将为该类型的所有用户统一设置权限。"
type="info"
show-icon
:closable="false"
/>
</div>
<el-form ref="batchFormRef" :model="batchForm" label-width="100px">
<el-form-item label="角色类型" prop="roleTypes">
<el-checkbox-group v-model="batchForm.roleTypes">
<el-checkbox value="student">学生</el-checkbox>
<el-checkbox value="counselor">辅导员</el-checkbox>
<el-checkbox value="class_teacher">班主任</el-checkbox>
<el-checkbox value="college_admin">学院管理员</el-checkbox>
<el-checkbox value="student_affairs">学生处</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="权限设置">
<div class="permission-grid">
<el-checkbox v-model="batchForm.canView">查看权限</el-checkbox>
<el-checkbox v-model="batchForm.canEdit">编辑权限</el-checkbox>
<el-checkbox v-model="batchForm.canAudit">审核权限</el-checkbox>
<el-checkbox v-model="batchForm.canStatistics">统计权限</el-checkbox>
<el-checkbox v-model="batchForm.canExport">导出权限</el-checkbox>
</div>
</el-form-item>
<el-form-item label="有效期">
<el-date-picker
v-model="batchForm.validPeriod"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="batchForm.status">
<el-radio value="active">启用</el-radio>
<el-radio value="inactive">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="batchDialog.visible = false">取 消</el-button>
<el-button type="primary" @click="submitBatchForm">确 定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="CollectionBatchPermission" lang="ts">
import { getActiveBatches } from '@/api/xg-xx/collectionBatch';
import { CollectionBatchVO } from '@/api/xg-xx/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter();
// 数据定义
const batchList = ref<CollectionBatchVO[]>([]);
const permissionList = ref<any[]>([]);
const loading = ref(false);
const total = ref(0);
// 查询参数
const queryParams = ref({
pageNum: 1,
pageSize: 10,
batchId: undefined
});
// 对话框
const dialog = reactive({
visible: false,
title: ''
});
const batchDialog = reactive({
visible: false,
title: '批量权限配置'
});
// 表单数据
const form = ref({
id: undefined,
batchId: undefined,
roleType: '',
roleName: '',
targetScope: '',
canView: false,
canEdit: false,
canAudit: false,
canStatistics: false,
canExport: false,
validPeriod: [],
validFrom: '',
validTo: '',
status: 'active'
});
const batchForm = ref({
roleTypes: [],
canView: false,
canEdit: false,
canAudit: false,
canStatistics: false,
canExport: false,
validPeriod: [],
status: 'active'
});
// 表单验证规则
const rules = ref({
roleType: [{ required: true, message: '请选择角色类型', trigger: 'change' }],
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
targetScope: [{ required: true, message: '请输入管理范围', trigger: 'blur' }],
validPeriod: [{ required: true, message: '请选择有效期', trigger: 'change' }]
});
const permissionFormRef = ref<ElFormInstance>();
const batchFormRef = ref<ElFormInstance>();
/** 获取批次列表 */
const getBatchList = async () => {
try {
const res = await getActiveBatches();
batchList.value = res.data;
} catch (error) {
proxy?.$modal.msgError('获取批次列表失败');
}
};
/** 获取权限列表 */
const getList = async () => {
if (!queryParams.value.batchId) {
permissionList.value = [];
total.value = 0;
return;
}
loading.value = true;
// 模拟数据
setTimeout(() => {
permissionList.value = [
{
id: 1,
batchId: queryParams.value.batchId,
roleType: 'student',
roleName: '学生',
targetScope: '本人信息',
canView: true,
canEdit: true,
canAudit: false,
canStatistics: false,
canExport: false,
validFrom: '2024-01-01',
validTo: '2024-12-31',
status: 'active'
},
{
id: 2,
batchId: queryParams.value.batchId,
roleType: 'counselor',
roleName: '辅导员',
targetScope: '所负责学生',
canView: true,
canEdit: true,
canAudit: true,
canStatistics: true,
canExport: true,
validFrom: '2024-01-01',
validTo: '2024-12-31',
status: 'active'
},
{
id: 3,
batchId: queryParams.value.batchId,
roleType: 'class_teacher',
roleName: '班主任',
targetScope: '所负责班级学生',
canView: true,
canEdit: true,
canAudit: true,
canStatistics: true,
canExport: true,
validFrom: '2024-01-01',
validTo: '2024-12-31',
status: 'active'
},
{
id: 4,
batchId: queryParams.value.batchId,
roleType: 'college_admin',
roleName: '学院管理员',
targetScope: '本学院所有学生',
canView: true,
canEdit: true,
canAudit: true,
canStatistics: true,
canExport: true,
validFrom: '2024-01-01',
validTo: '2024-12-31',
status: 'active'
},
{
id: 5,
batchId: queryParams.value.batchId,
roleType: 'student_affairs',
roleName: '学生处',
targetScope: '全校所有学生',
canView: true,
canEdit: true,
canAudit: true,
canStatistics: true,
canExport: true,
validFrom: '2024-01-01',
validTo: '2024-12-31',
status: 'active'
}
];
total.value = permissionList.value.length;
loading.value = false;
}, 500);
};
/** 批次变更处理 */
const handleBatchChange = () => {
getList();
};
/** 查询 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 权限变更处理 */
const handlePermissionChange = (row: any, field: string) => {
// 这里应该调用API更新权限
console.log('更新权限:', row.id, field, row[field]);
proxy?.$modal.msgSuccess('权限更新成功');
};
/** 编辑 */
const handleEdit = (row: any) => {
form.value = {
...row,
validPeriod: [row.validFrom, row.validTo]
};
dialog.visible = true;
dialog.title = '编辑权限';
};
/** 删除 */
const handleDelete = async (row: any) => {
try {
await proxy?.$modal.confirm('确认要删除此权限配置吗?');
// 这里应该调用API删除权限
proxy?.$modal.msgSuccess('删除成功');
getList();
} catch (error) {
if (error !== 'cancel') {
proxy?.$modal.msgError('删除失败');
}
}
};
/** 批量配置 */
const handleBatchPermission = () => {
batchForm.value = {
roleTypes: [],
canView: false,
canEdit: false,
canAudit: false,
canStatistics: false,
canExport: false,
validPeriod: [],
status: 'active'
};
batchDialog.visible = true;
};
/** 提交表单 */
const submitForm = () => {
permissionFormRef.value?.validate((valid: boolean) => {
if (valid) {
// 处理有效期
if (form.value.validPeriod && form.value.validPeriod.length === 2) {
form.value.validFrom = form.value.validPeriod[0];
form.value.validTo = form.value.validPeriod[1];
}
// 这里应该调用API保存权限
console.log('保存权限:', form.value);
proxy?.$modal.msgSuccess('保存成功');
dialog.visible = false;
getList();
}
});
};
/** 提交批量表单 */
const submitBatchForm = () => {
if (batchForm.value.roleTypes.length === 0) {
proxy?.$modal.msgError('请至少选择一个角色类型');
return;
}
// 这里应该调用API批量保存权限
console.log('批量保存权限:', batchForm.value);
proxy?.$modal.msgSuccess('批量配置成功');
batchDialog.visible = false;
getList();
};
/** 取消 */
const cancel = () => {
dialog.visible = false;
form.value = {
id: undefined,
batchId: undefined,
roleType: '',
roleName: '',
targetScope: '',
canView: false,
canEdit: false,
canAudit: false,
canStatistics: false,
canExport: false,
validPeriod: [],
validFrom: '',
validTo: '',
status: 'active'
};
};
/** 返回 */
const goBack = () => {
router.push('/xg-xx/collectionBatch');
};
onMounted(() => {
getBatchList();
});
</script>
<style scoped>
.page-container {
background-color: #f5f7fa;
}
.permission-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.text-lg {
font-size: 1.125rem;
}
.font-semibold {
font-weight: 600;
}
.mb-4 {
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.permission-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,719 +0,0 @@
<template>
<div class="p-2 page-container">
<el-card shadow="never" class="mb-4">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg font-semibold">批次统计分析</span>
<div class="flex space-x-2">
<el-select v-model="selectedBatch" placeholder="选择批次" @change="handleBatchChange" style="width: 200px">
<el-option v-for="batch in batchList" :key="batch.id" :label="batch.batchName" :value="batch.id" />
</el-select>
<el-button @click="goBack" icon="Back">返回</el-button>
</div>
</div>
</template>
<!-- 总览统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-icon bg-blue-100">
<el-icon class="text-blue-500 text-2xl"><User /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value text-blue-600">{{ overallStats.totalStudents || 0 }}</div>
<div class="stat-label">目标学生</div>
</div>
</div>
</el-card>
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-icon bg-green-100">
<el-icon class="text-green-500 text-2xl"><Check /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value text-green-600">{{ overallStats.submittedCount || 0 }}</div>
<div class="stat-label">已提交</div>
</div>
</div>
</el-card>
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-icon bg-purple-100">
<el-icon class="text-purple-500 text-2xl"><CircleCheck /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value text-purple-600">{{ overallStats.approvedCount || 0 }}</div>
<div class="stat-label">已审核</div>
</div>
</div>
</el-card>
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-icon bg-orange-100">
<el-icon class="text-orange-500 text-2xl"><TrendCharts /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value text-orange-600">{{ (overallStats.completionRate || 0).toFixed(1) }}%</div>
<div class="stat-label">完成率</div>
</div>
</div>
</el-card>
</div>
<!-- 图表区域 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- 完成度饼图 -->
<el-card shadow="hover">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-blue-500"><PieChart /></el-icon>
<span>完成度分布</span>
</div>
</template>
<div ref="completionChartRef" style="width: 100%; height: 300px"></div>
</el-card>
<!-- 进度趋势图 -->
<el-card shadow="hover">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-green-500"><TrendCharts /></el-icon>
<span>提交趋势</span>
</div>
</template>
<div ref="trendChartRef" style="width: 100%; height: 300px"></div>
</el-card>
</div>
<!-- 学院统计表格 -->
<el-card shadow="hover" class="mb-6">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-purple-500"><DataAnalysis /></el-icon>
<span>学院完成情况</span>
</div>
</template>
<el-table :data="collegeStats" v-loading="loading" stripe>
<el-table-column prop="collegeName" label="学院名称" align="center" min-width="150" />
<el-table-column prop="totalStudents" label="目标学生" align="center" width="100" />
<el-table-column prop="submittedCount" label="已提交" align="center" width="100" />
<el-table-column prop="approvedCount" label="已审核" align="center" width="100" />
<el-table-column prop="rejectedCount" label="被驳回" align="center" width="100" />
<el-table-column prop="pendingCount" label="待审核" align="center" width="100" />
<el-table-column label="提交率" align="center" width="120">
<template #default="scope">
<el-progress :percentage="scope.row.submitRate || 0" :stroke-width="8" :color="getProgressColor(scope.row.submitRate || 0)" />
</template>
</el-table-column>
<el-table-column label="完成率" align="center" width="120">
<template #default="scope">
<el-progress :percentage="scope.row.completionRate || 0" :stroke-width="8" :color="getProgressColor(scope.row.completionRate || 0)" />
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 多维度统计 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 年级分布 -->
<el-card shadow="hover">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-cyan-500"><School /></el-icon>
<span>年级分布</span>
</div>
</template>
<div ref="gradeChartRef" style="width: 100%; height: 250px"></div>
</el-card>
<!-- 专业分布 -->
<el-card shadow="hover">
<template #header>
<div class="flex items-center">
<el-icon class="mr-2 text-orange-500"><Reading /></el-icon>
<span>专业分布</span>
</div>
</template>
<div ref="majorChartRef" style="width: 100%; height: 250px"></div>
</el-card>
</div>
</el-card>
</div>
</template>
<script setup name="CollectionBatchStatistics" lang="ts">
import {
getActiveBatches,
getBatchStatistics,
getOverallStatistics,
getBatchCompletionStatistics,
getBatchDimensionStatistics
} from '@/api/xg-xx/collectionBatch';
import {
CollectionBatchVO,
BatchStatistics,
BatchCompletionStatistics,
CollegeCompletionStatistics,
BatchDimensionStatistics
} from '@/api/xg-xx/types';
import * as echarts from 'echarts';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const batchList = ref<CollectionBatchVO[]>([]);
const selectedBatch = ref<number>();
const loading = ref(false);
const overallStats = ref<BatchStatistics>({});
const completionStats = ref<BatchCompletionStatistics>({});
const collegeStats = ref<CollegeCompletionStatistics[]>([]);
const gradeStats = ref<BatchDimensionStatistics>({});
const majorStats = ref<BatchDimensionStatistics>({});
// 图表引用
const completionChartRef = ref<HTMLDivElement>();
const trendChartRef = ref<HTMLDivElement>();
const gradeChartRef = ref<HTMLDivElement>();
const majorChartRef = ref<HTMLDivElement>();
// 图表实例
let completionChart: echarts.ECharts;
let trendChart: echarts.ECharts;
let gradeChart: echarts.ECharts;
let majorChart: echarts.ECharts;
/** 获取批次列表 */
const getBatchList = async () => {
try {
const res = await getActiveBatches();
batchList.value = res.data;
// 如果有路由参数,设置选中的批次
const batchId = route.query.batchId as string;
if (batchId) {
selectedBatch.value = parseInt(batchId);
} else if (batchList.value.length > 0) {
selectedBatch.value = batchList.value[0].id;
}
} catch (error) {
proxy?.$modal.msgError('获取批次列表失败');
}
};
/** 获取整体统计 */
const getOverallStats = async () => {
try {
const res = await getOverallStatistics();
overallStats.value = res.data;
} catch (error) {
proxy?.$modal.msgError('获取整体统计失败');
}
};
/** 获取批次完成统计 */
const getBatchCompletionStats = async () => {
if (!selectedBatch.value) return;
try {
loading.value = true;
const res = await getBatchCompletionStatistics(selectedBatch.value);
completionStats.value = res.data;
collegeStats.value = res.data.collegeStatistics || [];
} catch (error) {
proxy?.$modal.msgError('获取批次完成统计失败');
} finally {
loading.value = false;
}
};
/** 获取维度统计 */
const getDimensionStats = async () => {
if (!selectedBatch.value) return;
try {
const [gradeRes, majorRes] = await Promise.all([
getBatchDimensionStatistics(selectedBatch.value, 'grade'),
getBatchDimensionStatistics(selectedBatch.value, 'major')
]);
gradeStats.value = gradeRes.data;
majorStats.value = majorRes.data;
} catch (error) {
proxy?.$modal.msgError('获取维度统计失败');
}
};
/** 批次变更处理 */
const handleBatchChange = () => {
loadStatistics();
};
/** 加载统计数据 */
const loadStatistics = async () => {
await Promise.all([getBatchCompletionStats(), getDimensionStats()]);
// 更新图表
nextTick(() => {
updateCharts();
});
};
/** 初始化图表 */
const initCharts = () => {
if (completionChartRef.value) {
completionChart = echarts.init(completionChartRef.value);
}
if (trendChartRef.value) {
trendChart = echarts.init(trendChartRef.value);
}
if (gradeChartRef.value) {
gradeChart = echarts.init(gradeChartRef.value);
}
if (majorChartRef.value) {
majorChart = echarts.init(majorChartRef.value);
}
};
/** 更新图表 */
const updateCharts = () => {
updateCompletionChart();
updateTrendChart();
updateGradeChart();
updateMajorChart();
};
/** 更新完成度饼图 */
const updateCompletionChart = () => {
if (!completionChart || !completionStats.value) return;
const option = {
title: {
text: '完成度分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '完成情况',
type: 'pie',
radius: '50%',
data: [
{ value: completionStats.value.approvedCount || 0, name: '已审核' },
{ value: completionStats.value.pendingCount || 0, name: '待审核' },
{ value: completionStats.value.rejectedCount || 0, name: '被驳回' },
{
value: (completionStats.value.totalStudents || 0) - (completionStats.value.submittedCount || 0),
name: '未提交'
}
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
completionChart.setOption(option);
};
/** 更新趋势图 */
const updateTrendChart = () => {
if (!trendChart) return;
// 模拟趋势数据
const dates = [];
const submitted = [];
const approved = [];
for (let i = 7; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
dates.push(date.toISOString().split('T')[0]);
submitted.push(Math.floor(Math.random() * 100) + ((completionStats.value.submittedCount || 0) * (7 - i)) / 7);
approved.push(Math.floor(Math.random() * 80) + ((completionStats.value.approvedCount || 0) * (7 - i)) / 7);
}
const option = {
title: {
text: '提交趋势',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['已提交', '已审核']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates
},
yAxis: {
type: 'value'
},
series: [
{
name: '已提交',
type: 'line',
stack: 'Total',
data: submitted,
smooth: true,
lineStyle: {
color: '#67c23a'
},
areaStyle: {
color: 'rgba(103, 194, 58, 0.3)'
}
},
{
name: '已审核',
type: 'line',
stack: 'Total',
data: approved,
smooth: true,
lineStyle: {
color: '#409eff'
},
areaStyle: {
color: 'rgba(64, 158, 255, 0.3)'
}
}
]
};
trendChart.setOption(option);
};
/** 更新年级图表 */
const updateGradeChart = () => {
if (!gradeChart || !gradeStats.value.items) return;
const option = {
title: {
text: '年级分布',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: gradeStats.value.items.map((item) => item.label)
},
yAxis: {
type: 'value'
},
series: [
{
data: gradeStats.value.items.map((item) => item.count),
type: 'bar',
itemStyle: {
color: '#5470c6'
}
}
]
};
gradeChart.setOption(option);
};
/** 更新专业图表 */
const updateMajorChart = () => {
if (!majorChart || !majorStats.value.items) return;
const option = {
title: {
text: '专业分布',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: majorStats.value.items.map((item) => item.label),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value'
},
series: [
{
data: majorStats.value.items.map((item) => item.count),
type: 'bar',
itemStyle: {
color: '#91cc75'
}
}
]
};
majorChart.setOption(option);
};
/** 获取进度条颜色 */
const getProgressColor = (percentage: number) => {
if (percentage < 30) return '#f56c6c';
if (percentage < 70) return '#e6a23c';
return '#67c23a';
};
/** 返回 */
const goBack = () => {
router.push('/xg-xx/collectionBatch');
};
/** 窗口大小变化处理 */
const handleResize = () => {
completionChart?.resize();
trendChart?.resize();
gradeChart?.resize();
majorChart?.resize();
};
onMounted(async () => {
await getBatchList();
await getOverallStats();
nextTick(() => {
initCharts();
if (selectedBatch.value) {
loadStatistics();
}
});
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
completionChart?.dispose();
trendChart?.dispose();
gradeChart?.dispose();
majorChart?.dispose();
});
watch(selectedBatch, (newVal) => {
if (newVal) {
loadStatistics();
}
});
</script>
<style scoped>
.page-container {
background-color: #f5f7fa;
}
.stat-card {
height: 120px;
}
.stat-item {
display: flex;
align-items: center;
height: 100%;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: bold;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.gap-4 {
gap: 1rem;
}
.gap-6 {
gap: 1.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.space-x-2 > * + * {
margin-left: 0.5rem;
}
.text-lg {
font-size: 1.125rem;
}
.text-2xl {
font-size: 1.5rem;
}
.font-semibold {
font-weight: 600;
}
.bg-blue-100 {
background-color: #e3f2fd;
}
.bg-green-100 {
background-color: #e8f5e8;
}
.bg-purple-100 {
background-color: #f3e5f5;
}
.bg-orange-100 {
background-color: #fff3e0;
}
.text-blue-500 {
color: #3b82f6;
}
.text-green-500 {
color: #10b981;
}
.text-purple-500 {
color: #8b5cf6;
}
.text-orange-500 {
color: #f59e0b;
}
.text-cyan-500 {
color: #06b6d4;
}
.text-blue-600 {
color: #2563eb;
}
.text-green-600 {
color: #059669;
}
.text-purple-600 {
color: #7c3aed;
}
.text-orange-600 {
color: #ea580c;
}
@media (min-width: 768px) {
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.lg\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
</style>
@@ -1,458 +0,0 @@
<template>
<div class="page-container">
<h2>信息采集批次管理 - 样式测试</h2>
<!-- 统计卡片测试 -->
<el-row :gutter="10" class="mb-8">
<el-col :span="6">
<el-card class="stat-card stat-card-primary" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Collection /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">12</div>
<div class="stat-label">总批次数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card-success" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Select /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">5</div>
<div class="stat-label">进行中批次</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card-warning" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">1,234</div>
<div class="stat-label">参与学生数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card stat-card-info" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><DocumentCopy /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">7</div>
<div class="stat-label">已完成批次</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 按钮卡片测试 -->
<el-card class="button-card" shadow="never">
<template #header>
<div class="card-header">
<span class="toolbar-title">信息采集批次管理</span>
<div class="toolbar-actions">
<el-button type="primary">
<el-icon><Plus /></el-icon>
新增批次
</el-button>
<el-button type="success">
<el-icon><Download /></el-icon>
导出数据
</el-button>
</div>
</div>
</template>
</el-card>
<!-- 表格测试 -->
<el-card class="table-card" shadow="never">
<el-table :data="testData" class="scroll-table" stripe>
<el-table-column label="批次名称" prop="batchName" />
<el-table-column label="批次类型" prop="batchType" width="120" align="center">
<template #default="scope">
<el-tag type="primary" size="small">{{ scope.row.batchType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" prop="batchStatus" width="120" align="center">
<template #default="scope">
<el-tag type="success" size="small">{{ getStatusText(scope.row.batchStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="学生数" prop="totalStudents" width="120" align="center">
<template #default="scope">
<span class="student-count">{{ scope.row.totalStudents }}</span>
</template>
</el-table-column>
<el-table-column label="完成率" prop="completionRate" width="150" align="center">
<template #default="scope">
<el-progress :percentage="scope.row.completionRate" :stroke-width="6" :show-text="false" color="#5d87ff" />
<div class="progress-text">{{ scope.row.completionRate }}%</div>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<div class="action-buttons">
<el-button type="primary" size="small" circle>
<el-icon><View /></el-icon>
</el-button>
<el-button type="warning" size="small" circle>
<el-icon><Edit /></el-icon>
</el-button>
<el-button type="danger" size="small" circle>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- API 测试结果 -->
<el-card class="api-test-card" shadow="never">
<template #header>
<div class="card-header">
<span class="toolbar-title">API 测试结果</span>
<el-button type="primary" @click="testApi">测试 API</el-button>
</div>
</template>
<div v-if="loading" class="loading">
<el-icon class="is-loading"><Loading /></el-icon>
正在加载...
</div>
<div v-else-if="apiResult">
<pre>{{ JSON.stringify(apiResult, null, 2) }}</pre>
</div>
<div v-else class="no-data">点击"测试 API"按钮查看结果</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { listCollectionBatch } from '@/api/xg-xx/collectionBatch';
import { Collection, Select, User, DocumentCopy, Plus, Download, View, Edit, Delete, Loading } from '@element-plus/icons-vue';
const loading = ref(false);
const apiResult = ref(null);
// 状态处理方法
const getStatusText = (status: string) => {
const statusMap = {
'1': '草稿',
'2': '发布',
'3': '进行中',
'4': '已结束',
'5': '已归档'
};
return statusMap[status] || status;
};
const testData = ref([
{
batchName: '2024春季学生信息采集',
batchType: '学生信息',
batchStatus: '3',
totalStudents: 1234,
completionRate: 75
},
{
batchName: '2024家庭信息补充',
batchType: '家庭信息',
batchStatus: '4',
totalStudents: 856,
completionRate: 100
},
{
batchName: '2024其他信息收集',
batchType: '其他信息',
batchStatus: '1',
totalStudents: 0,
completionRate: 0
}
]);
const testApi = async () => {
loading.value = true;
try {
console.log('开始测试 API...');
const response = await listCollectionBatch({
pageNum: 1,
pageSize: 10
});
console.log('API 响应:', response);
apiResult.value = response;
} catch (error) {
console.error('API 测试失败:', error);
apiResult.value = { error: (error as Error).message };
} finally {
loading.value = false;
}
};
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 统计卡片样式 - 参考后勤管理模块 */
.stat-card {
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 90px;
border: none;
overflow: hidden;
position: relative;
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
padding: 20px;
height: 100%;
box-sizing: border-box;
position: relative;
z-index: 1;
.stat-icon {
font-size: 28px;
margin-right: 16px;
opacity: 0.9;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}
.stat-info {
flex: 1;
.stat-value {
font-size: 24px;
font-weight: 700;
line-height: 1;
margin-bottom: 6px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.stat-label {
font-size: 14px;
opacity: 0.9;
line-height: 1;
font-weight: 500;
}
}
}
/* 渐变背景 - 参考后勤管理模块 */
&.stat-card-primary {
background: linear-gradient(135deg, rgb(93, 135, 255) 0%, rgb(73, 190, 255) 100%);
color: white;
}
&.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
&.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
&.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
/* 添加光泽效果 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
}
}
/* 按钮卡片样式 */
.button-card {
margin: 10px 0;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
&::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-radius: 2px;
margin-right: 12px;
}
}
.toolbar-actions {
display: flex;
gap: 12px;
}
}
}
/* 表格卡片样式 */
.table-card {
margin: 10px 0;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-card__body) {
padding: 20px;
}
}
/* API 测试卡片样式 */
.api-test-card {
margin: 10px 0;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
:deep(.el-card__body) {
padding: 20px;
}
}
/* 表格样式 - 参考后勤管理模块 */
.scroll-table {
border-radius: 8px;
overflow: hidden;
:deep(.el-table__header-wrapper) {
.el-table__header {
th {
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
}
}
}
:deep(.el-table__body-wrapper) {
.el-table__row {
transition: all 0.3s ease;
&:hover {
background-color: rgba(93, 135, 255, 0.04) !important;
}
}
}
}
/* 学生数量样式 */
.student-count {
font-weight: 600;
color: rgb(93, 135, 255);
}
/* 进度条样式 */
.progress-text {
font-size: 12px;
color: #606266;
margin-top: 4px;
text-align: center;
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
.el-button {
width: 32px !important;
height: 32px !important;
padding: 0 !important;
border-radius: 8px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
}
/* 加载和无数据样式 */
.loading {
text-align: center;
padding: 40px;
color: #909399;
}
.no-data {
text-align: center;
padding: 40px;
color: #909399;
}
/* 代码显示样式 */
pre {
background: #f5f7fa;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
font-size: 12px;
color: #333;
}
</style>
@@ -1,526 +0,0 @@
<template>
<el-dialog title="缓存管理" v-model="dialogVisible" width="600px" :before-close="handleClose" append-to-body>
<div class="cache-manage-container" v-loading="loading">
<!-- 缓存统计信息 -->
<div class="cache-stats">
<div class="stats-header">
<h3>缓存统计信息</h3>
<el-button type="primary" icon="Refresh" @click="refreshCacheInfo" :loading="refreshing"> 刷新 </el-button>
</div>
<el-row :gutter="16" class="stats-cards">
<el-col :span="8">
<div class="stat-card hit-rate">
<div class="stat-icon">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ cacheInfo.hitRate }}%</div>
<div class="stat-label">命中率</div>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card hit-count">
<div class="stat-icon">
<el-icon><Check /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ cacheInfo.hitCount }}</div>
<div class="stat-label">命中次数</div>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="stat-card miss-count">
<div class="stat-icon">
<el-icon><Close /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ cacheInfo.missCount }}</div>
<div class="stat-label">未命中次数</div>
</div>
</div>
</el-col>
</el-row>
<div class="cache-details">
<el-descriptions :column="2" border>
<el-descriptions-item label="缓存大小">
{{ formatBytes(cacheInfo.size) }}
</el-descriptions-item>
<el-descriptions-item label="最后访问时间">
{{ formatTime(cacheInfo.lastAccessTime) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatTime(cacheInfo.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="过期时间">
{{ formatTime(cacheInfo.expireTime) }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<!-- 缓存操作 -->
<div class="cache-operations">
<div class="operations-header">
<h3>缓存操作</h3>
</div>
<div class="operations-content">
<div class="operation-item">
<div class="operation-info">
<div class="operation-title">清空缓存</div>
<div class="operation-desc">清空所有统计分析缓存数据,下次查询将重新计算</div>
</div>
<el-button type="danger" :loading="clearingCache" @click="handleClearCache"> 清空缓存 </el-button>
</div>
<div class="operation-item">
<div class="operation-info">
<div class="operation-title">预热缓存</div>
<div class="operation-desc">预先加载常用统计数据到缓存,提升查询性能</div>
</div>
<el-button type="success" :loading="warmingCache" @click="handleWarmupCache"> 预热缓存 </el-button>
</div>
</div>
</div>
<!-- 缓存配置 -->
<div class="cache-config">
<div class="config-header">
<h3>缓存配置</h3>
</div>
<el-form :model="cacheConfig" label-width="120px">
<el-form-item label="缓存时间">
<el-select v-model="cacheConfig.expireTime" @change="handleConfigChange">
<el-option label="5分钟" :value="5" />
<el-option label="10分钟" :value="10" />
<el-option label="30分钟" :value="30" />
<el-option label="1小时" :value="60" />
<el-option label="2小时" :value="120" />
<el-option label="6小时" :value="360" />
</el-select>
</el-form-item>
<el-form-item label="最大缓存数">
<el-input-number v-model="cacheConfig.maxSize" :min="10" :max="1000" :step="10" @change="handleConfigChange" />
</el-form-item>
<el-form-item label="自动清理">
<el-switch v-model="cacheConfig.autoClean" @change="handleConfigChange" />
</el-form-item>
</el-form>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" @click="handleSaveConfig" :loading="saving"> 保存配置 </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getCacheStatistics, clearStatisticsCache, warmupStatisticsCache, type StatisticsCacheInfo } from '@/api/xg-xx/statistics';
// Props
interface Props {
visible: boolean;
}
const props = withDefaults(defineProps<Props>(), {
visible: false
});
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
// 响应式数据
const loading = ref(false);
const refreshing = ref(false);
const clearingCache = ref(false);
const warmingCache = ref(false);
const saving = ref(false);
// 缓存信息
const cacheInfo = reactive<StatisticsCacheInfo>({
key: '',
hitCount: 0,
missCount: 0,
hitRate: 0,
size: 0,
lastAccessTime: '',
createTime: '',
expireTime: ''
});
// 缓存配置
const cacheConfig = reactive({
expireTime: 30, // 分钟
maxSize: 100,
autoClean: true
});
// 计算属性
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
// 监听器
watch(
() => props.visible,
(newVal) => {
if (newVal) {
loadCacheInfo();
}
}
);
// 方法定义
const loadCacheInfo = async () => {
loading.value = true;
try {
const response = await getCacheStatistics();
Object.assign(cacheInfo, response.data);
} catch (error) {
console.error('获取缓存信息失败:', error);
ElMessage.error('获取缓存信息失败');
} finally {
loading.value = false;
}
};
const refreshCacheInfo = async () => {
refreshing.value = true;
try {
const response = await getCacheStatistics();
Object.assign(cacheInfo, response.data);
ElMessage.success('缓存信息已刷新');
} catch (error) {
console.error('刷新缓存信息失败:', error);
ElMessage.error('刷新缓存信息失败');
} finally {
refreshing.value = false;
}
};
const handleClearCache = async () => {
try {
await ElMessageBox.confirm('确定要清空所有统计分析缓存吗?此操作不可恢复。', '清空缓存', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
clearingCache.value = true;
await clearStatisticsCache();
ElMessage.success('缓存已清空');
await refreshCacheInfo();
} catch (error) {
if (error !== 'cancel') {
console.error('清空缓存失败:', error);
ElMessage.error('清空缓存失败');
}
} finally {
clearingCache.value = false;
}
};
const handleWarmupCache = async () => {
try {
await ElMessageBox.confirm('确定要预热缓存吗?此操作可能需要一些时间。', '预热缓存', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
});
warmingCache.value = true;
await warmupStatisticsCache();
ElMessage.success('缓存预热完成');
await refreshCacheInfo();
} catch (error) {
if (error !== 'cancel') {
console.error('预热缓存失败:', error);
ElMessage.error('预热缓存失败');
}
} finally {
warmingCache.value = false;
}
};
const handleConfigChange = () => {
// 配置变更时的处理逻辑
console.log('缓存配置变更:', cacheConfig);
};
const handleSaveConfig = async () => {
saving.value = true;
try {
// 这里应该调用保存配置的API
// await saveCacheConfig(cacheConfig)
// 模拟保存
await new Promise((resolve) => setTimeout(resolve, 1000));
ElMessage.success('配置已保存');
} catch (error) {
console.error('保存配置失败:', error);
ElMessage.error('保存配置失败');
} finally {
saving.value = false;
}
};
const handleClose = () => {
dialogVisible.value = false;
};
// 工具方法
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatTime = (timeStr: string): string => {
if (!timeStr) return '-';
try {
const date = new Date(timeStr);
return date.toLocaleString('zh-CN');
} catch (error) {
return timeStr;
}
};
// 生命周期
onMounted(() => {
if (props.visible) {
loadCacheInfo();
}
});
</script>
<style scoped>
.cache-manage-container {
max-height: 70vh;
overflow-y: auto;
}
.cache-stats {
margin-bottom: 24px;
}
.stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.stats-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.stats-cards {
margin-bottom: 16px;
}
.stat-card {
display: flex;
align-items: center;
padding: 16px;
background: #ffffff;
border: 1px solid #e4e7ed;
border-radius: 8px;
transition: all 0.3s ease;
}
.stat-card:hover {
border-color: rgba(93, 135, 255, 0.3);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-card.hit-rate {
border-left: 4px solid #67c23a;
}
.stat-card.hit-count {
border-left: 4px solid #409eff;
}
.stat-card.miss-count {
border-left: 4px solid #f56c6c;
}
.stat-icon {
font-size: 24px;
margin-right: 12px;
opacity: 0.8;
}
.hit-rate .stat-icon {
color: #67c23a;
}
.hit-count .stat-icon {
color: #409eff;
}
.miss-count .stat-icon {
color: #f56c6c;
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: 18px;
font-weight: 600;
line-height: 1;
margin-bottom: 4px;
}
.hit-rate .stat-value {
color: #67c23a;
}
.hit-count .stat-value {
color: #409eff;
}
.miss-count .stat-value {
color: #f56c6c;
}
.stat-label {
font-size: 12px;
color: #909399;
line-height: 1;
}
.cache-details {
margin-top: 16px;
}
.cache-operations {
margin-bottom: 24px;
padding-top: 24px;
border-top: 1px solid #e4e7ed;
}
.operations-header h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.operations-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.operation-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.operation-info {
flex: 1;
}
.operation-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.operation-desc {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
.cache-config {
padding-top: 24px;
border-top: 1px solid #e4e7ed;
}
.config-header h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.stats-cards {
flex-direction: column;
gap: 12px;
}
.operation-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.operation-item .el-button {
width: 100%;
}
}
/* 滚动条样式 */
.cache-manage-container::-webkit-scrollbar {
width: 6px;
}
.cache-manage-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.cache-manage-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.cache-manage-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
@@ -1,272 +0,0 @@
<template>
<el-dialog title="配置详情" v-model="dialogVisible" width="600px" :before-close="handleClose" append-to-body>
<div class="config-detail-container" v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item label="配置名称">
{{ configData?.configName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="配置类型">
<el-tag v-if="configData?.configType === 'dimension'" type="primary">维度统计</el-tag>
<el-tag v-else-if="configData?.configType === 'report'" type="success">报表配置</el-tag>
<el-tag v-else-if="configData?.configType === 'chart'" type="info">图表配置</el-tag>
<el-tag v-else type="warning">其他</el-tag>
</el-descriptions-item>
<el-descriptions-item label="维度类型">
{{ getDimensionTypeName(configData?.dimensionType) }}
</el-descriptions-item>
<el-descriptions-item label="图表类型">
<el-tag v-if="configData?.chartType === 'bar'" size="small">柱状图</el-tag>
<el-tag v-else-if="configData?.chartType === 'pie'" size="small" type="success">饼图</el-tag>
<el-tag v-else-if="configData?.chartType === 'line'" size="small" type="info">折线图</el-tag>
<el-tag v-else-if="configData?.chartType === 'doughnut'" size="small" type="warning">环形图</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="configData?.isEnabled" type="success">启用</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</el-descriptions-item>
<el-descriptions-item label="排序序号">
{{ configData?.sortOrder || 0 }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ formatTime(configData?.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ formatTime(configData?.updateTime) }}
</el-descriptions-item>
</el-descriptions>
<div class="config-json-section" v-if="configData?.queryConditions">
<h3>查询条件</h3>
<div class="json-viewer">
<pre>{{ formatJson(configData.queryConditions) }}</pre>
</div>
</div>
<div class="config-json-section" v-if="configData?.displaySettings">
<h3>显示设置</h3>
<div class="json-viewer">
<pre>{{ formatJson(configData.displaySettings) }}</pre>
</div>
</div>
<div class="config-remark-section" v-if="configData?.remark">
<h3>备注</h3>
<div class="remark-content">
{{ configData.remark }}
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import type { StatisticsConfigVo } from '@/api/xg-xx/statistics';
// Props
interface Props {
visible: boolean;
configData?: StatisticsConfigVo | null;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
configData: null
});
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
// 响应式数据
const loading = false;
// 计算属性
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
// 方法定义
const getDimensionTypeName = (type?: string): string => {
const dimensionMap: Record<string, string> = {
college: '学院统计',
major: '专业统计',
class: '班级统计',
grade: '年级统计',
gender: '性别统计',
age: '年龄统计',
political: '政治面貌统计',
nation: '民族统计',
sourceRegion: '生源地统计',
bloodType: '血型统计',
religion: '宗教信仰统计',
studentStatus: '学籍状态统计',
trainingType: '培养方式统计'
};
return type ? dimensionMap[type] || type : '-';
};
const formatTime = (timeStr?: string): string => {
if (!timeStr) return '-';
try {
const date = new Date(timeStr);
return date.toLocaleString('zh-CN');
} catch (error) {
return timeStr;
}
};
const formatJson = (jsonStr?: string): string => {
if (!jsonStr) return '';
try {
const parsed = JSON.parse(jsonStr);
return JSON.stringify(parsed, null, 2);
} catch (error) {
return jsonStr;
}
};
const handleClose = () => {
dialogVisible.value = false;
};
</script>
<style scoped>
.config-detail-container {
max-height: 70vh;
overflow-y: auto;
}
.config-json-section {
margin-top: 24px;
}
.config-json-section h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.json-viewer {
background: #f8f9fa;
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 16px;
overflow-x: auto;
}
.json-viewer pre {
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.5;
color: #303133;
white-space: pre-wrap;
word-break: break-all;
}
.config-remark-section {
margin-top: 24px;
}
.config-remark-section h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.remark-content {
background: #f8f9fa;
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 16px;
line-height: 1.6;
color: #303133;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 描述列表样式优化 */
:deep(.el-descriptions__header) {
margin-bottom: 16px;
}
:deep(.el-descriptions__title) {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
:deep(.el-descriptions__body) {
.el-descriptions__table {
border-radius: 6px;
overflow: hidden;
}
.el-descriptions__label {
background: #f8f9fa;
color: #606266;
font-weight: 500;
}
.el-descriptions__content {
background: #ffffff;
color: #303133;
}
}
/* 滚动条样式 */
.config-detail-container::-webkit-scrollbar {
width: 6px;
}
.config-detail-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.config-detail-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.config-detail-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.json-viewer::-webkit-scrollbar {
height: 6px;
}
.json-viewer::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.json-viewer::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.json-viewer::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
@@ -1,310 +0,0 @@
<template>
<el-dialog :title="isEdit ? '编辑配置' : '新增配置'" v-model="dialogVisible" width="600px" :before-close="handleClose" append-to-body>
<el-form ref="configForm" :model="form" :rules="rules" label-width="100px" v-loading="loading">
<el-form-item label="配置名称" prop="configName">
<el-input v-model="form.configName" placeholder="请输入配置名称" clearable />
</el-form-item>
<el-form-item label="配置类型" prop="configType">
<el-select v-model="form.configType" placeholder="请选择配置类型" @change="handleConfigTypeChange">
<el-option label="维度统计" value="dimension" />
<el-option label="报表配置" value="report" />
<el-option label="图表配置" value="chart" />
</el-select>
</el-form-item>
<el-form-item label="维度类型" prop="dimensionType" v-if="form.configType === 'dimension'">
<el-select v-model="form.dimensionType" placeholder="请选择维度类型">
<el-option label="学院统计" value="college" />
<el-option label="专业统计" value="major" />
<el-option label="班级统计" value="class" />
<el-option label="年级统计" value="grade" />
<el-option label="性别统计" value="gender" />
<el-option label="年龄统计" value="age" />
<el-option label="政治面貌统计" value="political" />
<el-option label="民族统计" value="nation" />
<el-option label="生源地统计" value="sourceRegion" />
<el-option label="血型统计" value="bloodType" />
<el-option label="宗教信仰统计" value="religion" />
<el-option label="学籍状态统计" value="studentStatus" />
<el-option label="培养方式统计" value="trainingType" />
</el-select>
</el-form-item>
<el-form-item label="图表类型" prop="chartType">
<el-select v-model="form.chartType" placeholder="请选择图表类型">
<el-option label="柱状图" value="bar" />
<el-option label="饼图" value="pie" />
<el-option label="折线图" value="line" />
<el-option label="环形图" value="doughnut" />
</el-select>
</el-form-item>
<el-form-item label="查询条件" prop="queryConditions">
<el-input v-model="form.queryConditions" type="textarea" :rows="3" placeholder="请输入查询条件(JSON格式)" />
</el-form-item>
<el-form-item label="显示设置" prop="displaySettings">
<el-input v-model="form.displaySettings" type="textarea" :rows="3" placeholder="请输入显示设置(JSON格式)" />
</el-form-item>
<el-form-item label="排序序号" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="999" placeholder="排序序号" />
</el-form-item>
<el-form-item label="状态" prop="isEnabled">
<el-switch v-model="form.isEnabled" active-text="启用" inactive-text="禁用" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import {
addStatisticsConfig,
updateStatisticsConfig,
getStatisticsConfig,
type StatisticsConfigVo,
type StatisticsConfigBo
} from '@/api/xg-xx/statistics';
// Props
interface Props {
visible: boolean;
configData?: StatisticsConfigVo | null;
isEdit?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
configData: null,
isEdit: false
});
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean];
'refresh': [];
}>();
// 响应式数据
const loading = ref(false);
const configForm = ref();
// 表单数据
const form = reactive<StatisticsConfigBo>({
id: undefined,
configName: '',
configType: '',
dimensionType: '',
queryConditions: '',
displaySettings: '',
chartType: '',
isEnabled: true,
sortOrder: 0,
remark: ''
});
// 表单验证规则
const rules = {
configName: [
{ required: true, message: '请输入配置名称', trigger: 'blur' },
{ min: 2, max: 50, message: '配置名称长度在 2 到 50 个字符', trigger: 'blur' }
],
configType: [{ required: true, message: '请选择配置类型', trigger: 'change' }],
dimensionType: [{ required: true, message: '请选择维度类型', trigger: 'change' }],
chartType: [{ required: true, message: '请选择图表类型', trigger: 'change' }],
queryConditions: [{ validator: validateJson, trigger: 'blur' }],
displaySettings: [{ validator: validateJson, trigger: 'blur' }],
sortOrder: [{ type: 'number' as const, message: '排序序号必须是数字', trigger: 'blur' }]
};
// 计算属性
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
// 监听器
watch(
() => props.visible,
(newVal) => {
if (newVal) {
resetForm();
if (props.isEdit && props.configData) {
loadConfigData();
}
}
}
);
// 方法定义
function validateJson(rule: any, value: string, callback: any) {
if (!value) {
callback();
return;
}
try {
JSON.parse(value);
callback();
} catch (error) {
callback(new Error('请输入有效的JSON格式'));
}
}
const resetForm = () => {
Object.assign(form, {
id: undefined,
configName: '',
configType: '',
dimensionType: '',
queryConditions: '',
displaySettings: '',
chartType: '',
isEnabled: true,
sortOrder: 0,
remark: ''
});
configForm.value?.clearValidate();
};
const loadConfigData = async () => {
if (!props.configData?.id) return;
loading.value = true;
try {
const response = await getStatisticsConfig(props.configData.id);
const data = response.data;
Object.assign(form, {
id: data.id,
configName: data.configName,
configType: data.configType,
dimensionType: data.dimensionType,
queryConditions: data.queryConditions,
displaySettings: data.displaySettings,
chartType: data.chartType,
isEnabled: data.isEnabled,
sortOrder: data.sortOrder,
remark: data.remark
});
} catch (error) {
console.error('加载配置数据失败:', error);
ElMessage.error('加载配置数据失败');
} finally {
loading.value = false;
}
};
const handleConfigTypeChange = (value: string) => {
if (value !== 'dimension') {
form.dimensionType = '';
}
};
const handleSubmit = async () => {
if (!configForm.value) return;
try {
await configForm.value.validate();
} catch (error) {
return;
}
loading.value = true;
try {
if (props.isEdit) {
await updateStatisticsConfig(form);
ElMessage.success('配置更新成功');
} else {
await addStatisticsConfig(form);
ElMessage.success('配置创建成功');
}
emit('refresh');
handleClose();
} catch (error) {
console.error('保存配置失败:', error);
ElMessage.error('保存配置失败');
} finally {
loading.value = false;
}
};
const handleClose = () => {
dialogVisible.value = false;
};
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 表单样式优化 */
:deep(.el-form-item__label) {
font-weight: 500;
color: #1e293b;
}
:deep(.el-input__wrapper) {
border-radius: 6px;
transition: all 0.3s ease;
}
:deep(.el-input__wrapper:hover) {
border-color: rgba(93, 135, 255, 0.6);
}
:deep(.el-input__wrapper.is-focus) {
border-color: rgb(93, 135, 255);
box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.2);
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-textarea__inner) {
border-radius: 6px;
transition: all 0.3s ease;
}
:deep(.el-textarea__inner:hover) {
border-color: rgba(93, 135, 255, 0.6);
}
:deep(.el-textarea__inner:focus) {
border-color: rgb(93, 135, 255);
box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.2);
}
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-switch.is-checked .el-switch__core) {
background-color: rgb(93, 135, 255);
border-color: rgb(93, 135, 255);
}
</style>
@@ -1,493 +0,0 @@
<template>
<el-dialog title="统计配置管理" v-model="dialogVisible" width="80%" :before-close="handleClose" append-to-body class="config-manage-dialog">
<div class="config-manage-container">
<!-- 搜索区域 -->
<div class="search-section">
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="80px">
<el-form-item label="配置名称" prop="configName">
<el-input v-model="queryParams.configName" placeholder="请输入配置名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="配置类型" prop="configType">
<el-select v-model="queryParams.configType" placeholder="请选择配置类型" clearable>
<el-option label="维度统计" value="dimension" />
<el-option label="报表配置" value="report" />
<el-option label="图表配置" value="chart" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="isEnabled">
<el-select v-model="queryParams.isEnabled" placeholder="请选择状态" clearable>
<el-option label="启用" :value="true" />
<el-option label="禁用" :value="false" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 操作按钮 -->
<div class="toolbar-section">
<div class="toolbar-left">
<el-button type="primary" icon="Plus" @click="handleAdd">新增配置</el-button>
<el-button type="success" icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
<el-button type="danger" icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</div>
<div class="toolbar-right">
<el-button type="info" icon="Download" @click="handleExport">导出</el-button>
</div>
</div>
<!-- 配置列表 -->
<div class="table-section">
<el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange" class="config-table">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="配置名称" prop="configName" width="150" />
<el-table-column label="配置类型" prop="configType" width="100">
<template #default="scope">
<el-tag v-if="scope.row.configType === 'dimension'" type="primary">维度统计</el-tag>
<el-tag v-else-if="scope.row.configType === 'report'" type="success">报表配置</el-tag>
<el-tag v-else-if="scope.row.configType === 'chart'" type="info">图表配置</el-tag>
<el-tag v-else type="warning">其他</el-tag>
</template>
</el-table-column>
<el-table-column label="维度类型" prop="dimensionType" width="120" />
<el-table-column label="图表类型" prop="chartType" width="100">
<template #default="scope">
<el-tag v-if="scope.row.chartType === 'bar'" size="small">柱状图</el-tag>
<el-tag v-else-if="scope.row.chartType === 'pie'" size="small" type="success">饼图</el-tag>
<el-tag v-else-if="scope.row.chartType === 'line'" size="small" type="info">折线图</el-tag>
<el-tag v-else-if="scope.row.chartType === 'doughnut'" size="small" type="warning">环形图</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" prop="isEnabled" width="80" align="center">
<template #default="scope">
<el-switch v-model="scope.row.isEnabled" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="排序" prop="sortOrder" width="80" align="center" />
<el-table-column label="创建时间" prop="createTime" width="160" />
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-button type="primary" icon="View" size="small" @click="handleView(scope.row)" title="查看" />
<el-button type="success" icon="Edit" size="small" @click="handleUpdate(scope.row)" title="编辑" />
<el-button type="danger" icon="Delete" size="small" @click="handleDelete(scope.row)" title="删除" />
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-section">
<el-pagination
v-show="total > 0"
:current-page="queryParams.pageNum"
:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 配置编辑对话框 -->
<ConfigEditDialog
:visible="editVisible"
:config-data="selectedConfig"
:is-edit="isEdit"
@update:visible="(val) => (editVisible = val)"
@refresh="getList"
/>
<!-- 配置详情对话框 -->
<ConfigDetailDialog :visible="detailVisible" :config-data="selectedConfig" @update:visible="(val) => (detailVisible = val)" />
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
listStatisticsConfig,
delStatisticsConfig,
changeStatisticsConfigStatus,
exportStatisticsConfig,
type StatisticsQuery,
type StatisticsConfigVo
} from '@/api/xg-xx/statistics';
import ConfigEditDialog from './ConfigEditDialog.vue';
import ConfigDetailDialog from './ConfigDetailDialog.vue';
// Props
interface Props {
visible: boolean;
}
const props = withDefaults(defineProps<Props>(), {
visible: false
});
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean];
'refresh': [];
}>();
// 响应式数据
const loading = ref(false);
const queryRef = ref();
// 查询参数
const queryParams = reactive<StatisticsQuery>({
pageNum: 1,
pageSize: 10,
configName: '',
configType: '',
isEnabled: undefined
});
// 列表数据
const configList = ref<StatisticsConfigVo[]>([]);
const total = ref(0);
// 选择数据
const selectedIds = ref<number[]>([]);
const selectedRows = ref<StatisticsConfigVo[]>([]);
const single = ref(true);
const multiple = ref(true);
// 对话框状态
const editVisible = ref(false);
const detailVisible = ref(false);
const selectedConfig = ref<StatisticsConfigVo | null>(null);
const isEdit = ref(false);
// 计算属性
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
// 方法定义
const getList = async () => {
loading.value = true;
try {
const response = await listStatisticsConfig(queryParams);
configList.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取配置列表失败:', error);
ElMessage.error('获取配置列表失败');
} finally {
loading.value = false;
}
};
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
const resetQuery = () => {
queryRef.value?.resetFields();
queryParams.pageNum = 1;
queryParams.pageSize = 10;
queryParams.configName = '';
queryParams.configType = '';
queryParams.isEnabled = undefined;
getList();
};
const handleSelectionChange = (selection: StatisticsConfigVo[]) => {
selectedIds.value = selection.map((item) => item.id!).filter((id) => id !== undefined);
selectedRows.value = selection;
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
const handleAdd = () => {
selectedConfig.value = null;
isEdit.value = false;
editVisible.value = true;
};
const handleUpdate = (row?: StatisticsConfigVo) => {
const config = row || selectedRows.value[0];
selectedConfig.value = config;
isEdit.value = true;
editVisible.value = true;
};
const handleView = (row: StatisticsConfigVo) => {
selectedConfig.value = row;
detailVisible.value = true;
};
const handleDelete = async (row?: StatisticsConfigVo) => {
const ids = row ? [row.id!] : selectedIds.value;
const names = row ? [row.configName!] : selectedRows.value.map((item) => item.configName!);
try {
await ElMessageBox.confirm(`确定要删除配置"${names.join('、')}"吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
await delStatisticsConfig(ids);
ElMessage.success('删除成功');
getList();
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error);
ElMessage.error('删除失败');
}
}
};
const handleStatusChange = async (row: StatisticsConfigVo) => {
try {
await changeStatisticsConfigStatus(row.id!, row.isEnabled!);
ElMessage.success(`${row.isEnabled ? '启用' : '禁用'}成功`);
} catch (error) {
console.error('状态变更失败:', error);
ElMessage.error('状态变更失败');
// 恢复原状态
row.isEnabled = !row.isEnabled;
}
};
const handleExport = async () => {
try {
const response = await exportStatisticsConfig(queryParams);
// 处理文件下载
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `统计配置_${new Date().toISOString().slice(0, 10)}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
ElMessage.success('导出成功');
} catch (error) {
console.error('导出失败:', error);
ElMessage.error('导出失败');
}
};
const handleSizeChange = (size: number) => {
queryParams.pageSize = size;
getList();
};
const handleCurrentChange = (page: number) => {
queryParams.pageNum = page;
getList();
};
const handleClose = () => {
dialogVisible.value = false;
};
// 生命周期
onMounted(() => {
if (props.visible) {
getList();
}
});
</script>
<style scoped>
.config-manage-dialog {
:deep(.el-dialog) {
border-radius: 12px;
overflow: hidden;
}
:deep(.el-dialog__header) {
background: linear-gradient(135deg, rgb(93, 135, 255) 0%, rgb(73, 190, 255) 100%);
color: white;
padding: 20px 24px;
margin: 0;
}
:deep(.el-dialog__title) {
color: white;
font-size: 18px;
font-weight: 600;
}
:deep(.el-dialog__headerbtn) {
.el-dialog__close {
color: white;
font-size: 20px;
}
}
:deep(.el-dialog__body) {
padding: 20px 24px;
}
:deep(.el-dialog__footer) {
padding: 16px 24px 20px;
border-top: 1px solid rgba(93, 135, 255, 0.1);
}
}
.config-manage-container {
max-height: 70vh;
overflow-y: auto;
}
.search-section {
margin-bottom: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.toolbar-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 0 4px;
}
.toolbar-left {
display: flex;
gap: 8px;
}
.toolbar-right {
display: flex;
gap: 8px;
}
.table-section {
margin-bottom: 16px;
}
.config-table {
width: 100%;
border-radius: 8px;
overflow: hidden;
}
.config-table :deep(.el-table__header-wrapper) {
.el-table__header {
th {
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
}
}
}
.config-table :deep(.el-table__body-wrapper) {
.el-table__row {
transition: all 0.3s ease;
}
.el-table__row:hover {
background-color: rgba(93, 135, 255, 0.04) !important;
}
}
.action-buttons {
display: flex;
gap: 4px;
justify-content: center;
align-items: center;
}
.action-buttons .el-button {
width: 28px !important;
height: 28px !important;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.action-buttons .el-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.pagination-section {
display: flex;
justify-content: flex-end;
padding: 16px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.toolbar-section {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.toolbar-left,
.toolbar-right {
width: 100%;
justify-content: center;
}
.config-table {
font-size: 12px;
}
.action-buttons {
flex-direction: column;
gap: 2px;
}
}
/* 滚动条样式 */
.config-manage-container::-webkit-scrollbar {
width: 6px;
}
.config-manage-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.config-manage-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.config-manage-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
@@ -1,271 +0,0 @@
<template>
<el-dialog title="导出统计数据" v-model="dialogVisible" width="500px" :before-close="handleClose" append-to-body>
<el-form ref="exportForm" :model="form" :rules="rules" label-width="100px" v-loading="loading">
<el-form-item label="导出格式" prop="format">
<el-radio-group v-model="form.format">
<el-radio label="excel">Excel格式</el-radio>
<el-radio label="csv">CSV格式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="包含内容" prop="includeChart">
<el-checkbox v-model="form.includeChart" :disabled="form.format === 'csv'"> 包含图表 </el-checkbox>
</el-form-item>
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="form.fileName" placeholder="请输入文件名称" :suffix="form.format === 'excel' ? '.xlsx' : '.csv'" />
</el-form-item>
<el-form-item label="数据预览">
<div class="data-preview">
<div class="preview-header">
<span>共 {{ statisticsData.length }} 条数据</span>
</div>
<div class="preview-content">
<div v-for="(item, index) in statisticsData.slice(0, 5)" :key="index" class="preview-item">
<span class="item-label">{{ item.label }}</span>
<span class="item-value">{{ item.value }}</span>
<span class="item-percentage">{{ item.percentage }}%</span>
</div>
<div v-if="statisticsData.length > 5" class="preview-more">...还有 {{ statisticsData.length - 5 }} 条数据</div>
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleExport" :loading="loading"> 导出 </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { exportStatisticsResult, type StatisticsExportQuery } from '@/api/xg-xx/statistics';
// Props
interface Props {
visible: boolean;
queryParams: any;
statisticsData: Array<{
label: string;
value: number;
percentage: number;
}>;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
queryParams: () => ({}),
statisticsData: () => []
});
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
// 响应式数据
const loading = ref(false);
const exportForm = ref();
// 表单数据
const form = reactive({
format: 'excel' as 'excel' | 'csv',
includeChart: true,
fileName: ''
});
// 表单验证规则
const rules = {
format: [{ required: true, message: '请选择导出格式', trigger: 'change' }],
fileName: [
{ required: true, message: '请输入文件名称', trigger: 'blur' },
{ min: 1, max: 50, message: '文件名称长度在 1 到 50 个字符', trigger: 'blur' }
]
};
// 计算属性
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
// 监听器
watch(
() => props.visible,
(newVal) => {
if (newVal) {
resetForm();
generateFileName();
}
}
);
watch(
() => form.format,
(newVal) => {
if (newVal === 'csv') {
form.includeChart = false;
}
generateFileName();
}
);
// 方法定义
const generateFileName = () => {
const now = new Date();
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
const timeStr = now.toTimeString().slice(0, 8).replace(/:/g, '');
let prefix = '统计数据';
if (props.queryParams.dimension) {
const dimensionMap: Record<string, string> = {
college: '学院统计',
major: '专业统计',
class: '班级统计',
grade: '年级统计',
gender: '性别统计',
age: '年龄统计',
political: '政治面貌统计',
nation: '民族统计',
sourceRegion: '生源地统计',
bloodType: '血型统计',
religion: '宗教信仰统计',
studentStatus: '学籍状态统计',
trainingType: '培养方式统计'
};
prefix = dimensionMap[props.queryParams.dimension] || '统计数据';
}
form.fileName = `${prefix}_${dateStr}_${timeStr}`;
};
const resetForm = () => {
form.format = 'excel';
form.includeChart = true;
form.fileName = '';
};
const handleClose = () => {
dialogVisible.value = false;
};
const handleExport = async () => {
if (!exportForm.value) return;
try {
await exportForm.value.validate();
} catch (error) {
return;
}
loading.value = true;
try {
const exportQuery: StatisticsExportQuery = {
...props.queryParams,
format: form.format,
includeChart: form.includeChart
};
const response = await exportStatisticsResult(exportQuery);
// 创建下载链接
const blob = new Blob([response.data], {
type: form.format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'text/csv;charset=utf-8'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${form.fileName}.${form.format === 'excel' ? 'xlsx' : 'csv'}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
ElMessage.success('导出成功');
handleClose();
} catch (error) {
console.error('导出失败:', error);
ElMessage.error('导出失败');
} finally {
loading.value = false;
}
};
</script>
<style scoped>
.data-preview {
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
}
.preview-header {
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e4e7ed;
font-size: 14px;
color: #606266;
font-weight: 500;
}
.preview-content {
max-height: 200px;
overflow-y: auto;
}
.preview-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
border-bottom: 1px solid #f0f2f5;
font-size: 13px;
}
.preview-item:last-child {
border-bottom: none;
}
.item-label {
flex: 1;
color: #303133;
}
.item-value {
width: 80px;
text-align: right;
color: rgb(93, 135, 255);
font-weight: 500;
}
.item-percentage {
width: 60px;
text-align: right;
color: #909399;
font-size: 12px;
}
.preview-more {
padding: 8px 16px;
text-align: center;
color: #909399;
font-size: 12px;
font-style: italic;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
@@ -1,521 +0,0 @@
<template>
<div class="statistics-chart">
<div class="chart-header">
<div class="chart-title">{{ title }}</div>
<div class="chart-actions">
<el-radio-group v-model="currentChartType" @change="handleChartTypeChange">
<el-radio-button label="bar">柱状图</el-radio-button>
<el-radio-button label="pie">饼图</el-radio-button>
<el-radio-button label="line">折线图</el-radio-button>
<el-radio-button label="doughnut">环形图</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="chart-container">
<div ref="chartRef" class="chart-wrapper" v-loading="loading"></div>
<div v-if="!data || data.length === 0" class="chart-empty">
<el-empty description="暂无数据">
<el-icon class="empty-icon"><DataAnalysis /></el-icon>
</el-empty>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
// Props
interface ChartData {
label: string;
value: number;
percentage: number;
}
interface Props {
title?: string;
data: ChartData[];
chartType?: 'bar' | 'pie' | 'line' | 'doughnut';
loading?: boolean;
height?: number;
}
const props = withDefaults(defineProps<Props>(), {
title: '统计图表',
data: () => [],
chartType: 'bar',
loading: false,
height: 400
});
// Emits
const emit = defineEmits<{
'chart-click': [data: ChartData];
'chart-type-change': [type: string];
}>();
// 响应式数据
const chartRef = ref<HTMLElement>();
const chartInstance = ref<echarts.ECharts>();
const currentChartType = ref(props.chartType);
// 图表配色方案
const colorScheme = [
'#5d87ff',
'#49beff',
'#ff9a56',
'#ffad56',
'#4facfe',
'#00f2fe',
'#a8edea',
'#fed6e3',
'#667eea',
'#764ba2',
'#f093fb',
'#f5576c',
'#4facfe',
'#00f2fe',
'#43e97b',
'#38f9d7'
];
// 监听器
watch(
() => props.data,
() => {
renderChart();
},
{ deep: true }
);
watch(
() => props.loading,
(newVal) => {
if (!newVal) {
nextTick(() => {
renderChart();
});
}
}
);
// 方法定义
const initChart = () => {
if (!chartRef.value) return;
chartInstance.value = echarts.init(chartRef.value);
// 监听图表点击事件
chartInstance.value.on('click', (params: any) => {
const clickedData = props.data.find((item) => item.label === params.name);
if (clickedData) {
emit('chart-click', clickedData);
}
});
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
};
const handleResize = () => {
if (chartInstance.value) {
chartInstance.value.resize();
}
};
const handleChartTypeChange = (type: string) => {
currentChartType.value = type as any;
emit('chart-type-change', type);
renderChart();
};
const renderChart = () => {
if (!chartInstance.value || !props.data || props.data.length === 0) {
return;
}
const option = generateChartOption();
chartInstance.value.setOption(option, true);
};
const generateChartOption = (): echarts.EChartsCoreOption => {
const labels = props.data.map((item) => item.label);
const values = props.data.map((item) => item.value);
const baseOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: 'rgba(0, 0, 0, 0.8)',
textStyle: {
color: '#fff'
}
},
legend: {
show: currentChartType.value === 'pie' || currentChartType.value === 'doughnut',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
textStyle: {
color: '#606266'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
}
};
switch (currentChartType.value) {
case 'pie':
return {
...baseOption,
tooltip: {
...baseOption.tooltip,
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
series: [
{
name: props.title,
type: 'pie',
radius: '60%',
center: ['40%', '50%'],
data: props.data.map((item, index) => ({
name: item.label,
value: item.value,
itemStyle: {
color: colorScheme[index % colorScheme.length]
}
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
label: {
show: true,
formatter: '{b}: {d}%'
}
}
]
};
case 'doughnut':
return {
...baseOption,
tooltip: {
...baseOption.tooltip,
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
series: [
{
name: props.title,
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
data: props.data.map((item, index) => ({
name: item.label,
value: item.value,
itemStyle: {
color: colorScheme[index % colorScheme.length]
}
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
label: {
show: true,
formatter: '{b}: {d}%'
}
}
]
};
case 'line':
return {
...baseOption,
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: 'rgba(0, 0, 0, 0.8)',
textStyle: {
color: '#fff'
}
},
xAxis: {
type: 'category',
data: labels,
axisLabel: {
color: '#606266',
interval: 0,
rotate: labels.length > 6 ? 45 : 0
},
axisLine: {
lineStyle: {
color: '#e4e7ed'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#606266'
},
axisLine: {
lineStyle: {
color: '#e4e7ed'
}
},
splitLine: {
lineStyle: {
color: '#f0f2f5'
}
}
},
series: [
{
name: props.title,
type: 'line',
data: values,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
color: colorScheme[0],
width: 3
},
itemStyle: {
color: colorScheme[0]
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colorScheme[0] + '40' },
{ offset: 1, color: colorScheme[0] + '10' }
])
}
}
]
};
default: // bar
return {
...baseOption,
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: 'rgba(0, 0, 0, 0.8)',
textStyle: {
color: '#fff'
}
},
xAxis: {
type: 'category',
data: labels,
axisLabel: {
color: '#606266',
interval: 0,
rotate: labels.length > 6 ? 45 : 0
},
axisLine: {
lineStyle: {
color: '#e4e7ed'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#606266'
},
axisLine: {
lineStyle: {
color: '#e4e7ed'
}
},
splitLine: {
lineStyle: {
color: '#f0f2f5'
}
}
},
series: [
{
name: props.title,
type: 'bar',
data: values.map((value, index) => ({
value,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colorScheme[index % colorScheme.length] },
{ offset: 1, color: colorScheme[index % colorScheme.length] + '80' }
])
}
})),
barWidth: '60%',
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
}
};
const exportChart = (type: 'png' | 'jpg' | 'svg' = 'png') => {
if (!chartInstance.value) return null;
return chartInstance.value.getDataURL({
type: type,
pixelRatio: 2,
backgroundColor: '#fff'
});
};
// 暴露方法给父组件
defineExpose({
exportChart,
resize: handleResize
});
// 生命周期
onMounted(() => {
nextTick(() => {
initChart();
renderChart();
});
});
onUnmounted(() => {
if (chartInstance.value) {
chartInstance.value.dispose();
}
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
.statistics-chart {
width: 100%;
height: 100%;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.chart-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
}
.chart-title::before {
content: '';
width: 4px;
height: 16px;
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-radius: 2px;
margin-right: 8px;
}
.chart-actions {
display: flex;
align-items: center;
}
.chart-container {
position: relative;
width: 100%;
height: 400px;
}
.chart-wrapper {
width: 100%;
height: 100%;
}
.chart-empty {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
}
.empty-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.chart-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.chart-actions {
width: 100%;
overflow-x: auto;
}
.chart-container {
height: 300px;
}
}
/* 图表类型切换按钮样式 */
.chart-actions :deep(.el-radio-button__inner) {
border-radius: 6px;
border: 1px solid #e4e7ed;
background: #ffffff;
color: #606266;
transition: all 0.3s ease;
}
.chart-actions :deep(.el-radio-button__inner:hover) {
border-color: rgba(93, 135, 255, 0.6);
color: rgb(93, 135, 255);
}
.chart-actions :deep(.el-radio-button.is-active .el-radio-button__inner) {
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-color: rgb(93, 135, 255);
color: white;
box-shadow: 0 2px 8px rgba(93, 135, 255, 0.3);
}
.chart-actions :deep(.el-radio-group) {
display: flex;
gap: 4px;
}
.chart-actions :deep(.el-radio-button) {
margin: 0;
}
</style>
@@ -1,676 +0,0 @@
<template>
<div class="page-container">
<!-- 统计概览卡片 -->
<div class="mb-[10px]">
<el-row :gutter="16">
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-primary">
<div class="stat-content">
<div class="stat-icon">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ overview.totalStudents }}</div>
<div class="stat-label">学生总数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><School /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ overview.totalColleges }}</div>
<div class="stat-label">学院数量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Reading /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ overview.totalMajors }}</div>
<div class="stat-label">专业数量</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Tickets /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ overview.totalClasses }}</div>
<div class="stat-label">班级数量</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 查询条件 -->
<el-card shadow="never" class="search-card">
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="80px">
<el-form-item label="统计维度" prop="dimension">
<el-select v-model="queryParams.dimension" placeholder="请选择统计维度" clearable>
<el-option v-for="item in dimensionOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="学院" prop="collegeIds">
<el-select v-model="queryParams.collegeIds" placeholder="请选择学院" clearable multiple @change="handleCollegeChange">
<el-option v-for="item in collegeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="专业" prop="majorIds">
<el-select v-model="queryParams.majorIds" placeholder="请选择专业" clearable multiple>
<el-option v-for="item in majorOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<div class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">统计分析</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</div>
</el-form>
</el-card>
<!-- 操作按钮 -->
<el-card shadow="never" class="button-card">
<template #header>
<div class="card-header">
<div class="toolbar-title">数据统计分析</div>
<div class="toolbar-actions">
<el-button type="primary" icon="DataAnalysis" @click="handleQuickAnalysis">快速分析</el-button>
<el-button type="success" icon="Download" @click="handleExport">导出数据</el-button>
<el-button type="info" icon="Setting" @click="handleConfigManage">配置管理</el-button>
<el-button type="warning" icon="RefreshRight" @click="handleCacheManage">缓存管理</el-button>
</div>
</div>
</template>
</el-card>
<!-- 统计结果展示 -->
<el-row :gutter="16" v-if="statisticsData.length > 0">
<el-col :span="16">
<el-card shadow="never" class="chart-card">
<StatisticsChart
:title="currentDimensionLabel + '统计图表'"
:data="statisticsData"
:chart-type="chartType"
:loading="loading"
@chart-click="handleChartClick"
@chart-type-change="handleChartTypeChange"
/>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="data-card">
<template #header>
<div class="data-title">统计数据详情</div>
</template>
<div class="data-list">
<div v-for="(item, index) in statisticsData" :key="index" class="data-item">
<div class="item-info">
<div class="item-label">{{ item.label }}</div>
<div class="item-value">{{ item.value }}</div>
</div>
<div class="item-percentage">{{ item.percentage }}%</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 空状态 -->
<el-card shadow="never" class="empty-card" v-else>
<el-empty description="请选择统计维度并点击统计分析">
<el-button type="primary" @click="handleQuickAnalysis">开始分析</el-button>
</el-empty>
</el-card>
<!-- 导出对话框 -->
<ExportDialog
:visible="exportVisible"
:query-params="queryParams"
:statistics-data="statisticsData"
@update:visible="(val) => (exportVisible = val)"
/>
<!-- 配置管理对话框 -->
<ConfigManageDialog :visible="configVisible" @update:visible="(val) => (configVisible = val)" @refresh="handleQuery" />
<!-- 缓存管理对话框 -->
<CacheManageDialog :visible="cacheVisible" @update:visible="(val) => (cacheVisible = val)" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import {
getStatisticsOverview,
getStatisticsDimensionOptions,
getCollegeOptions,
getMajorOptions,
getCollegeStatistics,
getMajorStatistics,
getClassStatistics,
getGradeStatistics,
getGenderStatistics,
getAgeStatistics,
getPoliticalStatusStatistics,
getNationStatistics,
getSourceRegionStatistics,
getBloodTypeStatistics,
getTrainingTypeStatistics,
type StatisticsAnalysisQuery
} from '@/api/xg-xx/statistics';
import ExportDialog from './components/ExportDialog.vue';
import StatisticsChart from './components/StatisticsChart.vue';
import CacheManageDialog from './components/CacheManageDialog.vue';
import ConfigManageDialog from './components/ConfigManageDialog.vue';
// 响应式数据
const loading = ref(false);
const chartRef = ref<HTMLElement>();
const chartType = ref<'bar' | 'pie' | 'line' | 'doughnut'>('bar');
const exportVisible = ref(false);
const configVisible = ref(false);
const cacheVisible = ref(false);
const currentDimensionLabel = ref('');
// 统计概览
const overview = reactive({
totalStudents: 0,
totalColleges: 0,
totalMajors: 0,
totalClasses: 0
});
// 查询参数
const queryParams = reactive<StatisticsAnalysisQuery>({
statisticsType: 'dimension',
dimension: '',
collegeIds: [],
majorIds: [],
classIds: [],
grades: [],
genders: [],
politicalStatuses: [],
nationalities: [],
studentStatuses: [],
trainingTypes: [],
useCache: true
});
// 选项数据
const dimensionOptions = ref<Array<{ label: string; value: string }>>([]);
const collegeOptions = ref<Array<{ label: string; value: string }>>([]);
const majorOptions = ref<Array<{ label: string; value: string }>>([]);
// 统计数据
const statisticsData = ref<
Array<{
label: string;
value: number;
percentage: number;
}>
>([]);
// 方法定义
const getOverview = async () => {
try {
const response = await getStatisticsOverview();
Object.assign(overview, response.data);
} catch (error) {
console.error('获取概览失败:', error);
}
};
const getDimensionOptions = async () => {
try {
const response = await getStatisticsDimensionOptions();
dimensionOptions.value = response.data;
} catch (error) {
console.error('获取维度选项失败:', error);
}
};
const getCollegeList = async () => {
try {
const response = await getCollegeOptions();
collegeOptions.value = response.data;
} catch (error) {
console.error('获取学院选项失败:', error);
}
};
const getMajorList = async (collegeId?: string) => {
try {
const response = await getMajorOptions(collegeId);
majorOptions.value = response.data;
} catch (error) {
console.error('获取专业选项失败:', error);
}
};
const handleQuery = async () => {
if (!queryParams.dimension) {
ElMessage.warning('请选择统计维度');
return;
}
loading.value = true;
try {
let response;
// 根据统计维度调用不同的API
switch (queryParams.dimension) {
case 'college':
response = await getCollegeStatistics(queryParams);
break;
case 'major':
response = await getMajorStatistics(queryParams);
break;
case 'class':
response = await getClassStatistics(queryParams);
break;
case 'grade':
response = await getGradeStatistics(queryParams);
break;
case 'gender':
response = await getGenderStatistics(queryParams);
break;
case 'age':
response = await getAgeStatistics(queryParams);
break;
case 'political_status':
response = await getPoliticalStatusStatistics(queryParams);
break;
case 'nationality':
response = await getNationStatistics(queryParams);
break;
case 'source_region':
response = await getSourceRegionStatistics(queryParams);
break;
case 'blood_type':
response = await getBloodTypeStatistics(queryParams);
break;
case 'training_type':
response = await getTrainingTypeStatistics(queryParams);
break;
default:
ElMessage.error('不支持的统计维度');
return;
}
const result = response.data;
if (result.data && result.data.length > 0) {
statisticsData.value = result.data.map((item) => ({
label: item.label || item.name || item.key,
value: Number(item.value || item.count),
percentage: Number(((Number(item.value || item.count) / (result.total || 1)) * 100).toFixed(2))
}));
// 设置当前维度标签
const dimensionOption = dimensionOptions.value.find((opt) => opt.value === queryParams.dimension);
currentDimensionLabel.value = dimensionOption ? dimensionOption.label : queryParams.dimension;
} else {
statisticsData.value = [];
ElMessage.warning('暂无统计数据');
}
ElMessage.success('统计分析完成');
} catch (error) {
console.error('统计分析失败:', error);
ElMessage.error('统计分析失败');
} finally {
loading.value = false;
}
};
const resetQuery = () => {
Object.assign(queryParams, {
dimension: '',
collegeIds: [],
majorIds: [],
classIds: [],
grades: [],
genders: [],
politicalStatuses: [],
nationalities: [],
studentStatuses: [],
trainingTypes: []
});
statisticsData.value = [];
};
const handleQuickAnalysis = () => {
if (!queryParams.dimension) {
queryParams.dimension = 'college';
}
handleQuery();
};
const handleExport = () => {
if (statisticsData.value.length === 0) {
ElMessage.warning('请先执行统计分析');
return;
}
exportVisible.value = true;
};
const handleChartClick = (data: any) => {
console.log('图表点击:', data);
// 可以在这里处理图表点击事件
};
const handleChartTypeChange = (type: string) => {
chartType.value = type as 'bar' | 'pie' | 'line' | 'doughnut';
};
const handleConfigManage = () => {
configVisible.value = true;
};
const handleCacheManage = () => {
cacheVisible.value = true;
};
// 初始化
onMounted(async () => {
await Promise.all([getOverview(), getDimensionOptions(), getCollegeList()]);
});
// 学院变更处理
const handleCollegeChange = async () => {
// 清空专业选择
queryParams.majorIds = [];
// 获取当前选中学院的专业选项
if (queryParams.collegeIds && queryParams.collegeIds.length > 0) {
// 如果选择了学院,获取对应的专业选项
await getMajorList(queryParams.collegeIds[0].toString());
} else {
// 如果没有选择学院,获取所有专业选项
await getMajorList();
}
};
</script>
<style scoped>
.page-container {
padding: 8px;
}
/* 统计卡片样式 - 参考后勤管理模块 */
.stat-card {
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 90px;
border: none;
overflow: hidden;
position: relative;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
padding: 20px;
height: 100%;
box-sizing: border-box;
position: relative;
z-index: 1;
}
.stat-icon {
font-size: 28px;
margin-right: 16px;
opacity: 0.9;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: 700;
line-height: 1;
margin-bottom: 6px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.stat-label {
font-size: 14px;
opacity: 0.9;
line-height: 1;
font-weight: 500;
}
/* 渐变背景 */
.stat-card-primary {
background: linear-gradient(135deg, rgb(93, 135, 255) 0%, rgb(73, 190, 255) 100%);
color: white;
}
.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
/* 搜索卡片样式 */
.search-card {
margin-bottom: 10px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.search-card :deep(.el-card__body) {
padding: 20px;
}
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
/* 按钮卡片样式 */
.button-card {
margin-bottom: 10px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.button-card :deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
}
.toolbar-title::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-radius: 2px;
margin-right: 12px;
}
.toolbar-actions {
display: flex;
gap: 12px;
}
/* 图表卡片样式 */
.chart-card {
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 16px;
}
.chart-card :deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.chart-container {
padding: 20px;
height: 400px;
}
.chart-wrapper {
width: 100%;
height: 100%;
}
/* 数据卡片样式 */
.data-card {
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 16px;
}
.data-card :deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
.data-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.data-list {
padding: 20px;
max-height: 400px;
overflow-y: auto;
}
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background: #ffffff;
border: 1px solid #e4e7ed;
border-radius: 8px;
transition: all 0.3s ease;
}
.data-item:hover {
border-color: rgba(93, 135, 255, 0.3);
background: rgba(93, 135, 255, 0.02);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.item-info {
flex: 1;
}
.item-label {
font-size: 14px;
color: #1e293b;
font-weight: 500;
margin-bottom: 4px;
}
.item-value {
font-size: 16px;
font-weight: 600;
color: rgb(93, 135, 255);
}
.item-percentage {
font-size: 12px;
color: #606266;
font-weight: 500;
}
/* 空状态卡片样式 */
.empty-card {
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-top: 16px;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -1,453 +0,0 @@
<template>
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="60%" :before-close="handleClose" append-to-body>
<div class="audit-container">
<!-- 审核信息 -->
<div v-if="!batchMode" class="student-info">
<h4>学生信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ studentData.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ studentData.name }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ studentData.gender === 'M' ? '男' : '女' }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ studentData.className }}</el-descriptions-item>
<el-descriptions-item label="当前状态">
<el-tag :type="getInfoStatusType(studentData.infoStatus)">
{{ getInfoStatusText(studentData.infoStatus) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="信息完整度">
<el-progress :percentage="studentData.completionRate || 0" :color="getCompletionColor(studentData.completionRate)" :stroke-width="6" />
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 批量审核信息 -->
<div v-else class="batch-info">
<h4>批量审核</h4>
<el-alert title="批量审核提示" type="warning" :closable="false" show-icon>
<template v-slot:title>
<div>
<p>即将审核 {{ selectedIds.length }} 条学生信息</p>
<p>请选择审核结果并填写审核意见</p>
</div>
</template>
</el-alert>
</div>
<!-- 审核表单 -->
<el-form ref="auditForm" :model="auditForm" :rules="auditRules" label-width="120px" style="margin-top: 20px">
<el-form-item label="审核结果" prop="auditStatus">
<el-radio-group v-model="auditForm.auditStatus">
<el-radio label="1">审核通过</el-radio>
<el-radio label="4">审核驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见" prop="auditRemark">
<el-input v-model="auditForm.auditRemark" type="textarea" :rows="4" placeholder="请输入审核意见" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<!-- 审核历史 -->
<div v-if="!batchMode && auditHistory.length > 0" class="audit-history">
<h4>审核历史</h4>
<el-timeline>
<el-timeline-item
v-for="(item, index) in auditHistory"
:key="index"
:timestamp="item.auditTime"
:type="item.auditStatus === '1' ? 'success' : 'danger'"
>
<div class="history-item">
<div class="history-status">
<el-tag :type="item.auditStatus === '1' ? 'success' : 'danger'">
{{ item.auditStatus === '1' ? '审核通过' : '审核驳回' }}
</el-tag>
</div>
<div class="history-auditor">审核人:{{ item.auditorName }}</div>
<div class="history-remark" v-if="item.auditRemark">审核意见:{{ item.auditRemark }}</div>
</div>
</el-timeline-item>
</el-timeline>
</div>
</div>
<template v-slot:footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitAudit" :loading="loading"> 确认审核 </el-button>
<el-button @click="handleClose">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import { auditStudent, batchAuditStudents } from '@/api/xg-xx/student';
export default {
name: 'AuditDialog',
props: {
visible: {
type: Boolean,
default: false
},
studentData: {
type: Object,
default: () => ({})
},
batchMode: {
type: Boolean,
default: false
},
selectedIds: {
type: Array,
default: () => []
}
},
data() {
return {
loading: false,
auditForm: {
auditStatus: '1',
auditRemark: ''
},
auditRules: {
auditStatus: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
auditRemark: [
{ required: true, message: '请输入审核意见', trigger: 'blur' },
{ min: 5, max: 500, message: '审核意见长度在5到500个字符', trigger: 'blur' }
]
},
auditHistory: []
};
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
},
dialogTitle() {
return this.batchMode ? '批量审核学生信息' : '审核学生信息';
}
},
watch: {
visible(val) {
if (val) {
this.initForm();
if (!this.batchMode) {
this.loadAuditHistory();
}
}
}
},
methods: {
/** 初始化表单 */
initForm() {
this.auditForm = {
auditStatus: '1',
auditRemark: ''
};
},
/** 加载审核历史 */
loadAuditHistory() {
// 这里可以调用API获取审核历史
// 暂时使用模拟数据
this.auditHistory = [
{
auditTime: '2025-01-10 14:30:00',
auditStatus: '4',
auditorName: '张老师',
auditRemark: '身份证号格式不正确,请核实后重新提交'
},
{
auditTime: '2025-01-08 09:15:00',
auditStatus: '1',
auditorName: '李老师',
auditRemark: '信息完整,审核通过'
}
];
},
/** 提交审核 */
submitAudit() {
this.$refs.auditForm.validate((valid) => {
if (valid) {
this.loading = true;
const auditData = {
auditStatus: this.auditForm.auditStatus,
auditRemark: this.auditForm.auditRemark
};
let apiCall;
if (this.batchMode) {
apiCall = batchAuditStudents(this.selectedIds, auditData.auditStatus, auditData.auditRemark);
} else {
apiCall = auditStudent(this.studentData.id, auditData.auditStatus, auditData.auditRemark);
}
apiCall
.then((response) => {
this.$modal.msgSuccess('审核成功');
this.handleClose();
this.$emit('refresh');
this.loading = false;
})
.catch(() => {
this.loading = false;
});
}
});
},
/** 获取信息状态类型 */
getInfoStatusType(status) {
const statusMap = {
'1': 'success',
'2': 'warning',
'3': 'info',
'4': 'danger',
'5': 'info'
};
return statusMap[status] || 'info';
},
/** 获取信息状态文本 */
getInfoStatusText(status) {
const statusMap = {
'1': '正常',
'2': '待审核',
'3': '审核中',
'4': '已驳回',
'5': '已锁定'
};
return statusMap[status] || '未知';
},
/** 获取完整度颜色 */
getCompletionColor(rate) {
if (rate >= 90) return '#67C23A';
if (rate >= 70) return '#E6A23C';
if (rate >= 50) return '#F56C6C';
return '#909399';
},
/** 关闭弹窗 */
handleClose() {
this.dialogVisible = false;
this.initForm();
this.auditHistory = [];
}
}
};
</script>
<style scoped>
.audit-container {
padding: 20px;
}
.audit-info {
margin-bottom: 20px;
padding: 16px 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.audit-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.audit-details {
color: #606266;
font-size: 14px;
line-height: 1.6;
}
.student-list {
margin-bottom: 20px;
}
.student-item {
display: flex;
align-items: center;
padding: 12px 16px;
margin-bottom: 8px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 8px;
transition: all 0.3s ease;
}
.student-item:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
.student-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
background: #f0f2f5;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-weight: 600;
}
.student-info {
flex: 1;
}
.student-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.student-details {
font-size: 14px;
color: #909399;
}
.audit-form {
padding: 20px;
background: #fafbfc;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.audit-form :deep(.el-form-item__label) {
font-weight: 600;
color: #303133;
}
.audit-form :deep(.el-radio-group) {
display: flex;
gap: 20px;
}
.audit-form :deep(.el-radio) {
margin-right: 0;
padding: 8px 16px;
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #fff;
transition: all 0.3s ease;
}
.audit-form :deep(.el-radio:hover) {
border-color: #409eff;
background: #f0f9ff;
}
.audit-form :deep(.el-radio.is-checked) {
border-color: #409eff;
background: #f0f9ff;
}
.audit-form :deep(.el-textarea__inner) {
min-height: 100px;
resize: vertical;
}
.audit-history {
margin-top: 20px;
}
.history-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
display: flex;
align-items: center;
}
.history-icon {
color: #409eff;
margin-right: 8px;
}
.history-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e4e7ed;
border-radius: 8px;
}
.history-item {
padding: 12px 16px;
border-bottom: 1px solid #f0f2f5;
display: flex;
align-items: flex-start;
}
.history-item:last-child {
border-bottom: none;
}
.history-status {
margin-right: 12px;
flex-shrink: 0;
}
.history-content {
flex: 1;
}
.history-info {
font-size: 14px;
color: #303133;
margin-bottom: 4px;
}
.history-time {
font-size: 12px;
color: #909399;
}
.history-remark {
font-size: 13px;
color: #606266;
margin-top: 8px;
line-height: 1.5;
}
.no-history {
text-align: center;
padding: 40px 20px;
color: #909399;
font-size: 14px;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
width: 100%;
}
.footer-left {
display: flex;
align-items: center;
gap: 12px;
}
.footer-right {
display: flex;
align-items: center;
gap: 12px;
}
.selected-count {
color: #909399;
font-size: 14px;
}
</style>
@@ -1,684 +0,0 @@
<template>
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="60%" :before-close="handleClose" append-to-body>
<div class="batch-operation-container">
<!-- 操作信息 -->
<div class="operation-info">
<el-alert :title="alertTitle" type="warning" :closable="false" show-icon>
<template v-slot:title>
<div>
<p>{{ alertTitle }}</p>
<p>选中了 {{ selectedIds.length }} 条学生信息</p>
</div>
</template>
</el-alert>
</div>
<!-- 批量更新状态 -->
<div v-if="operationType === 'updateStatus'" class="operation-form">
<el-form ref="statusForm" :model="statusForm" :rules="statusRules" label-width="120px">
<el-form-item label="状态类型" prop="statusType">
<el-radio-group v-model="statusForm.statusType">
<el-radio label="info">信息状态</el-radio>
<el-radio label="student">学籍状态</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="目标状态" prop="targetStatus">
<el-select v-model="statusForm.targetStatus" placeholder="请选择目标状态" style="width: 100%">
<el-option v-for="option in statusOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item label="操作说明" prop="remark">
<el-input v-model="statusForm.remark" type="textarea" :rows="3" placeholder="请输入操作说明" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
</div>
<!-- 更新完整度 -->
<div v-if="operationType === 'updateCompletion'" class="operation-form">
<el-alert title="更新完整度说明" type="info" :closable="false" show-icon>
<template v-slot:title>
<div>
<p>将重新计算选中学生的信息完整度</p>
<p>系统会根据必填字段的填写情况自动计算完整度百分比</p>
</div>
</template>
</el-alert>
</div>
<!-- 数据清理 -->
<div v-if="operationType === 'dataClean'" class="operation-form">
<el-form ref="cleanForm" :model="cleanForm" :rules="cleanRules" label-width="120px">
<el-form-item label="清理类型" prop="cleanType">
<el-checkbox-group v-model="cleanForm.cleanType">
<el-checkbox label="duplicateStudentNo">重复学号</el-checkbox>
<el-checkbox label="duplicateIdCard">重复身份证</el-checkbox>
<el-checkbox label="incompleteInfo">信息不完整</el-checkbox>
<el-checkbox label="invalidPhone">无效手机号</el-checkbox>
<el-checkbox label="invalidEmail">无效邮箱</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="处理方式" prop="cleanAction">
<el-radio-group v-model="cleanForm.cleanAction">
<el-radio label="mark">标记问题</el-radio>
<el-radio label="fix">自动修复</el-radio>
<el-radio label="delete">删除记录</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<!-- 数据导出 -->
<div v-if="operationType === 'export'" class="operation-form">
<el-form ref="exportForm" :model="exportForm" :rules="exportRules" label-width="120px">
<el-form-item label="导出格式" prop="exportFormat">
<el-radio-group v-model="exportForm.exportFormat">
<el-radio label="excel">Excel格式</el-radio>
<el-radio label="csv">CSV格式</el-radio>
<el-radio label="pdf">PDF格式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="导出字段" prop="exportFields">
<el-checkbox-group v-model="exportForm.exportFields">
<el-checkbox label="basic">基本信息</el-checkbox>
<el-checkbox label="academic">学籍信息</el-checkbox>
<el-checkbox label="contact">联系信息</el-checkbox>
<el-checkbox label="family">家庭信息</el-checkbox>
<el-checkbox label="health">健康信息</el-checkbox>
<el-checkbox label="extended">扩展信息</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="exportForm.fileName" placeholder="请输入文件名称" style="width: 300px">
<template v-slot:append>.{{ exportForm.exportFormat }}</template>
</el-input>
</el-form-item>
</el-form>
</div>
<!-- 选中数据预览 -->
<div class="selected-data-preview">
<h4>选中数据预览(前5条)</h4>
<el-table :data="previewData" size="small" max-height="200" style="width: 100%">
<el-table-column prop="studentNo" label="学号" width="120" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="gender" label="性别" width="60">
<template v-slot="scope">
{{ scope.row.gender === 'M' ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="className" label="班级" width="140" />
<el-table-column prop="studentStatus" label="学籍状态" width="100">
<template v-slot="scope">
<el-tag :type="getStatusType(scope.row.studentStatus)" size="mini">
{{ getStatusText(scope.row.studentStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="infoStatus" label="信息状态" width="100">
<template v-slot="scope">
<el-tag :type="getInfoStatusType(scope.row.infoStatus)" size="mini">
{{ getInfoStatusText(scope.row.infoStatus) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template v-slot:footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitOperation" :loading="loading"> 确认执行 </el-button>
<el-button @click="handleClose">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import { batchUpdateInfoStatus, batchUpdateStudentStatus, updateCompletionRate, exportStudents } from '@/api/xg-xx/student';
export default {
name: 'BatchOperationDialog',
props: {
visible: {
type: Boolean,
default: false
},
operationType: {
type: String,
default: 'updateStatus'
},
selectedIds: {
type: Array,
default: () => []
},
selectedRows: {
type: Array,
default: () => []
}
},
data() {
return {
loading: false,
statusForm: {
statusType: 'info',
targetStatus: '',
remark: ''
},
statusRules: {
statusType: [{ required: true, message: '请选择状态类型', trigger: 'change' }],
targetStatus: [{ required: true, message: '请选择目标状态', trigger: 'change' }]
},
cleanForm: {
cleanType: [],
cleanAction: 'mark'
},
cleanRules: {
cleanType: [{ required: true, message: '请选择清理类型', trigger: 'change' }],
cleanAction: [{ required: true, message: '请选择处理方式', trigger: 'change' }]
},
exportForm: {
exportFormat: 'excel',
exportFields: ['basic', 'academic', 'contact'],
fileName: '学生信息导出'
},
exportRules: {
exportFormat: [{ required: true, message: '请选择导出格式', trigger: 'change' }],
exportFields: [{ required: true, message: '请选择导出字段', trigger: 'change' }],
fileName: [{ required: true, message: '请输入文件名称', trigger: 'blur' }]
}
};
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
},
dialogTitle() {
const titleMap = {
updateStatus: '批量更新状态',
updateCompletion: '批量更新完整度',
dataClean: '数据清理',
export: '批量导出'
};
return titleMap[this.operationType] || '批量操作';
},
alertTitle() {
const titleMap = {
updateStatus: '即将批量更新学生状态',
updateCompletion: '即将批量更新信息完整度',
dataClean: '即将进行数据清理',
export: '即将导出学生数据'
};
return titleMap[this.operationType] || '批量操作';
},
statusOptions() {
if (this.statusForm.statusType === 'info') {
return [
{ label: '正常', value: '1' },
{ label: '待审核', value: '2' },
{ label: '审核中', value: '3' },
{ label: '已驳回', value: '4' },
{ label: '已锁定', value: '5' }
];
} else {
return [
{ label: '在籍', value: '1' },
{ label: '休学', value: '2' },
{ label: '复学', value: '3' },
{ label: '退学', value: '4' },
{ label: '毕业', value: '5' }
];
}
},
previewData() {
return this.selectedRows.slice(0, 5);
}
},
methods: {
/** 提交操作 */
submitOperation() {
switch (this.operationType) {
case 'updateStatus':
this.handleUpdateStatus();
break;
case 'updateCompletion':
this.handleUpdateCompletion();
break;
case 'dataClean':
this.handleDataClean();
break;
case 'export':
this.handleExport();
break;
}
},
/** 处理状态更新 */
handleUpdateStatus() {
this.$refs.statusForm.validate((valid) => {
if (valid) {
this.loading = true;
let apiCall;
if (this.statusForm.statusType === 'info') {
apiCall = batchUpdateInfoStatus(this.selectedIds, this.statusForm.targetStatus);
} else {
apiCall = batchUpdateStudentStatus(this.selectedIds, this.statusForm.targetStatus);
}
apiCall
.then((response) => {
this.$modal.msgSuccess('批量更新成功');
this.handleClose();
this.$emit('refresh');
this.loading = false;
})
.catch(() => {
this.loading = false;
});
}
});
},
/** 处理完整度更新 */
handleUpdateCompletion() {
this.loading = true;
const promises = this.selectedIds.map((id) => updateCompletionRate(id));
Promise.all(promises)
.then(() => {
this.$modal.msgSuccess('批量更新完整度成功');
this.handleClose();
this.$emit('refresh');
this.loading = false;
})
.catch(() => {
this.loading = false;
});
},
/** 处理数据清理 */
handleDataClean() {
this.$refs.cleanForm.validate((valid) => {
if (valid) {
this.loading = true;
// 这里可以调用数据清理API
// 暂时使用模拟处理
setTimeout(() => {
this.$modal.msgSuccess('数据清理完成');
this.handleClose();
this.$emit('refresh');
this.loading = false;
}, 2000);
}
});
},
/** 处理导出 */
handleExport() {
this.$refs.exportForm.validate((valid) => {
if (valid) {
this.loading = true;
const exportQuery = {
ids: this.selectedIds,
format: this.exportForm.exportFormat,
fields: this.exportForm.exportFields
};
exportStudents(exportQuery)
.then((response) => {
const fileName = `${this.exportForm.fileName}.${this.exportForm.exportFormat}`;
this.$download.excel(response, fileName);
this.$modal.msgSuccess('导出成功');
this.handleClose();
this.loading = false;
})
.catch(() => {
this.loading = false;
});
}
});
},
/** 获取学籍状态类型 */
getStatusType(status) {
const statusMap = {
'1': 'success',
'2': 'warning',
'3': 'info',
'4': 'danger',
'5': 'primary'
};
return statusMap[status] || 'info';
},
/** 获取学籍状态文本 */
getStatusText(status) {
const statusMap = {
'1': '在籍',
'2': '休学',
'3': '复学',
'4': '退学',
'5': '毕业'
};
return statusMap[status] || '未知';
},
/** 获取信息状态类型 */
getInfoStatusType(status) {
const statusMap = {
'1': 'success',
'2': 'warning',
'3': 'info',
'4': 'danger',
'5': 'info'
};
return statusMap[status] || 'info';
},
/** 获取信息状态文本 */
getInfoStatusText(status) {
const statusMap = {
'1': '正常',
'2': '待审核',
'3': '审核中',
'4': '已驳回',
'5': '已锁定'
};
return statusMap[status] || '未知';
},
/** 重置表单 */
resetForms() {
this.statusForm = {
statusType: 'info',
targetStatus: '',
remark: ''
};
this.cleanForm = {
cleanType: [],
cleanAction: 'mark'
};
this.exportForm = {
exportFormat: 'excel',
exportFields: ['basic', 'academic', 'contact'],
fileName: '学生信息导出'
};
},
/** 关闭弹窗 */
handleClose() {
this.dialogVisible = false;
this.resetForms();
}
}
};
</script>
<style scoped>
.batch-operation-container {
padding: 20px;
}
.operation-selector {
margin-bottom: 20px;
padding: 16px 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.selector-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.operation-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.operation-option {
padding: 12px 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
}
.operation-option:hover {
border-color: #409eff;
background: #f0f9ff;
}
.operation-option.selected {
border-color: #409eff;
background: #f0f9ff;
color: #409eff;
}
.option-icon {
margin-right: 8px;
font-size: 16px;
}
.option-text {
flex: 1;
font-size: 14px;
font-weight: 500;
}
.selected-students {
margin-bottom: 20px;
}
.students-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.students-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.students-count {
font-size: 14px;
color: #909399;
}
.students-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e4e7ed;
border-radius: 8px;
}
.student-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f2f5;
}
.student-item:last-child {
border-bottom: none;
}
.student-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 12px;
background: #f0f2f5;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-weight: 600;
font-size: 12px;
}
.student-info {
flex: 1;
}
.student-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 2px;
}
.student-details {
font-size: 12px;
color: #909399;
}
.operation-form {
padding: 20px;
background: #fafbfc;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.operation-form :deep(.el-form-item__label) {
font-weight: 600;
color: #303133;
}
.operation-form :deep(.el-select) {
width: 100%;
}
.operation-form :deep(.el-textarea__inner) {
min-height: 80px;
resize: vertical;
}
.operation-preview {
margin-top: 20px;
padding: 16px 20px;
background: #f0f9ff;
border: 1px solid #d9ecff;
border-radius: 8px;
}
.preview-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.preview-content {
font-size: 14px;
color: #606266;
line-height: 1.6;
}
.operation-result {
margin-top: 20px;
}
.result-summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e4e7ed;
margin-bottom: 16px;
}
.summary-item {
text-align: center;
}
.summary-label {
font-size: 14px;
color: #909399;
margin-bottom: 4px;
}
.summary-value {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.summary-value.success {
color: #67c23a;
}
.summary-value.error {
color: #f56c6c;
}
.result-details {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e4e7ed;
border-radius: 8px;
}
.result-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f2f5;
}
.result-item:last-child {
border-bottom: none;
}
.result-status {
margin-right: 12px;
flex-shrink: 0;
}
.result-message {
flex: 1;
font-size: 14px;
color: #606266;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
width: 100%;
}
.footer-left {
display: flex;
align-items: center;
gap: 12px;
}
.footer-right {
display: flex;
align-items: center;
gap: 12px;
}
.progress-info {
color: #909399;
font-size: 14px;
}
</style>
@@ -1,541 +0,0 @@
<template>
<el-dialog title="批量导入学生信息" v-model="dialogVisible" width="70%" :before-close="handleClose" append-to-body>
<div class="import-container">
<!-- 步骤条 -->
<el-steps :active="currentStep" finish-status="success" align-center>
<el-step title="下载模板" description="下载Excel导入模板"></el-step>
<el-step title="上传文件" description="上传填写好的Excel文件"></el-step>
<el-step title="数据验证" description="验证数据格式和内容"></el-step>
<el-step title="确认导入" description="确认导入数据到系统"></el-step>
</el-steps>
<!-- 步骤内容 -->
<div class="step-content">
<!-- 步骤1: 下载模板 -->
<div v-if="currentStep === 0" class="step-panel">
<div class="template-info">
<el-alert title="导入说明" type="info" :closable="false" show-icon>
<template v-slot:title>
<div>
<h4>导入说明</h4>
<ul>
<li>请先下载Excel模板文件,按照模板格式填写学生信息</li>
<li>必填字段:学号、姓名、性别、身份证号、学院、专业、班级</li>
<li>学号不能重复,身份证号不能重复</li>
<li>性别填写:男 或 女</li>
<li>日期格式:yyyy-MM-dd,如:2000-01-01</li>
<li>支持最大导入数量:1000条</li>
</ul>
</div>
</template>
</el-alert>
</div>
<div class="template-download">
<el-button type="primary" icon="el-icon-download" size="large" @click="downloadTemplate" :loading="downloadLoading">
下载导入模板
</el-button>
</div>
</div>
<!-- 步骤2: 上传文件 -->
<div v-if="currentStep === 1" class="step-panel">
<div class="upload-area">
<el-upload
ref="upload"
class="upload-demo"
drag
:action="uploadUrl"
:headers="uploadHeaders"
:data="uploadData"
:file-list="fileList"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-remove="handleRemove"
:auto-upload="false"
accept=".xlsx,.xls"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template v-slot:tip>
<div class="el-upload__tip">只能上传Excel文件,且不超过10MB</div>
</template>
</el-upload>
</div>
<div class="upload-options">
<el-checkbox v-model="updateSupport"> 允许更新已存在的学生信息 </el-checkbox>
</div>
</div>
<!-- 步骤3: 数据验证 -->
<div v-if="currentStep === 2" class="step-panel">
<div v-if="validationResult" class="validation-result">
<el-alert
:title="validationResult.success ? '验证通过' : '验证失败'"
:type="validationResult.success ? 'success' : 'error'"
:closable="false"
show-icon
>
<template v-slot:title>
<div>
<h4>验证结果</h4>
<p>总计:{{ validationResult.total }}条,成功:{{ validationResult.successCount }}条,失败:{{ validationResult.errorCount }}条</p>
</div>
</template>
</el-alert>
<!-- 错误信息 -->
<div v-if="validationResult.errors && validationResult.errors.length > 0" class="error-list">
<h4>错误详情</h4>
<el-table :data="validationResult.errors" size="small" max-height="300" style="width: 100%">
<el-table-column prop="row" label="行号" width="80" />
<el-table-column prop="field" label="字段" width="120" />
<el-table-column prop="value" label="值" width="150" show-overflow-tooltip />
<el-table-column prop="message" label="错误信息" show-overflow-tooltip />
</el-table>
</div>
<!-- 预览数据 -->
<div v-if="validationResult.preview && validationResult.preview.length > 0" class="preview-data">
<h4>数据预览(前10条)</h4>
<el-table :data="validationResult.preview" size="small" max-height="300" style="width: 100%">
<el-table-column prop="studentNo" label="学号" width="120" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="gender" label="性别" width="60" />
<el-table-column prop="idCard" label="身份证号" width="160" />
<el-table-column prop="collegeName" label="学院" width="120" />
<el-table-column prop="majorName" label="专业" width="140" />
<el-table-column prop="className" label="班级" width="140" />
<el-table-column prop="phone" label="手机号" width="120" />
</el-table>
</div>
</div>
</div>
<!-- 步骤4: 确认导入 -->
<div v-if="currentStep === 3" class="step-panel">
<div class="import-confirm">
<el-alert title="确认导入" type="warning" :closable="false" show-icon>
<template v-slot:title>
<div>
<h4>确认导入</h4>
<p>即将导入 {{ validationResult.successCount }} 条学生信息,请确认是否继续?</p>
<p v-if="updateSupport" style="color: #e6a23c">
<i class="el-icon-warning"></i>
注意:已开启更新模式,重复的学生信息将被更新
</p>
</div>
</template>
</el-alert>
</div>
<div v-if="importResult" class="import-result">
<el-alert
:title="importResult.success ? '导入成功' : '导入失败'"
:type="importResult.success ? 'success' : 'error'"
:closable="false"
show-icon
>
<template v-slot:title>
<div>
<h4>导入结果</h4>
<div v-html="importResult.message"></div>
</div>
</template>
</el-alert>
</div>
</div>
</div>
</div>
<template v-slot:footer>
<div class="dialog-footer">
<el-button v-if="currentStep > 0" @click="prevStep">上一步</el-button>
<el-button v-if="currentStep < 3" type="primary" @click="nextStep" :disabled="!canNext"> 下一步 </el-button>
<el-button
v-if="currentStep === 2"
type="success"
@click="startImport"
:disabled="!validationResult || !validationResult.success"
:loading="importLoading"
>
开始导入
</el-button>
<el-button v-if="currentStep === 3" type="primary" @click="handleClose"> 完成 </el-button>
<el-button @click="handleClose">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import { getImportTemplate, importStudents } from '@/api/xg-xx/student';
import { getToken } from '@/utils/auth';
export default {
name: 'ImportDialog',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
currentStep: 0,
downloadLoading: false,
importLoading: false,
fileList: [],
updateSupport: false,
validationResult: null,
importResult: null,
uploadUrl: process.env.VUE_APP_BASE_API + '/xg/studentInfo/import',
uploadHeaders: {
Authorization: 'Bearer ' + getToken()
},
uploadData: {
updateSupport: false
}
};
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
},
canNext() {
switch (this.currentStep) {
case 0:
return true;
case 1:
return this.fileList.length > 0;
case 2:
return this.validationResult && this.validationResult.success;
default:
return false;
}
}
},
watch: {
updateSupport(val) {
this.uploadData.updateSupport = val;
}
},
methods: {
/** 下载模板 */
downloadTemplate() {
this.downloadLoading = true;
getImportTemplate()
.then((response) => {
this.$download.excel(response, '学生信息导入模板.xlsx');
this.downloadLoading = false;
})
.catch(() => {
this.downloadLoading = false;
});
},
/** 上传前验证 */
beforeUpload(file) {
const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.type === 'application/vnd.ms-excel';
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isExcel) {
this.$message.error('只能上传Excel文件!');
return false;
}
if (!isLt10M) {
this.$message.error('文件大小不能超过10MB!');
return false;
}
return true;
},
/** 上传成功 */
handleUploadSuccess(response, file, fileList) {
if (response.code === 200) {
this.validationResult = response.data;
this.currentStep = 2;
this.$message.success('文件上传成功,正在验证数据...');
} else {
this.$message.error(response.msg || '上传失败');
this.handleRemove(file, fileList);
}
},
/** 上传失败 */
handleUploadError(error, file, fileList) {
this.$message.error('上传失败: ' + error.message);
this.handleRemove(file, fileList);
},
/** 移除文件 */
handleRemove(file, fileList) {
this.fileList = fileList;
this.validationResult = null;
},
/** 上一步 */
prevStep() {
if (this.currentStep > 0) {
this.currentStep--;
}
},
/** 下一步 */
nextStep() {
if (this.currentStep < 3) {
if (this.currentStep === 1) {
// 开始上传文件
this.$refs.upload.submit();
return;
}
this.currentStep++;
}
},
/** 开始导入 */
startImport() {
this.importLoading = true;
const importData = {
studentInfoList: this.validationResult.validData,
isUpdateSupport: this.updateSupport,
operName: this.$store.state.user.name
};
importStudents(importData)
.then((response) => {
this.importResult = {
success: response.code === 200,
message: response.data || response.msg
};
this.currentStep = 3;
this.importLoading = false;
if (response.code === 200) {
this.$message.success('导入成功');
this.$emit('refresh');
} else {
this.$message.error(response.msg || '导入失败');
}
})
.catch((error) => {
this.importResult = {
success: false,
message: error.message || '导入失败'
};
this.currentStep = 3;
this.importLoading = false;
});
},
/** 重置数据 */
resetData() {
this.currentStep = 0;
this.fileList = [];
this.updateSupport = false;
this.validationResult = null;
this.importResult = null;
},
/** 关闭弹窗 */
handleClose() {
this.dialogVisible = false;
this.resetData();
}
}
};
</script>
<style scoped>
.import-container {
padding: 20px;
}
.import-steps {
margin-bottom: 30px;
}
.import-steps :deep(.el-steps) {
margin-bottom: 20px;
}
.step-content {
min-height: 200px;
padding: 20px;
background: #fafbfc;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.download-section {
text-align: center;
padding: 40px 20px;
}
.download-section .el-button {
margin: 0 10px;
}
.upload-section {
text-align: center;
padding: 20px;
}
.upload-dragger {
background: #fff;
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 40px;
transition: all 0.3s ease;
}
.upload-dragger:hover {
border-color: #409eff;
background: #f0f9ff;
}
.upload-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
}
.upload-text {
color: #606266;
font-size: 14px;
margin-bottom: 8px;
}
.upload-hint {
color: #909399;
font-size: 12px;
}
.validation-section {
padding: 20px;
}
.validation-result {
margin-bottom: 20px;
}
.result-summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e4e7ed;
margin-bottom: 20px;
}
.summary-item {
text-align: center;
}
.summary-label {
font-size: 14px;
color: #909399;
margin-bottom: 4px;
}
.summary-value {
font-size: 20px;
font-weight: bold;
color: #303133;
}
.error-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e4e7ed;
border-radius: 8px;
}
.error-item {
padding: 12px 16px;
border-bottom: 1px solid #f0f2f5;
display: flex;
align-items: center;
}
.error-item:last-child {
border-bottom: none;
}
.error-icon {
color: #f56c6c;
margin-right: 8px;
}
.error-message {
flex: 1;
color: #606266;
font-size: 14px;
}
.preview-table {
margin-top: 20px;
}
.preview-table :deep(.el-table__header th) {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
.preview-table :deep(.el-table__body-wrapper .el-table__row:hover) {
background-color: #f5f7fa;
}
.confirm-section {
padding: 20px;
text-align: center;
}
.confirm-info {
padding: 20px;
background: #f0f9ff;
border: 1px solid #d9ecff;
border-radius: 8px;
margin-bottom: 20px;
}
.confirm-title {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.confirm-details {
color: #606266;
font-size: 14px;
line-height: 1.6;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
width: 100%;
}
.footer-left {
display: flex;
align-items: center;
gap: 12px;
}
.footer-right {
display: flex;
align-items: center;
gap: 12px;
}
.progress-info {
color: #909399;
font-size: 14px;
}
</style>
@@ -1,615 +0,0 @@
<template>
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="90%" :before-close="handleClose" append-to-body>
<div v-loading="loading" class="statistics-container">
<!-- 图表选择器 -->
<div class="chart-selector">
<el-radio-group v-model="activeChart" @change="handleChartChange">
<el-radio-button label="college">学院分布</el-radio-button>
<el-radio-button label="grade">年级分布</el-radio-button>
<el-radio-button label="major">专业分布</el-radio-button>
<el-radio-button label="status">学籍状态</el-radio-button>
<el-radio-button label="completion">完整度分布</el-radio-button>
<el-radio-button label="gender">性别分布</el-radio-button>
</el-radio-group>
</div>
<!-- 图表区域 -->
<div class="chart-content">
<el-row :gutter="20">
<!-- 左侧图表 -->
<el-col :span="16">
<div class="chart-wrapper">
<div :id="chartId" class="chart-container"></div>
</div>
</el-col>
<!-- 右侧数据表格 -->
<el-col :span="8">
<div class="data-table-wrapper">
<h4>详细数据</h4>
<el-table :data="tableData" size="small" max-height="400" style="width: 100%">
<el-table-column prop="name" label="名称" show-overflow-tooltip />
<el-table-column prop="count" label="数量" width="80" align="right" />
<el-table-column prop="percentage" label="占比" width="80" align="right">
<template v-slot="scope"> {{ scope.row.percentage }}% </template>
</el-table-column>
</el-table>
</div>
</el-col>
</el-row>
</div>
<!-- 统计摘要 -->
<div class="statistics-summary">
<el-row :gutter="20">
<el-col :span="6">
<div class="summary-item">
<div class="summary-label">总数</div>
<div class="summary-value">{{ summaryData.total }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="summary-item">
<div class="summary-label">类别数</div>
<div class="summary-value">{{ summaryData.categories }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="summary-item">
<div class="summary-label">最大值</div>
<div class="summary-value">{{ summaryData.max }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="summary-item">
<div class="summary-label">平均值</div>
<div class="summary-value">{{ summaryData.average }}</div>
</div>
</el-col>
</el-row>
</div>
</div>
<template v-slot:footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleExport">导出图表</el-button>
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import {
getCollegeStatistics,
getGradeStatistics,
getMajorStatistics,
getStudentStatusStatistics,
getCompletionRateStatistics,
getStatisticsOverview
} from '@/api/xg-xx/student';
import * as echarts from 'echarts';
export default {
name: 'StatisticsChartDialog',
props: {
visible: {
type: Boolean,
default: false
},
chartType: {
type: String,
default: 'college'
}
},
data() {
return {
loading: false,
activeChart: 'college',
chartInstance: null,
chartId: 'statisticsChart',
tableData: [],
summaryData: {
total: 0,
categories: 0,
max: 0,
average: 0
},
chartColors: [
'#409EFF',
'#67C23A',
'#E6A23C',
'#F56C6C',
'#909399',
'#5470C6',
'#91CC75',
'#FAC858',
'#EE6666',
'#73C0DE',
'#3BA272',
'#FC8452',
'#9A60B4',
'#EA7CCC',
'#95D4F2'
]
};
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
},
dialogTitle() {
const titleMap = {
college: '学院分布统计',
grade: '年级分布统计',
major: '专业分布统计',
status: '学籍状态统计',
completion: '信息完整度统计',
gender: '性别分布统计'
};
return titleMap[this.activeChart] || '统计图表';
}
},
watch: {
visible(val) {
if (val) {
this.activeChart = this.chartType || 'college';
this.$nextTick(() => {
this.initChart();
this.loadChartData();
});
} else {
this.destroyChart();
}
}
},
methods: {
/** 初始化图表 */
initChart() {
const chartDom = document.getElementById(this.chartId);
if (chartDom) {
this.chartInstance = echarts.init(chartDom);
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize);
}
},
/** 销毁图表 */
destroyChart() {
if (this.chartInstance) {
this.chartInstance.dispose();
this.chartInstance = null;
}
window.removeEventListener('resize', this.handleResize);
},
/** 处理窗口大小变化 */
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize();
}
},
/** 图表类型切换 */
handleChartChange(type) {
this.activeChart = type;
this.loadChartData();
},
/** 加载图表数据 */
loadChartData() {
this.loading = true;
let apiCall;
switch (this.activeChart) {
case 'college':
apiCall = getCollegeStatistics();
break;
case 'grade':
apiCall = getGradeStatistics();
break;
case 'major':
apiCall = getMajorStatistics();
break;
case 'status':
apiCall = getStudentStatusStatistics();
break;
case 'completion':
apiCall = getCompletionRateStatistics();
break;
case 'gender':
apiCall = this.getGenderStatistics();
break;
default:
apiCall = getCollegeStatistics();
}
apiCall
.then((response) => {
const data = response.data;
this.processChartData(data);
this.renderChart(data);
this.loading = false;
})
.catch(() => {
this.loading = false;
});
},
/** 处理图表数据 */
processChartData(data) {
let tableData = [];
let total = 0;
let max = 0;
switch (this.activeChart) {
case 'college':
tableData = data.map((item) => ({
name: item.collegeName,
count: item.count,
percentage: 0
}));
break;
case 'grade':
tableData = data.map((item) => ({
name: item.grade + '级',
count: item.count,
percentage: 0
}));
break;
case 'major':
tableData = data.map((item) => ({
name: item.majorName,
count: item.count,
percentage: 0
}));
break;
case 'status':
tableData = data.map((item) => ({
name: this.getStatusText(item.studentStatus),
count: item.count,
percentage: 0
}));
break;
case 'completion':
tableData = data.map((item) => ({
name: item.rateRange,
count: item.count,
percentage: 0
}));
break;
case 'gender':
tableData = data.map((item) => ({
name: item.gender === 'M' ? '男' : '女',
count: item.count,
percentage: 0
}));
break;
}
// 计算总数和百分比
total = tableData.reduce((sum, item) => sum + item.count, 0);
tableData.forEach((item) => {
item.percentage = total > 0 ? ((item.count / total) * 100).toFixed(1) : 0;
max = Math.max(max, item.count);
});
this.tableData = tableData;
this.summaryData = {
total,
categories: tableData.length,
max,
average: total > 0 ? Math.round(total / tableData.length) : 0
};
},
/** 渲染图表 */
renderChart(data) {
if (!this.chartInstance) return;
const option = this.getChartOption(data);
this.chartInstance.setOption(option, true);
},
/** 获取图表配置 */
getChartOption(data) {
const isBar = ['college', 'major'].includes(this.activeChart);
const isPie = ['status', 'completion', 'gender'].includes(this.activeChart);
if (isPie) {
return this.getPieChartOption(data);
} else if (isBar) {
return this.getBarChartOption(data);
} else {
return this.getLineChartOption(data);
}
},
/** 获取饼图配置 */
getPieChartOption(data) {
const pieData = this.tableData.map((item) => ({
name: item.name,
value: item.count
}));
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
data: pieData.map((item) => item.name)
},
series: [
{
name: this.dialogTitle,
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: pieData,
itemStyle: {
color: (params) => {
return this.chartColors[params.dataIndex % this.chartColors.length];
}
}
}
]
};
},
/** 获取柱状图配置 */
getBarChartOption(data) {
const xData = this.tableData.map((item) => item.name);
const yData = this.tableData.map((item) => item.count);
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: xData,
axisLabel: {
rotate: 45,
interval: 0
}
},
yAxis: {
type: 'value'
},
series: [
{
name: '学生数量',
type: 'bar',
data: yData,
itemStyle: {
color: (params) => {
return this.chartColors[params.dataIndex % this.chartColors.length];
}
}
}
]
};
},
/** 获取折线图配置 */
getLineChartOption(data) {
const xData = this.tableData.map((item) => item.name);
const yData = this.tableData.map((item) => item.count);
return {
tooltip: {
trigger: 'axis'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
series: [
{
name: '学生数量',
type: 'line',
data: yData,
smooth: true,
lineStyle: {
color: '#409EFF'
},
areaStyle: {
color: 'rgba(64, 158, 255, 0.1)'
}
}
]
};
},
/** 获取性别统计数据 */
getGenderStatistics() {
return getStatisticsOverview().then((response) => {
const data = response.data;
return {
data: [
{ gender: 'M', count: data.maleCount || 0 },
{ gender: 'F', count: data.femaleCount || 0 }
]
};
});
},
/** 获取学籍状态文本 */
getStatusText(status) {
const statusMap = {
'1': '在籍',
'2': '休学',
'3': '复学',
'4': '退学',
'5': '毕业'
};
return statusMap[status] || '未知';
},
/** 导出图表 */
handleExport() {
if (this.chartInstance) {
const url = this.chartInstance.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff'
});
const link = document.createElement('a');
link.download = `${this.dialogTitle}.png`;
link.href = url;
link.click();
}
},
/** 关闭弹窗 */
handleClose() {
this.dialogVisible = false;
this.destroyChart();
}
},
beforeUnmount() {
this.destroyChart();
}
};
</script>
<style scoped>
.statistics-container {
height: 600px;
padding: 20px;
}
.statistics-header {
margin-bottom: 20px;
padding: 16px 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.statistics-tabs {
height: 100%;
display: flex;
flex-direction: column;
}
.statistics-tabs :deep(.el-tabs__header) {
margin: 0 0 20px 0;
}
.statistics-tabs :deep(.el-tabs__nav-wrap::after) {
height: 1px;
background: #e4e7ed;
}
.statistics-tabs :deep(.el-tabs__item) {
font-size: 15px;
font-weight: 500;
padding: 0 20px;
height: 44px;
line-height: 44px;
}
.statistics-tabs :deep(.el-tabs__item.is-active) {
color: rgb(93, 135, 255);
font-weight: 600;
}
.statistics-tabs :deep(.el-tabs__active-bar) {
background: rgb(93, 135, 255);
}
.statistics-tabs :deep(.el-tabs__content) {
flex: 1;
overflow-y: auto;
padding: 0;
}
.chart-container {
height: 400px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-item {
padding: 20px;
background: white;
border-radius: 8px;
border: 1px solid #e4e7ed;
text-align: center;
transition: all 0.3s ease;
}
.stat-item:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #303133;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.data-table {
margin-top: 20px;
}
.data-table :deep(.el-table__header th) {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
.data-table :deep(.el-table__body-wrapper .el-table__row:hover) {
background-color: #f5f7fa;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
width: 100%;
}
</style>
@@ -1,758 +0,0 @@
<template>
<el-dialog title="学生详细信息" v-model="dialogVisible" width="80%" :before-close="handleClose" append-to-body>
<div v-loading="loading" class="student-detail">
<el-tabs v-model="activeTab" type="border-card" class="detail-tabs">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="basic">
<div class="info-section">
<div class="section-header">
<h3>个人基本信息</h3>
</div>
<el-row :gutter="20">
<el-col :span="6">
<div class="photo-container">
<el-image :src="studentInfo.photoUrl || '/static/images/default-avatar.png'" fit="cover" class="student-photo" />
</div>
</el-col>
<el-col :span="18">
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<label>学号:</label>
<span>{{ studentInfo.studentNo }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>姓名:</label>
<span>{{ studentInfo.name }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>性别:</label>
<span>{{ studentInfo.gender === 'M' ? '男' : '女' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>出生日期:</label>
<span>{{ studentInfo.birthDate }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>身份证号:</label>
<span>{{ studentInfo.idCard }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>民族:</label>
<span>{{ studentInfo.nation }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>政治面貌:</label>
<span>{{ studentInfo.politicalStatus }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>婚姻状况:</label>
<span>{{ studentInfo.maritalStatus }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>宗教信仰:</label>
<span>{{ studentInfo.religion || '无' }}</span>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
<div class="info-section">
<div class="section-header">
<h3>联系信息</h3>
</div>
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<label>手机号:</label>
<span>{{ studentInfo.phone }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>邮箱:</label>
<span>{{ studentInfo.email }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>QQ:</label>
<span>{{ studentInfo.qq }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>微信:</label>
<span>{{ studentInfo.wechat }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>宿舍楼:</label>
<span>{{ studentInfo.dormitoryBuilding }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>宿舍号:</label>
<span>{{ studentInfo.dormitoryRoom }}</span>
</div>
</el-col>
<el-col :span="24">
<div class="info-item">
<label>家庭地址:</label>
<span>{{ studentInfo.homeAddress }}</span>
</div>
</el-col>
</el-row>
</div>
</el-tab-pane>
<!-- 学籍信息 -->
<el-tab-pane label="学籍信息" name="academic">
<div class="info-section">
<div class="section-header">
<h3>学籍信息</h3>
</div>
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<label>入学年份:</label>
<span>{{ studentInfo.enrollmentYear }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>入学日期:</label>
<span>{{ studentInfo.enrollmentDate }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>学制:</label>
<span>{{ studentInfo.schoolSystem }}年</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>学历层次:</label>
<span>{{ studentInfo.educationLevel }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>培养方式:</label>
<span>{{ studentInfo.trainingType }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>学籍状态:</label>
<el-tag :type="getStatusType(studentInfo.studentStatus)">
{{ getStatusText(studentInfo.studentStatus) }}
</el-tag>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>学院:</label>
<span>{{ studentInfo.collegeName }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>专业:</label>
<span>{{ studentInfo.majorName }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>班级:</label>
<span>{{ studentInfo.className }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>年级:</label>
<span>{{ studentInfo.grade }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>辅导员:</label>
<span>{{ studentInfo.counselorName }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>班主任:</label>
<span>{{ studentInfo.classTeacherName }}</span>
</div>
</el-col>
</el-row>
</div>
</el-tab-pane>
<!-- 家庭信息 -->
<el-tab-pane label="家庭信息" name="family">
<div class="info-section">
<div class="section-header">
<h3>紧急联系人</h3>
</div>
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<label>紧急联系人:</label>
<span>{{ studentInfo.emergencyContactName }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>联系电话:</label>
<span>{{ studentInfo.emergencyContactPhone }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>关系:</label>
<span>{{ studentInfo.emergencyContactRelation }}</span>
</div>
</el-col>
</el-row>
</div>
<div class="info-section">
<div class="section-header">
<h3>父母信息</h3>
</div>
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<label>父亲姓名:</label>
<span>{{ studentInfo.fatherName }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>父亲电话:</label>
<span>{{ studentInfo.fatherPhone }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>父亲职业:</label>
<span>{{ studentInfo.fatherWorkUnit }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>母亲姓名:</label>
<span>{{ studentInfo.motherName }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>母亲电话:</label>
<span>{{ studentInfo.motherPhone }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>母亲职业:</label>
<span>{{ studentInfo.motherWorkUnit }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>家庭收入:</label>
<span>{{ studentInfo.familyIncome }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>家庭成员数:</label>
<span>{{ studentInfo.familyMembers }}人</span>
</div>
</el-col>
</el-row>
</div>
<div class="info-section">
<div class="section-header">
<h3>特殊情况</h3>
</div>
<el-row :gutter="20">
<el-col :span="6">
<div class="info-item">
<label>贫困生:</label>
<el-tag :type="studentInfo.isPoorStudent ? 'danger' : 'success'">
{{ studentInfo.isPoorStudent ? '是' : '否' }}
</el-tag>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>少数民族:</label>
<el-tag :type="studentInfo.isMinority ? 'warning' : 'info'">
{{ studentInfo.isMinority ? '是' : '否' }}
</el-tag>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>残疾学生:</label>
<el-tag :type="studentInfo.isDisabled ? 'danger' : 'success'">
{{ studentInfo.isDisabled ? '是' : '否' }}
</el-tag>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>孤儿:</label>
<el-tag :type="studentInfo.isOrphan ? 'danger' : 'success'">
{{ studentInfo.isOrphan ? '是' : '否' }}
</el-tag>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>留守儿童:</label>
<el-tag :type="studentInfo.isLeftBehindChild ? 'warning' : 'info'">
{{ studentInfo.isLeftBehindChild ? '是' : '否' }}
</el-tag>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>独生子女:</label>
<el-tag :type="studentInfo.isSingleChild ? 'primary' : 'info'">
{{ studentInfo.isSingleChild ? '是' : '否' }}
</el-tag>
</div>
</el-col>
</el-row>
</div>
</el-tab-pane>
<!-- 健康信息 -->
<el-tab-pane label="健康信息" name="health">
<div class="info-section">
<div class="section-header">
<h3>体征信息</h3>
</div>
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<label>身高:</label>
<span>{{ studentInfo.height }}cm</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>体重:</label>
<span>{{ studentInfo.weight }}kg</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>血型:</label>
<span>{{ studentInfo.bloodType }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>健康状况:</label>
<span>{{ studentInfo.healthStatus }}</span>
</div>
</el-col>
<el-col :span="24">
<div class="info-item">
<label>过敏史:</label>
<span>{{ studentInfo.allergies || '无' }}</span>
</div>
</el-col>
<el-col :span="24">
<div class="info-item">
<label>病史:</label>
<span>{{ studentInfo.medicalHistory || '无' }}</span>
</div>
</el-col>
</el-row>
</div>
</el-tab-pane>
<!-- 银行信息 -->
<el-tab-pane label="银行信息" name="bank">
<div class="info-section">
<div class="section-header">
<h3>银行卡信息</h3>
</div>
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<label>银行卡号:</label>
<span>{{ studentInfo.bankCardNo }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>开户银行:</label>
<span>{{ studentInfo.bankName }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>开户支行:</label>
<span>{{ studentInfo.bankBranch }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>持卡人姓名:</label>
<span>{{ studentInfo.cardHolderName }}</span>
</div>
</el-col>
</el-row>
</div>
</el-tab-pane>
<!-- 扩展信息 -->
<el-tab-pane label="扩展信息" name="extended">
<div class="info-section">
<div class="section-header">
<h3>特长爱好</h3>
</div>
<div class="info-item">
<p>{{ studentInfo.specialties || '无' }}</p>
</div>
</div>
<div class="info-section">
<div class="section-header">
<h3>获奖情况</h3>
</div>
<div class="info-item">
<p>{{ studentInfo.awards || '无' }}</p>
</div>
</div>
<div class="info-section">
<div class="section-header">
<h3>违纪记录</h3>
</div>
<div class="info-item">
<p>{{ studentInfo.violations || '无' }}</p>
</div>
</div>
<div class="info-section">
<div class="section-header">
<h3>社会活动</h3>
</div>
<div class="info-item">
<p>{{ studentInfo.socialActivities || '无' }}</p>
</div>
</div>
</el-tab-pane>
<!-- 系统信息 -->
<el-tab-pane label="系统信息" name="system">
<div class="info-section">
<div class="section-header">
<h3>信息状态</h3>
</div>
<el-row :gutter="20">
<el-col :span="8">
<div class="info-item">
<label>信息状态:</label>
<el-tag :type="getInfoStatusType(studentInfo.infoStatus)">
{{ getInfoStatusText(studentInfo.infoStatus) }}
</el-tag>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>数据来源:</label>
<span>{{ getDataSourceText(studentInfo.dataSource) }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>信息完整度:</label>
<el-progress :percentage="studentInfo.completionRate || 0" :color="getCompletionColor(studentInfo.completionRate)" />
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>最后审核时间:</label>
<span>{{ studentInfo.lastAuditTime || '未审核' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>创建时间:</label>
<span>{{ studentInfo.createTime }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<label>更新时间:</label>
<span>{{ studentInfo.updateTime }}</span>
</div>
</el-col>
</el-row>
</div>
<div class="info-section" v-if="studentInfo.remark">
<div class="section-header">
<h3>备注信息</h3>
</div>
<div class="info-item">
<p>{{ studentInfo.remark }}</p>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template v-slot:footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleEdit" v-hasPermi="['xg:studentInfo:edit']"> 编辑信息 </el-button>
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import { getStudent } from '@/api/xg-xx/student';
export default {
name: 'StudentDetailDialog',
props: {
visible: {
type: Boolean,
default: false
},
studentId: {
type: Number,
required: true
}
},
data() {
return {
loading: false,
activeTab: 'basic',
studentInfo: {}
};
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
}
},
watch: {
visible(val) {
if (val && this.studentId) {
this.getStudentInfo();
}
}
},
methods: {
/** 获取学生信息 */
getStudentInfo() {
this.loading = true;
getStudent(this.studentId)
.then((response) => {
this.studentInfo = response.data;
this.loading = false;
})
.catch(() => {
this.loading = false;
});
},
/** 编辑按钮 */
handleEdit() {
this.$emit('edit', this.studentInfo);
this.handleClose();
},
/** 关闭弹窗 */
handleClose() {
this.dialogVisible = false;
this.activeTab = 'basic';
this.studentInfo = {};
},
/** 获取学籍状态类型 */
getStatusType(status) {
const statusMap = {
'1': 'success',
'2': 'warning',
'3': 'info',
'4': 'danger',
'5': 'primary'
};
return statusMap[status] || 'info';
},
/** 获取学籍状态文本 */
getStatusText(status) {
const statusMap = {
'1': '在籍',
'2': '休学',
'3': '复学',
'4': '退学',
'5': '毕业'
};
return statusMap[status] || '未知';
},
/** 获取信息状态类型 */
getInfoStatusType(status) {
const statusMap = {
'1': 'success',
'2': 'warning',
'3': 'info',
'4': 'danger',
'5': 'info'
};
return statusMap[status] || 'info';
},
/** 获取信息状态文本 */
getInfoStatusText(status) {
const statusMap = {
'1': '正常',
'2': '待审核',
'3': '审核中',
'4': '已驳回',
'5': '已锁定'
};
return statusMap[status] || '未知';
},
/** 获取数据来源文本 */
getDataSourceText(source) {
const sourceMap = {
'1': '学生录入',
'2': '批量导入',
'3': '系统迁移',
'4': '第三方同步'
};
return sourceMap[source] || '未知';
},
/** 获取完整度颜色 */
getCompletionColor(rate) {
if (rate >= 90) return '#67C23A';
if (rate >= 70) return '#E6A23C';
if (rate >= 50) return '#F56C6C';
return '#909399';
}
}
};
</script>
<style scoped>
.student-detail {
padding: 20px;
}
.detail-tabs {
border: none;
box-shadow: none;
}
.info-section {
margin-bottom: 30px;
}
.section-header {
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.section-header h3 {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 600;
}
.photo-container {
text-align: center;
padding: 20px;
}
.student-photo {
width: 120px;
height: 160px;
border-radius: 8px;
border: 2px solid #e0e0e0;
}
.info-item {
margin-bottom: 15px;
display: flex;
align-items: center;
}
.info-item label {
font-weight: 600;
color: #666;
min-width: 100px;
margin-right: 10px;
}
.info-item span {
color: #333;
flex: 1;
}
.info-item p {
margin: 0;
color: #333;
line-height: 1.6;
text-align: justify;
}
.dialog-footer {
text-align: right;
}
/* 响应式设计 */
@media (max-width: 768px) {
.student-photo {
width: 80px;
height: 100px;
}
.info-item {
flex-direction: column;
align-items: flex-start;
}
.info-item label {
min-width: auto;
margin-bottom: 5px;
}
}
</style>
File diff suppressed because it is too large. Load diff
@@ -1,962 +0,0 @@
<template>
<div class="page-container">
<!-- 统计卡片区域 -->
<div class="mb-[10px]">
<el-row :gutter="16">
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-primary">
<div class="stat-content">
<div class="stat-icon">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.totalStudents }}</div>
<div class="stat-label">学生总数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Check /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.activeStudents }}</div>
<div class="stat-label">在籍学生</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.completeStudents }}</div>
<div class="stat-label">信息完整</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Warning /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.pendingStudents }}</div>
<div class="stat-label">待审核</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 搜索区域 -->
<el-card shadow="never" class="button-card">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-select v-model="queryParams.gender" placeholder="请选择性别" clearable>
<el-option label="男" value="M" />
<el-option label="女" value="F" />
</el-select>
</el-form-item>
<el-form-item label="学院" prop="collegeCode">
<el-select v-model="queryParams.collegeCode" placeholder="请选择学院" clearable @change="handleCollegeChange">
<el-option v-for="college in collegeOptions" :key="college.value" :label="college.label" :value="college.value" />
</el-select>
</el-form-item>
<el-form-item label="专业" prop="majorCode">
<el-select v-model="queryParams.majorCode" placeholder="请选择专业" clearable @change="handleMajorChange">
<el-option v-for="major in majorOptions" :key="major.value" :label="major.label" :value="major.value" />
</el-select>
</el-form-item>
<el-form-item label="班级" prop="classCode">
<el-select v-model="queryParams.classCode" placeholder="请选择班级" clearable>
<el-option v-for="classItem in classOptions" :key="classItem.value" :label="classItem.label" :value="classItem.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="studentStatus">
<el-select v-model="queryParams.studentStatus" placeholder="请选择状态" clearable>
<el-option label="在籍" value="1" />
<el-option label="休学" value="2" />
<el-option label="复学" value="3" />
<el-option label="退学" value="4" />
<el-option label="毕业" value="5" />
</el-select>
</el-form-item>
<el-form-item label="班主任" prop="classTeacherId">
<el-select v-model="queryParams.classTeacherId" placeholder="请选择班主任" clearable>
<el-option v-for="teacher in classTeacherOptions" :key="teacher.value" :label="teacher.label" :value="teacher.value" />
</el-select>
</el-form-item>
<div class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</div>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" class="table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['xg:studentInfo:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate" v-hasPermi="['xg:studentInfo:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['xg:studentInfo:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['xg:studentInfo:export']">导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['xg:studentInfo:import']">导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="DataAnalysis" @click="handleStatistics" v-hasPermi="['xg:studentInfo:statistics']">统计</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Check" :disabled="multiple" @click="handleBatchAudit" v-hasPermi="['xg:studentInfo:audit']"
>批量审核</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Operation" :disabled="multiple" @click="handleBatchOperation" v-hasPermi="['xg:studentInfo:batch']"
>批量操作</el-button
>
</el-col>
<right-toolbar :showSearch="showSearch" @update:showSearch="(val) => (showSearch = val)" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<div class="table-container">
<el-table v-loading="loading" :data="studentList" @selection-change="handleSelectionChange" class="scroll-table">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="name" width="100" />
<el-table-column label="性别" align="center" prop="gender" width="80">
<template #default="scope">
<el-tag v-if="scope.row.gender === 'M'" type="primary">男</el-tag>
<el-tag v-else-if="scope.row.gender === 'F'" type="success">女</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学院" align="center" prop="collegeName" width="120" />
<el-table-column label="专业" align="center" prop="majorName" width="120" />
<el-table-column label="班级" align="center" prop="className" width="100" />
<el-table-column label="年级" align="center" prop="grade" width="80" />
<el-table-column label="状态" align="center" prop="studentStatus" width="80">
<template #default="scope">
<el-tag v-if="scope.row.studentStatus === '1'" type="success">在籍</el-tag>
<el-tag v-else-if="scope.row.studentStatus === '2'" type="warning">休学</el-tag>
<el-tag v-else-if="scope.row.studentStatus === '3'" type="info">复学</el-tag>
<el-tag v-else-if="scope.row.studentStatus === '4'" type="danger">退学</el-tag>
<el-tag v-else-if="scope.row.studentStatus === '5'" type="info">毕业</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="完整度" align="center" prop="completionRate" width="100">
<template #default="scope">
<el-progress
:percentage="scope.row.completionRate || 0"
:color="getCompletenessColor(scope.row.completionRate)"
:stroke-width="8"
:show-text="false"
/>
<span class="ml-2">{{ scope.row.completionRate || 0 }}%</span>
</template>
</el-table-column>
<el-table-column label="联系电话" align="center" prop="phone" width="120" />
<el-table-column label="入学时间" align="center" prop="enrollmentDate" width="110">
<template #default="scope">
<span>{{ parseTime(scope.row.enrollmentDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="查看详情" placement="top">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)" v-hasPermi="['xg:studentInfo:query']" />
</el-tooltip>
<el-tooltip content="编辑" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['xg:studentInfo:edit']" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['xg:studentInfo:remove']" />
</el-tooltip>
<el-tooltip content="审核" placement="top">
<el-button link type="success" icon="Check" @click="handleAudit(scope.row)" v-hasPermi="['xg:studentInfo:audit']" />
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<pagination
v-show="total > 0"
:total="total"
:page="queryParams.pageNum"
:limit="queryParams.pageSize"
@update:page="(val) => (queryParams.pageNum = val)"
@update:limit="(val) => (queryParams.pageSize = val)"
@pagination="getList"
/>
</div>
</el-card>
<!-- 学生详情弹窗 -->
<StudentDetailDialog
:visible="detailVisible"
:student-id="selectedStudentId"
@update:visible="(val) => (detailVisible = val)"
@refresh="getList"
/>
<!-- 学生编辑弹窗 -->
<StudentEditDialog
:visible="editVisible"
:student-id="selectedStudentId"
:is-edit="isEdit"
@update:visible="(val) => (editVisible = val)"
@refresh="getList"
/>
<!-- 统计图表弹窗 -->
<StatisticsChartDialog :visible="statisticsVisible" @update:visible="(val) => (statisticsVisible = val)" />
<!-- 导入弹窗 -->
<ImportDialog :visible="importVisible" @update:visible="(val) => (importVisible = val)" @refresh="getList" />
<!-- 审核弹窗 -->
<AuditDialog
:visible="auditVisible"
:student-ids="selectedStudentIds"
:is-batch="isBatchAudit"
@update:visible="(val) => (auditVisible = val)"
@refresh="getList"
/>
<!-- 批量操作弹窗 -->
<BatchOperationDialog
:visible="batchOperationVisible"
:student-ids="selectedStudentIds"
@update:visible="(val) => (batchOperationVisible = val)"
@refresh="getList"
/>
</div>
</template>
<script>
import { listStudents, getStudent, delStudent, exportStudents, getStatisticsOverview, getClassTeacherOptions } from '@/api/xg-xx/student';
import { getCollegeOptions, getMajorOptions, getClassOptions } from '@/api/xg-xx/statistics';
import StudentDetailDialog from './components/StudentDetailDialog';
import StudentEditDialog from './components/StudentEditDialog';
import AuditDialog from './components/AuditDialog';
import ImportDialog from './components/ImportDialog';
import BatchOperationDialog from './components/BatchOperationDialog';
import StatisticsChartDialog from './components/StatisticsChartDialog';
export default {
name: 'StudentInfo',
components: {
StudentDetailDialog,
StudentEditDialog,
AuditDialog,
ImportDialog,
BatchOperationDialog,
StatisticsChartDialog
},
data() {
return {
// 加载状态
loading: true,
// 选中数组
ids: [],
selectedIds: [],
selectedRows: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 学生表格数据
studentList: [],
// 弹窗标题
title: '',
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
studentNo: null,
name: null,
gender: null,
collegeCode: null,
majorCode: null,
classCode: null,
grade: null,
studentStatus: null,
infoStatus: null,
classTeacherId: null
},
// 表单参数
form: {},
// 表单校验
rules: {},
// 统计卡片数据
statistics: {
totalStudents: 0,
activeStudents: 0,
completeStudents: 0,
pendingStudents: 0
},
// 选项数据
collegeOptions: [],
majorOptions: [],
classOptions: [],
classTeacherOptions: [],
// 弹窗显示状态
detailVisible: false,
editVisible: false,
auditVisible: false,
importVisible: false,
batchOperationVisible: false,
statisticsVisible: false,
// 当前操作数据
selectedStudentId: null,
isEdit: false,
isBatchAudit: false,
selectedStudentIds: []
};
},
created() {
this.getList();
this.getStatistics();
this.getCollegeList();
this.getClassTeacherOptions(); // 调用API获取班主任数据
},
methods: {
/** 查询学生列表 */
getList() {
this.loading = true;
console.log('开始查询学生列表,查询参数:', this.queryParams);
listStudents(this.queryParams)
.then((response) => {
console.log('查询学生列表成功,响应数据:', response);
this.studentList = response.rows;
this.total = response.total;
this.loading = false;
})
.catch((error) => {
console.error('查询学生列表失败,错误详情:', error);
// 提供更详细的错误信息
let errorMessage = '查询学生列表失败';
if (error.response) {
// 服务器返回了错误响应
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
errorMessage = '认证失败,请重新登录';
// 可以在这里跳转到登录页面
// this.$router.push('/login')
} else if (status === 403) {
errorMessage = '权限不足,无法访问学生信息';
} else if (status === 500) {
errorMessage = '服务器内部错误';
if (data && data.msg) {
errorMessage += ': ' + data.msg;
}
} else if (data && data.msg) {
errorMessage = data.msg;
}
} else if (error.request) {
// 请求发送了但没有收到响应
errorMessage = '网络连接失败,请检查网络状态';
} else {
// 请求配置错误
errorMessage = '请求配置错误: ' + error.message;
}
this.$modal.msgError(errorMessage);
this.loading = false;
});
},
/** 获取统计数据 */
getStatistics() {
getStatisticsOverview()
.then((response) => {
console.log('获取统计数据成功:', response);
this.statistics = response.data;
})
.catch((error) => {
console.error('获取统计数据失败:', error);
this.$modal.msgError('获取统计数据失败');
});
},
/** 获取学院列表 */
getCollegeList() {
console.log('开始获取学院列表');
getCollegeOptions()
.then((response) => {
console.log('获取学院列表成功:', response);
this.collegeOptions = response.data || [];
})
.catch((error) => {
console.error('获取学院列表失败:', error);
let errorMessage = '获取学院列表失败';
if (error.response && error.response.data && error.response.data.msg) {
errorMessage += ': ' + error.response.data.msg;
}
this.$modal.msgError(errorMessage);
});
},
/** 获取专业列表 */
getMajorList(collegeId) {
console.log('开始获取专业列表:', collegeId);
getMajorOptions(collegeId)
.then((response) => {
console.log('获取专业列表成功:', response);
this.majorOptions = response.data || [];
})
.catch((error) => {
console.error('获取专业列表失败:', error);
let errorMessage = '获取专业列表失败';
if (error.response && error.response.data && error.response.data.msg) {
errorMessage += ': ' + error.response.data.msg;
}
this.$modal.msgError(errorMessage);
});
},
/** 获取班级列表 */
getClassList(majorId) {
console.log('开始获取班级列表:', majorId);
getClassOptions(majorId)
.then((response) => {
console.log('获取班级列表成功:', response);
this.classOptions = response.data || [];
})
.catch((error) => {
console.error('获取班级列表失败:', error);
let errorMessage = '获取班级列表失败';
if (error.response && error.response.data && error.response.data.msg) {
errorMessage += ': ' + error.response.data.msg;
}
this.$modal.msgError(errorMessage);
});
},
/** 获取班主任选项 */
getClassTeacherOptions() {
console.log('开始获取班主任列表');
getClassTeacherOptions()
.then((response) => {
console.log('获取班主任列表成功,原始响应:', JSON.stringify(response));
// 检查响应中的数据结构
if (response.data && typeof response.data === 'object') {
console.log('响应数据类型:', Object.prototype.toString.call(response.data));
if (response.data.code === 200) {
// 标准成功响应
console.log('标准成功响应,data字段:', JSON.stringify(response.data.data));
this.classTeacherOptions = response.data.data || [];
} else if (Array.isArray(response.data)) {
// 直接返回数组
console.log('直接返回数组,长度:', response.data.length);
console.log('数组第一个元素:', JSON.stringify(response.data[0]));
this.classTeacherOptions = response.data;
} else if (response.data && Array.isArray(response.data.rows)) {
// 分页响应格式
console.log('分页响应格式,rows字段:', JSON.stringify(response.data.rows));
this.classTeacherOptions = response.data.rows;
} else {
// 其他格式,尝试直接使用
console.log('其他格式,直接使用response.data');
this.classTeacherOptions = response.data || [];
}
} else {
console.log('响应数据为空或不是对象类型');
this.classTeacherOptions = [];
}
console.log('设置后的班主任选项:', JSON.stringify(this.classTeacherOptions));
// 检查每个选项的格式是否正确
if (this.classTeacherOptions && this.classTeacherOptions.length > 0) {
console.log('选项数量:', this.classTeacherOptions.length);
this.classTeacherOptions.forEach((option, index) => {
console.log(`选项[${index}]:`, JSON.stringify(option));
if (!option.value || !option.label) {
console.warn(`选项[${index}]格式不正确:`, option);
}
});
}
// 如果没有获取到数据,给出提示
if (!this.classTeacherOptions.length) {
this.$modal.msgWarning('未找到班主任数据,请确认数据库中是否存在班主任角色(head_teacher)的用户');
}
})
.catch((error) => {
console.error('获取班主任列表失败,错误详情:', error);
// 提供更详细的错误信息
let errorMessage = '获取班主任列表失败';
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
console.error('认证失败(401),Token可能已过期,尝试重新登录');
errorMessage = '认证失败,请重新登录';
// 可以在这里添加重新登录的逻辑
// this.$store.dispatch('LogOut').then(() => {
// this.$router.push('/login')
// })
} else if (status === 403) {
console.error('权限不足(403),无法访问班主任数据');
errorMessage = '权限不足,无法获取班主任列表';
} else if (data && data.msg) {
errorMessage = errorMessage + ': ' + data.msg;
}
}
this.$modal.msgError(errorMessage);
});
},
/** 学院变化处理 */
handleCollegeChange(collegeId) {
console.log('学院变化,清空专业和班级选择:', collegeId);
this.queryParams.majorCode = null;
this.queryParams.classCode = null;
this.majorOptions = [];
this.classOptions = [];
this.queryParams.classTeacherId = null; // 清空班主任选择
if (collegeId) {
console.log('学院变化,获取专业列表:', collegeId);
// 根据学院ID获取专业列表
this.getMajorList(collegeId);
}
},
/** 专业变化处理 */
handleMajorChange(majorId) {
console.log('专业变化,清空班级选择:', majorId);
this.queryParams.classCode = null;
this.classOptions = [];
this.queryParams.classTeacherId = null; // 清空班主任选择
if (majorId) {
console.log('专业变化,获取班级列表:', majorId);
// 根据专业ID获取班级列表
this.getClassList(majorId);
}
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.$refs.queryRef?.resetFields();
this.queryParams = {
pageNum: 1,
pageSize: 10,
studentNo: null,
name: null,
gender: null,
collegeCode: null,
majorCode: null,
classCode: null,
grade: null,
studentStatus: null,
infoStatus: null,
classTeacherId: null
};
// 重置级联选择器
this.majorOptions = [];
this.classOptions = [];
this.classTeacherOptions = []; // 重置班主任选项
this.handleQuery();
},
/** 多选框选中数据 */
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.id);
this.selectedIds = selection.map((item) => item.id);
this.selectedRows = selection;
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增按钮操作 */
handleAdd() {
this.selectedStudentId = null;
this.isEdit = false;
this.editVisible = true;
},
/** 修改按钮操作 */
handleUpdate(row) {
const id = row.id || this.ids[0];
this.selectedStudentId = id;
this.isEdit = true;
this.editVisible = true;
},
/** 查看详情 */
handleDetail(row) {
this.selectedStudentId = row.id;
this.detailVisible = true;
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id ? [row.id] : this.ids;
this.$modal
.confirm('是否确认删除学生编号为"' + ids + '"的数据项?')
.then(function () {
return delStudent(ids);
})
.then(() => {
this.getList();
this.$modal.msgSuccess('删除成功');
})
.catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
this.$modal
.confirm('是否确认导出所有学生数据项?')
.then(() => {
this.loading = true;
return exportStudents(this.queryParams);
})
.then((response) => {
this.$download.excel(response, '学生信息.xlsx');
this.loading = false;
})
.catch(() => {
this.loading = false;
});
},
/** 导入按钮操作 */
handleImport() {
this.importVisible = true;
},
/** 审核操作 */
handleAudit(row) {
this.selectedStudentIds = [row.id];
this.isBatchAudit = false;
this.auditVisible = true;
},
/** 批量审核 */
handleBatchAudit() {
this.selectedStudentIds = this.selectedIds;
this.isBatchAudit = true;
this.auditVisible = true;
},
/** 批量操作 */
handleBatchOperation() {
if (this.selectedIds.length === 0) {
this.$modal.msgError('请选择要操作的数据');
return;
}
this.batchOperationVisible = true;
},
/** 显示统计图表 */
handleStatistics() {
this.statisticsVisible = true;
},
/** 获取学籍状态类型 */
getStatusType(status) {
const statusMap = {
'1': 'success',
'2': 'warning',
'3': 'info',
'4': 'danger',
'5': 'primary'
};
return statusMap[status] || 'info';
},
/** 获取学籍状态文本 */
getStatusText(status) {
const statusMap = {
'1': '在籍',
'2': '休学',
'3': '复学',
'4': '退学',
'5': '毕业'
};
return statusMap[status] || '未知';
},
/** 获取信息状态类型 */
getInfoStatusType(status) {
const statusMap = {
'1': 'success',
'2': 'warning',
'3': 'info',
'4': 'danger',
'5': 'info'
};
return statusMap[status] || 'info';
},
/** 获取信息状态文本 */
getInfoStatusText(status) {
const statusMap = {
'1': '正常',
'2': '待审核',
'3': '审核中',
'4': '已驳回',
'5': '已锁定'
};
return statusMap[status] || '未知';
},
/** 获取完整度颜色 */
getCompletenessColor(rate) {
if (rate >= 90) return '#67C23A';
if (rate >= 70) return '#E6A23C';
if (rate >= 50) return '#F56C6C';
return '#909399';
}
}
};
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 统计卡片样式 */
.stat-card {
border-radius: 8px;
transition: all 0.3s ease;
height: 90px;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-content {
display: flex;
align-items: center;
padding: 5px 12px;
height: 100%;
box-sizing: border-box;
}
.stat-icon {
font-size: 24px;
margin-right: 12px;
opacity: 0.8;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 20px;
font-weight: bold;
line-height: 1;
margin-bottom: 3px;
}
.stat-label {
font-size: 13px;
opacity: 0.7;
line-height: 1;
}
.stat-card-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 表格容器 - 支持横向滚动 */
.table-container {
width: 100%;
margin-bottom: 0;
}
/* 横向滚动表格样式 */
.scroll-table {
width: 100%;
}
/* 表格横向滚动条样式优化 */
.scroll-table :deep(.el-table__body-wrapper) {
overflow-x: auto !important;
overflow-y: auto;
}
.scroll-table :deep(.el-table__header-wrapper) {
overflow-x: hidden;
}
/* 强制显示表格横向滚动条 - 适用于所有浏览器 */
.scroll-table :deep(.el-table__body-wrapper) {
scrollbar-width: thin;
scrollbar-color: #409eff #f0f0f0;
}
/* Webkit 浏览器的横向滚动条样式 */
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar {
height: 12px !important;
width: 12px !important;
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-track {
background: #f0f0f0 !important;
border-radius: 6px !important;
border: 1px solid #e4e7ed !important;
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb {
background: #409eff !important;
border-radius: 6px !important;
border: 2px solid #f0f0f0 !important;
min-height: 20px !important;
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover {
background: #337ecc !important;
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:active {
background: #2b6cb0 !important;
}
/* 确保滚动条始终可见的额外设置 */
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-corner {
background: #f0f0f0;
}
/* 固定列样式优化 */
.scroll-table :deep(.el-table__fixed-right) {
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
}
.scroll-table :deep(.el-table__fixed-left) {
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
/* 确保固定列在滚动时有正确的背景色 */
.scroll-table :deep(.el-table__fixed-right .el-table__cell),
.scroll-table :deep(.el-table__fixed-left .el-table__cell) {
background-color: #fff;
}
/* 表格行悬停效果在固定列上的同步 */
.scroll-table :deep(.el-table__row:hover .el-table__cell) {
background-color: #f5f7fa !important;
}
/* 表格样式 */
.scroll-table :deep(.el-table__header-wrapper .el-table__header th) {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
.scroll-table :deep(.el-table__body-wrapper .el-table__row:hover) {
background-color: #f5f7fa;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
white-space: nowrap;
}
/* 确保操作按钮宽度一致 */
.action-buttons .el-button {
width: 27px !important;
height: 27px !important;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
</style>