wuzhen 信息采集和统计分析完成
This commit is contained in:
1 parent
27fcea613c
commit
2f56204113
122 files changed
+29130
-101
No files matched your search
@@ -0,0 +1,283 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { CollectionBatchVO, CollectionBatchForm, CollectionBatchQuery } from './types';
|
||||
|
||||
/**
|
||||
* 查询信息采集批次列表
|
||||
*/
|
||||
export const listCollectionBatch = (query?: CollectionBatchQuery): AxiosPromise<CollectionBatchVO[]> => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询信息采集批次详细
|
||||
*/
|
||||
export const getCollectionBatch = (id: string | number): AxiosPromise<CollectionBatchVO> => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据批次编码查询批次信息
|
||||
*/
|
||||
export const getCollectionBatchByCode = (batchCode: string): AxiosPromise<CollectionBatchVO> => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/code/${batchCode}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增信息采集批次
|
||||
*/
|
||||
export const addCollectionBatch = (data: CollectionBatchForm) => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 修改信息采集批次
|
||||
*/
|
||||
export const updateCollectionBatch = (data: CollectionBatchForm) => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch',
|
||||
method: 'put',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除信息采集批次
|
||||
*/
|
||||
export const delCollectionBatch = (ids: string | number | Array<string | number>) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/${ids}`,
|
||||
method: 'delete'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出信息采集批次
|
||||
*/
|
||||
export const exportCollectionBatch = (query?: CollectionBatchQuery) => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/export',
|
||||
method: 'post',
|
||||
data: query
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询用户权限内的批次列表
|
||||
*/
|
||||
export const getUserPermissionBatches = (): AxiosPromise<CollectionBatchVO[]> => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/userPermission',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询活跃的批次列表
|
||||
*/
|
||||
export const getActiveBatches = (): AxiosPromise<CollectionBatchVO[]> => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/active',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询即将开始的批次列表
|
||||
*/
|
||||
export const getUpcomingBatches = (): AxiosPromise<CollectionBatchVO[]> => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/upcoming',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询即将结束的批次列表
|
||||
*/
|
||||
export const getEndingSoonBatches = (): AxiosPromise<CollectionBatchVO[]> => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/endingSoon',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 开始批次
|
||||
*/
|
||||
export const startBatch = (batchId: string | number) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/start/${batchId}`,
|
||||
method: 'post'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 暂停批次
|
||||
*/
|
||||
export const pauseBatch = (batchId: string | number) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/pause/${batchId}`,
|
||||
method: 'post'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 恢复批次
|
||||
*/
|
||||
export const resumeBatch = (batchId: string | number) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/resume/${batchId}`,
|
||||
method: 'post'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 结束批次
|
||||
*/
|
||||
export const endBatch = (batchId: string | number) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/end/${batchId}`,
|
||||
method: 'post'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 复制批次
|
||||
*/
|
||||
export const copyBatch = (batchId: string | number, newBatchCode: string, newBatchName: string) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/copy/${batchId}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
newBatchCode,
|
||||
newBatchName
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取批次统计信息
|
||||
*/
|
||||
export const getBatchStatistics = (batchId: string | number) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/statistics/${batchId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取整体统计信息
|
||||
*/
|
||||
export const getOverallStatistics = () => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/statistics/overall',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取批次完成度统计
|
||||
*/
|
||||
export const getBatchCompletionStatistics = (batchId: string | number) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/statistics/completion/${batchId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取批次多维度统计
|
||||
*/
|
||||
export const getBatchDimensionStatistics = (batchId: string | number, dimension: string) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/statistics/dimension/${batchId}`,
|
||||
method: 'get',
|
||||
params: { dimension }
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量更新批次状态
|
||||
*/
|
||||
export const batchUpdateStatus = (batchIds: Array<string | number>, status: string) => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/batchUpdateStatus',
|
||||
method: 'post',
|
||||
data: {
|
||||
batchIds,
|
||||
status
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查批次是否可编辑
|
||||
*/
|
||||
export const checkBatchEditable = (batchId: string | number) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/checkEditable/${batchId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查批次是否可删除
|
||||
*/
|
||||
export const checkBatchDeletable = (batchId: string | number) => {
|
||||
return request({
|
||||
url: `/xg/collectionBatch/checkDeletable/${batchId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取可用批次选项
|
||||
*/
|
||||
export const getAvailableBatches = () => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/available',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 导入批次数据
|
||||
*/
|
||||
export const importBatchData = (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return request({
|
||||
url: '/xg/collectionBatch/import',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载批次数据导入模板
|
||||
*/
|
||||
export const downloadBatchTemplate = () => {
|
||||
return request({
|
||||
url: '/xg/collectionBatch/importTemplate',
|
||||
method: 'post',
|
||||
responseType: 'blob'
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,501 @@
|
||||
import request from '@/utils/request'
|
||||
import { AxiosPromise } from 'axios'
|
||||
|
||||
// ========== 统计分析相关类型定义 ==========
|
||||
|
||||
// 统计查询参数
|
||||
export interface StatisticsQuery {
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
configName?: string
|
||||
configType?: string
|
||||
dimensionType?: string
|
||||
isEnabled?: boolean
|
||||
createTime?: string[]
|
||||
}
|
||||
|
||||
// 统计分析查询参数
|
||||
export interface StatisticsAnalysisQuery {
|
||||
statisticsType?: string
|
||||
dimension?: string
|
||||
field?: string
|
||||
groupBy?: string
|
||||
conditions?: Record<string, any>
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
collegeIds?: number[]
|
||||
majorIds?: number[]
|
||||
classIds?: number[]
|
||||
grades?: string[]
|
||||
genders?: string[]
|
||||
politicalStatuses?: string[]
|
||||
nationalities?: string[]
|
||||
studentStatuses?: string[]
|
||||
trainingTypes?: string[]
|
||||
useCache?: boolean
|
||||
}
|
||||
|
||||
// 统计配置视图对象
|
||||
export interface StatisticsConfigVo {
|
||||
id?: number
|
||||
configName?: string
|
||||
configType?: string
|
||||
dimensionType?: string
|
||||
queryConditions?: string
|
||||
displaySettings?: string
|
||||
chartType?: string
|
||||
isEnabled?: boolean
|
||||
sortOrder?: number
|
||||
remark?: string
|
||||
createBy?: number
|
||||
createTime?: string
|
||||
updateBy?: number
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
// 统计配置业务对象
|
||||
export interface StatisticsConfigBo {
|
||||
id?: number
|
||||
configName?: string
|
||||
configType?: string
|
||||
dimensionType?: string
|
||||
queryConditions?: string
|
||||
displaySettings?: string
|
||||
chartType?: string
|
||||
isEnabled?: boolean
|
||||
sortOrder?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 统计结果视图对象
|
||||
export interface StatisticsResultVo {
|
||||
title?: string
|
||||
type?: string
|
||||
dimension?: string
|
||||
data?: Array<Record<string, any>>
|
||||
total?: number
|
||||
statisticsTime?: string
|
||||
fromCache?: boolean
|
||||
cacheKey?: string
|
||||
extra?: Record<string, any>
|
||||
chartConfig?: Record<string, any>
|
||||
groupData?: Record<string, Array<Record<string, any>>>
|
||||
summary?: Record<string, any>
|
||||
}
|
||||
|
||||
// 统计报表视图对象
|
||||
export interface StatisticsReportVo {
|
||||
reportType?: string
|
||||
title?: string
|
||||
description?: string
|
||||
data?: Array<Record<string, any>>
|
||||
summary?: Record<string, any>
|
||||
generateTime?: string
|
||||
metadata?: {
|
||||
total: number
|
||||
conditions: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
// 统计缓存信息
|
||||
export interface StatisticsCacheInfo {
|
||||
key?: string
|
||||
hitCount?: number
|
||||
missCount?: number
|
||||
hitRate?: number
|
||||
size?: number
|
||||
lastAccessTime?: string
|
||||
createTime?: string
|
||||
expireTime?: string
|
||||
}
|
||||
|
||||
// 导出参数
|
||||
export interface StatisticsExportQuery {
|
||||
dimension?: string
|
||||
format?: 'excel' | 'csv'
|
||||
includeChart?: boolean
|
||||
conditions?: Record<string, any>
|
||||
reportType?: string
|
||||
}
|
||||
|
||||
// ========== 统计配置管理API ==========
|
||||
|
||||
// 查询统计配置列表
|
||||
export function listStatisticsConfig(query: StatisticsQuery): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsConfig/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取统计配置详情
|
||||
export function getStatisticsConfig(id: number): AxiosPromise<StatisticsConfigVo> {
|
||||
return request({
|
||||
url: `/xg/statisticsConfig/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增统计配置
|
||||
export function addStatisticsConfig(data: StatisticsConfigBo): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsConfig',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改统计配置
|
||||
export function updateStatisticsConfig(data: StatisticsConfigBo): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsConfig',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除统计配置
|
||||
export function delStatisticsConfig(ids: number[]): AxiosPromise<any> {
|
||||
return request({
|
||||
url: `/xg/statisticsConfig/${ids}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 导出统计配置
|
||||
export function exportStatisticsConfig(query: StatisticsQuery): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsConfig/export',
|
||||
method: 'post',
|
||||
data: query
|
||||
})
|
||||
}
|
||||
|
||||
// 启用/禁用统计配置
|
||||
export function changeStatisticsConfigStatus(id: number, status: boolean): AxiosPromise<any> {
|
||||
return request({
|
||||
url: `/xg/statisticsConfig/changeStatus`,
|
||||
method: 'put',
|
||||
data: { id, isEnabled: status }
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 统计分析API ==========
|
||||
|
||||
// 获取统计概览
|
||||
export function getStatisticsOverview(): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/overview',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 学院统计
|
||||
export function getCollegeStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/college',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 专业统计
|
||||
export function getMajorStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/major',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 班级统计
|
||||
export function getClassStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/class',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 年级统计
|
||||
export function getGradeStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/grade',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 性别统计
|
||||
export function getGenderStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/gender',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 年龄统计
|
||||
export function getAgeStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/age',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 政治面貌统计
|
||||
export function getPoliticalStatusStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/political',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 民族统计
|
||||
export function getNationStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/nation',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 生源地统计
|
||||
export function getSourceRegionStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/sourceRegion',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 血型统计
|
||||
export function getBloodTypeStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/bloodType',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 宗教信仰统计
|
||||
export function getReligionStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/religion',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 学籍状态统计
|
||||
export function getStudentStatusStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/studentStatus',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按维度统计 - 培养方式统计
|
||||
export function getTrainingTypeStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/trainingType',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 高级统计 - 身高统计
|
||||
export function getHeightStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/height',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 高级统计 - 体重统计
|
||||
export function getWeightStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/weight',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 多维度组合统计
|
||||
export function getMultiDimensionStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/multiDimension',
|
||||
method: 'post',
|
||||
data: query
|
||||
})
|
||||
}
|
||||
|
||||
// 时间趋势统计
|
||||
export function getTimeTrendStatistics(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/timeTrend',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 统计报表API ==========
|
||||
|
||||
// 生成学生信息汇总报表
|
||||
export function generateStudentSummaryReport(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsReportVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/report/studentSummary',
|
||||
method: 'post',
|
||||
data: query
|
||||
})
|
||||
}
|
||||
|
||||
// 生成班级统计报表
|
||||
export function generateClassStatisticsReport(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsReportVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/report/classStatistics',
|
||||
method: 'post',
|
||||
data: query
|
||||
})
|
||||
}
|
||||
|
||||
// 生成学院统计报表
|
||||
export function generateCollegeStatisticsReport(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsReportVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/report/collegeStatistics',
|
||||
method: 'post',
|
||||
data: query
|
||||
})
|
||||
}
|
||||
|
||||
// 生成困难学生统计报表
|
||||
export function generateDifficultStudentReport(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsReportVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/report/difficultStudent',
|
||||
method: 'post',
|
||||
data: query
|
||||
})
|
||||
}
|
||||
|
||||
// 生成特殊群体统计报表
|
||||
export function generateSpecialGroupReport(query: StatisticsAnalysisQuery): AxiosPromise<StatisticsReportVo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/report/specialGroup',
|
||||
method: 'post',
|
||||
data: query
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 统计导出API ==========
|
||||
|
||||
// 导出统计结果
|
||||
export function exportStatisticsResult(query: StatisticsExportQuery): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/export',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 导出统计报表
|
||||
export function exportStatisticsReport(query: StatisticsExportQuery): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/exportReport',
|
||||
method: 'post',
|
||||
data: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 统计缓存API ==========
|
||||
|
||||
// 获取缓存统计信息
|
||||
export function getCacheStatistics(): AxiosPromise<StatisticsCacheInfo> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/cache/info',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 清空统计缓存
|
||||
export function clearStatisticsCache(): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/cache/clear',
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 预热统计缓存
|
||||
export function warmupStatisticsCache(): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/cache/warmup',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 辅助API ==========
|
||||
|
||||
// 获取统计维度选项
|
||||
export function getStatisticsDimensionOptions(): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/dimensionOptions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取图表类型选项
|
||||
export function getChartTypeOptions(): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/chartTypeOptions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取学院选项
|
||||
export function getCollegeOptions(): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/collegeOptions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取专业选项
|
||||
export function getMajorOptions(collegeCode?: string): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/majorOptions',
|
||||
method: 'get',
|
||||
params: { collegeCode }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取班级选项
|
||||
export function getClassOptions(majorCode?: string): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/classOptions',
|
||||
method: 'get',
|
||||
params: { majorCode }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取年级选项
|
||||
export function getGradeOptions(): AxiosPromise<any> {
|
||||
return request({
|
||||
url: '/xg/statisticsAnalysis/gradeOptions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 执行统计配置
|
||||
export function executeStatisticsConfig(configId: number, query: StatisticsAnalysisQuery): AxiosPromise<StatisticsResultVo> {
|
||||
return request({
|
||||
url: `/xg/statisticsAnalysis/executeConfig/${configId}`,
|
||||
method: 'post',
|
||||
data: query
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,157 @@
|
||||
// 学生信息管理模块类型定义
|
||||
|
||||
// ========== 信息采集批次管理相关类型 ==========
|
||||
|
||||
// 信息采集批次VO
|
||||
export interface CollectionBatchVO {
|
||||
id?: number
|
||||
batchCode?: string
|
||||
batchName?: string
|
||||
batchType?: string
|
||||
academicYear?: string
|
||||
semester?: string
|
||||
batchStatus?: string
|
||||
publishTime?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
archiveTime?: string
|
||||
targetColleges?: string
|
||||
targetGrades?: string
|
||||
targetMajors?: string
|
||||
requiredFields?: string
|
||||
optionalFields?: string
|
||||
auditLevels?: string
|
||||
totalStudents?: number
|
||||
submittedCount?: number
|
||||
approvedCount?: number
|
||||
rejectedCount?: number
|
||||
completionRate?: number
|
||||
// 新增统计字段
|
||||
studentCount?: number // 兼容性字段,映射到 totalStudents
|
||||
pendingCount?: number // 待提交数量
|
||||
description?: string
|
||||
editPermissions?: string
|
||||
auditPermissions?: string
|
||||
viewPermissions?: string
|
||||
autoReminder?: number
|
||||
reminderFrequency?: number
|
||||
allowModifyAfterSubmit?: number
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
createBy?: number
|
||||
updateBy?: number
|
||||
}
|
||||
|
||||
// 信息采集批次表单
|
||||
export interface CollectionBatchForm {
|
||||
id?: number
|
||||
batchCode?: string
|
||||
batchName?: string
|
||||
batchType?: string
|
||||
academicYear?: string
|
||||
semester?: string
|
||||
batchStatus?: string
|
||||
publishTime?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
archiveTime?: string
|
||||
targetColleges?: string
|
||||
targetGrades?: string
|
||||
targetMajors?: string
|
||||
requiredFields?: string
|
||||
optionalFields?: string
|
||||
auditLevels?: string
|
||||
description?: string
|
||||
editPermissions?: string
|
||||
auditPermissions?: string
|
||||
viewPermissions?: string
|
||||
autoReminder?: number
|
||||
reminderFrequency?: number
|
||||
allowModifyAfterSubmit?: number
|
||||
}
|
||||
|
||||
// 信息采集批次查询参数
|
||||
export interface CollectionBatchQuery {
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
batchCode?: string
|
||||
batchName?: string
|
||||
batchType?: string
|
||||
academicYear?: string
|
||||
semester?: string
|
||||
batchStatus?: string
|
||||
targetColleges?: string
|
||||
targetGrades?: string
|
||||
targetMajors?: string
|
||||
publishTimeStart?: string
|
||||
publishTimeEnd?: string
|
||||
startTimeStart?: string
|
||||
startTimeEnd?: string
|
||||
endTimeStart?: string
|
||||
endTimeEnd?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
// 批次统计信息
|
||||
export interface BatchStatistics {
|
||||
totalBatches?: number
|
||||
activeBatches?: number
|
||||
completedBatches?: number
|
||||
draftBatches?: number
|
||||
totalStudents?: number
|
||||
submittedCount?: number
|
||||
approvedCount?: number
|
||||
rejectedCount?: number
|
||||
completionRate?: number
|
||||
averageCompletionTime?: number
|
||||
}
|
||||
|
||||
// 批次完成度统计
|
||||
export interface BatchCompletionStatistics {
|
||||
batchId?: number
|
||||
batchName?: string
|
||||
totalStudents?: number
|
||||
submittedCount?: number
|
||||
approvedCount?: number
|
||||
rejectedCount?: number
|
||||
pendingCount?: number
|
||||
completionRate?: number
|
||||
submitRate?: number
|
||||
approvalRate?: number
|
||||
collegeStatistics?: CollegeCompletionStatistics[]
|
||||
}
|
||||
|
||||
// 学院完成度统计
|
||||
export interface CollegeCompletionStatistics {
|
||||
collegeId?: number
|
||||
collegeName?: string
|
||||
totalStudents?: number
|
||||
submittedCount?: number
|
||||
approvedCount?: number
|
||||
rejectedCount?: number
|
||||
pendingCount?: number
|
||||
completionRate?: number
|
||||
submitRate?: number
|
||||
approvalRate?: number
|
||||
}
|
||||
|
||||
// 批次多维度统计
|
||||
export interface BatchDimensionStatistics {
|
||||
dimension?: string
|
||||
dimensionName?: string
|
||||
items?: DimensionStatisticsItem[]
|
||||
}
|
||||
|
||||
// 维度统计项
|
||||
export interface DimensionStatisticsItem {
|
||||
key?: string
|
||||
label?: string
|
||||
count?: number
|
||||
percentage?: number
|
||||
}
|
||||
|
||||
// ========== 学生信息相关类型 ==========
|
||||
|
||||
// 学生信息完整数据结构
|
||||
export interface StudentInfoVo {
|
||||
id?: number
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
# 信息采集批次管理前端应用
|
||||
|
||||
## 📋 概述
|
||||
|
||||
信息采集批次管理模块是学工管理系统的重要组成部分,用于管理学生信息采集的批次流程,包括批次创建、状态管理、权限配置、统计分析等功能。
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
web/src/views/xg-xx/collectionBatch/
|
||||
├── components/ # 组件目录
|
||||
│ ├── BatchActions.vue # 批次操作组件
|
||||
│ ├── BatchStatusTag.vue # 批次状态标签组件
|
||||
│ ├── BatchTypeTag.vue # 批次类型标签组件
|
||||
│ ├── ProgressBar.vue # 进度条组件
|
||||
│ └── SearchForm.vue # 搜索表单组件
|
||||
├── index.vue # 批次列表页面
|
||||
├── form.vue # 批次编辑表单页面
|
||||
├── detail.vue # 批次详情页面
|
||||
├── statistics.vue # 批次统计页面
|
||||
├── permission.vue # 权限配置页面
|
||||
├── route.ts # 路由配置
|
||||
└── README.md # 使用文档
|
||||
```
|
||||
|
||||
## 🎯 功能特性
|
||||
|
||||
### 1. 批次列表管理
|
||||
- ✅ 批次列表展示(支持分页)
|
||||
- ✅ 多条件搜索过滤
|
||||
- ✅ 批次状态和类型标签展示
|
||||
- ✅ 完成进度可视化
|
||||
- ✅ 批次操作(新增、编辑、删除)
|
||||
|
||||
### 2. 批次表单管理
|
||||
- ✅ 批次新增和编辑
|
||||
- ✅ 分组表单设计(基本信息、时间设置、目标设置等)
|
||||
- ✅ 表单验证
|
||||
- ✅ 保存和发布功能
|
||||
|
||||
### 3. 批次详情查看
|
||||
- ✅ 批次详细信息展示
|
||||
- ✅ 卡片式布局
|
||||
- ✅ 实时状态更新
|
||||
- ✅ 批次操作按钮
|
||||
|
||||
### 4. 统计分析
|
||||
- ✅ 批次统计数据展示
|
||||
- ✅ 图表可视化(ECharts)
|
||||
- ✅ 学院完成情况统计
|
||||
- ✅ 多维度数据分析
|
||||
|
||||
### 5. 权限配置
|
||||
- ✅ 角色权限管理
|
||||
- ✅ 批量权限配置
|
||||
- ✅ 权限有效期设置
|
||||
- ✅ 权限状态管理
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **Vue 3.5.13** - 前端框架
|
||||
- **TypeScript 5.8.3** - 类型支持
|
||||
- **Element Plus** - UI组件库
|
||||
- **ECharts** - 图表库
|
||||
- **Vue Router** - 路由管理
|
||||
- **Axios** - HTTP请求
|
||||
|
||||
## 📦 组件说明
|
||||
|
||||
### BatchStatusTag.vue
|
||||
批次状态标签组件,用于显示批次的当前状态。
|
||||
|
||||
```vue
|
||||
<BatchStatusTag :status="batchStatus" :show-icon="true" />
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `status`: 批次状态(draft, published, active, paused, ended, archived)
|
||||
- `showIcon`: 是否显示图标
|
||||
- `effect`: 标签效果(dark, light, plain)
|
||||
|
||||
### BatchTypeTag.vue
|
||||
批次类型标签组件,用于显示批次的类型。
|
||||
|
||||
```vue
|
||||
<BatchTypeTag :type="batchType" :show-icon="true" />
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `type`: 批次类型(student_info, scholarship, evaluation, other)
|
||||
- `showIcon`: 是否显示图标
|
||||
- `effect`: 标签效果
|
||||
|
||||
### ProgressBar.vue
|
||||
进度条组件,用于显示批次的完成进度。
|
||||
|
||||
```vue
|
||||
<ProgressBar :current="submittedCount" :total="totalStudents" />
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `current`: 当前完成数量
|
||||
- `total`: 总数量
|
||||
- `strokeWidth`: 进度条宽度
|
||||
- `showText`: 是否显示文本
|
||||
- `showDetail`: 是否显示详细信息
|
||||
|
||||
### BatchActions.vue
|
||||
批次操作组件,提供批次相关的操作按钮。
|
||||
|
||||
```vue
|
||||
<BatchActions :batch="batchData" @start="handleStart" @pause="handlePause" />
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `batch`: 批次数据对象
|
||||
|
||||
**Events:**
|
||||
- `start`: 开始批次
|
||||
- `pause`: 暂停批次
|
||||
- `resume`: 恢复批次
|
||||
- `end`: 结束批次
|
||||
- `copy`: 复制批次
|
||||
- `statistics`: 统计分析
|
||||
- `permission`: 权限配置
|
||||
- `export`: 导出数据
|
||||
- `delete`: 删除批次
|
||||
|
||||
### SearchForm.vue
|
||||
搜索表单组件,用于批次列表的搜索过滤。
|
||||
|
||||
```vue
|
||||
<SearchForm @search="handleSearch" @reset="handleReset" />
|
||||
```
|
||||
|
||||
**Events:**
|
||||
- `search`: 搜索事件,返回查询参数
|
||||
- `reset`: 重置事件
|
||||
|
||||
## 🚀 使用指南
|
||||
|
||||
### 1. 批次管理流程
|
||||
|
||||
1. **创建批次**
|
||||
- 点击"新增批次"按钮
|
||||
- 填写批次基本信息、时间设置、目标设置等
|
||||
- 保存草稿或直接发布
|
||||
|
||||
2. **管理批次**
|
||||
- 在列表中查看批次状态和进度
|
||||
- 使用搜索功能快速定位批次
|
||||
- 通过操作按钮管理批次状态
|
||||
|
||||
3. **查看统计**
|
||||
- 进入统计页面查看批次数据
|
||||
- 查看图表分析和学院完成情况
|
||||
- 导出统计报告
|
||||
|
||||
4. **配置权限**
|
||||
- 设置不同角色的权限范围
|
||||
- 配置权限有效期
|
||||
- 批量设置权限
|
||||
|
||||
### 2. 页面导航
|
||||
|
||||
- `/xg-xx/collectionBatch` - 批次列表页面
|
||||
- `/xg-xx/collectionBatch/add` - 新增批次页面
|
||||
- `/xg-xx/collectionBatch/edit?id={id}` - 编辑批次页面
|
||||
- `/xg-xx/collectionBatch/detail?id={id}` - 批次详情页面
|
||||
- `/xg-xx/collectionBatch/statistics` - 统计分析页面
|
||||
- `/xg-xx/collectionBatch/permission` - 权限配置页面
|
||||
|
||||
### 3. 权限控制
|
||||
|
||||
系统基于角色和权限进行访问控制:
|
||||
|
||||
- `xg:collectionBatch:list` - 查看批次列表
|
||||
- `xg:collectionBatch:add` - 新增批次
|
||||
- `xg:collectionBatch:edit` - 编辑批次
|
||||
- `xg:collectionBatch:remove` - 删除批次
|
||||
- `xg:collectionBatch:query` - 查看批次详情
|
||||
- `xg:collectionBatch:export` - 导出批次数据
|
||||
- `xg:collectionBatch:statistics` - 查看统计分析
|
||||
- `xg:collectionBatch:permission` - 配置权限
|
||||
|
||||
## 🔧 开发指南
|
||||
|
||||
### 1. 本地开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 2. 代码规范
|
||||
|
||||
- 使用 TypeScript 进行类型检查
|
||||
- 遵循 Vue 3 Composition API 规范
|
||||
- 使用 ESLint 进行代码检查
|
||||
- 组件命名使用 PascalCase
|
||||
|
||||
### 3. 样式规范
|
||||
|
||||
- 使用 Element Plus 设计系统
|
||||
- 遵循后勤管理模块的颜色和样式规范
|
||||
- 使用 Tailwind CSS 类名进行样式定制
|
||||
- 响应式设计适配不同屏幕尺寸
|
||||
|
||||
## 📝 API 接口
|
||||
|
||||
### 批次管理接口
|
||||
|
||||
```typescript
|
||||
// 查询批次列表
|
||||
listCollectionBatch(query: CollectionBatchQuery): Promise<CollectionBatchVO[]>
|
||||
|
||||
// 获取批次详情
|
||||
getCollectionBatch(id: string | number): Promise<CollectionBatchVO>
|
||||
|
||||
// 新增批次
|
||||
addCollectionBatch(data: CollectionBatchForm): Promise<void>
|
||||
|
||||
// 更新批次
|
||||
updateCollectionBatch(data: CollectionBatchForm): Promise<void>
|
||||
|
||||
// 删除批次
|
||||
delCollectionBatch(ids: string | number | Array<string | number>): Promise<void>
|
||||
|
||||
// 批次状态操作
|
||||
startBatch(batchId: string | number): Promise<void>
|
||||
pauseBatch(batchId: string | number): Promise<void>
|
||||
resumeBatch(batchId: string | number): Promise<void>
|
||||
endBatch(batchId: string | number): Promise<void>
|
||||
|
||||
// 统计分析
|
||||
getBatchStatistics(batchId: string | number): Promise<any>
|
||||
getOverallStatistics(): Promise<any>
|
||||
```
|
||||
|
||||
## 🐛 问题排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **页面无法访问**
|
||||
- 检查路由配置是否正确
|
||||
- 确认用户权限是否充足
|
||||
|
||||
2. **数据加载失败**
|
||||
- 检查 API 接口是否正常
|
||||
- 确认后端服务是否启动
|
||||
|
||||
3. **图表显示异常**
|
||||
- 检查 ECharts 是否正确引入
|
||||
- 确认数据格式是否正确
|
||||
|
||||
4. **权限配置无效**
|
||||
- 检查权限标识是否正确
|
||||
- 确认用户角色是否匹配
|
||||
|
||||
### 调试技巧
|
||||
|
||||
1. 使用浏览器开发者工具查看网络请求
|
||||
2. 检查 Console 中的错误信息
|
||||
3. 使用 Vue DevTools 调试组件状态
|
||||
4. 查看后端日志确认接口调用情况
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请联系开发团队或查看相关文档:
|
||||
|
||||
- 项目文档:`docs/开发文档/API/`
|
||||
- 后端接口:`backend/ruoyi-modules/ruoyi-xg-xx/`
|
||||
- 前端代码:`web/src/views/xg-xx/collectionBatch/`
|
||||
|
||||
---
|
||||
|
||||
**版本信息**:V1.0.0
|
||||
**更新时间**:2025-01-17
|
||||
**维护团队**:前端开发组
|
||||
@@ -0,0 +1,123 @@
|
||||
<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>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<el-tag :type="tagType" :effect="effect">
|
||||
<el-icon v-if="showIcon" class="mr-1">
|
||||
<component :is="iconComponent" />
|
||||
</el-icon>
|
||||
{{ statusText }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
status: string;
|
||||
showIcon?: boolean;
|
||||
effect?: 'dark' | 'light' | 'plain';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showIcon: true,
|
||||
effect: 'light'
|
||||
});
|
||||
|
||||
// 状态映射配置
|
||||
const statusConfig = {
|
||||
draft: {
|
||||
text: '草稿',
|
||||
type: 'info',
|
||||
icon: 'Edit'
|
||||
},
|
||||
published: {
|
||||
text: '已发布',
|
||||
type: 'warning',
|
||||
icon: 'Bell'
|
||||
},
|
||||
active: {
|
||||
text: '进行中',
|
||||
type: 'success',
|
||||
icon: 'VideoPlay'
|
||||
},
|
||||
paused: {
|
||||
text: '已暂停',
|
||||
type: 'warning',
|
||||
icon: 'VideoPause'
|
||||
},
|
||||
ended: {
|
||||
text: '已结束',
|
||||
type: 'danger',
|
||||
icon: 'VideoCamera'
|
||||
},
|
||||
archived: {
|
||||
text: '已归档',
|
||||
type: 'info',
|
||||
icon: 'FolderOpened'
|
||||
}
|
||||
};
|
||||
|
||||
const tagType = computed(() => {
|
||||
return statusConfig[props.status as keyof typeof statusConfig]?.type || 'default';
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
return statusConfig[props.status as keyof typeof statusConfig]?.text || props.status;
|
||||
});
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
return statusConfig[props.status as keyof typeof statusConfig]?.icon || 'QuestionFilled';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<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>
|
||||
@@ -0,0 +1,81 @@
|
||||
<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>
|
||||
@@ -0,0 +1,145 @@
|
||||
<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>
|
||||
@@ -0,0 +1,590 @@
|
||||
<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>
|
||||
@@ -0,0 +1,490 @@
|
||||
<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>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,568 @@
|
||||
<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>
|
||||
@@ -0,0 +1,72 @@
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
// 信息采集批次管理路由配置
|
||||
export const collectionBatchRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/xg-xx/collectionBatch',
|
||||
name: 'CollectionBatch',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '信息采集批次管理',
|
||||
icon: 'list',
|
||||
permissions: ['xg:collectionBatch:list']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/xg-xx/collectionBatch/add',
|
||||
name: 'CollectionBatchAdd',
|
||||
component: () => import('./form.vue'),
|
||||
meta: {
|
||||
title: '新增批次',
|
||||
activeMenu: '/xg-xx/collectionBatch',
|
||||
permissions: ['xg:collectionBatch:add']
|
||||
},
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/xg-xx/collectionBatch/edit',
|
||||
name: 'CollectionBatchEdit',
|
||||
component: () => import('./form.vue'),
|
||||
meta: {
|
||||
title: '编辑批次',
|
||||
activeMenu: '/xg-xx/collectionBatch',
|
||||
permissions: ['xg:collectionBatch:edit']
|
||||
},
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/xg-xx/collectionBatch/detail',
|
||||
name: 'CollectionBatchDetail',
|
||||
component: () => import('./detail.vue'),
|
||||
meta: {
|
||||
title: '批次详情',
|
||||
activeMenu: '/xg-xx/collectionBatch',
|
||||
permissions: ['xg:collectionBatch:query']
|
||||
},
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/xg-xx/collectionBatch/statistics',
|
||||
name: 'CollectionBatchStatistics',
|
||||
component: () => import('./statistics.vue'),
|
||||
meta: {
|
||||
title: '批次统计',
|
||||
activeMenu: '/xg-xx/collectionBatch',
|
||||
permissions: ['xg:collectionBatch:statistics']
|
||||
},
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/xg-xx/collectionBatch/permission',
|
||||
name: 'CollectionBatchPermission',
|
||||
component: () => import('./permission.vue'),
|
||||
meta: {
|
||||
title: '权限配置',
|
||||
activeMenu: '/xg-xx/collectionBatch',
|
||||
permissions: ['xg:collectionBatch:permission']
|
||||
},
|
||||
hidden: true
|
||||
}
|
||||
];
|
||||
|
||||
export default collectionBatchRoutes;
|
||||
@@ -0,0 +1,735 @@
|
||||
<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>
|
||||
@@ -0,0 +1,476 @@
|
||||
<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>
|
||||
@@ -0,0 +1,567 @@
|
||||
<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>
|
||||
@@ -0,0 +1,278 @@
|
||||
<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>
|
||||
@@ -0,0 +1,359 @@
|
||||
<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>
|
||||
@@ -0,0 +1,539 @@
|
||||
<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>
|
||||
@@ -0,0 +1,295 @@
|
||||
<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>
|
||||
@@ -0,0 +1,494 @@
|
||||
<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>
|
||||
@@ -0,0 +1,704 @@
|
||||
<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>
|
||||
@@ -377,11 +377,13 @@ import {
|
||||
getStudent,
|
||||
delStudent,
|
||||
exportStudents,
|
||||
getStatisticsOverview,
|
||||
getStatisticsOverview
|
||||
} from '@/api/xg-xx/student'
|
||||
import {
|
||||
getCollegeOptions,
|
||||
getMajorOptions,
|
||||
getClassOptions
|
||||
} from '@/api/xg-xx/student'
|
||||
} from '@/api/xg-xx/statistics'
|
||||
import StudentDetailDialog from './components/StudentDetailDialog'
|
||||
import StudentEditDialog from './components/StudentEditDialog'
|
||||
import AuditDialog from './components/AuditDialog'
|
||||
@@ -518,35 +520,22 @@ export default {
|
||||
},
|
||||
/** 获取统计数据 */
|
||||
getStatistics() {
|
||||
console.log('开始获取统计数据')
|
||||
|
||||
getStatisticsOverview().then(response => {
|
||||
console.log('获取统计数据成功:', response)
|
||||
const data = response.data
|
||||
this.statistics.totalStudents = data.totalCount || 0
|
||||
this.statistics.activeStudents = data.activeCount || 0
|
||||
this.statistics.completeStudents = data.completeCount || 0
|
||||
this.statistics.pendingStudents = data.pendingCount || 0
|
||||
this.statistics = 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)
|
||||
this.$modal.msgError('获取统计数据失败')
|
||||
})
|
||||
},
|
||||
/** 获取学院列表 */
|
||||
getCollegeList() {
|
||||
console.log('开始获取学院列表')
|
||||
|
||||
getCollegeOptions().then(response => {
|
||||
console.log('获取学院列表成功:', response)
|
||||
this.collegeOptions = response.data
|
||||
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
|
||||
@@ -555,20 +544,21 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 学院变化处理 */
|
||||
handleCollegeChange(collegeCode) {
|
||||
handleCollegeChange(collegeId) {
|
||||
console.log('学院变化,清空专业和班级选择:', collegeId)
|
||||
this.queryParams.majorCode = null
|
||||
this.queryParams.classCode = null
|
||||
this.majorOptions = []
|
||||
this.classOptions = []
|
||||
|
||||
if (collegeCode) {
|
||||
console.log('学院变化,获取专业列表:', collegeCode)
|
||||
getMajorOptions(collegeCode).then(response => {
|
||||
if (collegeId) {
|
||||
console.log('学院变化,获取专业列表:', collegeId)
|
||||
// 根据学院ID获取专业列表
|
||||
getMajorOptions(collegeId).then(response => {
|
||||
console.log('获取专业列表成功:', response)
|
||||
this.majorOptions = response.data
|
||||
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
|
||||
@@ -578,18 +568,19 @@ export default {
|
||||
}
|
||||
},
|
||||
/** 专业变化处理 */
|
||||
handleMajorChange(majorCode) {
|
||||
handleMajorChange(majorId) {
|
||||
console.log('专业变化,清空班级选择:', majorId)
|
||||
this.queryParams.classCode = null
|
||||
this.classOptions = []
|
||||
|
||||
if (majorCode) {
|
||||
console.log('专业变化,获取班级列表:', majorCode)
|
||||
getClassOptions(majorCode).then(response => {
|
||||
if (majorId) {
|
||||
console.log('专业变化,获取班级列表:', majorId)
|
||||
// 根据专业ID获取班级列表
|
||||
getClassOptions(majorId).then(response => {
|
||||
console.log('获取班级列表成功:', response)
|
||||
this.classOptions = response.data
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user