first commit
Some checks failed
huaian-screen / build (push) Has been cancelled

This commit is contained in:
wangbin
2025-10-27 23:54:39 +08:00
parent 6e05e87474
commit d2bd0a60a4
209 changed files with 82726 additions and 0 deletions

View File

@ -0,0 +1,35 @@
## 工作流名称
name: huaian-screen
## 工作流触发时机
on: [ push ]
env:
VERSION: $(echo '${{ github.event.head_commit.message }}' | sed -n 's/.*release(\([^)]*\)).*/\1/p')
DOMAIN_NAME: ts-official.tmp.tieshengkeji.cn
FLODER_NAME: huaian
jobs:
## 任务名称
build:
## 任务执行的服务器
runs-on: tiesheng-local
if: ${{ startsWith(github.event.head_commit.message,'release') }}
## 任务步骤
steps:
## 检出代码(固定配置)
- name: Check out repository code
uses: https://git.tieshengkeji.com/actions/checkout@v4
## 下载前端依赖
- name: Install Dependencies
run: npm install
## 前端项目打包并发送到本地
- name: Build Frontend
run: |
npm run build
rm -rf /usr/local/nginx/html/${{env.DOMAIN_NAME}}/static/${{env.FLODER_NAME}}
mv dist "${{env.FLODER_NAME}}"
mv ${{env.FLODER_NAME}} /usr/local/nginx/html/${{env.DOMAIN_NAME}}/static

View File

@ -0,0 +1,83 @@
## 工作流名称
name: yxr-manager
## 工作流触发时机
on:
push:
tags:
- '*' # 匹配所有标签
jobs:
## 任务名称
build:
## 任务执行的服务器
runs-on: tiesheng-local
## 任务步骤
steps:
## 检出代码(固定配置)
- name: Check out repository code
uses: https://git.tieshengkeji.com/actions/checkout@v4
## 下载前端依赖
- name: Install Dependencies
run: npm install
## 获取当前仓库名称和标签名称
- name: Get repository name and tag name
id: repo-info
run: |
echo "::set-output name=repo_name::$(basename ${GITHUB_REPOSITORY})"
echo "::set-output name=tag_name::${GITHUB_REF#refs/tags/}"
## 检查本地该项目文件夹是否存在
- name: Check if local directory exists
id: check-local-directory
run: |
if [ -d "/usr/local/nginx/html/dist.tmp.tieshengkeji.cn/${{ steps.repo-info.outputs.repo_name }}" ]; then
echo "Directory exists, skipping creation."
echo "::set-output name=directory_exists::true"
else
echo "Directory does not exist, will create."
echo "::set-output name=directory_exists::false"
fi
## 不存在则创建项目文件夹
- name: Create Project Directory
if: ${{ steps.check-local-directory.outputs.directory_exists != 'true' }}
run: |
mkdir /usr/local/nginx/html/dist.tmp.tieshengkeji.cn/${{ steps.repo-info.outputs.repo_name }}
## 前端项目打包并发送到本地
- name: Build Frontend
run: |
npm run build
mv dist "${{ steps.repo-info.outputs.tag_name }}"
zip -r ${{ steps.repo-info.outputs.repo_name }}${{ steps.repo-info.outputs.tag_name }}.zip ${{ steps.repo-info.outputs.tag_name }}
mv ${{ steps.repo-info.outputs.repo_name }}${{ steps.repo-info.outputs.tag_name }}.zip /usr/local/nginx/html/dist.tmp.tieshengkeji.cn/${{ steps.repo-info.outputs.repo_name }}
##获取最后一条提交消息
- name: Get commit title
id: get_commit_title
run: |
COMMIT_TITLE=$(echo "${{ github.event.head_commit.message }}" | head -n 1)
echo "::set-output name=commit_title::$COMMIT_TITLE"
## 发送钉钉消息
- name: Send DingDing notification using curl
run: |
curl -s -o /dev/null -w "%{http_code}" \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "text",
"at": {
"atMobiles":[
"18175194630",
],
"isAtAll": false
},
"text": {
"content": "@18175194630 项目【${{ steps.repo-info.outputs.repo_name }}】打包完成,版本号${{ steps.repo-info.outputs.tag_name }}。\n 操作人:${{ github.actor }} \n 内容:${{ steps.get_commit_title.outputs.commit_title }} \n https://dist.tmp.tieshengkeji.cn/${{ steps.repo-info.outputs.repo_name }}/${{ steps.repo-info.outputs.repo_name }}${{ steps.repo-info.outputs.tag_name }}.zip"
},
}' \
'https://oapi.dingtalk.com/robot/send?access_token=${{vars.DINGTALK_ACCESS_TOKEN}}'

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/dist
.swc
.DS_Store

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
registry=https://registry.npmmirror.com/

56
.umirc.ts Normal file
View File

@ -0,0 +1,56 @@
import { defineConfig } from "umi";
export default defineConfig({
routes: [
{ path: "/", redirect:'/login' },
{ path: "/login", component: "@/pages/login" },
{ path: "/home", component: "@/pages/home",
routes: [
// { path: '/home', redirect: '/home/statistics' },
{ path: "/home/statistics", component: "@/pages/statistics" },
{ path: "/home/analysis", component: "@/pages/analysis" },
{ path: "/home/report", component: "@/pages/report" },
{ path: "/home/report/online", component: "@/pages/report/online" },
{ path: "/home/report/student", component: "@/pages/report/student" },
{ path: "/home/report/examDistribution", component: "@/pages/report/examDistribution" },
{ path: "/home/report/school", component: "@/pages/report/school" },
{ path: "/home/report/teacher", component: "@/pages/report/teacher" },
{ path: "/home/report/question", component: "@/pages/report/question" },
{ path: "/home/report/quality", component: "@/pages/report/subpage/quality" },
{ path: "/home/report/enowledgepoint", component: "@/pages/report/enowledgepoint" },
{ path: "/home/examination/comparison", component: "@/pages/examination-comparison",redirect:'/home/ranking-comparison'},
{ path: "/home/ranking/comparison", component: "@/pages/examination-comparison/subpage/ranking-comparison" },
{ path: "/home/percentage/comparison", component: "@/pages/examination-comparison/subpage/percentage-comparison" },
{ path: "/home/params/comparison", component: "@/pages/examination-comparison/subpage/params-comparison" },
{ path: "/home/knowledge/comparison", component: "@/pages/examination-comparison/subpage/knowledge-comparison" },
{ path: "/home/online/comparison", component: "@/pages/examination-comparison/subpage/online-comparison" },
{ path: "/home/template", component: "@/pages/template" },
{ path: "/home/test", component: "@/pages/test" },
// { path: "/home/backup", component: "@/pages/backup" },
{ path: "/home/system/data", component: "@/pages/export-data" },
{ path: "/home/system/account", component: "@/pages/admin-account" },
{ path: "/home/system/account/admin", component: "@/pages/admin-account/subpage/account" },
{ path: "/home/system/account/access", component: "@/pages/admin-account/subpage/access" },
{ path: "/home/system/setup", component: "@/pages/setup"},
{ path: "/home/system/setup/subject", component: "@/pages/setup/subpage/subject" },
{ path: "/home/system/setup/district", component: "@/pages/setup/subpage/district" },
{ path: "/home/system/setup/school", component: "@/pages/setup/subpage/school" },
]
},
],
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
extraBabelPlugins: process.env.NODE_ENV === 'production'
? ['babel-plugin-dynamic-import-node']
: [],
history: { type: 'hash' },
hash: true,
title: false,
npmClient: 'pnpm',
proxy: {
'/api': {
target:"https://studies.hmbigdata.com",
changeOrigin: true,
pathRewrite: { '^/api': '/hly-huaian' },
},
},
});

17669
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"private": true,
"author": "赵晨 <chenson918@163.com>",
"scripts": {
"dev": "umi dev",
"build": "umi build",
"postinstall": "umi setup",
"setup": "umi setup",
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@ant-design/pro-components": "^2.8.6",
"@umijs/plugins": "^4.4.6",
"ahooks": "^3.8.4",
"antd": "^5.24.3",
"axios": "^1.8.3",
"babel-plugin-dynamic-import-node": "^2.3.3",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"moment": "^2.30.1",
"umi": "^4.4.6",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"typescript": "^5.0.3"
}
}

12701
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

20
src/apis/auth.js Normal file
View File

@ -0,0 +1,20 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config'
export const userLogin = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/auth/login`,
data,
});
};
export const logger = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/auth/savePathLog`,
params,
});
};

185
src/apis/common.js Normal file
View File

@ -0,0 +1,185 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config'
export const getSubjectList = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getSubjectList`,
params:{
type:0,
stage:3
}
});
};
export const getSchoolAreaList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getDistrictListNew`,
params:{
...params,
stage:3
}
});
};
export const getCurrentSelected = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/queryNewExam`,
params:{
stage:3
}
});
};
export const getExam = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/base/queryExam`,
data:{
...data,
stage:3
}
});
};
export const getSubjectListAll = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getSubjectListALL`,
params:{
stage:3
}
});
}
export const getGradeEnum = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getTermList`,
params:{
stage:3
}
});
}
export const getSemesterEnum = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getSemesterList`,
params:{
stage:3
}
});
}
export const getExamEnum = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getExamNameList`,
params:{
stage:3
}
});
}
export const getNoSingleSubject = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getSubjectListNoSingle`,
params:{
stage:3
}
});
}
export const getSort = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getSortItemList`,
params:{
stage:3,
...params,
}
});
}
export const getPercent = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getRateItemList`,
params:{
stage:3
}
});
}
export const getParamsType = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getParamTypeItemList`,
params:{
stage:3
}
});
}
export const getNoSingleCombine = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getSubjectListNoSingleCombine`,
params:{
stage:3
}
});
}
export const getSubjectListCombineList =() => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getSubjectListCombine`,
params:{
stage:3
}
});
}
export const getDataExplain =(params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getExamDescription`,
params:{
...params,
stage:3
}
});
}
// 获取学段
export const getStageList =(params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/common/getStageWithRole`,
params,
});
}
// 获取学科列表
export const getSubjectListSelect =(params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/common/getSubjectList`,
params,
});
}

128
src/apis/data.js Normal file
View File

@ -0,0 +1,128 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config';
//获取考试列表
export const getExamList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/common/getAllExams`,
params,
});
};
//创建考试
export const addExamList = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/upload_v2/createExam`,
data,
});
};
//获取考试
export const getExam = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/common/getExam`,
params
});
};
//修改考试
export const updateExam = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/upload_v2/updateExam`,
data,
});
};
//获取考试计算文件名
export const getExamFileName = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/upload_v2/getExamFile`,
params,
});
};
//上传考试计算文件
export const uploadFile = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/upload_v2/createSubjectDetail`,
headers: { 'Content-Type': 'multipart/form-data' },
data,
});
};
// 开始计算考试
export const startComputeExam = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/cal_v2/calExam`,
params,
});
}
// 重新计算考试
export const afreshComputeExam = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/upload_v2/reCalExam`,
params,
});
}
// 删除考试
export const deleteExam = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/cal_v2/delExam`,
params,
});
}
// 获取编辑描述
export const getExamDescription = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/base/getExamDescription`,
params,
});
}
// 获取分数线列表
export const getScoreLineList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/score/page`,
params,
});
}
// 保存分数线
export const saveScoreLine = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/score/edit`,
data,
});
}
// 删除分数线
export const deleteScoreLine = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/score/delete`,
data,
});
}

27
src/apis/district.js Normal file
View File

@ -0,0 +1,27 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config';
export const getDistrictList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/system/getDistrictListNew`,
params,
});
};
export const saveDistrict = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/system/saveDistrictNew`,
data,
});
};
export const getDistrictDetail = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/system/getDistrictById`,
params,
});
};

17
src/apis/download.js Normal file
View File

@ -0,0 +1,17 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config'
export const downloadTableData = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/download/downLoadExcel`,
responseType:"blob",
data:{
...data,
stage:3
}
});
};

93
src/apis/exam.js Normal file
View File

@ -0,0 +1,93 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config'
export const getExamRanking = (data) =>{
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamsCompare`,
data:{
...data,
stage:3
}
});
}
export const getPercentageRanking = (data) =>{
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamsCompareNumRate`,
data:{
...data,
stage:3
}
});
}
export const getPercentageRankingView = (data) =>{
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamsCompareNumRateView`,
data:{
...data,
stage:3
}
});
}
export const getParamsRanking = (data) =>{
return request({
method: 'post',
url: `${BASE_URL}/compare/params/analysis`,
data:{
...data,
stage:3
}
});
}
export const getKnowledgeRanking = (data) =>{
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamsSubjectKnowledge`,
data:{
...data,
stage:3
}
});
}
export const getKnowledgeRankingAnalysis = (data) =>{
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectQuestionDifficultByKnowLedgeV2`,
data:{
...data,
stage:3
}
});
}
export const getOnlineRankingAnalysis = (data) =>{
return request({
method: 'post',
url: `${BASE_URL}/compare/score-line/analysis`,
data:{
...data,
stage:3
}
});
}
export const getOnlineAnalysis = (data) =>{
return request({
method: 'post',
url: `${BASE_URL}/compare/online/analysis`,
data:{
...data,
stage:3
}
});
}

356
src/apis/leader.js Normal file
View File

@ -0,0 +1,356 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config'
export const getOnlineAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/compare/online/analysis`,
data:{
...data,
stage:3
}
});
};
export const getSchoolAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamSortAnalysisDistrict`,
data:{
...data,
stage:3
}
});
};
export const getSchoolPercentAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamLimitRateAnalysisDistrict`,
data:{
...data,
stage:3
}
});
};
export const getTopStudentAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamAnalysisStudent`,
data:{
...data,
stage:3
}
});
}
export const getCriticalStudentAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamAnalysisStudentLj`,
data:{
...data,
stage:3
}
});
}
export const getBadSubjectStudentAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamAnalysisStudentYslk`,
data:{
...data,
stage:3
}
});
}
export const getRashStudentAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamAnalysisStudentMjs`,
data:{
...data,
stage:3
}
});
}
export const getTeacherAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamTeacherSub`,
data:{
...data,
stage:3
}
});
}
export const getTeacherZScoreAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamTeacherZscore`,
data:{
...data,
stage:3
}
});
}
export const getTeacherTrace = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamsTeacher`,
data:{
...data,
stage:3
}
});
}
export const getTeacherGrade = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamsTeacherGrade`,
data:{
...data,
stage:3
}
});
}
export const getKnowledgePoint = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectKnowledge`,
data:{
...data,
stage:3
}
});
}
export const getQuestionParams = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectQuestion`,
data:{
...data,
stage:3
}
});
}
export const getAnswerRate = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectQuestionAnswer`,
data:{
...data,
stage:3
}
});
}
export const getLittleQuestion = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectQuestionDifficult`,
data:{
...data,
stage:3
}
});
}
//科目分布
export const getSubjectDistribution = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectDistribution`,
data:{
...data,
stage:3
}
});
}
//组合分布
export const getCombinationDistribution = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectCombinationDistribution`,
data:{
...data,
stage:3
}
});
}
//选考分析
export const getSelectedDistribution = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectCombinationAvg`,
data:{
...data,
stage:3
}
});
}
// 赋分分析
export const getAssignDistribution = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamSubjectElectiveLevel`,
data:{
...data,
stage:3
}
});
}
//获取学生群体
export const getStudentGroup = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamAnalysisStudentList`,
data:{
...data,
stage:3
}
});
};
//获取学生学情追踪
export const getStudentHistory = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamAnalysisStudentHistory`,
data:{
...data,
stage:3
}
});
};
//获取学生成绩册
export const getStudentBook = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamStudentSub`,
data:{
...data,
stage:3
}
});
};
//获取学生各小题
export const getStudentTopic = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamStudentQuestion`,
data:{
...data,
stage:3
}
});
};
//获取学生知识点
export const getStudentKnowledge = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamStudentQuestionGroup`,
data:{
...data,
stage:3
}
});
};
//教师群体排名分析
export const getTeacherSort = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamTeacherSort`,
data:{
...data,
stage:3
}
});
};
//教师群体均分分析
export const getTeacherAvg = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamTeacherAvg`,
data:{
...data,
stage:3
}
});
};
//当前情况基本参数分析
export const getSchoolParams = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamAnalysisDistrict`,
data:{
...data,
stage:3
}
});
};
//教师个人的Z值分析
export const getSingleTeacherZ = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/analysisExamTeacherSingleZscore`,
data:{
...data,
stage:3
}
});
};
// 上线分数线
export const getOnlineScoreAnalysis = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/leader/nowExamSortAnalysisDistrictWithScore`,
data:{
...data,
stage:3
}
});
};

148
src/apis/report.js Normal file
View File

@ -0,0 +1,148 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config';
export const getReportTemplateList = (params)=> {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/getSystemReportList`,
params:{
...params,
stage:3
}
});
};
export const saveReportTemplate = (data)=> {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/report/saveOrUpdateReportSystem`,
data:{
...data,
stage:3
}
});
};
export const deleteReportTemplate = (params)=> {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/delSysReport`,
params:{
...params,
stage:3
}
});
};
export const setupStandardTemplate = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/setDefaultReport`,
params:{
...params,
stage:3
}
});
}
export const getTemplateComponentList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/getReportItemList`,
params:{
...params,
stage:3
}
});
};
export const saveTemplateComponent = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/report/saveOrUpdateReportItem`,
data:{
...data,
stage:3
}
});
}
export const deleteComponent = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/delReportItem`,
params:{
...params,
stage:3
}
});
}
export const getSystemComponentList = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/getReportItemModelList`,
params:{
stage:3
}
});
}
/**
* @function 获取个人报告
*/
export const getPersonalReport = () => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/getAllReportList`,
params:{
stage:3
}
});
}
/**
* @function 复制模板
*/
export const copyTemplate = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/copyReport`,
params:{
...params,
stage:3
}
});
}
/**
* @function 获取模板详情
*/
export const getTemplateDetail = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/report/getReport`,
params:{
...params,
stage:3
}
});
}

48
src/apis/role.js Normal file
View File

@ -0,0 +1,48 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config';
export const getRoleList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/role/getRoleList`,
params,
});
};
export const getRoleSelect = () => {
return request({
method: 'post',
url: `${BASE_URL}/role/getAllRoleList`
});
}
// 删除角色
export const deleteRole = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/role/deleteRole`,
params,
});
}
// 保存角色
export const saveRole = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/role/saveRole`,
data,
});
}
// 获取权限树
export const getRoleTree = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/role/getRoleInfo`,
params,
});
}

9
src/apis/rqg.js Normal file
View File

@ -0,0 +1,9 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config'
export const getTeacherRateSort = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/rqg/dfl/sort`,
data,
});
};

38
src/apis/school.js Normal file
View File

@ -0,0 +1,38 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config';
export const getSchoolList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/system/getSchoolList`,
params,
});
};
export const saveSchool = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/system/saveSchool`,
data,
});
};
export const getSchoolDetail = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/system/getSchoolById`,
params
});
};
export const getSchoolDistrictList = (params)=> {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/system/getDistrictListOptionNew`,
params
});
}

29
src/apis/subject.js Normal file
View File

@ -0,0 +1,29 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config';
export const getSubjectList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/system/getSubjectList`,
params,
});
};
export const saveSubject = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/exam-data/system/saveSubject`,
data,
});
};
export const getSubjectDetail = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/exam-data/system/getSubjectDetail`,
params
});
};

87
src/apis/user.js Normal file
View File

@ -0,0 +1,87 @@
import request from "@/utils/request";
import {BASE_URL} from '@/config'
export const getUserInfo = () => {
return request({
method: 'get',
url: `${BASE_URL}/user/getUserInfo`,
params:{
stage:3
}
});
}
export const updatePassword = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/user/resetMyPassword`,
data:{
...data,
stage:3
}
});
}
export const getUserList = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/user/getUserList`,
params,
});
}
//重置密码
export const resetPassword = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/user/resetPassword`,
params,
});
}
//删除用户
export const deleteUser = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/user/deleteUser`,
params,
});
}
//保存用户
export const saveUser = (data) => {
return request({
method: 'post',
url: `${BASE_URL}/user/saveUser`,
data,
});
}
//获取区县列表下拉框
export const getDistrictSelect = () => {
return request({
method: 'get',
url: `${BASE_URL}/user/getBaseDistrict`,
});
}
//获取学校列表下拉框
export const getSchoolSelect = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/user/getBaseSchool`,
params,
});
}
export const getUserDetail = (params) => {
return request({
method: 'get',
url: `${BASE_URL}/user/getUser`,
params,
});
};

BIN
src/assets/file.xls Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

BIN
src/assets/icon/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 KiB

BIN
src/assets/image/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/image/down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src/assets/image/swipe1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
src/assets/image/swipe2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
src/assets/image/swipe3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
src/assets/image/swipe4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
src/assets/image/up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

View File

