优化 班级成绩分析的 分段展示 #17

Merged
hy merged 1 commits from dev into staging 2025-09-24 19:18:57 +08:00
10 changed files with 557 additions and 47 deletions
Showing only changes of commit 14b6a200af - Show all commits

View File

@ -1,4 +1,5 @@
import { http } from "@/utils/http";
import { Res } from "@/utils/http/types";
// import type { Res } from "@/utils/http/types";
/**
@ -29,7 +30,31 @@ export function ImportExamInfo(id: number, file: File) {
* @return {object}
*/
export function DeleteExamInfo(data: { classId: number; examId: number }) {
return http.request<any>("post", `ExamClassInfo/DeleteExamInfo`, {
return http.request<Res<any>>("post", `ExamClassInfo/DeleteExamInfo`, {
data
});
}
/**
* @description
* @return {object}
*/
export function RecalculateExamRankings(examId: Number) {
return http.request<Res<any>>(
"get",
`ExamClassInfo/RecalculateExamRankings`,
{
params: { examId }
}
);
}
/**
* @description
* @return {object}
*/
export function ClassRanking(examId: Number,classId: Number) {
return http.request<Res<any>>("get", `ExamClassInfo/ClassRanking`, {
params: { examId, classId }
});
}

View File

@ -22,7 +22,7 @@ export class hTableAPI {
delete(data) {
return http.request<Res<any>>("post", `${this.url}/Del`, { data });
}
querycombo(data) {
querycombo(data = {}) {
return http.request<Res<ComboModel[]>>("post", `${this.url}/QueryCombo`, {
data
});

View File

@ -198,18 +198,18 @@ export class TableColumnSearch {
/** 表格列配置 */
export class TableColumn {
constructor(
) {
constructor() {
this.type = "string";
this.show = true;
this.search = new TableColumnSearch();
this.edit = new TableColumnEdit();
this.fixed = false;
this.setting = {};
}
/** 显示标签 */
label: string;
fixed?: 'left' | 'right'| boolean;
/** 查询配置 */
search?: TableColumnSearch;
/** 编辑配置 */
@ -327,6 +327,8 @@ export interface TableConfig {
searchCallback?: (s: SearchConditions) => void;
/** 新增/修改回调函数 */
editCallback?: (from: any) => void;
/** 展开行的回调 */
expandChange?: (row: any, expandedRows: any[]) => void;
/** API地址 */
apiUrl: string;
/** 是否显示选择列 */
@ -335,6 +337,10 @@ export interface TableConfig {
search: SearchConditions;
/** 是否显示操作列 */
operationColumn: boolean;
/** 操作列是否固定列 */
operationColumnFixed?: "left" | "right" | boolean;
/** 是否允许展开列 */
expandColumn?: boolean;
/** 操作按钮配置 */
operationColumnData: OperationButton[];
/** 列配置 */
@ -357,6 +363,8 @@ export function intTableData(tValue: TableConfig): TableConfig {
if (!tValue.data) tValue.data = [];
if (!tValue.selectRows) tValue.selectRows = [];
if (tValue.border == null) tValue.border = true;
if (tValue.operationColumnFixed == null) tValue.operationColumnFixed = false;
if (tValue.expandColumn == null) tValue.expandColumn = false;
if (!tValue.pageData) tValue.pageData = { total: 0 };
if (tValue.operationTop === undefined) tValue.operationTop = true;

View File

@ -117,8 +117,8 @@ function handleResetForm() {
function fetchInitData() {}
function fetchFormData() {
editData.value.loading = false;
handleResetForm();
if (editData.value.isedit) {
handleResetForm();
Api.Info(props.id).then((res) => {
if (res.code === 200) {
editData.value.frorm = res.data;

View File

@ -207,16 +207,20 @@ function handleDelete(obj, row) {
row.forEach((it) => {
ids.push(it.id);
});
ElMessageBox.confirm("此操作将永久删除勾选记录, 是否继续?").then(() => {
Api.delete(ids).then((res) => {
if (res.code === 200) {
handleReloadPaged();
ElMessage.success("删除成功");
} else {
ElMessage.error(res.message);
}
ElMessageBox.confirm("此操作将永久删除勾选记录, 是否继续?")
.then(() => {
Api.delete(ids).then((res) => {
if (res.code === 200) {
handleReloadPaged();
ElMessage.success("删除成功");
} else {
ElMessage.error(res.message);
}
});
})
.catch(() => {
ElMessage.info("取消删除");
});
});
}
//
function handleAddCallback() {
@ -454,6 +458,7 @@ function fetchPagedData() {
:row-key="rowKeyFun"
@selection-change="handleSelectionChange"
@sort-change="sortChange"
@expand-change="table.expandChange"
>
<el-table-column v-if="table.selectColumn" type="selection" width="40" />
<el-table-column
@ -461,6 +466,7 @@ function fetchPagedData() {
table.operationColumn &&
table.operationColumnData.filter((s) => !s.topBtn).length > 0
"
:fixed="table.operationColumnFixed"
label="操作"
:width="getOperationColumnWidth()"
>
@ -483,12 +489,21 @@ function fetchPagedData() {
<!-- 行内按钮组 -->
</template>
</el-table-column>
<!-- 拓展列 -->
<el-table-column v-if="table.expandColumn" type="expand">
<template #default="props">
<slot name="expandSlot" :props="props"></slot>
</template>
</el-table-column>
<el-table-column
v-for="(item, name, i) of tableShowColumn"
:key="i"
:prop="name"
:label="item.label"
:width="item.width"
:fixed="item.fixed"
:sortable="item.search.sort ? `custom` : false"
>
<template v-slot="scope">

View File

@ -76,7 +76,7 @@ const convertKeysToCamelCase = <T>(data: any): T => {
const defaultConfig: AxiosRequestConfig = {
baseURL: import.meta.env.VITE_API_BASEURL,
// 请求超时时间
timeout: 20 * 1000,
timeout: 30 * 1000,
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",

View File

@ -11,6 +11,8 @@ import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
import { ClassRanking } from "@/api/exam";
import { ElMessage } from "element-plus";
const ControllerName = "ExamClassInfo";
defineOptions({
@ -25,9 +27,11 @@ function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = intTableData({
apiUrl: ControllerName,
expandColumn: true,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
expandChange: expandChange,
search: {
//
show: true,
@ -73,19 +77,6 @@ const tableData: TableConfig = intTableData({
},
peopleCount: {
label: "参考人数",
width: "100px",
},
onLineCount: {
label: "重本人数",
width: "100px",
},
onLineRate: {
label: "重本率",
width: "100px",
custom: (row) => `${row.onLineRate * 100}%`,
},
onLineRanking: {
label: "重本率排名",
},
},
data: [],
@ -101,8 +92,275 @@ onMounted(async () => {
showTable.value = true;
});
// types/exam.ts
export interface ExamClassTag {
Name: string; //
SubjectStr?: string; // null
OnLineRanking: number; // 线
OnLineRate: number; // 线C# decimalTS number
OnLineCount: number; // 线
}
async function expandChange(row: any, expandedRows: any[]) {
if (row.rankingList != null) return;
let res = await ClassRanking(row.examId, row.classId);
if (res.code != 200) return ElMessage.error(res.message || "获取数据失败");
row.rankingList = res.data;
}
// CSS
const getSubjectClass = (subjectStr) => {
if (!subjectStr) return "";
const subjectMap = {
总分: "total",
数学: "math",
英语: "english",
物理: "physics",
化学: "chemistry",
生物: "biology",
};
return subjectMap[subjectStr] || "";
};
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
<div>
<ahTable v-if="showTable" ref="table" :tableConfig="tableData">
<template #expandSlot="{ props }">
<!-- 拓展内容 -->
<div class="expanded-content expandSlot">
<div
v-if="props.row.rankingList && props.row.rankingList.length > 0"
class="ranking-list"
>
<div
v-for="(tag, tagIndex) in props.row.rankingList"
:key="tagIndex"
class="tag-card"
:class="getSubjectClass(tag.subjectStr)"
>
<div class="tag-name">
<span>{{ tag.name }}</span>
<span class="subject-badge">{{ tag.subjectStr || "总分" }}</span>
</div>
<div class="tag-details">
<div class="detail-item">
<span class="detail-label">上线排名:</span>
<span class="detail-value"> {{ tag.onLineRanking }} </span>
</div>
<div class="detail-item">
<span class="detail-label">上线人数:</span>
<span class="detail-value">{{ tag.onLineCount }} </span>
</div>
<div class="detail-item">
<span class="detail-label">上线率:</span>
<span class="detail-value"
>{{ (tag.onLineRate * 100).toFixed(0) }}%</span
>
</div>
<div class="progress-container">
<div
class="progress-bar"
:style="{ width: tag.onLineRate * 100 + '%' }"
></div>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<i class="fas fa-inbox" style="font-size: 3rem; margin-bottom: 15px"></i>
<p>暂无标签数据</p>
</div>
</div>
</template>
</ahTable>
</div>
</template>
<style>
.expandSlot {
padding: 10px 20px;
background: #f5f7fa;
}
.container {
width: 100%;
max-width: 1200px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(90deg, #3498db, #2c3e50);
color: white;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-weight: 600;
font-size: 1.8rem;
}
.header .subtitle {
opacity: 0.9;
font-size: 1rem;
}
.table-container {
padding: 20px;
}
.main-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.main-table th {
background-color: #f8f9fa;
padding: 15px;
text-align: left;
font-weight: 600;
color: #2c3e50;
border-bottom: 2px solid #e9ecef;
}
.main-table td {
padding: 15px;
border-bottom: 1px solid #e9ecef;
vertical-align: top;
}
.main-table tr:hover {
background-color: #f8f9fa;
}
.expand-btn {
background: #3498db;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.3s;
}
.expand-btn:hover {
background: #2980b9;
transform: translateY(-2px);
}
.ranking-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 15px;
margin-top: 10px;
}
.tag-card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
border-left: 4px solid #3498db;
transition: transform 0.3s, box-shadow 0.3s;
}
.tag-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
}
.tag-card.total {
border-left-color: #777f8a;
}
.tag-card.math {
border-left-color: #e74c3c;
}
.tag-card.english {
border-left-color: #f39c12;
}
.tag-card.physics {
border-left-color: #9b59b6;
}
.tag-card.chemistry {
border-left-color: #1abc9c;
}
.tag-card.biology {
border-left-color: #2ecc71;
}
.tag-name {
font-weight: 600;
font-size: 1.1rem;
color: #2c3e50;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.subject-badge {
padding: 3px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
background: #ecf0f1;
}
.tag-details {
margin-top: 10px;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 0px;
font-size: 0.9rem;
}
.detail-label {
color: #7f8c8d;
}
.detail-value {
font-weight: 500;
color: #2c3e50;
}
.progress-container {
height: 8px;
background: #ecf0f1;
border-radius: 4px;
margin-top: 5px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #3498db, #2ecc71);
border-radius: 4px;
transition: width 0.5s ease;
}
.empty-state {
text-align: center;
padding: 30px;
color: #7f8c8d;
font-style: italic;
}
</style>

View File

@ -90,19 +90,19 @@ const tableData: TableConfig = intTableData({
search: new TableColumnSearch(true),
},
onLineCount: {
label: "重本人数",
width: "80px",
},
onLineRate: {
label: "重本率",
custom: (row) => `${Math.round(row.onLineRate * 100)}%`,
width: "80px",
},
onLineRanking: {
label: "重本率排名",
width: "100px",
},
// onLineCount: {
// label: "",
// width: "80px",
// },
// onLineRate: {
// label: "",
// custom: (row) => `${Math.round(row.onLineRate * 100)}%`,
// width: "80px",
// },
// onLineRanking: {
// label: "",
// width: "100px",
// },
maxScore: {
label: "最高分[赋分]",
width: "130px",
@ -122,10 +122,10 @@ const tableData: TableConfig = intTableData({
},
averageRank: {
label: "总平均分排名",
width: "110px",
},
rank: {
label: "远端平均/资源校平均",
width: "95px",
custom: (row) =>
`${
row.baseSchoolScore == 0

188
src/views/exam/examTags.vue Normal file
View File

@ -0,0 +1,188 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import {
ConditionalType,
intTableData,
TableColumnSearch,
TableConfig,
} from "@/components/hTable/hTable";
import { onMounted, ref } from "vue";
import { fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
import {
ruleNumber,
ruleRequired,
ruleRequiredGrade,
ruleRequiredI,
ruleRequiredNumber,
} from "@/utils/rules";
import { ImportExamInfo, RecalculateExamRankings } from "@/api/exam";
import { ElMessage, ElMessageBox } from "element-plus";
import { entryExamInfo } from "./examFun";
const ControllerName = "ExamTags";
defineOptions({
name: ControllerName,
});
const props = defineProps<{
data: any;
}>();
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = intTableData({
apiUrl: ControllerName,
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
editCallback: async (from) => {
if (from.subjectId == -1) from.subjectId = null;
from.examId = props.data[0].id;
},
search: {
//
show: false,
showPage: false,
PageIndex: 0,
PageSize: 1000,
OrderBy: "Id", //
defaultConditions: [
{
FieldName: "ExamId",
FieldValue: props.data[0].id + "",
ConditionalType: ConditionalType.Equal,
},
], //
Conditions: [],
},
operationColumn: true, //
operationColumnData: [
// {
// //
// topBtn: false, //
// label: "",
// btnType: "edit", // add edit del custom
// },
{
//
topBtn: true, //
label: "添加",
btnStyle: "success",
btnType: "add", // add edit del custom
},
{
topBtn: false,
label: "删除",
btnType: "del",
btnStyle: "danger",
},
{
topBtn: true,
label: "重新计算考试上线数据",
click: RExamRankings,
btnStyle: "info",
},
],
column: {
examId: {
label: "考试",
width: "180px",
search: new TableColumnSearch(true, ConditionalType.Equal), //
type: "dropdown",
},
tagName: {
label: "分段名称",
width: "180px",
edit: {
add: true,
rules: ruleRequiredI(20, 1),
},
},
subjectId: {
label: "学科",
width: "100px",
search: new TableColumnSearch(true),
type: "dropdown",
custom: (row) =>
row.subjectId
? tableData.column.subjectId.setting.datasource.find(
(s) => s.value == row.subjectId
)?.text ?? "--"
: "总分",
edit: {
add: true,
rules: ruleRequired,
editDefault: -1,
},
},
maxScore: {
label: "最大分数",
width: "120px",
edit: {
add: true,
rules: ruleRequiredNumber,
},
},
minScore: {
label: "最小分数",
width: "120px",
edit: {
add: true,
rules: ruleRequiredNumber,
},
},
createTime: {
label: "创建时间",
type: "datetime",
custom: (row) => row.createTime?.replace("T", " ").substring(0, 10) ?? "",
},
},
data: [],
pageData: {
total: 0,
},
selectRows: [],
});
const Api = new hTableAPI(`Exam`);
const showTable = ref(false);
onMounted(async () => {
//
tableData.column.examId.setting.datasource = (await Api.querycombo()).data;
tableData.column.subjectId.setting.datasource = [
{ text: "总分", value: -1 },
...(await getenum("SubjectEnum")).data,
];
showTable.value = true;
});
async function RExamRankings() {
try {
await ElMessageBox.confirm(
`重新计算考试上线数据,可能需要较长时间,是否继续?`,
"确认",
{
confirmButtonText: "继续",
cancelButtonText: "取消",
type: "warning",
}
);
const res = await RecalculateExamRankings(props.data[0].id);
if (res.code == 200) {
ElMessage.success("操作成功");
} else {
ElMessage.error(res.message || "操作失败");
}
} catch (error) {
ElMessage.info("取消操作");
}
}
</script>
<template>
<div><ahTable v-if="showTable" ref="table" :tableConfig="tableData" /></div>
</template>

View File

@ -78,6 +78,19 @@ const tableData: TableConfig = intTableData({
height: "800px", //
},
},
{
topBtn: false, //
show: true,
label: "分段",
btnType: "custom", // add edit del
btnStyle: "info",
custom: {
title: "考试分段", // title
src: "exam/examTags", //
width: "1200px", //
height: "800px", //
},
},
{
topBtn: false, //
show: true,
@ -107,7 +120,7 @@ const tableData: TableConfig = intTableData({
edit: {
add: true,
edit: true,
rules: ruleRequired,
rules: ruleRequiredI(20, 2),
},
},
gradeLevel: {
@ -210,8 +223,11 @@ const showTable = ref(false);
onMounted(async () => {
//
tableData.column.gradeLevel.setting.datasource =
(await getenum("GradeLevelEnum")).data.map(s=>{return {value : s.text,text:s.text}});
tableData.column.gradeLevel.setting.datasource = (
await getenum("GradeLevelEnum")
).data.map((s) => {
return { value: s.text, text: s.text };
});
tableData.column.testPaperType.setting.datasource = (
await getenum("TestPaperTypeEnum")