提交代码
This commit is contained in:
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>
|
||||
Reference in new issue
Block a user