@ -0,0 +1,493 @@
import { useEffect, useRef, useState, useImperativeHandle, forwardRef,memo } from "react";
import { Button, Modal, Image, Space, Empty,Tag } from "antd";
import Flex from "@/components/Flex";
import * as echarts from "echarts";
import "./index.less";
let chartInstance = null;
const Echart = ({
data = {},
type = "line",
hasExport = true,
hasTable = true,
limitNum = 0,
lengButton = [],
isShow = true,
onTableClick = () => { },
text='',
isNoneTag = false,
isShowTag = true,
}, ref) => {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null); // 新增用于存储图表实例的 ref
const [selectButton, setSelectButton] = useState(lengButton);
const [activeButtons, setActiveButtons] = useState([]); // 添加新的状态来跟踪选中的按钮
const [dataCopy, setDataCopy] = useState(); // 添加新的状态来跟踪选中的按钮
const [option, setOption] = useState({}); //用于渲染图表的
const [echartImg, setEchartImg] = useState(); //用于导出图表 图片的
useImperativeHandle(ref, () => ({
exportChart,
}));
useEffect(() => {
if (!lengButton?.length) {
return;
}
setSelectButton(lengButton);
if(isNoneTag){
setActiveButtons(lengButton);
}else{
setActiveButtons([lengButton[0]]);
}
}, [lengButton,isNoneTag]);
//控制筛选按钮
const toggleButton = (data) => {
setActiveButtons((prev) => {
if (prev.includes(data)) {
// 如果只剩一个按钮,不允许取消选中
if (prev.length === 1) {
return prev;
}
return prev.filter((i) => i !== data);
} else {
// 检查是否达到最大限制
if (limitNum > 0 && prev.length >= limitNum) {
// 如果已达到限制,返回原数组
return prev;
}
// 未达到限制,添加新按钮
return [...prev, data];
}
});
};
// 处理二维折线和柱状图和饼图
const createOption = () => {
let option = {};
const colors = [
"#91cc75",
"#fac858",
"#ee6666",
"#73c0de",
"#3ba272",
"#fc8452",
"#9a60b4",
];
if (type === "radar") {
option = {
color: colors,
title: {
// 添加标题配置
text,
left: 'center',
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 'bold',
},
},
legend: {
type: "scroll",
data: dataCopy?.radarText.yChartData?.map((item) => item.name),
right: 10,
top: 16,
selectedMode: false,
},
tooltip: {
trigger: "item",
},
radar: {
indicator: dataCopy.radarText.indicator,
shape: "polygon",
splitNumber: 5,
axisName: {
color: "#666",
fontSize: 10,
},
splitLine: {
lineStyle: {
color: "rgba(238, 238, 238, 0.8)",
opacity: 0.2
},
},
splitArea: {
show: false, // 关闭区域填充
},
},
series: [
{
type: "radar",
data: dataCopy?.radarText?.yChartData?.map((item, index) => ({
name: item.name,
value: item.data,
itemStyle: {
color: colors[index % colors.length],
},
lineStyle: {
width: 2,
color: colors[index % colors.length], // 设置线条颜色
},
// 移除 areaStyle 配置
emphasis: {
lineStyle: {
width: 4,
},
},
})),
},
],
};
} else if (type == "pie") {
// 计算总数
const totalSum = dataCopy?.yChartData?.reduce((sum, item) => {
return sum + item.data.reduce((a, b) => a + b, 0);
}, 0);
option = {
title: {
// 添加标题配置
text,
left: 'center',
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 'bold',
},
},
legend: {
type: "scroll",
orient: "vertical",
// 简化图例数据处理
data:
dataCopy?.yChartData?.map((item) => ({
name: item.name,
icon: "circle", // 设置图例图标为圆形
})) || [],
right: "18%",
top: 16,
selectedMode: true,
textStyle: {
color: "#333",
fontSize: 12,
},
formatter: (name) => {
const data = dataCopy?.yChartData?.find(
(item) => item.name === name
);
if (!data) return name;
return `${name}${data?.data?.[0]}%`;
},
},
tooltip: {
trigger: "item",
formatter: "{b}:{c}%",
},
series: [
{
radius: ["40%", "70%"],
type: type,
label: {
show: true,
formatter: (name) => {
return `${name?.data?.name}${name?.data?.value}%`
},
},
data:
dataCopy?.yChartData?.map((key) => {
return {
name: key.name,
value: key?.data?.[0],
};
}) || [],
},
],
};
} else {
option = {
//图例 不用 纯展示
legend: {
type: "scroll",
data: dataCopy?.yChartData?.map((item) => item.name),
right: 10, // 设置右侧边距
top: 24, // 设置顶部边距
selectedMode: false,
textStyle:{
fontSize:12
}
},
title: {
// 添加标题配置
text,
left: 'center',
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 'bold',
},
},
label:{
show: true,
position: 'top',
distance: 12,
align: 'center',
verticalAlign: 'middle',
formatter: (params) => {
return `${params.value}`
},
fontSize: 10,
rich: {
name: {}
}
},
// dataZoom: [
// {
// type: 'slider', // 滑动条型数据区域缩放组件
// show: true,
// start: 0, // 初始起点
// end: 30, // 初始终点
// handleSize: 8,
// xAxisIndex: [0],
// height: 24
// },
// ],
axisLabel: {
rotate: 35,
interval: 0,
fontSize: 9,
autoSkip: true,
},
barGap: '16%',
barWidth: "8%",
// 布局
grid: {
left: "16px", // 左边距
right: "4%", // 右边距
bottom: "48px", // 底部边距,留出空间显示 x 轴标签
top: "88px", // 顶部边距
containLabel: true, // 确保刻度标签在区域内
},
tooltip: {
show: true,
},
...dataCopy,
xAxis: {
...dataCopy?.xAxis,
axisLabel: {
rotate: dataCopy?.xAxis?.data?.length > 6 ? 45 : 0,
interval: 0,
textStyle: {
fontSize: 12,
}
},
nameTextStyle: {
fontSize: 16,
}
},
yAxis:{
...dataCopy?.yAxis,
axisLabel: {
textStyle: {
fontSize: 14,
}
},
nameTextStyle: {
fontSize:14,
}
},
series: dataCopy?.yChartData?.map((item) => ({
type,
// barGap: 0,
barWidth: '8%', // 控制柱子的宽度
barGap: '10%' , // 控制柱子之间的间距
emphasis: {
focus: "series",
},
name: item.name,
data: item.data
})),
};
}
setOption(option);
};
//下载图片
const downloadImg = () => {
const link = document.createElement("a");
link.download = `图表_${new Date().getTime()}.png`;
link.href = echartImg;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setEchartImg("");
};
// 导出图片
const exportChart = () => {
const chartInstance = echarts.getInstanceByDom(chartRef.current);
if (chartInstance) {
const base64 = chartInstance.getDataURL({
type: "png",
pixelRatio: 2, // 导出的图片分辨率比例
backgroundColor: "#fff", // 导出图片的背景色
});
setEchartImg(base64);
}
};
useEffect(() => {
if (dataCopy) {
createOption();
}
}, [dataCopy, activeButtons]);
// useEffect(() => {
// setSelectButton(lengButton);
// }, [lengButton]);
useEffect(() => {
if (!activeButtons.length > 0) {
return;
}
// 根据选中的按钮筛选数据
let filteredData = [];
if (type === "radar") {
filteredData = data?.radarText?.yChartData?.filter((item) =>
activeButtons.includes(item.name)
);
setDataCopy({
...data,
radarText: {
...data.radarText,
yChartData: filteredData,
},
});
} else {
if(type === 'pie'){
setDataCopy({ ...data});
return;
}
filteredData = data?.yChartData?.filter((item) =>
activeButtons.includes(item.name)
);
setDataCopy({ ...data, yChartData: filteredData });
}
}, [activeButtons, data]);
//图表初始化
useEffect(() => {
// 如果 DOM 元素存在,初始化图表
if (chartRef.current) {
// 如果已有实例,先销毁
if (chartInstanceRef.current) {
chartInstanceRef.current.dispose();
}
// 创建新实例
chartInstanceRef.current = echarts.init(chartRef.current);
if (Object.values(option)?.length) {
chartInstanceRef.current?.setOption(option);
}
}
// 响应式调整
const resizeHandler = () => chartInstanceRef.current?.resize();
window.addEventListener('resize', resizeHandler);
// 组件卸载时清理
return () => {
if (chartInstanceRef.current) {
window.removeEventListener('resize', resizeHandler);
chartInstanceRef.current.dispose();
chartInstanceRef.current = null;
}
};
}, [option]);
return (
<div style={{ marginBottom: 20 }}>
{isShowTag ? (
<Space style={{marginBottom: 16}} wrap>
{!!Array.isArray(selectButton) &&
isShow &&
!!selectButton.length > 0 &&
selectButton.map((but, index) => {
return (
<Tag
key={index}
color={activeButtons.includes(but) ? "green" : "default"}
onClick={() => toggleButton(but)}
style={{cursor: 'pointer'}}
>
{but}
</Tag>
);
})}
</Space>
) : null}
{/* 图表 */}
{lengButton?.length > 0 ? (<div ref={chartRef} style={{ width: "100%", height: "446px" }} />) : <Empty description='暂无图表数据' />}
{/* 导出和显示为表格 */}
<div>
{hasExport && (
<Button
type="primary"
style={{ marginRight: 20 }}
disabled={!lengButton?.length}
onClick={exportChart}
>
导出图表
</Button>
)}
{hasTable && (
<Button
type="primary"
onClick={() => {
//关闭图表
onTableClick(false);
}}
>
显示为表格
</Button>
)}
</div>
{/* 导出为图片 */}
{echartImg && (
<Modal
title="导出图表"
className="custom-modal"
width={"80vw"}
open={true}
okText="下载"
onCancel={() => {
setEchartImg("");
}}
onOk={downloadImg}
>
<Flex style={{ height: "100%" }} alignItems="center" justify="center">
<Image src={echartImg} width={"100%"} />
</Flex>
</Modal>
)}
</div>
);
};
export default memo(forwardRef(Echart));

View File

@ -0,0 +1,6 @@
.custom-modal {
.ant-modal-body {
height: 70vh;
overflow: auto;
}
}

21
src/components/Flex/index.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type {CSSProperties} from 'react'
export interface IFlexProps {
direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
wrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
justify?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around';
alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch';
itemSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch';
itemOrder?: number;
itemGrow?: number;
style?: CSSProperties;
className?: string;
onClick?: any,
}
export default class Flex extends React.Component<IFlexProps, any> {
}

View File

@ -0,0 +1,60 @@
import React, {memo} from "react";
import styles from './index.less';
const Index = memo((props) => {
const {
direction, wrap, justify, alignItems,
itemOrder, itemSelf, itemGrow, onClick,
className, style, ...other
} = props;
let newStyle = {};
if (direction) {
newStyle['flexDirection'] = direction;
newStyle['WebkitFlexDirection'] = direction;
}
if (wrap) {
newStyle['flexWrap'] = wrap;
newStyle['WebkitFlexWrap'] = wrap;
}
if (justify) {
newStyle['justifyContent'] = justify;
newStyle['WebkitJustifyContent'] = justify;
}
if (alignItems) {
newStyle['alignItems'] = alignItems;
newStyle['WebkitAlignItems'] = alignItems;
}
if (itemSelf) {
newStyle['alignSelf'] = itemSelf;
newStyle['WebkitAlignSelf'] = itemSelf;
}
if (itemOrder) {
newStyle['order'] = itemOrder;
}
if (itemGrow) {
newStyle['flexGrow'] = itemGrow;
newStyle['WebkitFlexGrow'] = itemGrow;
}
let clazz = styles.flex;
if (className) {
clazz = clazz + ' ' + className;
}
return <div
{...other} className={clazz} style={{...newStyle, ...style}} onClick={onClick}
>{props.children}</div>
});
const Flex = (props) => {
return <Index {...props} />
};
export default Flex;

View File

@ -0,0 +1,60 @@
import React, {memo} from "react";
import styles from './index.less';
const Index = memo((props) => {
const {
direction, wrap, justify, alignItems,
itemOrder, itemSelf, itemGrow, onClick,
className, style, ...other
} = props;
let newStyle = {};
if (direction) {
newStyle['flexDirection'] = direction;
newStyle['WebkitFlexDirection'] = direction;
}
if (wrap) {
newStyle['flexWrap'] = wrap;
newStyle['WebkitFlexWrap'] = wrap;
}
if (justify) {
newStyle['justifyContent'] = justify;
newStyle['WebkitJustifyContent'] = justify;
}
if (alignItems) {
newStyle['alignItems'] = alignItems;
newStyle['WebkitAlignItems'] = alignItems;
}
if (itemSelf) {
newStyle['alignSelf'] = itemSelf;
newStyle['WebkitAlignSelf'] = itemSelf;
}
if (itemOrder) {
newStyle['order'] = itemOrder;
}
if (itemGrow) {
newStyle['flexGrow'] = itemGrow;
newStyle['WebkitFlexGrow'] = itemGrow;
}
let clazz = styles.flex;
if (className) {
clazz = clazz + ' ' + className;
}
return <div
{...other} className={clazz} style={{...newStyle, ...style}} onClick={onClick}
>{props.children}</div>
});
const Flex = (props) => {
return <Index {...props} />
};
export default Flex;

View File

@ -0,0 +1,15 @@
.flex {
display: -webkit-flex; /* Safari */
display: flex;
position: relative;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
word-break: break-all;
white-space: normal;
flex-shrink: 0;
-webkit-flex-shrink: 0;
}

View File

@ -0,0 +1,141 @@
import { useState, useEffect } from 'react';
import { Select, Space, Button, Modal } from 'antd';
import { getSchoolAreaList } from '@/apis/common'
import styles from './index.less';
/**
*
* @param {Function} onChange
*/
const SchoolChoice = (props) => {
const { onChange = () => { } } = props
const typeColorEnum = {
"Ⅰ类": "#722ED1",
"Ⅱ类": "#FADB14",
}
const [schoolTypeList, setSchoolTypeList] = useState([]);
const [schoolList, setSchoolList] = useState([]);
const [value, setValue] = useState(null);
const [label, setLabel] = useState(null);
const [open, setOpen] = useState(false);
const [selectedSchoolList, setSelectedSchoolList] = useState([]);
const [selectedSchoolTypeList, setSelectedSchoolTypeList] = useState([]);
useEffect(() => {
init();
}, []);
const init = async () => {
try {
const res = await getSchoolAreaList({ type: 1 });
if (res?.code === 200) {
setSchoolTypeList(res?.data || []);
setLabel(res?.data?.[0]?.districtName);
onChange(res?.data?.[0]?.schoolList?.map(item => item?.id));
}
const result = await getSchoolAreaList({ type: 0 });
if (result?.code === 200) {
setSchoolList(result?.data || []);
}
} catch (error) {
console.log(error, 'init-error');
}
};
const handleOk = () => {
setOpen(false);
const ids = Array.from(new Set([...selectedSchoolTypeList, ...selectedSchoolList]));
onChange(ids);
};
return (
<div className={styles.school_select}>
<Space>
<Select
fieldNames={{
label: 'districtName',
value: 'id'
}}
value={label}
style={{ width: 120 }}
allowClear
options={schoolTypeList}
placeholder='请选择学校'
onChange={(value, options) => {
if (!value) {
setSelectedSchoolTypeList([]);
setLabel(null);
setValue(null);
return;
}
setLabel(options?.districtName);
const ids = Array.from(new Set([...options?.schoolList?.map(item => item?.id), ...selectedSchoolList]));
onChange(ids);
setSelectedSchoolTypeList(options?.schoolList?.map(item => item?.id));
}} />
<Button type="default" onClick={() => setOpen(true)}>自选学校范围</Button>
</Space>
<Modal
open={open}
maskClosable={false}
centered
title='选择学校'
width={800}
styles={{ body: { height: "60vh", overflowY: "scroll" } }}
onCancel={() => {
setOpen(false);
}}
onOk={handleOk}
>
{schoolList?.length > 0 ? schoolList?.map((item) => {
return (
<section className={styles.area} key={item?.id}>
<section className={styles.area_title}>
{item?.districtName}
</section>
<section className={styles.area_list}>
{item?.schoolList?.length > 0 ? item?.schoolList?.map((element) => {
const isSelected = selectedSchoolList.includes(element?.id);
return (
<section style={{ backgroundColor: isSelected ? "#4bbb6b" : "#fff", color: isSelected ? "#fff" : "#333" }} className={styles.area_list_item} key={element?.id} onClick={() => {
setSelectedSchoolList(prev => {
if (isSelected) {
return prev.filter(item => item !== element?.id)
} else {
return [...prev, element?.id]
}
})
}}>
<span className={styles.area_list_item_name}>{element?.cname}&nbsp;</span>
<span className={styles.area_list_item_type} style={{ backgroundColor: typeColorEnum?.[element?.schoolType] }}>{element?.schoolType}</span>
</section>
)
}) : null}
</section>
</section>
)
}) : null}
</Modal>
</div>
)
};
export default SchoolChoice;

View File

@ -0,0 +1,41 @@
.school {
&_select {
box-sizing: border-box;
padding: 16px;
}
}
.area {
margin-top: 8px;
&_title {
border-left: 3px solid #4bbb6b;
box-sizing: border-box;
padding-left: 8px;
font-size: 16px;
color: #333;
}
&_list {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
&_item {
box-sizing: border-box;
padding: 4px;
border-radius: 4px;
margin-left: 4px;
margin-top: 4px;
cursor: pointer;
display:flex;
align-items: center;
&_type {
padding: 2px 6px;
color: #fff;
border-radius: 2px;
font-size: 9px;
}
}
}
}

View File

@ -0,0 +1,59 @@
import { useState} from 'react';
import { Space, Button, InputNumber, Tag, message } from 'antd';
import { CloseOutlined } from '@ant-design/icons'
import styles from './index.less';
const ScoreLineSelect = (props) => {
const { onChange = () => { },styles={padding: 8,} } = props;
const [score, setScore] = useState(null);
const [scoreList, setScoreList] = useState([]);
const handleClose = (item) => {
const newList = scoreList.filter(i => i.id !== item.id);
setScoreList(newList);
onChange(newList.map(item => item.id));
};
return (
<div className={styles.score_line_select} style={styles}>
设置分数线:&nbsp;&nbsp;
<InputNumber
min={1}
value={score}
onChange={(value) => setScore(value)}
style={{ width: 120 }}
/>
&nbsp;&nbsp;分以上&nbsp;&nbsp;
<Button onClick={() => {
if (typeof score !== 'number') {
message.error('请输入数字');
return;
}
if (scoreList.findIndex(item => item.id === score) !== -1) {
message.error('该分数已存在');
return;
}
setScore(null);
const newItem = {
id: score,
label: `${score}分以上`
};
const newList = [...scoreList, newItem];
setScoreList(newList);
onChange(newList.map(item => item.id));
}}>
确定
</Button>
<Space wrap style={{ width: '100%', marginTop: 8 }}>
{scoreList?.map((item, index) => (
<Tag key={index} style={{ marginTop: 8 }}>
{item?.label} &nbsp; <CloseOutlined style={{ cursor: 'pointer' }} onClick={() => handleClose(item)} />
</Tag>
))}
</Space>
</div>
)
};
export default ScoreLineSelect;

View File

@ -0,0 +1,10 @@
.score{
&_line{
&_select{
width: 100%;
box-sizing: border-box;
padding: 8px;
}
}
}

View File

@ -0,0 +1,648 @@
import Flex from "@/components/Flex";
import less from "./index.less";
import { useEffect, useState } from "react";
import { Modal, Select, ConfigProvider } from "antd";
import { getSubjectList, getSchoolAreaList, getCurrentSelected, getGradeEnum, getSemesterEnum, getExamEnum, getNoSingleSubject,getSort,getExam } from "@/apis/common";
const ScreenPage = ({
isShow = true,
isSingle = true, //是否单选
isTeacher = false,//是否为老师相关的
onChange = () => { },
saveshow = () => { },
isShowSubject = false, //是否显示学科
isShowSort = false,//是否显示排名
isShowSchool = true, //是否显示学校
onSendSortList = () => { }, //是否发送排名
}) => {
const [viewIsShow, setViewIsShow] = useState(isShow);
const [grade, setGrade] = useState([]); //届别
const [currentGrade, setCurrentGrade] = useState([]); //当前属别
const [semester, setSemester] = useState([]); //学期
const [currentSemester, setCurrentSemester] = useState([]); //当前学期
const [examination, setExamination] = useState([]); //考试
const [currentExamination, setCurrentExamination] = useState([]); //当前考试
const [school, setSchool] = useState([]); //学校
const [currentSchool, setCurrentSchool] = useState([]); //当前学校
const [currentDiscipline, setCurrentDiscipline] = useState([]); //当前学科
const [show, setShow] = useState(false);
const [chooseList, setChooseList] = useState([]); //自选学校列表
const [sortList, setSortList] = useState([]); //排名列表
const [currentSort, setCurrentSort] = useState([]); //当前排名
const [nonSingleSubject, setNonSingleSubject] = useState([]); //非单一学科
const [singleSubjectOptions, setSingleSubjectOptions] = useState([]);
const [optionSchoolList, setOptionSchoolList] = useState([]);
// 考试id
const [examId, setExamId] = useState('');
useEffect(() => {
setViewIsShow(isShow);
}, [isShow]);
useEffect(() => {
if (isShowSubject) {
singleSubjectListInit();
}
}, [isShowSubject]);
// useEffect(() => {
// if(isShowSort){
// sortInit();
// }
// }, [isShowSort]);
useEffect(() => {
optionalSchoolInit();
getCurrentSelectedInit();
}, []);
// //获取排名
// const sortInit = async () => {
// try {
// const res = await getSort();
// if(res?.code === 200){
// }
// } catch (error) {
// console.log(error,'get-sort-error');
// }
// };
//获取单一学科
const singleSubjectListInit = async () => {
try {
const res = await getSubjectList();
if (res?.code === 200) {
setSingleSubjectOptions(() => {
return res?.data?.map((item) => {
return {
value: item?.id,
label: item?.cname,
};
});
});
}
} catch (error) {
console.log(error, "get-subject-list-error");
}
};
// 获取自选范围学校
const optionalSchoolInit = async () => {
try {
const res = await getSchoolAreaList({ type: 0 });
if (res?.code === 200) {
setOptionSchoolList(res?.data || []);
}
} catch (error) {
console.log(error, "get-option-school-list-error");
}
};
// 获取当前考试
const getCurrentSelectedInit = async () => {
try {
const grade = await getGradeEnum();
const semester = await getSemesterEnum();
const exam = await getExamEnum();
const subject = await getNoSingleSubject();
const res = await getCurrentSelected();
const res2 = await getSchoolAreaList({ type: 1 });
let sort;
if(isShowSort){
const res = await getSort();
if(res?.code === 200){
sort = res?.data || [];
setSortList(res?.data || []);
setCurrentSort([res?.data?.[0]]);
onSendSortList && onSendSortList(res?.data || []);
}
}
setNonSingleSubject(() => {
let list = subject?.data?.map(item => {
return {
id: item.id,
name: item.cname
}
});
if(isTeacher){
list = list?.filter(item=>item.name !== '总分');
}
return [...list, {
id: "single",
name: "单一学科",
type: "choose",
},]
})
setGrade(() => {
return grade?.data?.map((item) => {
return {
name: item
}
})
});
setSemester(() => {
return semester?.data?.map((item) => {
return {
name: item
}
})
});
setExamination(() => {
return exam?.data?.map((item) => {
return {
name: item
}
})
});
if (res?.code === 200) {
setCurrentGrade([{ name: res?.data?.term }]);
setCurrentSemester([{ name: res?.data?.semester }]);
setCurrentExamination([{ name: res?.data?.examName }]);
setExamId(res?.data?.id);
const element = res2?.data?.find(item => item.districtName === '全部学校');
setCurrentSchool([element]);
let totalPoints = subject?.data?.map(item=>{
return {
...item,
name: item.cname
}
})?.find(item=>item.cname === '总分');
if(isTeacher){
const list = subject?.data?.filter(item=>item.cname !== '总分')?.map(item=>{
return {
...item,
name: item.cname
}
});
setCurrentDiscipline([list?.[0]]);
}else{
setCurrentDiscipline([totalPoints])
}
setSchool(() => {
return [
...res2.data,
{ id: "area", districtName: "自选学校范围", type: "choose" },
];
});
const noTotal = subject?.data?.filter(item=>item.cname !== '总分')?.map(item=>{
return {
...item,
name: item.cname
}
})?.[0];
if(isShowSort){
onChange &&
onChange({
currentGrade: [{ name: res?.data?.term }],
currentSemester: [{ name: res?.data?.semester }],
currentExamination: [{ name: res?.data?.examName }],
currentSchool: [element],
currentDiscipline:[isTeacher ? noTotal : totalPoints],
examId: res?.data?.id,
currentSort:sort?.[0],
});
return;
}
onChange &&
onChange({
currentGrade: [{ name: res?.data?.term }],
currentSemester: [{ name: res?.data?.semester }],
currentExamination: [{ name: res?.data?.examName }],
currentSchool: [element],
currentDiscipline:[isTeacher ? noTotal : totalPoints],
examId: res?.data?.id
});
}
} catch (error) {
console.log(error, 'get-current-selected-error');
}
};
/////////////////////
/**
* 自选学校
* @param record
*/
const updateModal = (chooseList, schoolList) => {
let datalist = schoolList || [];
let colorList = {
"Ⅰ类": "#722ED1",
"Ⅱ类": "#FADB14",
};
let chooselist = [...chooseList];
let modal = null;
const showModal = () => {
let content = (
<Flex direction="column" className={less.schoolsList}>
{datalist.map((item) => {
return (
<Flex
key={item?.id}
className={less.schoolsitem}
direction="column"
>
<Flex className={less.address}>{item.districtName}</Flex>
<Flex className={less.children} wrap="wrap">
{item?.schoolList?.map((child) => {
return (
<Flex
key={child?.id}
className={
chooselist?.find((it) => it?.id == child?.id)
? less.childchecked
: less.child
}
alignItems="center"
onClick={() => {
if (
chooselist?.find((it) => it?.id == child?.id)
) {
chooselist = chooselist.filter(
(it) => it?.id != child?.id
);
showModal();
return;
}
chooselist = [...chooselist, child];
showModal();
}}
>
{child?.cname}
<Flex
className={less.color}
style={{ backgroundColor: colorList[child?.schoolType] }}
>
{child?.schoolType}
</Flex>
</Flex>
);
})}
</Flex>
</Flex>
);
})}
</Flex>
);
if (modal) {
modal.update({ content });
} else {
modal = Modal.confirm({
title: "选择学校",
className: "globalModal",
okText: "确定",
cancelText: "取消",
content,
width: 900,
closable: true,
styles: { content: { height: "60vh", overflow: "auto" } },
onOk: () => {
setChooseList(chooselist);
},
});
}
};
showModal();
};
const titlePage = (name) => {
return (
<Flex>
{name}&nbsp;&nbsp;&nbsp;
<Flex
style={{
color: "#c0c0c0",
fontWeight: "bold",
}}
>
|
</Flex>
&nbsp;&nbsp;
</Flex>
);
};
return (
<Flex
className={less.frame}
direction="column"
style={{ display: viewIsShow ? "flex" : "none" }}
>
<Flex className={less.chooseCard} direction="column">
<Flex className={less.chooseItem}>
{titlePage("届别")}
<Flex className={less.list}>
{grade?.map((item) => {
return (
<Flex
key={item?.name}
className={
currentGrade?.find((it) => it?.name == item?.name)
? less.itemchecked
: less.item
}
onClick={() => {
if (isSingle) {
setCurrentGrade([item]);
return;
}
if (currentGrade?.find((it) => it?.name == item?.name)) {
setCurrentGrade(
currentGrade.filter((it) => it?.name != item?.name)
);
return;
}
setCurrentGrade([...currentGrade, item]);
}}
>
{item.name}
</Flex>
);
})}
</Flex>
</Flex>
<Flex className={less.chooseItem}>
{titlePage("学期")}
<Flex className={less.list}>
{semester?.map((item,index) => {
return (
<Flex
key={index}
className={
currentSemester?.find((it) => it?.name == item?.name)
? less.itemchecked
: less.item
}
onClick={() => {
if (isSingle) {
setCurrentSemester([item]);
return;
}
if (currentSemester?.find((it) => it?.name == item?.name)) {
setCurrentSemester(
currentSemester.filter((it) => it?.name != item?.name)
);
return;
}
setCurrentSemester([...currentSemester, item]);
}}
>
{item.name}
</Flex>
);
})}
</Flex>
</Flex>
<Flex className={less.chooseItem}>
{titlePage("考试")}
<Flex className={less.list}>
{examination?.map((item) => {
return (
<Flex
key={item?.name}
className={
currentExamination?.find((it) => it?.name == item?.name)
? less.itemchecked
: less.item
}
onClick={() => {
if (isSingle) {
setCurrentExamination([item]);
return;
}
if (
currentExamination?.find((it) => it?.name == item?.name)
) {
setCurrentExamination(
currentExamination.filter(
(it) => it?.name != item?.name
)
);
return;
}
setCurrentExamination([...currentExamination, item]);
}}
>
{item.name}
</Flex>
);
})}
</Flex>
</Flex>
{isShowSubject ? (
<Flex className={less.chooseItem}>
{titlePage("学科")}
<Flex className={less.list} alignItems="center" style={{ marginTop: -6 }}>
{nonSingleSubject?.map((item) => {
if (item.type == "choose") {
return (
<Flex
key={item?.id}
className={show ? less.itemchecked1 : less.item1}
alignItems="center"
justify="center"
>
<ConfigProvider theme={{
components: {
Select: {
selectorBg: 'transparent',
colorTextPlaceholder: "#666",
fontSize: 16
}
}
}}>
<Select
placeholder="单一学科"
variant="borderless"
onChange={(e,option) => {
setShow(true);
setCurrentDiscipline([{name:option?.label,id:e}]);
}}
options={singleSubjectOptions || []}
/>
</ConfigProvider>
</Flex>
);
}
return (
<Flex
key={item?.id}
className={
currentDiscipline?.[0]?.name == item?.name
? less.itemchecked
: less.item
}
onClick={() => {
setShow(false);
setCurrentDiscipline([item]);
}}
>
{item.name}
</Flex>
);
})}
</Flex>
</Flex>
) : null}
{isShowSchool ? ( <Flex className={less.chooseItem}>
{titlePage("学校")}
<Flex className={less.list}>
{school?.map((item) => {
return (
<Flex
key={item?.id}
className={
currentSchool?.find((it) => it?.id == item?.id)
? less.itemchecked
: less.item
}
onClick={() => {
if (item.type == "choose") {
updateModal(chooseList, optionSchoolList);
setCurrentSchool([]);
return;
}
if (isSingle) {
setCurrentSchool([item]);
return;
}
if (currentSchool?.find((it) => it?.name == item?.name)) {
setCurrentSchool(
currentSchool.filter((it) => it?.name != item?.name)
);
return;
}
setCurrentSchool([...currentSchool, item]);
}}
>
{item.districtName}
</Flex>
);
})}
</Flex>
</Flex>) : null}
<Flex className={less.otherSchools}>
{chooseList?.map((item) => {
return (
<Flex key={item?.name} className={less.otheritem}>
{item?.cname}
</Flex>
);
})}
</Flex>
{isShowSort ? (
<Flex className={less.chooseItem}>
{titlePage("排名")}
<Flex className={less.list} alignItems="center">
{sortList?.map((item) => {
return (
<Flex
key={item?.value}
className={
currentSort?.[0]?.label == item?.label
? less.itemchecked
: less.item
}
onClick={() => {
setShow(false);
console.log(item,'item')
setCurrentSort([item]);
}}
>
{item.label}
</Flex>
);
})}
</Flex>
</Flex>
) : null}
<Flex className={less.btns}>
<Flex
className={less.btn}
onClick={async() => {
try {
const res = await getExam({
term: [currentGrade?.[0]?.name],
semester:[currentSemester?.[0]?.name],
examName:[currentExamination?.[0]?.name],
});
if(res?.code === 200){
onChange &&
onChange({
currentGrade,
currentSemester,
currentExamination,
currentSchool: [...currentSchool, ...chooseList],
currentDiscipline,
currentSort,
examId:res?.data[0]?.id,
});
setViewIsShow(false);
saveshow && saveshow(false);
}
} catch (error) {
console.log(error,'get-exam-error');
}
}}
>
确定
</Flex>
<Flex
className={less.btn}
style={{
color: "#c0c0c0",
border: "1px solid #c0c0c0",
backgroundColor: "#fff",
}}
onClick={() => {
setViewIsShow(false);
// getCurrentSelectedInit();
setCurrentSchool([]);
setCurrentDiscipline([]);
saveshow && saveshow(false);
}}
>
取消
</Flex>
</Flex>
</Flex>
</Flex>
);
};
export default ScreenPage;

View File

@ -0,0 +1,124 @@
.frame {
width: 100%;
padding: 12px;
border-bottom: 1px solid #000;
.chooseCard {
width: 100%;
.chooseItem {
width: 100%;
font-size: 16px;
margin-bottom: 10px;
.list {
flex: 1;
flex-wrap: wrap;
.item {
padding: 3px 8px;
margin: 0 10px;
color: #666;
margin-bottom: 8px;
cursor: pointer;
}
.item1 {
padding: 3px 8px;
color: #666;
cursor: pointer;
margin-bottom: 5px;
}
.itemchecked {
padding: 3px 8px;
margin: 0 10px;
color: #42b562;
background-color: #ecf8f0;
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
}
.itemchecked1 {
padding: 3px 8px;
margin-bottom: 5px;
color: #42b562;
background-color: #ecf8f0;
border-radius: 4px;
cursor: pointer;
}
}
}
.otherSchools {
width: 100%;
padding-left: 70px;
margin-bottom: 8px;
.otheritem {
padding: 3px 8px;
margin-right: 8px;
margin-bottom: 5px;
font-size: 12px;
border-radius: 5px;
box-shadow: inset 0px 0px 3px #42b562;
}
}
.btns {
width: 100%;
.btn {
padding: 3px 25px;
background-color: #4abb6a;
color: #fff;
border-radius: 5px;
margin-right: 15px;
cursor: pointer;
}
}
}
}
// :global {
// .ant-select-selector {
// color: #666 !important;
// font-size: 16px !important;
// height: 20px !important;
// }
// }
.schoolsList {
width: 100%;
.schoolsitem {
width: 100%;
.address {
width: 100%;
border-left: 3px solid #4bbb6b;
margin: 10px 0;
padding-left: 12px;
}
.children {
width: 100%;
padding-left: 9px;
.child {
padding: 3px 8px;
margin-bottom: 5px;
cursor: pointer;
margin-right: 5px;
.color {
padding: 1px 2px;
font-size: 9px;
color: #fff;
border-radius: 3px;
margin-left: 3px;
}
}
.childchecked {
cursor: pointer;
padding: 3px 8px;
margin-bottom: 5px;
background-color: #4abb6a;
color: #fff;
margin-right: 5px;
border-radius: 5px;
.color {
padding: 1px 2px;
font-size: 9px;
color: #fff;
border-radius: 3px;
margin-left: 3px;
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,462 @@
import { useState, useEffect, useImperativeHandle, forwardRef, memo } from "react";
import { Modal,Table,Button } from "antd";
import ScreenPage from "@/components/ScreenPage";
import Flex from "@/components/Flex";
import { getTeacherAnalysis, getTeacherZScoreAnalysis, getTeacherTrace, getTeacherGrade, getTeacherSort, getTeacherAvg, getSingleTeacherZ } from '@/apis/leader';
import { getExam } from "@/apis/common";
import { downloadTableData } from '@/apis/download';
import { downloadExcelFile } from '@/utils/file';
import upSrc from "@/assets/image/up.png";
import downSrc from "@/assets/image/down.png";
import styles from "./index.less";
const TeacherGroup = (props,ref) => {
const { record = {}, params = {} } = props;
const [open, setOpen] = useState(false);
const [isShow, setIsShow] = useState(false);
const [isShow1, setIsShow1] = useState(false);
const [responseData1, setResponseData1] = useState({});
const [responseData2, setResponseData2] = useState({});
const [responseData3, setResponseData3] = useState({});
const [examId, setExamId] = useState('');
const [subTitle, setSubTitle] = useState('');
const [examList, setExamList] = useState([]);
const [columns1, setColumns1] = useState([]);
const [columns2, setColumns2] = useState([]);
const [columns3, setColumns3] = useState([]);
const [loading1, setLoading1] = useState(false);
const [loading2, setLoading2] = useState(false);
const [loading3, setLoading3] = useState(false);
const [dataSource1, setDataSource1] = useState([]);
const [dataSource2, setDataSource2] = useState([]);
const [dataSource3, setDataSource3] = useState([]);
useImperativeHandle(ref, () => ({
openModal,
closeModal
}));
useEffect(() => {
if (open) {
trackInit();
gradeInit();
}
}, [open]);
useEffect(()=>{
if(open && examList?.length > 0 || !examId) {
singleTeacherZ();
}
},[open,examList,examId])
const trackInit = async () => {
try {
setLoading1(true);
const res = await getTeacherTrace({
teacher: record?.teacherId,
examId: params?.examId,
subjectId: params?.subjectId
});
if (res?.code === 200) {
setResponseData1(res);
setLoading1(false);
setDataSource1(res?.data || []);
setColumns1(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
}
});
})
return;
}
setLoading1(false);
} catch (error) {
setLoading1(false);
console.log(error, 'track-error');
}
};
const gradeInit = async () => {
try {
setLoading2(true);
const res = await getTeacherGrade({
teacher: record?.teacherId,
examId: params?.examId,
subjectId: params?.subjectId
});
if (res?.code === 200) {
setResponseData2(res);
setLoading2(false);
setDataSource2(res?.data || []);
setColumns2(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
}
});
})
return;
}
setLoading2(false);
} catch (error) {
setLoading2(false);
console.log(error, 'track-error');
}
};
const singleTeacherZ = async () => {
try {
setLoading3(true);
const res = await getSingleTeacherZ({
teacher: record?.teacherId,
examId,
subjectId: params?.subjectId,
examIds: examList?.map(item => item?.id).filter(item=>item !== examId),
});
if (res?.code === 200) {
setResponseData3(res);
setLoading3(false);
setDataSource3(res?.data || []);
setColumns3(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
}
});
})
return;
}
setLoading3(false);
} catch (error) {
setLoading3(false);
console.log(error, 'track-error');
}
};
const handleDownload1 = async () => {
try {
const res = await downloadTableData(responseData1);
if (res?.status === 200) {
downloadExcelFile(res, `${record?.school}*${record?.name}教师情况追踪`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
const handleDownload2 = async () => {
try {
const res = await downloadTableData(responseData2);
if (res?.status === 200) {
downloadExcelFile(res, `${record?.school}*${record?.name}教师成绩分析`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
const handleDownload3 = async () => {
try {
const res = await downloadTableData(responseData3);
if (res?.status === 200) {
downloadExcelFile(res, `${record?.school}*${record?.name}教师增量评价`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
const tableCommonProps = {
style: { maxHeight: 460},
rowKey: "teacherId",
scroll: { x: 1400,y:340 },
sticky: true,
pagination: false,
};
const openModal = () => {
setOpen(true);
};
const closeModal = () => {
setOpen(false);
};
return (
<div className={styles.teacher}>
<Modal
title={`教师成长性:${record?.school}*${record?.name}`}
width={'80vw'}
open={open}
centered
styles={{ body: { height: "80vh"} }}
maskClosable={false}
destroyOnClose
onCancel={() => {
setOpen(false);
}}
onOk={() => {
setOpen(false);
}}
>
<Flex
direction="column"
style={{ overflowY: "scroll", height: "80vh" }}
>
<Flex style={{ color: "#666" }}>教师情况跟踪</Flex>
<Table
dataSource={dataSource1}
columns={columns1}
loading={loading1}
{...tableCommonProps}
></Table>
<Flex>
<Button
style={{
backgroundColor: "#4abb6a",
color: "#fff",
marginTop: 8,
}}
onClick={handleDownload1}
>
导出表格
</Button>
</Flex>
<Flex style={{ color: "#666" }}>教师成绩分析</Flex>
<Table
dataSource={dataSource2}
columns={columns2}
loading={loading2}
{...tableCommonProps}
></Table>
<Flex>
<Button
style={{
backgroundColor: "#4abb6a",
color: "#fff",
marginTop: 8,
}}
onClick={handleDownload2}
>
导出表格
</Button>
</Flex>
<ScreenPage
isShow={isShow1}
isSingle={true}
isShowSubject={false}
isTeacher={true}
isShowSchool={false}
onChange={(e) => {
setSubTitle([e?.currentGrade?.[0]?.name, e?.currentSemester?.[0]?.name, e?.currentExamination?.[0]?.name].filter(item => !!item).join("*"));
setExamId(e?.examId || '');
}}
saveshow={(e) => {
setIsShow1(e);
}}
/>
<h3 style={{marginTop:16}}>教师增量评价{record?.name}</h3>
<Flex
style={{
width: "100%",
display: "flex",
alignItems: "center",
margin: "5px 0",
// gap: 12,
}}
justify="space-between"
alignItems="center"
>
<Flex style={{ flex: 1 }} alignItems="center">
<Flex
alignItems="center"
style={{
width: "100%",
display: "flex",
alignItems: "center",
color: "#666",
}}
>
<Flex>当前考试 : &nbsp;</Flex>
<Flex style={{ width: "92%" }} wrap="wrap">
<h3 style={{color:'#000'}}> {subTitle}</h3>
</Flex>
</Flex>
</Flex>
<Flex
style={{
padding: "4px 8px",
border: "1px solid #666",
borderRadius: 5,
cursor: "pointer",
marginLeft: 10,
}}
alignItems="center"
onClick={() => {
setIsShow1(true)
}}
>
选择考试
<img
src={isShow1 ? upSrc : downSrc}
width={14}
height={12}
style={{ marginLeft: 5 }}
/>
</Flex>
</Flex>
<ScreenPage
isShow={isShow}
isSingle={false}
isShowSubject={false}
isTeacher={true}
isShowSchool={false}
onChange={(e) => {
const term = e?.currentGrade?.map(item => item?.name);
const semester = e?.currentSemester?.map(item => item?.name);
const examName = e?.currentExamination?.map(item => item?.name);
getExam({
term,
semester,
examName,
}).then(res => {
if (res.code === 200) {
setExamList(res?.data || []);
}
}).catch(error => {
console.log(error, 'get-exam-error');
})
}}
saveshow={(e) => {
setIsShow(e);
}}
/>
<Flex
style={{
width: "100%",
display: "flex",
alignItems: "center",
margin: "5px 0",
// gap: 12,
}}
justify="space-between"
alignItems="center"
>
<Flex style={{ flex: 1 }} alignItems="center">
<Flex
alignItems="center"
style={{
width: "100%",
display: "flex",
alignItems: "center",
color: "#666",
}}
>
<Flex>对比考试 : &nbsp;</Flex>
<Flex style={{ width: "92%" }} wrap="wrap">
{examList?.filter((item) => item?.id !== params?.examId)?.map((item) => {
return (
<h3
style={{ color: "#000", marginRight: 8 }}
key={item?.id}
>
{`${item?.term}*${item?.semester}*${item?.examName};`}
</h3>
);
})}
</Flex>
</Flex>
</Flex>
<Flex
style={{
padding: "4px 8px",
border: "1px solid #666",
borderRadius: 5,
cursor: "pointer",
marginLeft: 10,
}}
alignItems="center"
onClick={() => {
setIsShow(true)
}}
>
选择考试
<img
src={isShow ? upSrc : downSrc}
width={14}
height={12}
style={{ marginLeft: 5 }}
/>
</Flex>
</Flex>
<Table
dataSource={dataSource3}
columns={columns3}
loading={loading3}
{...tableCommonProps}
>
</Table>
<Flex>
<Button
style={{
backgroundColor: "#4abb6a",
color: "#fff",
marginTop: 8,
}}
onClick={handleDownload3}
>
导出表格
</Button>
</Flex>
<Flex style={{ width: '100%', marginTop: 32 }} justify="center" alignItems="center">
<Button type="primary" style={{
backgroundColor: "#4abb6a",
color: "#fff",
marginTop: 10,
}}>{record?.name}课堂实录</Button>
</Flex>
</Flex>
</Modal>
</div>
)
};
export default memo(forwardRef(TeacherGroup));

View File

View File

@ -0,0 +1,151 @@
import { useState,useEffect } from 'react';
import { Button, message, Upload } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { getExamFileName,uploadFile } from '@/apis/data';
import { logger} from '@/apis/auth';
import styles from './index.less';
const allowedTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel', // .xls
'application/zip', // .zip
'application/x-zip-compressed' // .zip
];
const allowedExtensions = ['.xlsx', '.xls', '.zip'];
const UploadScore = (props) => {
const {examId='',onChange=()=>{}} = props;
const [fileList,setFileList] = useState([]);
useEffect(() => {
getExamFileNameInit();
}, []);
const getExamFileNameInit = async () => {
if(!examId){
return;
}
try {
const res = await getExamFileName({examId,});
if(res.code === 200){
if(!res.data?.studentSubjectDetailFileName){
setFileList([]);
onChange(null);
return;
}
setFileList([{
uid: res?.data?.nsId,
name: res.data?.studentSubjectDetailFileName,
status: 'done',
}]);
onChange(res.data?.studentSubjectDetailFileName);
return;
}
message.error(res.msg);
} catch (error) {
console.log(error,'getExamFileNameInit-error');
}
}
const checkFileType = (file) => {
const isAllowedType = allowedTypes.includes(file.type);
const fileName = file.name.toLowerCase();
const isAllowedExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
return isAllowedType || isAllowedExtension;
};
const handleBeforeUpload = (file) => {
if (!checkFileType(file)) {
message.error('只能上传 Excel (.xlsx, .xls) 或 ZIP (.zip) 格式的文件!');
return Upload.LIST_IGNORE;
}
const isLt2M = file.size / 1024 / 1024 < 2000;
if (!isLt2M) {
message.error('文件不能超过 2000MB!');
return Upload.LIST_IGNORE;
}
return true;
};
const handleCustomRequest = async({ file, onSuccess, onError}) => {
try {
let percent = 0;
const formData = new FormData();
formData.append('file', file);
formData.append('examId', examId);
const res = await uploadFile(formData,{
onUploadProgress: ({ loaded, total }) => {
percent = Math.floor((loaded / total) * 100);
},
});
if(res?.code === 200){
onSuccess('上传成功');
message.success('文件上传成功');
onChange(file?.name);
setFileList([
{
uid: examId,
name: file?.name,
status: 'done',
percent,
},
]);
await logger({description:"上传原始成绩"});
return;
}
message.error(res?.msg);
} catch (error) {
onError(error);
message.error('文件上传失败');
console.log(error,'upload-error');
}
};
const handleRemove = (file) => {
onChange(null);
setFileList((prev)=>{
return prev.filter(item=>item.uid !== file.uid);
})
};
const originProps = {
name: "file",
multiple: false,
accept: '.xlsx,.xls,.zip',
maxCount: 1,
beforeUpload: handleBeforeUpload,
};
return (
<div>
<Upload {...originProps} customRequest={handleCustomRequest} onRemove={handleRemove} fileList={fileList}>
<Button type='primary' icon={<UploadOutlined />}>上传原始成绩</Button>
</Upload>
<p className={styles.upload_tips}>请上传原始成绩文件请按照固定的excel表格格式进行上传勿修改表格结构及固定文案文件大小限制2000MB以内</p>
</div>
)
};
export default UploadScore;

View File

@ -0,0 +1,6 @@
.upload{
&_tips{
margin-top: 16px;
color: #faad14;
}
}

1
src/config/index.js Normal file
View File

@ -0,0 +1 @@
export const BASE_URL = 'https://studies.hmbigdata.com/hly-huaian';

94
src/global.less Normal file
View File

@ -0,0 +1,94 @@
* {
margin: 0;
padding: 0;
}
.globalModal {
.ant-modal-body {
padding: 12px !important;
}
.ant-modal-confirm-body > .anticon {
display: none;
}
.ant-modal-confirm-body .ant-modal-confirm-content {
margin-top: 12px;
padding-top: 12px;
// border-top: 1px solid #ddd;
}
.ant-modal-confirm-body
> .anticon
+ .ant-modal-confirm-title
+ .ant-modal-confirm-content {
margin-left: 0;
}
.ant-modal .ant-modal-content {
padding: 12px !important;
}
.ant-descriptions-item > span {
display: block;
}
.ant-descriptions-item-label {
font-weight: bold;
}
}
:where(.css-dev-only-do-not-override-240cud).ant-modal .ant-modal-content {
padding: 12px !important;
}
.globalModalNoPadding {
.globalModal;
.ant-modal-body {
padding: 0 !important;
}
}
.globalModalInfo {
.ant-modal-body {
padding: 12px !important;
}
.ant-modal-confirm-body > .anticon {
display: none;
}
.ant-modal-confirm-btns {
display: none;
}
.ant-descriptions-header {
margin-bottom: 4px;
}
.ant-modal-confirm-body
> .anticon
+ .ant-modal-confirm-title
+ .ant-modal-confirm-content {
margin-left: 0;
}
.ant-descriptions-row > th,
.ant-descriptions-row > td {
padding-bottom: 0;
border: 1px solid #eee;
}
.ant-descriptions-item-label {
padding: 6px 8px !important;
font-weight: bold;
}
.ant-descriptions-item-content {
padding: 6px 8px 6px 0 !important;
}
.ant-table-cell {
padding: 6px !important;
}
.ant-table-thead > tr > th {
padding: 8px 6px !important;
}
}

22
src/layouts/index.tsx Normal file
View File

@ -0,0 +1,22 @@
import {ConfigProvider} from 'antd';
import { Outlet } from 'umi';
import locale from 'antd/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
export default function Layout() {
return (
<ConfigProvider theme={{
token: {
colorPrimary: '#4ABB6A',
borderRadius: 4
},
}} locale={locale}>
<Outlet/>
</ConfigProvider>
);
};

View File

@ -0,0 +1,11 @@
const AdminAccountPage = () => {
return (
<div>管理账号</div>
);
};
export default AdminAccountPage;

View File

View File

@ -0,0 +1,663 @@
import { useState, useEffect, useMemo } from 'react';
import { Button,Form, Input, Select, Space, Card, Table, Checkbox, Tree,Modal, message, Flex, Typography, Drawer, Pagination } from 'antd';
import { getRoleList, deleteRole, saveRole, getRoleTree } from '@/apis/role';
import { getStageList, getSubjectListSelect } from '@/apis/common';
import { logger } from '@/apis/auth';
import { getAllCheckedListIds } from '@/utils/data';
import styles from './index.less';
const AccessPage = () => {
const [form] = Form.useForm();
const [tableLoading, setTableLoading] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [dataSource, setDataSource] = useState([]);
const [open, setOpen] = useState(false);
const [roleId, setRoleId] = useState(false);
const [stageOptions, setStageOptions] = useState([]);
const [stageIds, setStageIds] = useState([]);
const [subjectOptions, setSubjectOptions] = useState([]);
const [treeData, setTreeData] = useState({});
const [permissions, setPermissions] = useState([]);
const [total, setTotal] = useState(0);
const [pageNum, setPageNum] = useState(1);
const [pageSize, setPageSize] = useState(10);
const computedTitle = useMemo(() => {
return roleId ? '编辑角色' : '新增角色';
}, [roleId]);
useEffect(() => {
init();
getStageListData();
}, []);
useEffect(() => {
if (stageIds?.length > 0) {
getSubjectData();
}
}, [stageIds]);
useEffect(() => {
if (stageIds?.length > 0 && !roleId) {
getAccessTreeData();
}
}, [stageIds, roleId])
useEffect(() => {
if (!permissions?.length) {
form.setFieldValue('permissions', [])
}
form.setFieldValue('permissions', permissions);
}, [permissions])
const init = async () => {
try {
setTableLoading(true);
const res = await getRoleList({ pageNum, pageSize, });
if (res?.code === 200) {
setTableLoading(false);
setDataSource(res?.data || []);
setTotal(res?.total || 0);
return;
}
setTableLoading(false);
} catch (error) {
setTableLoading(false);
console.log(error, 'init-error');
}
};
const getStageListData = async () => {
try {
const res = await getStageList();
if (res?.code === 200) {
setStageOptions(() => {
return res?.data?.map((item) => {
return {
label: item.value,
value: item.key
}
})
});
setStageIds(res?.data?.map((item) => item.key));
}
} catch (error) {
console.log(error, 'getStageListData-error');
}
};
const getSubjectData = async () => {
try {
const res = await getSubjectListSelect({ stages: stageIds?.join(',') });
if (res?.code === 200) {
setSubjectOptions(() => {
return res?.data?.map((item) => {
return {
label: item?.cname,
value: item?.id
}
})
})
}
} catch (error) {
console.log(error, 'getStageListData-error');
}
};
const getAccessTreeData = async () => {
try {
const res = await getRoleTree({ stage: stageIds?.join(',') });
if (res?.code === 200) {
setTreeData(res?.data)
}
} catch (error) {
console.log(error, 'getStageListData-error');
}
};
const handlePrimaryCityCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handlePrimaryDistrictCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handlePrimarySchoolCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handleJuniorCityCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handleJuniorDistrictCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handleJuniorSchoolCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handleHighCityCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handleHighDistrictCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handleHighSchoolCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
})
};
const handleDataImportCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
});
};
const handleSecondaryAdminCheck = (value) => {
setPermissions((prev) => {
return Array.from(new Set([...prev, ...value]));
});
};
// const onFormValuesChange = async (values) => {
// try {
// setTableLoading(true);
// const res = await getSubjectList(values);
// if (res?.code === 200) {
// setTableLoading(false);
// setDataSource(res?.data || []);
// return;
// }
// setTableLoading(false);
// } catch (error) {
// setTableLoading(false);
// console.log(error, 'onFormValuesChange-error');
// }
// };
const handleCreateRole = () => {
setOpen(true);
setRoleId('');
form.resetFields();
};
const handleFinish = async () => {
setConfirmLoading(true);
form.validateFields().then(async () => {
const res = await saveRole(roleId ? {
id: roleId,
...form.getFieldsValue()
} : form.getFieldsValue());
if (res.code === 200) {
setConfirmLoading(false);
setOpen(false);
init();
form.resetFields();
setRoleId('');
setPermissions([]);
message.success('操作成功');
if(roleId){
await logger({description:"修改角色"});
}else{
await logger({description:"创建角色"});
}
return;
}
setConfirmLoading(false);
}).catch(error => {
setConfirmLoading(false);
console.log(error, 'handleFinish-error');
});
};
const handleDeleteRole = (record) => {
Modal.confirm({
title: '删除角色',
content: '确定删除角色吗?',
okText: '确定',
cancelText: '取消',
centered: true,
onOk() {
return new Promise((resolve, reject) => {
deleteRole({ roleId: record?.id }).then(async(res) => {
if (res.code === 200) {
message.success('删除角色成功');
resolve();
init();
await logger({description:"删除角色"});
return
}
message.error(res?.msg);
reject();
}).catch(error => {
console.log(error, 'handleResetPassword-error');
reject();
})
});
},
});
};
const handleEditRole = async (record) => {
try {
const res = await getRoleTree({ roleId: record?.id, stage: record?.stage });
if (res?.code === 200) {
setTreeData(res?.data)
setOpen(true);
setRoleId(record?.id);
form.setFieldsValue({
roleName: res?.data?.roleName,
subjects: res?.data?.subjects,
stages: res?.data?.stages
});
setStageIds(res?.data?.stages);
const checkIds = getAllCheckedListIds(res?.data);
form.setFieldValue('permissions', checkIds);
setPermissions(checkIds);
}
} catch (error) {
console.log(error, 'handleEditRole-error');
}
;
};
const onPageChange = async (pageNum, pageSize) => {
setPageNum(pageNum);
setPageSize(pageSize);
try {
setTableLoading(true);
const res = await getRoleList({ pageNum, pageSize });
if (res?.code === 200) {
setTableLoading(false);
setDataSource(res?.data || []);
setTotal(res?.total || 0);
return;
}
setTableLoading(false);
} catch (error) {
setTableLoading(false);
console.log(error, 'init-error');
}
};
const columns = [
{
title: '角色名称',
dataIndex: 'roleName',
key: 'roleName',
width: 280,
align: 'center',
},
{
title: '学科',
dataIndex: 'subject',
key: 'subject',
align: 'center',
},
{
title: '适用范围',
dataIndex: 'stage',
key: 'stage',
align: 'center',
width: 140
},
{
title: '操作',
key: 'action',
align: 'center',
render(_, record) {
return (
<Space size="middle">
<Button type="link" onClick={() => { handleEditRole(record) }}>编辑</Button>
<Button type='link' danger onClick={() => handleDeleteRole(record)}>删除</Button>
</Space>
);
}
},
];
return (
<div className={styles.access}>
<header className={styles.access_header}>
<Card style={{ height: "100%" }}>
<Flex justify="space-between">
<Typography.Title level={4} >
角色
</Typography.Title>
<Button type="primary" onClick={handleCreateRole}>
创建角色
</Button>
</Flex>
</Card>
</header>
<main className={styles.access_main}>
<Card>
<Table bordered columns={columns} loading={tableLoading} dataSource={dataSource} pagination={false} style={{ height: '100%' }} />
</Card>
</main>
<footer className={styles.access_footer}>
<Pagination showQuickJumper showSizeChanger total={total} onChange={onPageChange} showTotal={(total) => `${total}条数据`} />
</footer>
<Drawer
title={computedTitle}
open={open}
destroyOnClose
onClose={() => {
setOpen(false);
setRoleId('');
form.resetFields();
setPermissions([]);
setTreeData({});
}}
size="large"
width={1000}
>
<Form
form={form}
layout="vertical"
initialValues={{
stages: stageOptions?.map(item => item.value),
}}
>
<Form.Item label="角色名称" name="roleName" rules={[
{
required: true,
message: '请填写角色名称',
},
]}>
<Input placeholder="请输入角色名称" />
</Form.Item>
<Form.Item label="学段" name="stages" rules={[
{
required: true,
message: '请至少选择一个学段',
},
]}>
<Checkbox.Group options={stageOptions} onChange={(value) => {
if (!value?.length) {
// setStageIds([]);
return;
}
setStageIds(value);
}} />
</Form.Item>
<Form.Item label="学科" name="subjects" rules={[
{
required: true,
message: '请至少选择一门学科',
},
]}>
<Select
mode="multiple"
placeholder="请选择学科"
options={subjectOptions}
maxTagCount={6}
style={{ width: '100%' }}
allowClear
/>
</Form.Item>
<Form.Item label="权限" name="permissions" rules={[
{
required: true,
message: '请选择权限',
},
]}>
<div className={styles.access_list}>
{stageIds?.length > 0 ? stageIds.map((item) => {
if (item == 1) {
return (
<div>
<div className={styles.access_list_title} style={{ marginTop: 8 }}>小学</div>
<Flex justify="space-evenly" style={{ marginTop: 8 }}>
<Card title='市级报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.primarySchoolRole?.city?.checkedList}
onCheck={handlePrimaryCityCheck}
defaultExpandAll={true}
treeData={treeData?.primarySchoolRole?.city?.permissionTree}
/>
</Card>
<Card title='区县报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
defaultExpandAll={true}
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.primarySchoolRole?.district?.checkedList}
onCheck={handlePrimaryDistrictCheck}
treeData={treeData?.primarySchoolRole?.district?.permissionTree}
/>
</Card>
<Card title='学校报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
defaultExpandAll={true}
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.primarySchoolRole?.school?.checkedList}
onCheck={handlePrimarySchoolCheck}
treeData={treeData?.primarySchoolRole?.school?.permissionTree}
/>
</Card>
</Flex>
</div>
)
}
if (item == 2) {
return (
<div>
<div className={styles.access_list_title} style={{ marginTop: 8 }}>初中</div>
<Flex style={{ marginTop: 8 }}>
<Card title='市级报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.juniorSchoolRole?.city?.checkedList}
onCheck={handleJuniorCityCheck}
defaultExpandAll={true}
treeData={treeData?.juniorSchoolRole?.city?.permissionTree}
/>
</Card>
<Card title='区县报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
defaultExpandAll={true}
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.juniorSchoolRole?.district?.checkedList}
onCheck={handleJuniorDistrictCheck}
treeData={treeData?.juniorSchoolRole?.district.permissionTree}
/>
</Card>
<Card title='学校报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
defaultExpandAll={true}
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.juniorSchoolRole?.school?.checkedList}
onCheck={handleJuniorSchoolCheck}
treeData={treeData?.juniorSchoolRole?.school.permissionTree}
/>
</Card>
</Flex>
</div>
)
}
if (item == 3) {
return (
<div>
<div className={styles.access_list_title} style={{ marginTop: 8 }}>高中</div>
<Flex style={{ marginTop: 8 }}>
<Card title='市级报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.highSchoolRole?.city?.checkedList}
onCheck={handleHighCityCheck}
defaultExpandAll={true}
treeData={treeData?.highSchoolRole?.city?.permissionTree}
/>
</Card>
<Card title='区县报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
defaultExpandAll={true}
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.highSchoolRole?.district?.checkedList}
onCheck={handleHighDistrictCheck}
treeData={treeData?.highSchoolRole?.district.permissionTree}
/>
</Card>
<Card title='学校报告' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
defaultExpandAll={true}
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.highSchoolRole?.school?.checkedList}
onCheck={handleHighSchoolCheck}
treeData={treeData?.highSchoolRole?.school?.permissionTree}
/>
</Card>
</Flex>
</div>
)
}
}) : null}
{stageIds?.length > 0 ? (
<>
<div className={styles.access_list_title} style={{ marginTop: 8 }}>数据导入</div>
<Flex style={{ marginTop: 8 }}>
<Card title='数据导入' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.importDataRole?.upload?.checkedList}
onCheck={handleDataImportCheck}
defaultExpandAll={true}
treeData={treeData?.importDataRole?.upload?.permissionTree}
/>
</Card>
<Card title='次级管理员' hoverable style={{ width: 300, height: 320, overflow: "auto", marginLeft: 8 }}>
<Tree
checkable
defaultExpandAll={true}
fieldNames={{ title: 'label', key: 'id', children: "children" }}
defaultCheckedKeys={treeData?.importDataRole?.report?.checkedList}
onCheck={handleSecondaryAdminCheck}
treeData={treeData?.importDataRole?.report.permissionTree}
/>
</Card>
</Flex>
</>
) : null}
</div>
</Form.Item>
<Form.Item>
<Flex style={{ width: '100%' }}>
<Space>
<Button onClick={() => {
setOpen(false);
setRoleId("");
form.resetFields();
setPermissions([]);
setTreeData({});
}}>取消</Button>
<Button type='primary' loading={confirmLoading} onClick={handleFinish}>确定</Button>
</Space>
</Flex>
</Form.Item>
</Form>
</Drawer>
</div>
);
};
export default AccessPage;

View File

@ -0,0 +1,42 @@
.access{
width: 100%;
height: 100%;
// background-color: salmon;
display: flex;
flex-direction: column;
&_header{
height: 137px;
// background: sandybrown;
box-sizing: border-box;
padding: 16px;
}
&_main{
flex: 1;
box-sizing: border-box;
padding: 16px;
// background: yellow;
overflow: auto;
}
&_footer{
height: 80px;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 32px;
justify-content: flex-end;
// background: sandybrown;
}
&_list{
box-sizing: border-box;
padding: 8px;
border: 1px solid #eee;
height: 520px;
// background-color: salmon;
overflow: scroll;
&_title{
color: #333;
font-size: 16px;
line-height: 24px;
}
}
}

View File

@ -0,0 +1,571 @@
import { useState, useEffect, useMemo } from 'react';
import { Button,Form, Input,Select, Space, Card, Table, Tag, Badge, Modal, Radio, message, Flex, Typography, Drawer,Pagination } from 'antd';
import { getUserList, deleteUser, resetPassword, getDistrictSelect, getSchoolSelect,getUserDetail,saveUser } from '@/apis/user';
import { logger } from '@/apis/auth';
import { getRoleSelect } from '@/apis/role';
import styles from './index.less';
const RoleTypeBadges = {
0: <Badge color="lime" text="教师" />,
1: <Badge color="geekblue" text="教研员" />,
2: <Badge color="purple" text="校长" />,
3: <Badge color="yellow" text="院长" />,
4: <Badge color="cyan" text="局长" />,
}
const roleTypeOptions = [
{
value: 0,
label: '教师',
},
{
value: 1,
label: '教研员',
},
{
value: 2,
label: '校长',
},
{
value: 3,
label: '院长',
},
{
value: 4,
label: '局长',
},
];
const AccountPage = () => {
const [form] = Form.useForm();
const [tableLoading, setTableLoading] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [dataSource, setDataSource] = useState([]);
const [open, setOpen] = useState(false);
const [disabled, setDisabled] = useState(false);
const [districtOptions, setDistrictOptions] = useState([]);
const [schoolOptions, setSchoolOptions] = useState([]);
const [roleOptions, setRoleOptions] = useState([]);
const [districts, setDistricts] = useState([]);
const [userId, setUserId] = useState('');
const [total,setTotal] =useState(0);
const [pageNum,setPageNum] = useState(1);
const [pageSize,setPageSize] = useState(10);
const computedTitle = useMemo(() => {
if (userId) {
return '编辑账号';
}
return '新增账号';
}, [userId]);
useEffect(() => {
init();
getDistrictSelectData();
getRoleSelectData();
}, []);
useEffect(() => {
if (userId) {
getUserDetailData();
}
}, [userId]);
useEffect(() => {
if (districts?.length > 0) {
getSchoolSelectData();
}
}, [districts])
const init = async () => {
try {
setTableLoading(true);
const res = await getUserList({pageNum,pageSize});
if (res?.code === 200) {
setTableLoading(false);
setDataSource(res?.data || []);
setTotal(res?.total || 0);
return;
}
setTableLoading(false);
} catch (error) {
setTableLoading(false);
console.log(error, 'init-error');
}
};
const getDistrictSelectData = async () => {
try {
const res = await getDistrictSelect();
if (res.code === 200) {
setDistrictOptions(() => {
return res?.data?.map(item => {
return {
label: item.districtName,
value: item.id
}
})
})
return;
}
message.error(res?.msg);
} catch (error) {
console.log(error, 'getDistrictSelectData-error');
}
};
const getSchoolSelectData = async () => {
try {
const res = await getSchoolSelect({ baseDistrictIds: districts.join(',') });
if (res?.code === 200) {
setSchoolOptions(() => {
return res?.data?.map(item => {
return {
label: item.cname,
value: item.id
}
})
})
return;
}
message.error(res?.msg);
} catch (error) {
console.log(error, 'getSchoolSelectData-error');
}
};
const getRoleSelectData = async () => {
try {
const res = await getRoleSelect();
if (res?.code === 200) {
setRoleOptions(()=>{
return res?.data?.map(item=>{
return {
label:item.roleName,
value:item.id
}
})
})
return;
}
message.error(res?.msg);
} catch (error) {
console.log(error, 'getSchoolSelectData-error');
}
};
const getUserDetailData = async () => {
try {
const res = await getUserDetail({ userId, });
if (res?.code === 200) {
setDistricts(res.data.districts);
form.setFieldsValue(res.data);
}
} catch (error) {
console.log(error, 'getDistrictDetailData-error');
}
};
const handleCreateAccount = () => {
setOpen(true);
setUserId('');
form.resetFields();
setDisabled(false);
};
const handleFinish = async () => {
setConfirmLoading(true);
form.validateFields().then(async () => {
const res = await saveUser(userId ? {
id: userId,
...form.getFieldsValue()
} : form.getFieldsValue());
if (res.code === 200) {
setConfirmLoading(false);
setOpen(false);
init();
message.success('操作成功');
if(userId){
await logger({description:"创建账号"});
}else{
await logger({description:"修改账号"});
}
return;
}
setConfirmLoading(false);
}).catch(error => {
setConfirmLoading(false);
console.log(error, 'handleFinish-error');
});
};
const handleDeleteUser = (record) => {
Modal.confirm({
title: '删除账号',
content: '确定删除账号吗?',
okText: '确定',
cancelText: '取消',
centered: true,
onOk() {
return new Promise((resolve, reject) => {
deleteUser({ userId: record?.id }).then(async(res) => {
if (res.code === 200) {
message.success('删除账号成功');
resolve();
init();
await logger({description:"删除账号"});
return
}
message.error(res?.msg);
reject();
}).catch(error => {
console.log(error, 'handleResetPassword-error');
reject();
})
});
},
});
};
const handleResetPassword = (record) => {
Modal.confirm({
title: '重置密码',
content: '确定重置密码吗?',
okText: '确定',
cancelText: '取消',
centered: true,
onOk() {
return new Promise((resolve, reject) => {
resetPassword({ userId: record?.id }).then(async(res) => {
if (res.code === 200) {
message.success('重置密码成功');
init();
resolve();
await logger({description:"重置密码"});
return
}
message.error(res?.msg);
reject();
}).catch(error => {
console.log(error, 'handleResetPassword-error');
reject();
})
});
},
});
};
const onPageChange = async (pageNum, pageSize) => {
setPageNum(pageNum);
setPageSize(pageSize);
try {
setTableLoading(true);
const res = await getUserList({pageNum,pageSize});
if (res?.code === 200) {
setTableLoading(false);
setDataSource(res?.data || []);
setTotal(res?.total || 0);
return;
}
setTableLoading(false);
} catch (error) {
setTableLoading(false);
console.log(error, 'init-error');
}
};
const columns = [
{
title: '账号名称',
dataIndex: 'account',
key: 'account',
width: 200,
align: 'center',
},
{
title: '使用者名称',
dataIndex: 'userName',
key: 'userName',
width: 200,
align: 'center',
},
{
title: '区域',
dataIndex: 'district',
key: 'district',
// width: 360,
align: 'center',
render(value) {
if (!value?.length) {
return '-';
}
return <>
{value?.map(item => {
return (
<Space style={{ width: '100%' }} wrap>
<Tag bordered={false} color="cyan">
{item?.name}
</Tag>
{item?.schools?.length > 0 ? (
<>
{item?.schools?.map(item => {
return (
<Tag bordered={false} color="geekblue">
{item}
</Tag>
)
})}
</>
) : null}
</Space>
)
})}
</>
}
},
{
title: '角色',
dataIndex: 'roleName',
key: 'roleName',
width: 220,
align: 'center',
},
{
title: '是否为管理员',
dataIndex: 'isAdmin',
key: 'isAdmin',
width: 140,
align: 'center',
render(value) {
if (value === 1) {
return <Badge status="success" text="是" />;
}
if (value === 0) {
return <Badge status="warning" text="否" />;
}
}
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80,
align: 'center',
render(value) {
if (value === 0) {
return <Badge status="success" text="启用" />;
}
if (value === 1) {
return <Badge status="error" text="禁用" />;
}
}
},
{
title: '使用者称谓',
dataIndex: 'userRoleType',
key: 'userRoleType',
width: 120,
align: 'center',
render: (value) => RoleTypeBadges?.[value],
},
{
title: '操作',
key: 'action',
align: 'center',
width: 140,
render(_, record) {
return (
<Space size="middle">
<Button type="link" onClick={() => {
setOpen(true);
setUserId(record?.id);
setDisabled(true);
}}>编辑</Button>
<Button type='link' onClick={() => handleResetPassword(record)}>重置密码</Button>
<Button type='link' danger onClick={() => handleDeleteUser(record)}>删除</Button>
</Space>
);
}
},
];
return (
<div className={styles.account}>
<header className={styles.account_header}>
<Card style={{ height: "100%" }}>
<Flex justify="space-between">
<Typography.Title level={4} >
创建账号
</Typography.Title>
<Button type="primary" onClick={handleCreateAccount}>
创建账号
</Button>
</Flex>
</Card>
</header>
<main className={styles.account_main}>
<Card>
<Table scroll={{ x: 1200 }} bordered columns={columns} loading={tableLoading} dataSource={dataSource} pagination={false} style={{ height: '100%' }} />
</Card>
</main>
<footer className={styles.account_footer}>
<Pagination showQuickJumper showSizeChanger total={total} onChange={onPageChange} showTotal={(total)=>`${total}条数据`} />
</footer>
<Drawer
title={computedTitle}
open={open}
destroyOnClose
onClose={() => {
setOpen(false);
setUserId('');
form.resetFields();
}}
size="large"
>
<Form
form={form}
layout="vertical"
>
<Form.Item label="账号名称" name="account" rules={[
{
required: true,
message: '请填写账号名称',
},
]}>
<Input placeholder="请输入账号名称" disabled={disabled} />
</Form.Item>
<Form.Item label="使用者名称" name="userName" rules={[
{
required: true,
message: '请填写使用者名称',
},
]}>
<Input placeholder="请输入使用者名称" />
</Form.Item>
<Form.Item label="账号权限范围" style={{ marginBottom: 0 }}>
<Form.Item name='districts' rules={[
{
required: true,
message: '请选择区县',
},
]} style={{ display: 'inline-block', width: 'calc(50% - 8px)' }} >
<Select maxTagCount={2} options={districtOptions} placeholder="请选择区县" mode="multiple" allowClear onChange={(value) => {
if (!value?.length) {
setDistricts([]);
return;
}
setDistricts(value);
form.setFieldValue('districts', value);
}} />
</Form.Item>
<Form.Item name='schools' style={{ display: 'inline-block', width: 'calc(50% - 8px)', margin: '0 8px' }}>
<Select maxTagCount={1} options={schoolOptions} placeholder="请选择学校" mode="multiple" allowClear onChange={(value) => {
if (!value?.length) {
return;
}
form.setFieldValue('schools', value);
}} />
</Form.Item>
</Form.Item>
<Form.Item label="角色" name='roleId' rules={[
{
required: true,
message: "请选择角色"
},
]}>
<Select options={roleOptions} placeholder="请选择角色" />
</Form.Item>
<Form.Item label="是否启为管理员" name='isAdmin' rules={[
{
required: true,
message: "请选择是否为管理员"
},
]}>
<Radio.Group
options={[
{ value: 0, label: '否' },
{ value: 1, label: '是' },
]}
/>
</Form.Item>
<Form.Item label="使用者称谓" name='userRoleType' rules={[
{
required: true,
message: "请选择使用者称谓"
},
]}>
<Select options={roleTypeOptions} placeholder="请选择使用者称谓" />
</Form.Item>
<Form.Item label="是否启用账号" name='status' rules={[
{
required: true,
message: "请选择是否启用账号"
},
]}>
<Radio.Group
options={[
{ value: 0, label: '启用' },
{ value: 1, label: '禁用' },
]}
/>
</Form.Item>
<Form.Item>
<Flex style={{ width: '100%' }}>
<Space>
<Button onClick={() => {
setOpen(false);
setUserId('');
form.resetFields();
}}>取消</Button>
<Button type='primary' loading={confirmLoading} onClick={handleFinish}>确定</Button>
</Space>
</Flex>
</Form.Item>
</Form>
</Drawer>
</div>
);
};
export default AccountPage;

View File

@ -0,0 +1,29 @@
.account{
width: 100%;
height: 100%;
// background-color: salmon;
display: flex;
flex-direction: column;
&_header{
height: 137px;
// background: sandybrown;
box-sizing: border-box;
padding: 16px;
}
&_main{
flex: 1;
box-sizing: border-box;
padding: 16px;
// background: yellow;
overflow: auto;
}
&_footer{
height: 80px;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 32px;
justify-content: flex-end;
// background: sandybrown;
}
}

View File

@ -0,0 +1,179 @@
import { useState,useEffect } from 'react';
import navIconSrc from '@/assets/icon/analysis-navigation-icon.png';
import sectionSrc1 from '@/assets/icon/analysis-catalogue-1.png';
import sectionSrc2 from '@/assets/icon/analysis-catalogue-2.png';
import sectionSrc3 from '@/assets/icon/analysis-catalogue-3.png';
import sectionSrc4 from '@/assets/icon/analysis-catalogue-4.png';
import sectionSrc5 from '@/assets/icon/analysis-catalogue-5.png';
import sectionSrc6 from '@/assets/icon/analysis-catalogue-6.png';
import sectionSrc7 from '@/assets/icon/analysis-catalogue-7.png';
import sectionSrc8 from '@/assets/icon/analysis-catalogue-8.png';
import styles from './index.less';
const Catalogue = (props) => {
const { idx = '',modelList = [] } = props;
const [list,setList] = useState([]);
useEffect(()=>{
const arr = [];
modelList?.forEach(element => {
if(element?.reportItemModel?.id === 3){
arr.push({
title: '上线情况分析',
// text1: '考试排名分析',
// text2: '考试排名百分比分析',
icon: sectionSrc1,
left: 0
});
}
if([4,5,6].includes(element?.reportItemModel?.id)){
arr.push({
title: '学校对比分析',
// text1: '教师群体分析',
// text2: '教师绩效Z值分析',
icon: sectionSrc2,
left: 0
})
}
if([15,16,20].includes(element?.reportItemModel?.id)){
arr.push({
title: '教师群体分析',
// text1: '整体分析',
// text2: '后续措施',
icon: sectionSrc3,
left: 0
})
}
if([7,45].includes(element?.reportItemModel?.id)){
arr.push( {
title: '学生群体分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc4,
left: 0
})
}
if([22,23,24,25].includes(element?.reportItemModel?.id)){
arr.push({
title: '选考分布分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc5,
left: 0
})
}
if([28].includes(element?.reportItemModel?.id)){
arr.push({
title: '试卷质量分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc8,
left: 0
})
}
if([26,27].includes(element?.reportItemModel?.id)){
arr.push( {
title: '学科小题分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc6,
left: 0
})
}
if([47].includes(element?.reportItemModel?.id)){
arr.push({
title: '学科知识点分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc7,
left: 0
},)
}
});
const uniqueArray = arr.filter((item, index, self) =>
index === self.findIndex((t) => t.title === item.title)
);
setList(uniqueArray);
},[modelList])
const catalogueList = [
{
title: '上线情况分析',
// text1: '考试排名分析',
// text2: '考试排名百分比分析',
icon: sectionSrc1,
left: 0
},
{
title: '学校对比分析',
// text1: '教师群体分析',
// text2: '教师绩效Z值分析',
icon: sectionSrc2,
left: 0
},
{
title: '教师群体分析',
// text1: '整体分析',
// text2: '后续措施',
icon: sectionSrc3,
left: 0
},
{
title: '学生群体分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc4,
left: 0
},
{
title: '选考分布分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc5,
left: 0
},
{
title: '学科小题分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc6,
left: 0
},
{
title: '学科知识点分析',
// text1: '尖子生分析',
// text2: '临界生/冒进生分析',
icon: sectionSrc7,
left: 0
},
];
return (
<section className={styles.analysis_content_catalogue} id={`operationExport${idx}`}>
<section className={styles.analysis_content_catalogue_title}>
<section className={styles.page_title}>
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>目录</span>
</section>
</section>
<section className={styles.analysis_content_catalogue_content}>
{list?.map((item, index) => {
return (<section className={styles.analysis_content_catalogue_section} key={index} style={{ marginLeft: item?.left }}>
<img src={item.icon} className={styles.analysis_content_catalogue_section_img} />
<section className={styles.analysis_content_catalogue_section_description}>
<span className={styles.analysis_content_catalogue_section_title}>{item?.title}</span>
{/* <span className={styles.analysis_content_catalogue_section_text}>{item?.text1}&nbsp;&nbsp;&nbsp;&nbsp;{item?.text2}</span> */}
</section>
</section>)
})}
</section>
</section>
)
};
export default Catalogue;

View File

@ -0,0 +1,52 @@
@import '@/pages/analysis/styles/common.less';
.analysis_content{
&_catalogue{
margin-top: 16px;
height: 800px;
background: linear-gradient(134deg, #F7FEFF 0%, #FDFFFF 100%);
box-sizing: border-box;
padding: 56px;
&_content{
margin-top: 64px;
}
&_section{
margin-top: 8px;
width: 60%;
height: 64px;
display: flex;
align-items: center;
&_img{
height: 30px;
width: 30px;
}
&_description{
margin-left: 16px;
display: flex;
align-items: end;
}
&_title{
font-size: 26px;
color: #333;
}
&_text{
margin-left: 22px;
font-size: 14px;
color: #333;
}
}
}
}
.page{
&_title{
&:extend(.title);
&_img{
&:extend(.title_img);
}
&_text{
&:extend(.title_text);
}
}
}

View File

@ -0,0 +1,248 @@
import { useState, useEffect, useRef } from 'react';
import { Space, Button, Spin, Table, Tag } from 'antd';
import { getSelectedDistribution } from '@/apis/leader';
import { getNoSingleCombine } from '@/apis/common';
import SchoolChoice from '@/components/SchoolChoice';
import Echarts from "@/components/Echarts";
import { tableCommonProps } from '@/utils/config.js';
import { downloadTableData } from '@/apis/download';
import { downloadExcelFile } from '@/utils/file';
import sectionSrc from '@/assets/icon/analysis-catalogue-5.png';
import styles from './index.less';
const ChoiceExamAnalysis = (props) => {
const { examId = '', subtitle = '', isShowChart = false, isShowTable = false, idx = '' , actionShow = true } = props;
const [dataSource, setDataSource] = useState([]);
const [columns, setColumns] = useState([]);
const [responseData, setResponseData] = useState({});
const [loading, setLoading] = useState(false);
const [schoolIds, setSchoolIds] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [echartData, setEchartData] = useState({});
const [lengButton, setLengButton] = useState([]);
const echartRef = useRef();
const [subjectId, setSubjectId] = useState('');
const [tags, setTags] = useState([]);
const [selectedTag, setSelectedTag] = useState('');
const expandableConfig = {
expandedRowKeys: expandedRowKeys,
defaultExpandedRowKeys: [dataSource?.[0]?.id,dataSource?.[0]?.children?.[0]?.id],
onExpand: (expanded, record) => {
if (expanded) {
setExpandedRowKeys([...expandedRowKeys, record.id]);
} else {
setExpandedRowKeys(expandedRowKeys?.filter((k) => k !== record.id));
}
},
};
useEffect(() => {
getNoSingleCombineInit();
}, [])
useEffect(() => {
if (!examId) {
return;
}
init();
}, [examId, schoolIds,subjectId]);
const init = async () => {
setLoading(true);
try {
const res = await getSelectedDistribution({
examId,
subjectId,
schools: schoolIds
});
if (res?.code === 200) {
setLoading(false);
setResponseData(res);
setExpandedRowKeys([res?.data?.[0]?.id,res?.data?.[0]?.children?.[0]?.id]);
setDataSource(res?.data || []);
setColumns(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
sorter: item?.sortable ? (a, b) => a[item?.prop] - b[item?.prop] : false
}
})
});
if (!res?.columnDiagramResult?.avg?.length) {
return;
}
setLengButton(res?.columnDiagramResult?.avg?.map(item => item?.lineName));
setEchartData(() => {
return {
xAxis: {
name: "",
type: "category",
axisTick: { show: false },
data: res?.columnDiagramResult?.avg?.[0]?.xdata,
},
yAxis: {
name: '',
},
tooltip: {
show: true,
},
yChartData: res?.columnDiagramResult?.avg?.map(item => {
return {
name: item?.lineName,
data: item?.data
}
})
}
});
}
setLoading(false);
} catch (error) {
setLoading(false);
console.log(error, 'paramsInit-error');
}
};
const getNoSingleCombineInit = async () => {
try {
const list = await getNoSingleCombine();
if (list?.code === 200) {
setTags(list?.data || []);
setSelectedTag(list.data?.[0]?.cname || '');
setSubjectId(list.data?.[0]?.id || '');
}
} catch (error) {
console.log(error, 'getNoSingleCombineInit-error');
}
}
const handleDownload = async () => {
try {
const res = await downloadTableData(responseData);
if (res?.status === 200) {
downloadExcelFile(res, `${subtitle}*${selectedTag}考试选考组合分析`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
const handleNavTagClick = (tag) => {
setSelectedTag(tag?.cname || '');
setSubjectId(tag?.id || '');
};
return (
<div>
{isShowChart ? (
<section className={styles.analysis_content_chart} id={`operationExport${idx}`}>
<section className={styles.analysis_content_chart_title}>
<section className={styles.page_title}>
<img src={sectionSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}考试选考组合分析-图表</span>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</section>
{actionShow && (<Button type='primary' style={{ fontSize: 16 }} onClick={() => {
echartRef?.current?.exportChart();
}}>导出图表</Button>)}
</section>
<section className={styles.analysis_content_chart_content}>
<Space style={{marginBottom:8}}>
{tags.length > 0 ? (
tags.map((item, index) => {
const isSelected = selectedTag === item?.cname;
return (
<Tag
onClick={() => handleNavTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.cname}
</Tag>
);
})
) : null}
</Space>
<Spin spinning={loading} tip='计算中,请稍后'>
<Echarts
data={echartData}
text={`考试选考组合分析*${subtitle}*${selectedTag}`}
type={"bar"}
lengButton={lengButton}
ref={echartRef}
hasTable={false}
hasExport={false}
/>
</Spin>
</section>
</section>
) : null}
{isShowTable ? (
<section className={styles.analysis_content_table} id={`operationExport${idx + 1}`}>
<section className={styles.analysis_content_table_title}>
<section className={styles.page_title}>
<img src={sectionSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}考试选考组合分析-表格</span>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</section>
{actionShow && (<Button type='primary' style={{ fontSize: 16 }} onClick={handleDownload}>导出表格</Button>)}
</section>
<section className={styles.analysis_content_table_content}>
<Space style={{marginBottom:8}}>
{tags.length > 0 ? (
tags.map((item, index) => {
const isSelected = selectedTag === item?.cname;
return (
<Tag
onClick={() => handleNavTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.cname}
</Tag>
);
})
) : null}
</Space>
<Table
{...tableCommonProps}
dataSource={dataSource}
columns={columns}
locale={{ emptyText: loading ? "计算中,请稍后" : "暂无数据" }}
loading={{ spinning: loading, tip: "计算中,请稍后" }}
expandable={expandableConfig}
>
</Table>
</section>
</section>
) : null}
</div>
);
};
export default ChoiceExamAnalysis;

View File

@ -0,0 +1,33 @@
@import '@/pages/analysis/styles/common.less';
.analysis_content{
&_chart{
&:extend(.analysis_content_chart);
&_title{
&:extend(.analysis_content_chart_title);
}
&_content{
&:extend(.analysis_content_chart_content);
}
}
&_table{
&:extend(.analysis_content_table);
&_title{
&:extend(.analysis_content_table_title);
}
&_content{
&:extend(.analysis_content_table_content);
}
}
}
.page{
&_title{
&:extend(.title);
&_img{
&:extend(.title_img);
}
&_text{
&:extend(.title_text);
}
}
}

View File

@ -0,0 +1,300 @@
import { useState, useEffect, useRef } from 'react';
import { Space, Select, Button, Spin, Table } from 'antd';
import { getAnswerRate } from '@/apis/leader';
import { getSubjectList } from '@/apis/common';
import SchoolChoice from '@/components/SchoolChoice';
import Echarts from "@/components/Echarts";
import { tableCommonProps } from '@/utils/config.js';
import { downloadTableData } from '@/apis/download';
import { downloadExcelFile } from '@/utils/file';
import sectionSrc from '@/assets/icon/analysis-catalogue-6.png';
import styles from './index.less';
const ChoiceQuestionRate = (props) => {
const { examId = '', subtitle = '', isShowChart = false, isShowTable = false, idx = '' ,actionShow = true} = props;
const [dataSource, setDataSource] = useState([]);
const [columns, setColumns] = useState([]);
const [responseData, setResponseData] = useState({});
const [loading, setLoading] = useState(false);
const [subjectId, setSubjectId] = useState("");
const [subjectName, setSubjectName] = useState(null);
const [schoolIds, setSchoolIds] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [echartData1, setEchartData1] = useState({});
const [echartData2, setEchartData2] = useState({});
const [lengButton, setLengButton] = useState([]);
const echartRef1 = useRef();
const echartRef2 = useRef();
const [singleSubjectList, setSingleSubjectList] = useState([]);
const expandableConfig = {
expandedRowKeys: expandedRowKeys,
expandIconColumnIndex: 2,
onExpand: (expanded, record) => {
if (expanded) {
setExpandedRowKeys([...expandedRowKeys, record.id]);
} else {
setExpandedRowKeys(expandedRowKeys?.filter((k) => k !== record.id));
}
},
};
useEffect(() => {
singleSubjectInit();
}, []);
useEffect(() => {
if (!examId || !subjectId) {
return;
}
init();
}, [examId, schoolIds, subjectId]);
const init = async () => {
setLoading(true);
try {
const res = await getAnswerRate({
examId,
subjectId: subjectId,
schools: schoolIds
});
if (res?.code === 200) {
setLoading(false);
setResponseData(res);
setDataSource(res?.data || []);
setColumns(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
sorter: item?.sortable ? (a, b) => a[item?.prop] - b[item?.prop] : false
}
})
});
if (!res?.columnDiagramResult?.rate?.[0]?.data?.length || !res?.columnDiagramResult?.rate?.[0]?.xdata?.length) {
return;
}
setLengButton([res?.columnDiagramResult?.rate?.[0]?.lineName]);
setEchartData1(() => {
return {
xAxis: {
name: "",
type: "category",
axisTick: { show: false },
data: res?.columnDiagramResult?.rate?.[0]?.xdata,
},
yAxis: {
name: '',
},
tooltip: {
show: true,
},
yChartData: [
{
name: res?.columnDiagramResult?.rate?.[0]?.lineName,
data: res?.columnDiagramResult?.rate?.[0]?.data
}
],
}
});
setEchartData2(() => {
return {
radarText: {
indicator: res?.columnDiagramResult?.rate?.[0]?.xdata?.map(item => {
return {
name: item,
max: 100
}
}),
yChartData: [
{
name: res?.columnDiagramResult?.rate?.[0]?.lineName,
data: res?.columnDiagramResult?.rate?.[0]?.data,
},
],
},
}
})
}
setLoading(false);
} catch (error) {
setLoading(false);
console.log(error, 'init-error');
}
};
const singleSubjectInit = async () => {
try {
const res = await getSubjectList();
if (res?.code === 200) {
const element = res?.data?.find(item => item?.cname === '数学');
setSubjectId(element?.id || '');
setSubjectName(element?.cname || null);
setSingleSubjectList(() => {
return res?.data?.map(item => {
return {
label: item?.cname,
value: item?.id
}
})
})
}
} catch (error) {
console.log(error, 'singleSubjectInit-error');
}
};
const handleDownload = async () => {
try {
const res = await downloadTableData(responseData);
if (res?.status === 200) {
downloadExcelFile(res, `${subtitle}*${subjectName}考试选择题答题率`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
return (
<div>
{isShowChart ? (
<>
<section className={styles.analysis_content_chart} id={`operationExport${idx}`}>
<section className={styles.analysis_content_chart_title}>
<section className={styles.page_title}>
<img src={sectionSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}考试选择题答题率-图表</span>
<Space>
<Select
placeholder="请选择学科"
options={singleSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
onChange={(value, options) => {
if (!value) {
return;
}
setSubjectName(options?.label);
setSubjectId(value);
}}
/>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && ( <Button type='primary' style={{ fontSize: 16 }} onClick={() => {
echartRef1?.current?.exportChart();
}}>导出图表</Button>)}
</section>
<section className={styles.analysis_content_chart_content}>
<Spin spinning={loading} tip='计算中,请稍后'>
<Echarts
text={`考试选择题答题率*${subtitle}${subjectName}*折线图`}
data={echartData1}
ref={echartRef1}
isShow={false}
type={"line"}
needTab={true}
lengButton={lengButton}
hasExport={false}
hasTable={false}
/>
</Spin>
</section>
</section>
<section className={styles.analysis_content_chart} id={`operationExport${idx + 1}`}>
<section className={styles.analysis_content_chart_title}>
<section className={styles.page_title}>
<img src={sectionSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}考试选择题答题率-图表</span>
<Space>
<Select
placeholder="请选择学科"
options={singleSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
onChange={(value, options) => {
if (!value) {
return;
}
setSubjectName(options?.label);
setSubjectId(value);
}}
/>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && (<Button type='primary' style={{ fontSize: 16 }} onClick={() => {
echartRef2?.current?.exportChart();
}}>导出图表</Button>)}
</section>
<section className={styles.analysis_content_chart_content}>
<Spin spinning={loading} tip='计算中,请稍后'>
<Echarts
text={`考试选择题答题率*${subtitle}${subjectName}*雷达图`}
data={echartData2}
ref={echartRef2}
isShow={false}
type={"radar"}
needTab={true}
lengButton={lengButton}
hasExport={false}
hasTable={false}
/>
</Spin>
</section>
</section>
</>
) : null}
{isShowTable ? (<section className={styles.analysis_content_table} id={`operationExport${idx + 2}`}>
<section className={styles.analysis_content_table_title}>
<section className={styles.page_title}>
<img src={sectionSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}考试选择题答题率-表格</span>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</section>
{actionShow && ( <Button type='primary' style={{ fontSize: 16 }} onClick={handleDownload}>导出表格</Button>)}
</section>
<section className={styles.analysis_content_table_content}>
<Table
{...tableCommonProps}
dataSource={dataSource}
columns={columns}
locale={{ emptyText: loading ? "计算中,请稍后" : "暂无数据" }}
loading={{ spinning: loading, tip: "计算中,请稍后" }}
expandable={expandableConfig}
>
</Table>
</section>
</section>) : null}
</div>
)
};
export default ChoiceQuestionRate;

View File

@ -0,0 +1,33 @@
@import '@/pages/analysis/styles/common.less';
.analysis_content{
&_chart{
&:extend(.analysis_content_chart);
&_title{
&:extend(.analysis_content_chart_title);
}
&_content{
&:extend(.analysis_content_chart_content);
}
}
&_table{
&:extend(.analysis_content_table);
&_title{
&:extend(.analysis_content_table_title);
}
&_content{
&:extend(.analysis_content_table_content);
}
}
}
.page{
&_title{
&:extend(.title);
&_img{
&:extend(.title_img);
}
&_text{
&:extend(.title_text);
}
}
}

View File

@ -0,0 +1,334 @@
import { useState, useEffect, useRef } from 'react';
import { Space, Button, Spin, Table } from 'antd';
import { getCombinationDistribution } from '@/apis/leader';
import SchoolChoice from '@/components/SchoolChoice';
import Echarts from "@/components/Echarts";
import { tableCommonProps } from '@/utils/config.js';
import { downloadTableData } from '@/apis/download';
import { downloadExcelFile } from '@/utils/file';
import sectionSrc from '@/assets/icon/analysis-catalogue-5.png';
import styles from './index.less';
const CombineExaminationSubject = (props) => {
const { examId = '', subtitle = '', isShowChart = false, isShowTable = false,idx='' ,actionShow = true } = props;
const [dataSource, setDataSource] = useState([]);
const [columns, setColumns] = useState([]);
const [responseData, setResponseData] = useState({});
const [loading, setLoading] = useState(false);
const [schoolIds, setSchoolIds] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [echartData1, setEchartData1] = useState({});
const [echartData2, setEchartData2] = useState({});
const [lengButton, setLengButton] = useState([]);
const echartRef = useRef();
const [selectTag, setSelectTag] = useState('');
const [navTags, setNavTags] = useState([]);
const [allEchartData, setAllEchartData] = useState({});
const [selectedNavTag, setSelectedNavTag] = useState('');
const expandableConfig = {
expandedRowKeys: expandedRowKeys,
defaultExpandedRowKeys: [dataSource?.[0]?.id,dataSource?.[0]?.children?.[0]?.id],
onExpand: (expanded, record) => {
if (expanded) {
setExpandedRowKeys([...expandedRowKeys, record.id]);
} else {
setExpandedRowKeys(expandedRowKeys?.filter((k) => k !== record.id));
}
},
};
useEffect(() => {
if (!selectedNavTag) {
return;
}
const obj = {
num: allEchartData?.num?.filter(item => item?.lineName === selectedNavTag),
rate: allEchartData?.rate?.filter(item => item?.lineName === selectedNavTag),
};
setEchartData1(() => {
return {
xAxis: {
name: "",
type: "category",
axisTick: { show: false },
data: obj.num?.[0]?.xdata,
},
yAxis: {
name: "",
},
tooltip: {
show: true,
},
yChartData: [
{
name: '人数',
data: obj.num?.[0]?.data
}
]
}
});
setEchartData2(() => {
return {
xAxis: {
name: "",
type: "category",
axisTick: { show: false },
data: obj.rate?.[0]?.xdata,
},
yAxis: {
name: "人数占比",
},
tooltip: {
show: true,
},
yChartData: [
{
name: '人数占比',
data: obj.rate?.[0]?.data
}
]
}
});
}, [selectedNavTag]);
useEffect(() => {
if (!examId) {
return;
}
init();
}, [examId, schoolIds]);
const init = async () => {
setLoading(true);
try {
const res = await getCombinationDistribution({
examId,
schools: schoolIds
});
if (res?.code === 200) {
setLoading(false);
setResponseData(res);
setExpandedRowKeys([res?.data?.[0]?.id,res?.data?.[0]?.children?.[0]?.id]);
setDataSource(res?.data || []);
setColumns(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
width: 150,
sorter: item?.sortable ? (a, b) => a[item?.prop] - b[item?.prop] : false,
children: item?.children?.map(element => {
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 160,
sorter: element?.sortable ? (a, b) => a[element?.prop] - b[element?.prop] : false
}
})
}
});
});
if (!res?.columnDiagramResult || !res?.columnDiagramResult?.num?.length > 0 || !res?.columnDiagramResult?.rate?.length > 0) {
return;
}
const selectedName = res?.columnDiagramResult?.num?.map(item => item?.lineName)[0];
setNavTags(res?.columnDiagramResult?.num?.map(item => item?.lineName));
setSelectedNavTag(selectedName);
setLengButton(['人数', '人数占比']);
setSelectTag('人数');
setAllEchartData(res?.columnDiagramResult);
const obj = {
num: res?.columnDiagramResult?.num?.filter(item => item?.lineName === selectedName),
rate: res?.columnDiagramResult?.rate?.filter(item => item?.lineName === selectedName),
};
setEchartData1(() => {
return {
xAxis: {
name: "",
type: "category",
axisTick: { show: false },
data: obj.num?.[0]?.xdata,
},
yAxis: {
name: "人数",
},
tooltip: {
show: true,
},
yChartData: [
{
name: '人数',
data: obj.num?.[0]?.data
}
]
}
});
setEchartData2(() => {
return {
xAxis: {
name: "",
type: "category",
axisTick: { show: false },
data: obj.rate?.[0]?.xdata,
},
yAxis: {
name: "人数占比",
},
tooltip: {
show: true,
},
yChartData: [
{
name: '人数占比',
data: obj.rate?.[0]?.data
}
]
}
});
}
setLoading(false);
} catch (error) {
setLoading(false);
console.log(error, 'paramsInit-error');
}
};
const handleDownload = async () => {
try {
const res = await downloadTableData(responseData);
if (res?.status === 200) {
downloadExcelFile(res, `${subtitle}*${selectedNavTag}选考组合分布分析`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
return (
<div>
{isShowChart ? (
<section className={styles.analysis_content_chart} id={`operationExport${idx}`}>
<section className={styles.analysis_content_chart_title}>
<section className={styles.page_title}>
<img src={sectionSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}选考组合分布分析-图表</span>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</section>
{actionShow && (<Button type='primary' style={{ fontSize: 16 }} onClick={() => {
echartRef?.current?.exportChart();
}}>导出图表</Button>)}
</section>
<section className={styles.analysis_content_chart_content}>
<Space style={{ marginBottom: 8 }}>
{navTags?.map((item, index) => {
return (
<Button
key={index}
type={item === selectedNavTag ? 'primary' : 'default'}
size="small"
onClick={() => {
setSelectedNavTag(item);
}}
>
{item}
</Button>
)
})}
</Space>
<Spin spinning={loading} tip='计算中,请稍后'>
{lengButton?.length > 0 ? (
<Space>
{lengButton?.map((item, index) => {
return (
<Button
key={index}
type={item === selectTag ? 'primary' : 'default'}
size="small"
onClick={() => {
setSelectTag(item);
}}
>
{item}
</Button>
)
})}
</Space>
) : null}
{selectTag === '人数' ? (<Echarts
isShow={false}
data={echartData1}
hasExport={false}
ref={echartRef}
type='bar'
text={`选考组合分布分析*${subtitle}${selectedNavTag}*人数`}
lengButton={['人数']}
hasTable={false}
/>) : null}
{selectTag === '人数占比' ? (<Echarts
isShow={false}
type="bar"
data={echartData2}
ref={echartRef}
hasTable={false}
hasExport={false}
text={`选考组合分布分析*${subtitle}${selectedNavTag}*人数占比`}
lengButton={['人数占比']}
/>) : null}
</Spin>
</section>
</section>
) : null}
{isShowTable ? (
<section className={styles.analysis_content_table} id={`operationExport${idx + 1}`}>
<section className={styles.analysis_content_table_title}>
<section className={styles.page_title}>
<img src={sectionSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}选考组合分布分析-表格</span>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</section>
{actionShow && ( <Button type='primary' style={{ fontSize: 16 }} onClick={handleDownload}>导出表格</Button>)}
</section>
<section className={styles.analysis_content_table_content}>
<Table
{...tableCommonProps}
dataSource={dataSource}
columns={columns}
locale={{ emptyText: loading ? "计算中,请稍后" : "暂无数据" }}
loading={{ spinning: loading, tip: "计算中,请稍后" }}
expandable={expandableConfig}
>
</Table>
</section>
</section>
) : null}
</div>
);
};
export default CombineExaminationSubject;

View File

@ -0,0 +1,33 @@
@import '@/pages/analysis/styles/common.less';
.analysis_content{
&_chart{
&:extend(.analysis_content_chart);
&_title{
&:extend(.analysis_content_chart_title);
}
&_content{
&:extend(.analysis_content_chart_content);
}
}
&_table{
&:extend(.analysis_content_table);
&_title{
&:extend(.analysis_content_table_title);
}
&_content{
&:extend(.analysis_content_table_content);
}
}
}
.page{
&_title{
&:extend(.title);
&_img{
&:extend(.title_img);
}
&_text{
&:extend(.title_text);
}
}
}

View File

@ -0,0 +1,807 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { Space, Select, Button, Spin, Table, Tag,InputNumber,message } from 'antd';
import * as echarts from 'echarts';
import { getOnlineRankingAnalysis } from '@/apis/exam';
import { getSubjectList } from '@/apis/subject';
import { getScoreLineList } from '@/apis/data'
import { getSubjectListAll } from '@/apis/common';
import SchoolChoice from '@/components/SchoolChoice';
import StudentGroup from '@/components/StudentGroup';
import { tableCommonProps } from '@/utils/config.js';
import { downloadTableData } from '@/apis/download';
import { downloadExcelFile } from '@/utils/file';
import navIconSrc from '@/assets/icon/analysis-navigation-icon.png';
import styles from './index.less';
const ContrastExamOnline = (props) => {
const { examId = '', subtitle = '', examList = [], isShowChart = false, isShowTable = false, idx = '' ,actionShow = true } = props;
const [dataSource, setDataSource] = useState([]);
const [columns, setColumns] = useState([]);
const [responseData, setResponseData] = useState({});
const [loading, setLoading] = useState(false);
const [subjectId, setSubjectId] = useState("");
const [subjectName, setSubjectName] = useState(null);
const [schoolIds, setSchoolIds] = useState([]);
const [allSubjectList, setAllSubjectList] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [typeList, setTypeList] = useState([]);
const [selectType, setSelectType] = useState('');
const [tags, setTags] = useState([]);
const [selectedTags, setSelectedTags] = useState([]);
const [echartXData, setEchartXData] = useState([]);
const [mockChartData, setMockChartData] = useState([]);
const chartRef = useRef(null);
let chartInstance = null;
const [echartImg, setEchartImg] = useState();
const studentGroupRef = useRef();
const [title, setTitle] = useState('');
const [studentGroupParams, setStudentGroupParams] = useState({});
const [selectValues, setSelectValues] = useState([]);
const [subjectIds, setSubjectIds] = useState([]);
const [subjectOptions, setSubjectOptions] = useState([]);
const [scoreLine, setScoreLine] = useState({});
const [score, setScore] = useState(null);
const [scoreLineList, setScoreLineList] = useState([]);
const [allEchartData, setAllEchartData] = useState({});
const [rankingList, setRankingList] = useState([]);
const [selectRaking, setSelectRaking] = useState();
const labelOption = {
show: true,
position: 'top',
distance: 24,
align: 'center',
verticalAlign: 'middle',
formatter: (params) => {
if (['增幅','上线率','上线率增长'].includes(selectType)) {
return `${params.value}%`;
}
return `${params.value}`
},
fontSize: 12,
rich: {
name: {}
}
};
const commonSeries = {
type: 'line',
barGap: 0,
label: labelOption,
emphasis: {
focus: 'series'
},
};
const intersectionData = useMemo(() => {
return mockChartData?.filter((item) =>
selectedTags.includes(item?.name),
);
}, [selectedTags, mockChartData]);
useEffect(() => {
// allSubjectInit();
getSortInit();
getSubjectSelectOptions();
}, []);
useEffect(() => {
if (!examId || !examList.length > 0) {
return;
}
init();
}, [examId, schoolIds, examList, selectRaking, scoreLine]);
useEffect(() => {
if (selectType) {
setSelectType(selectType);
if (selectType === '上线人数') {
setMockChartData(() => {
return allEchartData.num?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
if (selectType === '上线人数增长') {
setMockChartData(() => {
return allEchartData?.incr?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
if (selectType === '上线率') {
setMockChartData(() => {
return allEchartData?.rate?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
if (selectType === '上线率增长') {
setMockChartData(() => {
return allEchartData?.rateRatio?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
if (selectType === '增幅') {
setMockChartData(() => {
return allEchartData?.zf?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
}
}, [selectType]);
useEffect(()=>{
if(!scoreLineList?.length){
setScoreLine({});
}
},[scoreLineList])
useEffect(() => {
if (!echartXData.length) {
return;
}
if (chartRef.current) {
// 初始化图表
chartInstance = echarts.init(chartRef.current);
// 设置配置项
const option = {
// dataZoom: [
// {
// type: 'slider', // 滑动条型数据区域缩放组件
// show: true,
// start: 0, // 初始起点
// end: 30, // 初始终点
// handleSize: 8,
// xAxisIndex: [0]
// },
// ],
title: {
// 添加标题配置
text: `${subtitle}${scoreLineList?.length > 0 ? scoreLine?.name : rankingList?.find(item=>item?.value === selectRaking)?.label}*对比考试上线情况分析-图表`,
left: 'center',
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 'bold',
},
},
axisLabel: {
rotate: 35,
interval: 0,
fontSize: 9,
autoSkip: true,
},
barGap: '10%',
barWidth: "10%",
xAxis: {
name: '考试',
type: 'category',
axisTick: { show: false },
data: echartXData,
axisLabel: {
rotate: echartXData?.length > 6 ? 45 : 0,
interval: 0,
textStyle: {
fontSize: 10,
}
},
nameTextStyle: {
fontSize: 16,
}
},
grid: {
containLabel: true,
top: "20%"
},
yAxis: {
name: "",
axisLabel: {
textStyle: {
fontSize: 16,
}
},
nameTextStyle: {
fontSize: 16,
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
type: "scroll",
data: tags,
right: 10, // 设置右侧边距
top: 16, // 设置顶部边距
selectedMode: false,
textStyle: {
fontSize: 12
}
},
series: intersectionData.map((item) => ({
...commonSeries,
name: item?.name || '',
data: item?.data || [],
}))
};
chartInstance.setOption(option);
// 响应式调整
const resizeHandler = () => chartInstance?.resize();
window.addEventListener('resize', resizeHandler);
// 清理函数
return () => {
window.removeEventListener('resize', resizeHandler);
chartInstance?.dispose();
};
}
}, [selectedTags, echartXData, mockChartData]);
const init = async () => {
setLoading(true);
try {
const res = await getOnlineRankingAnalysis({
examId,
examIds: examList.map(item => item.id),
lineId: selectRaking,
schools: schoolIds,
// subjectId:subjectIds,
scoreLine,
});
if (res.code === 200) {
setLoading(false);
setResponseData(res);
setDataSource(res?.data || []);
setExpandedRowKeys([res?.data?.[0]?.id, res?.data?.[0]?.children?.[0]?.id]);
setTags(res?.columnDiagramResult?.num?.map(item => item?.lineName) || []);
setSelectedTags([res?.columnDiagramResult?.num?.[0]?.lineName || '']);
setTypeList(['上线人数', '上线人数增长', '上线率', '上线率增长', '增幅']);
setSelectType('上线人数');
setEchartXData(res?.columnDiagramResult?.num?.[0]?.xdata || []);
setAllEchartData(res?.columnDiagramResult);
setMockChartData(() => {
return res?.columnDiagramResult?.num?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
setColumns(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
width: ['区县', '考试人数']?.includes(item?.label) ? 200 : 480,
children: item?.children?.map(element => {
if (element?.label=== '上线人数') {
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 120,
render: (text) => (
<Button type="link" style={{ color: "#4ABB6A" }} onClick={() => {
const examId = text?.examId;
setTitle(text?.typeDisc);
studentGroupRef.current?.openModal();
setStudentGroupParams({
examId,
students: text?.data
});
}}>{text?.value}</Button>
),
}
}
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 120,
sorter: (a, b) => a[element?.prop] - b[element?.prop],
}
})
}
})
})
return;
}
setLoading(false);
} catch (error) {
setLoading(false);
console.log(error, 'exam-ranking-error');
}
};
const allSubjectInit = async () => {
try {
const res = await getSubjectListAll();
if (res?.code === 200) {
setSubjectId(res?.data?.[0]?.id || '');
setSubjectName(res?.data?.[0]?.cname || null);
setAllSubjectList(() => {
return res?.data?.map(item => {
return {
label: item?.cname,
value: item?.id
}
})
})
}
} catch (error) {
console.log(error, 'singleSubjectInit-error');
}
};
const getSortInit = async () => {
try {
const res = await getScoreLineList({ stage: 3, examId });
if (res?.code === 200) {
setSelectRaking(res?.data?.[0]?.id || '');
setRankingList(() => {
return res?.data?.map(item => ({
value: item?.id,
label: item?.name
}))
});
}
} catch (error) {
console.log(error, 'get-sort-init-error');
}
};
const getSubjectSelectOptions = async () => {
try {
const res = await getSubjectList({ stages: 3, contain256: false, level: 1, single: true, showFlag: true });
if (res?.code === 200) {
setSelectValues(() => {
return res?.data?.map(item => {
return {
label: item?.cname,
value: item?.id
}
})
})
}
} catch (error) {
console.log(error, 'getSubjectSelectOptions-error');
}
};
const handleTagClick = (tag) => {
if (selectedTags.includes(tag)) {
// 如果 Tag 已经选中,则移除
setSelectedTags((prev) => {
if (prev.length === 1) {
return prev;
}
return selectedTags.filter((item) => item !== tag)
});
} else {
// 如果 Tag 未选中,则添加
setSelectedTags([...selectedTags, tag]);
}
};
const handleDownload = async () => {
try {
const res = await downloadTableData(responseData);
if (res?.status === 200) {
downloadExcelFile(res, `${subtitle}${scoreLineList?.length > 0 ? scoreLine?.name : rankingList?.find(item=>item?.value === selectRaking)?.label}*对比考试上线情况分析`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
// 导出图片
const exportChart = (ref) => {
const instance = echarts.getInstanceByDom(ref?.current);
if (instance) {
const base64 = instance?.getDataURL({
type: "png",
pixelRatio: 2, // 导出的图片分辨率比例
backgroundColor: "#fff", // 导出图片的背景色
});
setEchartImg(base64);
}
};
//下载图片
const downloadImg = () => {
const link = document.createElement("a");
link.download = `图表_${new Date().getTime()}.png`;
link.href = echartImg;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setEchartImg("");
};
const expandableConfig = {
expandedRowKeys: expandedRowKeys,
defaultExpandedRowKeys: [dataSource?.[0]?.id, dataSource?.[0]?.children?.[0]?.id],
onExpand: (expanded, record) => {
if (expanded) {
setExpandedRowKeys([...expandedRowKeys, record.id]);
} else {
setExpandedRowKeys(expandedRowKeys?.filter((k) => k !== record.id));
}
},
};
const handleRakingClick = (item) => {
setSelectRaking(item?.value);
};
return (
<div>
{isShowChart ? (<section className={styles.analysis_content_chart} id={`operationExport${idx}`}>
<section className={styles.analysis_content_chart_title}>
<section className={styles.page_title} >
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}对比考试上线情况分析-图表</span>
<Space>
{/* <Select
placeholder="请选择学科"
options={allSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
allowClear
onChange={(value, option) => {
if (!value) {
setSubjectName(null);
setSubjectId("");
return;
}
setSubjectName(option?.label);
setSubjectId(value);
}}
/> */}
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && (<Button type='primary' style={{ fontSize: 16 }} onClick={() => {
exportChart(chartRef)
}}>导出图表</Button>)}
</section>
<section style={{ display: "flex", alignItems: "center" }}>
<span style={{ color: " #8a8a8a" }}>设置选科范围</span>
<Select style={{ width: 260 }} maxTagCount={2} placeholder='请选择学科' value={subjectIds} mode='multiple' allowClear options={selectValues} onChange={(value, options) => {
if (!value.length) {
setSubjectIds([]);
setSubjectOptions([]);
return;
}
setSubjectIds(value);
setSubjectOptions(options);
}} />
</section>
<section style={{ display: "flex", alignItems: "center", marginTop: 16 }}>
<span style={{ color: " #8a8a8a" }}>设置分数线</span>
<InputNumber
min={1}
value={score}
onChange={(value) => setScore(value)}
style={{ width: 120 }}
/>
&nbsp;&nbsp;分以上&nbsp;&nbsp;
<Button onClick={() => {
if (typeof score !== 'number') {
message.error('请输入数字');
return;
}
const item = {
score,
name: !!subjectOptions?.length ?`总分${score}以上,选${subjectOptions?.map(item => item?.label).join('')}` : `总分${score}以上`,
combIds: subjectIds?.join(',')
};
if(scoreLineList?.map(item=>item?.name).includes(item.name)){
message.error('该分数线已存在,勿重新添加');
return;
}
setScoreLine(item);
setScoreLineList((prev) => {
return [...prev, item];
})
setScore(null);
}}>
确定
</Button>
</section>
<section style={{ marginTop: 8 }}>
{scoreLineList?.length ? (
<Space>
{scoreLineList?.map((item, index) => {
const isSelected = scoreLine?.name === item?.name;
return (<Tag
key={index}
color={isSelected ? "green" : "default"}
closable
style={{cursor:'pointer'}}
onClick={()=>{
setScoreLine(item);
}}
onClose={() => {
setScoreLineList(scoreLineList.filter(i => i?.name !== item?.name))
}}
>
{item?.name}
</Tag>);
})}
</Space>
) : null}
</section>
{!scoreLineList?.length ? (<Space style={{ marginTop: 8 }}>
{rankingList.length > 0 ? (
rankingList.map((item, index) => {
const isSelected = selectRaking === item?.value;
return (
<Tag
onClick={() => handleRakingClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>) : null}
<section style={{ marginTop: 8 }}>
<Space >
{typeList.length > 0 ? (
typeList.map((item, index) => {
const isSelected = selectType === item;
return (
<Tag
onClick={() => setSelectType(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item}
</Tag>
);
})
) : null}
</Space>
</section>
<section style={{ marginTop: 8 }}>
<Space >
{tags.length > 0 ? (
tags.map((item, index) => {
const isSelected = selectedTags.includes(item);
return (
<Tag
onClick={() => handleTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item}
</Tag>
);
})
) : null}
</Space>
</section>
<section className={styles.analysis_content_chart_content} >
<Spin spinning={loading} tip='计算中,请稍后'>
<div ref={chartRef} style={{ width: '100%', height: '460px' }}></div>
</Spin>
</section>
</section>) : null}
{isShowTable ? (<section className={styles.analysis_content_table} id={`operationExport${idx + 1}`}>
<section className={styles.analysis_content_table_title}>
<section className={styles.page_title}>
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}对比考试上线情况分析-表格</span>
<Space>
{/* <Select
placeholder="请选择学科"
options={allSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
allowClear
onChange={(value, option) => {
if (!value) {
setSubjectName(null);
setSubjectId("");
return;
}
setSubjectName(option?.label);
setSubjectId(value);
}}
/> */}
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && (<Button type='primary' style={{ fontSize: 16 }} onClick={handleDownload}>导出表格</Button>)}
</section>
<section>
<section >
{scoreLineList?.length ? (
<Space>
{scoreLineList?.map((item, index) => {
const isSelected = scoreLine?.name === item?.name;
return (<Tag
key={index}
color={isSelected ? "green" : "default"}
closable
style={{cursor:'pointer'}}
onClick={()=>{
setScoreLine(item);
}}
onClose={() => {
setScoreLineList(scoreLineList.filter(i => i?.name !== item?.name))
}}
>
{item?.name}
</Tag>);
})}
</Space>
) : null}
</section>
{!scoreLineList?.length ? (<Space style={{ marginTop: 8 }}>
{rankingList.length > 0 ? (
rankingList.map((item, index) => {
const isSelected = selectRaking === item?.value;
return (
<Tag
onClick={() => handleRakingClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>) : null}
</section>
<section className={styles.analysis_content_table_content}>
<Table
{...tableCommonProps}
dataSource={dataSource}
columns={columns}
locale={{ emptyText: loading ? "计算中,请稍后" : "暂无数据" }}
loading={{ spinning: loading, tip: "计算中,请稍后" }}
expandable={expandableConfig}
>
</Table>
</section>
</section>) : null}
<StudentGroup
ref={studentGroupRef}
params={studentGroupParams}
title={title}
/>
{echartImg && (
<Modal
title="导出图表"
width={'80vw'}
className="custom-modal"
open={true}
okText="下载"
onCancel={() => {
setEchartImg("");
}}
onOk={downloadImg}
>
<Flex style={{ height: "100%" }} alignItems="center" justify="center">
<Image src={echartImg} width={"100%"} />
</Flex>
</Modal>
)}
</div>
)
};
export default ContrastExamOnline;

View File

@ -0,0 +1,33 @@
@import '@/pages/analysis/styles/common.less';
.analysis_content{
&_chart{
&:extend(.analysis_content_chart);
&_title{
&:extend(.analysis_content_chart_title);
}
&_content{
&:extend(.analysis_content_chart_content);
}
}
&_table{
&:extend(.analysis_content_table);
&_title{
&:extend(.analysis_content_table_title);
}
&_content{
&:extend(.analysis_content_table_content);
}
}
}
.page{
&_title{
&:extend(.title);
&_img{
&:extend(.title_img);
}
&_text{
&:extend(.title_text);
}
}
}

View File

@ -0,0 +1,747 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { Space, Select, Button, Spin, Table, Tag, Modal } from 'antd';
import * as echarts from 'echarts';
import { getParamsRanking } from '@/apis/exam';
import { getSubjectListAll, getSort, getParamsType, } from '@/apis/common';
import SchoolChoice from '@/components/SchoolChoice';
import StudentGroup from '@/components/StudentGroup';
import { tableCommonProps } from '@/utils/config.js';
import { downloadTableData } from '@/apis/download';
import { downloadExcelFile } from '@/utils/file';
import navIconSrc from '@/assets/icon/analysis-navigation-icon.png';
import styles from './index.less';
const ContrastExamParams = (props) => {
const { examId = '', subtitle = '', examList = [], isShowChart = false, isShowTable = false,idx='',actionShow = true} = props;
const [dataSource, setDataSource] = useState([]);
const [columns, setColumns] = useState([]);
const [responseData, setResponseData] = useState({});
const [loading, setLoading] = useState(false);
const [subjectId, setSubjectId] = useState("");
const [subjectName, setSubjectName] = useState(null);
const [schoolIds, setSchoolIds] = useState([]);
const [allSubjectList, setAllSubjectList] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [spinning, setSpinning] = useState(false);
const [paramsType, setParamsType] = useState();
const [typeList, setTypeList] = useState([]);
const [selectType, setSelectType] = useState('');
const [allEchartDataParams, setAllEchartDataParams] = useState({});
const [sortParamsList, setSortParamsList] = useState([]);
const [selectSortParams, setSelectSortParams] = useState();
const [paramsLimit, setParamsLimit] = useState();
const [tags, setTags] = useState([]);
const [selectedTags, setSelectedTags] = useState([]);
const [echartXData, setEchartXData] = useState([]);
const [selectedNavTag, setSelectedNavTag] = useState('');
const [mockChartData, setMockChartData] = useState([]);
const chartRef = useRef(null);
let chartInstance = null;
const [echartImg, setEchartImg] = useState();
const studentGroupRef = useRef();
const [title, setTitle] = useState('');
const [studentGroupParams, setStudentGroupParams] = useState({});
const [paramsTypeList, setParamsTypeList] = useState([]);
const [type, setType] = useState();
const [chartTagList, setChartTagList] = useState([]);
const [chartTag, setChartTag] = useState('');
const labelOption = {
show: true,
position: 'top',
distance: 24,
align: 'center',
verticalAlign: 'middle',
formatter: (params) => {
return `${params.value}`
},
fontSize: 12,
rich: {
name: {}
}
};
const commonSeries = {
type: 'line',
barGap: 0,
label: labelOption,
emphasis: {
focus: 'series'
},
};
const intersectionData = useMemo(() => {
return mockChartData?.filter((item) =>
selectedTags.includes(item.name),
);
}, [selectedTags, mockChartData]);
useEffect(() => {
allSubjectInit();
getSortInit();
paramsTypeInit();
}, []);
useEffect(() => {
if (!examId || !examList.length > 0 || !subjectId) {
return;
}
init();
}, [examId, subjectId, schoolIds, examList, paramsLimit, paramsType]);
useEffect(() => {
if (type || subjectId) {
viewInit();
}
}, [examId, subjectId, schoolIds, examList, paramsLimit, type]);
useEffect(() => {
if (selectType) {
if (selectType === chartTag) {
setMockChartData(() => {
return allEchartDataParams.num?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
if (selectType === '增幅数') {
setMockChartData(() => {
return allEchartDataParams?.incr?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
if (selectType === '增幅') {
setMockChartData(() => {
return allEchartDataParams?.rate?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
}
}, [selectType]);
useEffect(() => {
if (!echartXData.length) {
return;
}
if (chartRef.current) {
// 初始化图表
chartInstance = echarts.init(chartRef.current);
// 设置配置项
const option = {
// dataZoom: [
// {
// type: 'slider', // 滑动条型数据区域缩放组件
// show: true,
// start: 0, // 初始起点
// end: 30, // 初始终点
// handleSize: 8,
// xAxisIndex: [0]
// },
// ],
title: {
// 添加标题配置
text: `${subtitle}${subjectName}*对比考试参数情况分析-图表`,
left: 'center',
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 'bold',
},
},
axisLabel: {
rotate: 35,
interval: 0,
fontSize: 9,
autoSkip: true,
},
xAxis: {
name: '考试',
type: 'category',
axisTick: { show: false },
data: echartXData,
axisLabel: {
rotate: echartXData?.length > 6 ? 45 : 0,
interval: 0,
textStyle: {
fontSize: 10,
}
},
nameTextStyle: {
fontSize: 16,
}
},
grid: {
containLabel: true,
},
yAxis: {
name: "",
axisLabel: {
textStyle: {
fontSize: 16,
}
},
nameTextStyle: {
fontSize: 16,
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
type: "scroll",
data: tags,
right: 10, // 设置右侧边距
top: 24, // 设置顶部边距
selectedMode: false,
textStyle: {
fontSize: 12
}
},
barGap: '10%',
barWidth: "10%",
series: intersectionData.map((item) => ({
...commonSeries,
name: item.name,
data: item.data,
}))
};
chartInstance.setOption(option);
// 响应式调整
const resizeHandler = () => chartInstance?.resize();
window.addEventListener('resize', resizeHandler);
// 清理函数
return () => {
window.removeEventListener('resize', resizeHandler);
chartInstance?.dispose();
};
}
}, [selectedTags, echartXData, mockChartData]);
const init = async () => {
setLoading(true);
try {
const res = await getParamsRanking({
examId,
examIds: examList.map(item => item.id),
limit: paramsLimit,
type: paramsType,
subjectId: subjectId,
schools: schoolIds
});
if (res.code === 200) {
setResponseData(res);
setLoading(false);
setDataSource(res?.data || []);
setExpandedRowKeys([res?.data?.[0]?.id,res?.data?.[0]?.children?.[0]?.id]);
setColumns(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
width: item?.label === '平均分' ? 90 : item?.label === '区县' ? 300 : 180,
children: item?.children?.map(element => {
if (element?.label === '人数') {
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 80,
render: (text) => (
<Button type="link" style={{ color: "#4ABB6A" }} onClick={() => {
const examId = element?.prop?.split('-')[1];
setTitle(text?.typeDisc);
studentGroupRef.current?.openModal();
setStudentGroupParams({
examId,
students: text?.data
});
}}>{text?.value}</Button>
),
}
}
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 80,
sorter: (a, b) => a[element?.prop] - b[element?.prop],
}
})
}
})
})
return;
}
setLoading(false);
} catch (error) {
setLoading(false);
console.log(error, 'exam-ranking-error');
}
}
const viewInit = async () => {
setSpinning(true);
try {
const res = await getParamsRanking({
examId,
examIds: examList.map(item => item.id),
limit: paramsLimit,
type: paramsType,
subjectId: subjectId,
schools: schoolIds
});
if (res.code === 200) {
setSpinning(false);
if (Object.values(res?.columnDiagramResult)?.length) {
setAllEchartDataParams(res?.columnDiagramResult || {});
setTypeList([chartTag, '增幅数', '增幅']);
setSelectType(chartTag);
setTags(res?.columnDiagramResult?.num?.map(item => item?.lineName));
setSelectedTags([res?.columnDiagramResult?.num?.[0]?.lineName]);
setEchartXData(res?.columnDiagramResult?.num?.[0]?.xdata);
setMockChartData(() => {
return res?.columnDiagramResult?.num?.map(item => {
return {
name: item?.lineName,
data: item?.data,
}
});
});
}
}
} catch (error) {
setSpinning(false);
console.log(error, 'exam-ranking-error');
}
}
const allSubjectInit = async () => {
try {
const res = await getSubjectListAll();
if (res?.code === 200) {
setSubjectId(res?.data?.[0]?.id || '');
setSubjectName(res?.data?.[0]?.cname || null);
setAllSubjectList(() => {
return res?.data?.map(item => {
return {
label: item?.cname,
value: item?.id
}
})
})
}
} catch (error) {
console.log(error, 'singleSubjectInit-error');
}
};
const getSortInit = async () => {
try {
const res = await getSort();
if (res?.code === 200) {
setSelectSortParams(999999);
setParamsLimit(999999);
setSortParamsList(() => {
return [{ label: "全部", value: 999999 }, ...res?.data];
});
}
} catch (error) {
console.log(error, 'get-sort-init-error');
}
};
const paramsTypeInit = async () => {
try {
const params = await getParamsType();
if (params.code === 200) {
setParamsTypeList(params?.data || []);
setParamsType(params?.data?.[0]?.value || '');
setSelectedNavTag(params?.data?.[0]?.label || '');
setChartTagList(params?.data || []);
setChartTag(params?.data?.[0]?.label || '');
setType(params?.data?.[0]?.value || '')
}
} catch (error) {
console.log(error, 'params-init-error');
}
};
const handleTagClick = (tag) => {
if (selectedTags.includes(tag)) {
// 如果 Tag 已经选中,则移除
setSelectedTags((prev) => {
if (prev.length === 1) {
return prev;
}
return selectedTags.filter((item) => item !== tag)
});
} else {
// 如果 Tag 未选中,则添加
setSelectedTags([...selectedTags, tag]);
}
};
const handleDownload = async () => {
try {
const res = await downloadTableData(responseData);
if (res?.status === 200) {
downloadExcelFile(res, `${subtitle}*${subjectName}对比考试参数分析`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
// 导出图片
const exportChart = (ref) => {
const instance = echarts.getInstanceByDom(ref?.current);
if (instance) {
const base64 = instance?.getDataURL({
type: "png",
pixelRatio: 2, // 导出的图片分辨率比例
backgroundColor: "#fff", // 导出图片的背景色
});
setEchartImg(base64);
}
};
//下载图片
const downloadImg = () => {
const link = document.createElement("a");
link.download = `图表_${new Date().getTime()}.png`;
link.href = echartImg;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setEchartImg("");
};
const expandableConfig = {
expandedRowKeys: expandedRowKeys,
defaultExpandedRowKeys: [dataSource?.[0]?.id,dataSource?.[0]?.children?.[0]?.id],
onExpand: (expanded, record) => {
if (expanded) {
setExpandedRowKeys([...expandedRowKeys, record.id]);
} else {
setExpandedRowKeys(expandedRowKeys?.filter((k) => k !== record.id));
}
},
};
const handleNavTagClick = (tag) => {
setSelectedNavTag(tag?.label || '');
setParamsType(tag?.value || '');
};
const handleNavTagClick2 = (tag) => {
setChartTag(tag?.label || '');
setType(tag?.value || '');
};
const handleRakingClick = (item) => {
setSelectSortParams(item?.value);
setParamsLimit(item?.value);
};
return (
<div>
{isShowChart ? (<section className={styles.analysis_content_chart} id={`operationExport${idx}`}>
<section className={styles.analysis_content_chart_title}>
<section className={styles.page_title}>
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}对比考试参数情况分析-图表</span>
<Space>
<Select
placeholder="请选择学科"
options={allSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
allowClear
onChange={(value, option) => {
if (!value) {
setSubjectName(null);
setSubjectId("");
return;
}
setSubjectName(option?.label);
setSubjectId(value);
}}
/>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && <Button type='primary' style={{ fontSize: 16 }} onClick={() => {
exportChart(chartRef)
}}>导出图表</Button>}
</section>
<Space>
{sortParamsList?.length > 0 ? (
sortParamsList?.map((item, index) => {
const isSelected = selectSortParams === item?.value;
return (
<Tag
onClick={() => handleRakingClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>
<Space style={{ marginTop: 8, width: '100%' }}>
{chartTagList?.length > 0 ? (
chartTagList?.map((item, index) => {
const isSelected = chartTag === item?.label;
return (
<Tag
onClick={() => handleNavTagClick2(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>
<Space style={{ marginTop: 8, width: '100%' }}>
{typeList.length > 0 ? (
typeList.map((item, index) => {
const isSelected = selectType === item;
return (
<Tag
onClick={() => setSelectType(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item}
</Tag>
);
})
) : null}
</Space>
<section style={{ marginTop: 8 }}>
<Space>
{tags?.length > 0 ? (
tags?.map((item, index) => {
const isSelected = selectedTags.includes(item);
return (
<Tag
onClick={() => handleTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item}
</Tag>
);
})
) : null}
</Space>
</section>
<section className={styles.analysis_content_chart_content}>
<Spin spinning={spinning} tip='计算中,请稍后'>
<div ref={chartRef} style={{ width: '100%', height: '460px' }}></div>
</Spin>
</section>
</section>) : null}
{isShowTable ? (
<section className={styles.analysis_content_table} id={`operationExport${idx + 1}`}>
<section className={styles.analysis_content_table_title}>
<section className={styles.page_title}>
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}对比考试参数情况分析-表格</span>
<Space>
<Select
placeholder="请选择学科"
options={allSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
allowClear
onChange={(value, option) => {
if (!value) {
setSubjectName(null);
setSubjectId("");
return;
}
setSubjectName(option?.label);
setSubjectId(value);
}}
/>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && <Button type='primary' style={{ fontSize: 16 }} onClick={handleDownload}>导出表格</Button> }
</section>
<section>
<Space style={{ marginTop: 8 }}>
{sortParamsList.length > 0 ? (
sortParamsList.map((item, index) => {
const isSelected = selectSortParams === item?.value;
return (
<Tag
onClick={() => handleRakingClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>
</section>
<Space style={{ marginTop: 8 }}>
{paramsTypeList.length > 0 ? (
paramsTypeList.map((item, index) => {
const isSelected = selectedNavTag === item?.label;
return (
<Tag
onClick={() => handleNavTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>
<section className={styles.analysis_content_table_content}>
<Table
{...tableCommonProps}
dataSource={dataSource}
columns={columns}
locale={{ emptyText: loading ? "计算中,请稍后" : "暂无数据" }}
loading={{ spinning: loading, tip: "计算中,请稍后" }}
expandable={expandableConfig}
>
</Table>
</section>
</section>) : null}
<StudentGroup
ref={studentGroupRef}
params={studentGroupParams}
title={title}
/>
{echartImg && (
<Modal
title="导出图表"
width={'80vw'}
className="custom-modal"
open={true}
okText="下载"
onCancel={() => {
setEchartImg("");
}}
onOk={downloadImg}
>
<Flex style={{ height: "100%" }} alignItems="center" justify="center">
<Image src={echartImg} width={"100%"} />
</Flex>
</Modal>
)}
</div>
)
};
export default ContrastExamParams;

View File

@ -0,0 +1,33 @@
@import '@/pages/analysis/styles/common.less';
.analysis_content{
&_chart{
&:extend(.analysis_content_chart);
&_title{
&:extend(.analysis_content_chart_title);
}
&_content{
&:extend(.analysis_content_chart_content);
}
}
&_table{
&:extend(.analysis_content_table);
&_title{
&:extend(.analysis_content_table_title);
}
&_content{
&:extend(.analysis_content_table_content);
}
}
}
.page{
&_title{
&:extend(.title);
&_img{
&:extend(.title_img);
}
&_text{
&:extend(.title_text);
}
}
}

View File

@ -0,0 +1,643 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { Space, Select, Button, Spin, Table, Tag } from 'antd';
import * as echarts from 'echarts';
import { getExamRanking, getPercentageRanking, getPercentageRankingView } from '@/apis/exam';
import { getSubjectListAll, getPercent } from '@/apis/common';
import SchoolChoice from '@/components/SchoolChoice';
import StudentGroup from '@/components/StudentGroup';
import { tableCommonProps } from '@/utils/config.js';
import { downloadTableData } from '@/apis/download';
import { downloadExcelFile } from '@/utils/file';
import navIconSrc from '@/assets/icon/analysis-navigation-icon.png';
import styles from './index.less';
const ContrastExamPercentage = (props) => {
const { examId = '', subtitle = '', examList = [], isShowChart = false, isShowTable = false,idx='' , actionShow = true } = props;
const [dataSource, setDataSource] = useState([]);
const [columns, setColumns] = useState([]);
const [responseData, setResponseData] = useState({});
const [loading, setLoading] = useState(false);
const [subjectId, setSubjectId] = useState("");
const [subjectName, setSubjectName] = useState(null);
const [schoolIds, setSchoolIds] = useState([]);
const [allSubjectList, setAllSubjectList] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [spinning, setSpinning] = useState(false);
const [typeList, setTypeList] = useState([]);
const [selectType, setSelectType] = useState('');
const [allEchartDataPercent, setAllEchartDataPercent] = useState({});
const [tags, setTags] = useState([]);
const [selectedTags, setSelectedTags] = useState([]);
const [echartXData, setEchartXData] = useState([]);
const [selectedNavTag, setSelectedNavTag] = useState('');
const [mockChartData, setMockChartData] = useState([]);
const chartRef = useRef(null);
let chartInstance = null;
const [echartImg, setEchartImg] = useState();
const studentGroupRef = useRef();
const [title, setTitle] = useState('');
const [studentGroupParams, setStudentGroupParams] = useState({});
const [rateList, setRateList] = useState([]);
const [rate, setRate] = useState();
const labelOption = {
show: true,
position: 'top',
distance: 24,
align: 'center',
verticalAlign: 'middle',
formatter: (params) => {
return `${params.value}`
},
fontSize: 12,
rich: {
name: {}
}
};
const commonSeries = {
type: 'line',
barGap: 0,
label: labelOption,
emphasis: {
focus: 'series'
},
};
const intersectionData = useMemo(() => {
return mockChartData?.filter((item) =>
selectedTags.includes(item.name),
);
}, [selectedTags, mockChartData]);
useEffect(() => {
allSubjectInit();
percentListInit();
}, []);
useEffect(() => {
if (!examId || !examList.length > 0 || !subjectId) {
return;
}
init();
viewInit();
}, [examId, schoolIds, examList, rate, subjectId]);
useEffect(() => {
if (selectType) {
setMockChartData(() => {
return allEchartDataPercent?.[selectType]?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
}
}, [selectType]);
useEffect(() => {
if (!echartXData.length) {
return;
}
if (chartRef.current) {
// 初始化图表
chartInstance = echarts.init(chartRef.current);
// 设置配置项
const option = {
// dataZoom: [
// {
// type: 'slider', // 滑动条型数据区域缩放组件
// show: true,
// start: 0, // 初始起点
// end: 30, // 初始终点
// handleSize: 8,
// xAxisIndex: [0]
// },
// ],
title: {
// 添加标题配置
text: `${subtitle}${subjectName}*对比考试百分比情况分析-图表`,
left: 'center',
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 'bold',
},
},
axisLabel: {
rotate: 35,
interval: 0,
fontSize: 9,
autoSkip: true,
},
barGap: '10%',
barWidth: "10%",
xAxis: {
name: '考试',
type: 'category',
axisTick: { show: false },
data: echartXData,
axisLabel: {
rotate: echartXData?.length > 6 ? 45 : 0,
interval: 0,
textStyle: {
fontSize: 10,
}
},
nameTextStyle: {
fontSize: 16,
}
},
grid: {
containLabel: true,
},
yAxis: {
name: "人",
axisLabel: {
textStyle: {
fontSize: 16,
}
},
nameTextStyle: {
fontSize: 16,
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
type: "scroll",
data: tags,
right: 10, // 设置右侧边距
top: 24, // 设置顶部边距
selectedMode: false,
textStyle: {
fontSize: 16
}
},
series: intersectionData?.map((item) => ({
...commonSeries,
name: item.name,
data: item.data,
}))
};
chartInstance.setOption(option);
// 响应式调整
const resizeHandler = () => chartInstance?.resize();
window.addEventListener('resize', resizeHandler);
// 清理函数
return () => {
window.removeEventListener('resize', resizeHandler);
chartInstance?.dispose();
};
}
}, [selectedTags, echartXData, mockChartData]);
const init = async () => {
setLoading(true);
try {
const res = await getPercentageRanking({
examId,
examIds: examList.map(item => item.id),
limit: rate,
rate,
subjectId: subjectId,
schools: schoolIds
});
if (res.code === 200) {
setResponseData(res);
setLoading(false);
setDataSource(res?.data || []);
setExpandedRowKeys([res?.data?.[0]?.id,res?.data?.[0]?.children?.[0]?.id]);
setColumns(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
width: item?.label === '人数' ? 80 : item?.label === '区县' ? 300 : 200,
children: item?.children?.map(element => {
if (element?.label === '人数') {
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 80,
render: (text) => (
<Button type="link" style={{ color: "#4ABB6A" }} onClick={() => {
const examId = element?.prop?.split('-')[1];
setTitle(text?.typeDisc);
studentGroupRef.current?.openModal();
setStudentGroupParams({
examId,
students: text?.data
});
}}>{text?.value}</Button>
),
}
}
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 80,
sorter: (a, b) => a[element?.prop] - b[element?.prop],
}
})
}
})
})
return;
}
setLoading(false);
} catch (error) {
setLoading(false);
console.log(error, 'exam-ranking-error');
}
};
const viewInit = async () => {
setSpinning(true);
try {
const res = await getPercentageRankingView({
examId,
examIds: examList.map(item => item.id),
limit: rate,
subjectId: subjectId,
schools: schoolIds
});
if (res.code === 200) {
setSpinning(false);
let list = Object.keys(res?.columnDiagramResult || {});
console.log(list, '西湖list');
setTypeList(list);
setSelectType(list?.[0]);
setAllEchartDataPercent(res?.columnDiagramResult || {});
const element = res?.columnDiagramResult?.[list?.[0]];
const label = element?.map(item => item?.lineName);
setTags(label);
setSelectedTags([label?.[0]]);
setEchartXData(element?.[0]?.xdata || []);
setMockChartData(() => {
return element?.map(item => {
return {
name: item?.lineName,
data: item?.data
}
});
})
}
setSpinning(false);
} catch (error) {
setSpinning(false);
console.log(error, 'ranking-view-init-error');
}
}
const allSubjectInit = async () => {
try {
const res = await getSubjectListAll();
if (res?.code === 200) {
setSubjectId(res?.data?.[0]?.id || '');
setSubjectName(res?.data?.[0]?.cname || null);
setAllSubjectList(() => {
return res?.data?.map(item => {
return {
label: item?.cname,
value: item?.id
}
})
})
}
} catch (error) {
console.log(error, 'singleSubjectInit-error');
}
};
const percentListInit = async () => {
try {
const percent = await getPercent();
if (percent.code === 200) {
setRateList(percent?.data || []);
setRate(percent?.data?.[0]?.value || '');
setSelectedNavTag(percent?.data?.[0]?.label || '');
}
} catch (error) {
console.log(error, 'percent-init-error');
}
};
const handleTagClick = (tag) => {
if (selectedTags.includes(tag)) {
// 如果 Tag 已经选中,则移除
setSelectedTags((prev) => {
if (prev.length === 1) {
return prev;
}
return selectedTags.filter((item) => item !== tag)
});
} else {
// 如果 Tag 未选中,则添加
setSelectedTags([...selectedTags, tag]);
}
};
const handleDownload = async () => {
try {
const res = await downloadTableData(responseData);
if (res?.status === 200) {
downloadExcelFile(res, `${subtitle}*${subjectName}对比考试百分比情况分析`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
// 导出图片
const exportChart = (ref) => {
const instance = echarts.getInstanceByDom(ref?.current);
if (instance) {
const base64 = instance?.getDataURL({
type: "png",
pixelRatio: 2, // 导出的图片分辨率比例
backgroundColor: "#fff", // 导出图片的背景色
});
setEchartImg(base64);
}
};
//下载图片
const downloadImg = () => {
const link = document.createElement("a");
link.download = `图表_${new Date().getTime()}.png`;
link.href = echartImg;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setEchartImg("");
};
const expandableConfig = {
expandedRowKeys: expandedRowKeys,
defaultExpandedRowKeys: [dataSource?.[0]?.id,dataSource?.[0]?.children?.[0]?.id],
onExpand: (expanded, record) => {
if (expanded) {
setExpandedRowKeys([...expandedRowKeys, record.id]);
} else {
setExpandedRowKeys(expandedRowKeys?.filter((k) => k !== record.id));
}
},
};
const handleNavTagClick = (tag) => {
setSelectedNavTag(tag?.label || '');
setRate(tag?.value || '');
};
return (
<div>
{isShowChart ? (<section className={styles.analysis_content_chart} id={`operationExport${idx}`} >
<section className={styles.analysis_content_chart_title}>
<section className={styles.page_title} >
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}对比考试百分比情况分析-图表</span>
<Space>
<Select
placeholder="请选择学科"
options={allSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
allowClear
onChange={(value, option) => {
if (!value) {
setSubjectName(null);
setSubjectId("");
return;
}
setSubjectName(option?.label);
setSubjectId(value);
}}
/>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && <Button type='primary' style={{ fontSize: 16 }} onClick={() => {
exportChart(chartRef)
}}>导出图表</Button>}
</section>
{/* <Space>
{rateList?.length > 0 ? (
rateList?.map((item, index) => {
const isSelected = selectedNavTag === item?.label;
return (
<Tag
onClick={() => handleNavTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
fontSize: 14,
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space> */}
<section>
<Space style={{ marginTop: 8, width: '100%' }}>
{typeList?.length > 0 ? (
typeList.map((item, index) => {
const isSelected = selectType === item;
return (
<Tag
onClick={() => setSelectType(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item}
</Tag>
);
})
) : null}
</Space>
</section>
<section style={{ marginTop: 8 }}>
<Space style={{ marginTop: 8, width: '100%' }}>
{tags?.length > 0 ? (
tags?.map((item, index) => {
const isSelected = selectedTags.includes(item);
return (
<Tag
onClick={() => handleTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item}
</Tag>
);
})
) : null}
</Space>
</section>
<section className={styles.analysis_content_chart_content} >
<Spin spinning={spinning} tip='计算中,请稍后'>
<div ref={chartRef} style={{ width: '100%', height: '460px' }}></div>
</Spin>
</section>
</section>) : null}
{isShowTable ? (<section className={styles.analysis_content_table} id={`operationExport${idx + 1}`}>
<section className={styles.analysis_content_table_title}>
<section className={styles.page_title}>
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}对比考试百分比情况分析-图表</span>
<Space>
<Select
placeholder="请选择学科"
options={allSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
allowClear
onChange={(value, option) => {
if (!value) {
setSubjectName(null);
setSubjectId("");
return;
}
setSubjectName(option?.label);
setSubjectId(value);
}}
/>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && (<Button type='primary' style={{ fontSize: 16 }} onClick={handleDownload}>导出表格</Button>)}
</section>
<Space>
{rateList?.length > 0 ? (
rateList?.map((item, index) => {
const isSelected = selectedNavTag === item?.label;
return (
<Tag
onClick={() => handleNavTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>
<section className={styles.analysis_content_table_content}>
<Table
{...tableCommonProps}
dataSource={dataSource}
columns={columns}
locale={{ emptyText: loading ? "计算中,请稍后" : "暂无数据" }}
loading={{ spinning: loading, tip: "计算中,请稍后" }}
expandable={expandableConfig}
>
</Table>
</section>
</section>) : null}
<StudentGroup
ref={studentGroupRef}
params={studentGroupParams}
title={title}
/>
{echartImg && (
<Modal
title="导出图表"
width={'80vw'}
className="custom-modal"
open={true}
okText="下载"
onCancel={() => {
setEchartImg("");
}}
onOk={downloadImg}
>
<Flex style={{ height: "100%" }} alignItems="center" justify="center">
<Image src={echartImg} width={"100%"} />
</Flex>
</Modal>
)}
</div>
)
};
export default ContrastExamPercentage;

View File

@ -0,0 +1,33 @@
@import '@/pages/analysis/styles/common.less';
.analysis_content{
&_chart{
&:extend(.analysis_content_chart);
&_title{
&:extend(.analysis_content_chart_title);
}
&_content{
&:extend(.analysis_content_chart_content);
}
}
&_table{
&:extend(.analysis_content_table);
&_title{
&:extend(.analysis_content_table_title);
}
&_content{
&:extend(.analysis_content_table_content);
}
}
}
.page{
&_title{
&:extend(.title);
&_img{
&:extend(.title_img);
}
&_text{
&:extend(.title_text);
}
}
}

View File

@ -0,0 +1,628 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { Space, Select, Button, Spin, Table, Tag } from 'antd';
import * as echarts from 'echarts';
import { getExamRanking } from '@/apis/exam';
import { getSubjectListAll, getSort } from '@/apis/common';
import SchoolChoice from '@/components/SchoolChoice';
import StudentGroup from '@/components/StudentGroup';
import { tableCommonProps } from '@/utils/config.js';
import { downloadTableData } from '@/apis/download';
import { downloadExcelFile } from '@/utils/file';
import navIconSrc from '@/assets/icon/analysis-navigation-icon.png';
import styles from './index.less';
const ContrastExamRanking = (props) => {
const { examId = '', subtitle = '', examList = [], isShowChart = false, isShowTable = false, idx = '' ,actionShow = true } = props;
const [dataSource, setDataSource] = useState([]);
const [columns, setColumns] = useState([]);
const [responseData, setResponseData] = useState({});
const [loading, setLoading] = useState(false);
const [subjectId, setSubjectId] = useState("");
const [subjectName, setSubjectName] = useState(null);
const [schoolIds, setSchoolIds] = useState([]);
const [allSubjectList, setAllSubjectList] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [examSortList, setExamSortList] = useState([]);
const [examSelectSort, setExamSelectSort] = useState();
const [typeList, setTypeList] = useState([]);
const [selectType, setSelectType] = useState('');
const [allEchartDataRanking, setAllEchartDataRanking] = useState({});
const [tags, setTags] = useState([]);
const [selectedTags, setSelectedTags] = useState([]);
const [echartXData, setEchartXData] = useState([]);
const [mockChartData, setMockChartData] = useState([]);
const chartRef = useRef(null);
let chartInstance = null;
const [echartImg, setEchartImg] = useState();
const studentGroupRef = useRef();
const [title, setTitle] = useState('');
const [studentGroupParams, setStudentGroupParams] = useState({});
const labelOption = {
show: true,
position: 'top',
distance: 24,
align: 'center',
verticalAlign: 'middle',
formatter: (params) => {
return `${params.value}`
},
fontSize: 12,
rich: {
name: {}
}
};
const commonSeries = {
type: 'line',
barGap: 0,
label: labelOption,
emphasis: {
focus: 'series'
},
};
const intersectionData = useMemo(() => {
return mockChartData?.filter((item) =>
selectedTags.includes(item.name),
);
}, [selectedTags, mockChartData]);
useEffect(() => {
allSubjectInit();
getSortInit();
}, []);
useEffect(() => {
if (!examId || !examList.length > 0 || !subjectId) {
return;
}
init();
}, [examId, schoolIds, subjectId, examList, examSelectSort]);
useEffect(() => {
if (selectType) {
if (selectType === '人数') {
setMockChartData(() => {
return allEchartDataRanking.num?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
if (selectType === '增长人数') {
setMockChartData(() => {
return allEchartDataRanking?.incr?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
if (selectType === '增幅') {
setMockChartData(() => {
return allEchartDataRanking?.rate?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
return;
}
}
}, [selectType]);
useEffect(() => {
if (!echartXData.length) {
return;
}
if (chartRef.current) {
// 初始化图表
chartInstance = echarts.init(chartRef.current);
// 设置配置项
const option = {
// dataZoom: [
// {
// type: 'slider', // 滑动条型数据区域缩放组件
// show: true,
// start: 0, // 初始起点
// end: 30, // 初始终点
// handleSize: 8,
// xAxisIndex: [0]
// },
// ],
title: {
// 添加标题配置
text: `${subtitle}${subjectName}*对比考试排名情况分析-图表`,
left: 'center',
top: 0,
textStyle: {
fontSize: 16,
fontWeight: 'bold',
},
},
axisLabel: {
rotate: 35,
interval: 0,
fontSize: 9,
autoSkip: true,
},
barGap: '10%',
barWidth: "10%",
xAxis: {
name: '考试',
type: 'category',
axisTick: { show: false },
data: echartXData,
axisLabel: {
rotate: echartXData?.length > 6 ? 45 : 0,
interval: 0,
textStyle: {
fontSize: 10,
}
},
nameTextStyle: {
fontSize: 16,
}
},
grid: {
containLabel: true,
top: "20%"
},
yAxis: {
name: "",
axisLabel: {
textStyle: {
fontSize: 16,
}
},
nameTextStyle: {
fontSize: 16,
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
type: "scroll",
data: tags,
right: 10, // 设置右侧边距
top: 16, // 设置顶部边距
selectedMode: false,
textStyle: {
fontSize: 12
}
},
series: intersectionData.map((item) => ({
...commonSeries,
name: item?.name || '',
data: item?.data || [],
}))
};
chartInstance.setOption(option);
// 响应式调整
const resizeHandler = () => chartInstance?.resize();
window.addEventListener('resize', resizeHandler);
// 清理函数
return () => {
window.removeEventListener('resize', resizeHandler);
chartInstance?.dispose();
};
}
}, [selectedTags, echartXData, mockChartData]);
const init = async () => {
setLoading(true);
try {
const res = await getExamRanking({
examId,
examIds: examList.map(item => item.id),
limit: examSelectSort,
subjectId: subjectId,
schools: schoolIds
});
if (res.code === 200) {
setResponseData(res);
setLoading(false);
setDataSource(res?.data || []);
setExpandedRowKeys([res?.data?.[0]?.id, res?.data?.[0]?.children?.[0]?.id]);
setTags(res?.columnDiagramResult?.num?.map(item => item?.lineName) || []);
setSelectedTags([res?.columnDiagramResult?.num?.[0]?.lineName || '']);
setTypeList(['人数', '增长人数', '增幅']);
setSelectType('人数');
setEchartXData(res?.columnDiagramResult?.num?.[0]?.xdata || []);
setAllEchartDataRanking(res?.columnDiagramResult);
setMockChartData(() => {
return res?.columnDiagramResult?.num?.map(item => {
return {
name: item?.lineName || '',
data: item?.data || [],
}
})
});
setColumns(() => {
return res?.columns?.map(item => {
return {
title: item?.label,
dataIndex: item?.prop,
align: "center",
width: item.label === "考试人数" ? 80 : item?.label === "区县" ? 300 : 240,
children: item?.children?.map(element => {
if (element?.label === '人数') {
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 120,
render: (text) => (
<Button type="link" style={{ color: "#4ABB6A" }} onClick={() => {
const examId = element?.prop?.split('-')[1];
setTitle(text?.typeDisc);
studentGroupRef.current?.openModal();
setStudentGroupParams({
examId,
students: text?.data
});
}}>{text?.value}</Button>
),
}
}
return {
title: element?.label,
dataIndex: element?.prop,
align: "center",
width: 120,
sorter: (a, b) => a[element?.prop] - b[element?.prop],
}
})
}
})
})
return;
}
setLoading(false);
} catch (error) {
setLoading(false);
console.log(error, 'exam-ranking-error');
}
};
const allSubjectInit = async () => {
try {
const res = await getSubjectListAll();
if (res?.code === 200) {
setSubjectId(res?.data?.[0]?.id || '');
setSubjectName(res?.data?.[0]?.cname || null);
setAllSubjectList(() => {
return res?.data?.map(item => {
return {
label: item?.cname,
value: item?.id
}
})
})
}
} catch (error) {
console.log(error, 'singleSubjectInit-error');
}
};
const getSortInit = async () => {
try {
const res = await getSort();
if (res?.code === 200) {
setExamSortList(res?.data || []);
setExamSelectSort(res?.data?.[0]?.value);
}
} catch (error) {
console.log(error, 'get-sort-init-error');
}
};
const handleTagClick = (tag) => {
if (selectedTags.includes(tag)) {
// 如果 Tag 已经选中,则移除
setSelectedTags((prev) => {
if (prev.length === 1) {
return prev;
}
return selectedTags.filter((item) => item !== tag)
});
} else {
// 如果 Tag 未选中,则添加
setSelectedTags([...selectedTags, tag]);
}
};
const handleDownload = async () => {
try {
const res = await downloadTableData(responseData);
if (res?.status === 200) {
downloadExcelFile(res, `${subtitle}*${subjectName}对比考试排名情况分析`);
}
} catch (error) {
console.log(error, 'download-error');
}
};
// 导出图片
const exportChart = (ref) => {
const instance = echarts.getInstanceByDom(ref?.current);
if (instance) {
const base64 = instance?.getDataURL({
type: "png",
pixelRatio: 2, // 导出的图片分辨率比例
backgroundColor: "#fff", // 导出图片的背景色
});
setEchartImg(base64);
}
};
//下载图片
const downloadImg = () => {
const link = document.createElement("a");
link.download = `图表_${new Date().getTime()}.png`;
link.href = echartImg;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setEchartImg("");
};
const expandableConfig = {
expandedRowKeys: expandedRowKeys,
defaultExpandedRowKeys: [dataSource?.[0]?.id, dataSource?.[0]?.children?.[0]?.id],
onExpand: (expanded, record) => {
if (expanded) {
setExpandedRowKeys([...expandedRowKeys, record.id]);
} else {
setExpandedRowKeys(expandedRowKeys?.filter((k) => k !== record.id));
}
},
};
return (
<div>
{isShowChart ? (<section className={styles.analysis_content_chart} id={`operationExport${idx}`}>
<section className={styles.analysis_content_chart_title}>
<section className={styles.page_title} >
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}对比考试排名情况分析-图表</span>
<Space>
<Select
placeholder="请选择学科"
options={allSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
allowClear
onChange={(value, option) => {
if (!value) {
setSubjectName(null);
setSubjectId("");
return;
}
setSubjectName(option?.label);
setSubjectId(value);
}}
/>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && ( <Button type='primary' style={{ fontSize: 16 }} onClick={() => {
exportChart(chartRef)
}}>导出图表</Button>)}
</section>
<Space style={{ marginBottom: 8 }}>
{examSortList.length > 0 ? (
examSortList.map((item, index) => {
const isSelected = item?.value === examSelectSort;
return (
<Tag
onClick={() => setExamSelectSort(item?.value)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>
<section>
<Space >
{typeList.length > 0 ? (
typeList.map((item, index) => {
const isSelected = selectType === item;
return (
<Tag
onClick={() => setSelectType(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item}
</Tag>
);
})
) : null}
</Space>
</section>
<section style={{ marginTop: 8 }}>
<Space >
{tags.length > 0 ? (
tags.map((item, index) => {
const isSelected = selectedTags.includes(item);
return (
<Tag
onClick={() => handleTagClick(item)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item}
</Tag>
);
})
) : null}
</Space>
</section>
<section className={styles.analysis_content_chart_content} >
<Spin spinning={loading} tip='计算中,请稍后'>
<div ref={chartRef} style={{ width: '100%', height: '460px' }}></div>
</Spin>
</section>
</section>) : null}
{isShowTable ? (<section className={styles.analysis_content_table} id={`operationExport${idx + 1}`}>
<section className={styles.analysis_content_table_title}>
<section className={styles.page_title}>
<img src={navIconSrc} className={styles.page_title_img} />
<span className={styles.page_title_text}>{subtitle}对比考试排名情况分析-表格</span>
<Space>
<Select
placeholder="请选择学科"
options={allSubjectList}
value={subjectName}
style={{ marginLeft: 20, width: "120px" }}
allowClear
onChange={(value, option) => {
if (!value) {
setSubjectName(null);
setSubjectId("");
return;
}
setSubjectName(option?.label);
setSubjectId(value);
}}
/>
<SchoolChoice onChange={(value) => {
setSchoolIds(value);
}} />
</Space>
</section>
{actionShow && <Button type='primary' style={{ fontSize: 16 }} onClick={handleDownload}>导出表格</Button>}
</section>
<section>
<Space >
{examSortList.length > 0 ? (
examSortList.map((item, index) => {
const isSelected = item?.value === examSelectSort;
return (
<Tag
onClick={() => setExamSelectSort(item?.value)}
color={isSelected ? 'green' : 'default'}
key={index}
style={{
cursor: 'pointer',
}}
>
{item?.label}
</Tag>
);
})
) : null}
</Space>
</section>
<section className={styles.analysis_content_table_content}>
<Table
{...tableCommonProps}
dataSource={dataSource}
columns={columns}
locale={{ emptyText: loading ? "计算中,请稍后" : "暂无数据" }}
loading={{ spinning: loading, tip: "计算中,请稍后" }}
expandable={expandableConfig}
>
</Table>
</section>
</section>) : null}
<StudentGroup
ref={studentGroupRef}
params={studentGroupParams}
title={title}
/>
{echartImg && (
<Modal
title="导出图表"
width={'80vw'}
className="custom-modal"
open={true}
okText="下载"
onCancel={() => {
setEchartImg("");
}}
onOk={downloadImg}
>
<Flex style={{ height: "100%" }} alignItems="center" justify="center">
<Image src={echartImg} width={"100%"} />
</Flex>
</Modal>
)}
</div>
)
};
export default ContrastExamRanking;

View File

@ -0,0 +1,33 @@
@import '@/pages/analysis/styles/common.less';
.analysis_content{
&_chart{
&:extend(.analysis_content_chart);
&_title{
&:extend(.analysis_content_chart_title);
}
&_content{
&:extend(.analysis_content_chart_content);
}
}
&_table{
&:extend(.analysis_content_table);
&_title{
&:extend(.analysis_content_table_title);
}
&_content{
&:extend(.analysis_content_table_content);
}
}
}
.page{
&_title{
&:extend(.title);
&_img{
&:extend(.title_img);
}
&_text{
&:extend(.title_text);
}
}
}

Some files were not shown because too many files have changed in this diff Show More