调整成绩分析

This commit is contained in:
ywyonui 2025-07-30 01:02:14 +08:00
parent 5ab1697d2b
commit 63eca2a0ea
12 changed files with 2036 additions and 879 deletions

View File

@ -80,8 +80,13 @@ export const jsFindByBzrNjIdApi = async (params: { njId: string }) => {
}; };
// 根据职务ID查询教师 // 根据职务ID查询教师
export const jsFindByZwIdApi = async (params: { zwId: string; zwType: string }) => { export const jsFindByZwIdApi = async (params: {
return await get(`/api/js/findByZwId?zwId=${params.zwId}&zwType=${params.zwType}`); zwId: string;
zwType: string;
}) => {
return await get(
`/api/js/findByZwId?zwId=${params.zwId}&zwType=${params.zwType}`
);
}; };
// 选课列表 // 选课列表
@ -98,6 +103,37 @@ export const jsdXkkcSaveApi = async (params: any) => {
export const jsdXkXsListApi = async (params: any) => { export const jsdXkXsListApi = async (params: any) => {
return await get("/mobile/js/xkxs/list", params); return await get("/mobile/js/xkxs/list", params);
}; };
// 获取班级学生考试成绩
export const jsdBjKscjApi = async (params: any) => {
return await get("/mobile/js/kscj/bj", params);
};
// 获取班级学生考试成绩(按科目)
export const jsdBjKscjKmApi = async (params: any) => {
return await get("/mobile/js/kscj/bjKm", params);
};
// 获取考试场次列表
export const jsdKsccListApi = async (params: any) => {
return await get("/api/kscc/findPage", params);
};
// 获取教师授课班级列表
export const jsdJsdkbApi = async (params: any) => {
return await get("/mobile/js/jsdkb", params);
};
// 获取班级考试场次列表
export const jsdKsccApi = async (params: any) => {
return await get("/mobile/js/kscc", params);
};
// 获取考试场次科目列表
export const ksccKmFindByKsccIdApi = async (params: any) => {
return await get("/api/kscc/findKsccKmmcById", params);
};
//根据年级ID和班级ID查询学生及家长信息 //根据年级ID和班级ID查询学生及家长信息
export const mobilejlstudentListApi = async (params: any) => { export const mobilejlstudentListApi = async (params: any) => {
return await get("/mobile/jl/studentList", params); return await get("/mobile/jl/studentList", params);
@ -113,7 +149,6 @@ export const getByJlIdApi = async (params: any) => {
return res.result; return res.result;
}; };
// 提交点名信息 // 提交点名信息
export const jsdXkdmListApi = async (params: any) => { export const jsdXkdmListApi = async (params: any) => {
return await post("/mobile/js/xkdm/add", params); return await post("/mobile/js/xkdm/add", params);
@ -182,8 +217,6 @@ export const getJsPjGzlApi = async () => {
return await get("/api/comConfig/getJsPjGzl"); return await get("/api/comConfig/getJsPjGzl");
}; };
// 接龙相关API // 接龙相关API
// 根据ID获取接龙详情 // 根据ID获取接龙详情
export const jlFindByIdApi = async (params: { id: string }) => { export const jlFindByIdApi = async (params: { id: string }) => {
@ -249,7 +282,10 @@ export const qdzxFindByQdParamsApi = async (params: { qdId: string }) => {
return await get("/api/qdzx/findByQdParams", params); return await get("/api/qdzx/findByQdParams", params);
}; };
export const qdzxFindByQdAndJsApi = async (params: { qdId: string; jsId: string }) => { export const qdzxFindByQdAndJsApi = async (params: {
qdId: string;
jsId: string;
}) => {
return await get("/api/qdzx/findByQdAndJs", params); return await get("/api/qdzx/findByQdAndJs", params);
}; };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,201 @@
<template>
<uni-section
title="本班与年级平均分对比分析图表"
type="line"
titleFontSize="16px"
padding
class="mt-10"
>
<!-- 图表容器 -->
<view class="tb-rq" id="line-chart-container">
<canvas
canvas-id="GradeAnalysisLineChart"
id="GradeAnalysisLineChart"
class="charts"
/>
<view v-if="isLoading" class="tb-placeholder">
图表加载中或无数据...
</view>
</view>
</uni-section>
</template>
<script lang="ts" setup>
import uCharts from "@/components/charts/u-charts.js";
import { nextTick, onMounted, ref, watch } from "vue";
import { lineOption } from "./cj.data";
interface BjPjInfo {
bjId: string;
bjmc: string;
njmc: string;
fs: number;
}
// --- Props ---
interface Props {
bjPjList?: BjPjInfo[];
}
const props = withDefaults(defineProps<Props>(), {
bjPjList: () => [],
});
// --- Emits ---
const emit = defineEmits(["update:selectedKmId", "change"]);
// --- State ---
const isLoading = ref(false);
// --- ---
const drawLineChart = () => {
isLoading.value = true;
setTimeout(() => {
nextTick(() => {
// 使 uni.createSelectorQuery() /
uni
.createSelectorQuery()
.select("#line-chart-container")
.boundingClientRect((rect) => {
if (
rect &&
!Array.isArray(rect) &&
typeof rect.width === "number" &&
rect.width > 0 &&
typeof rect.height === "number" &&
rect.height > 0
) {
const containerWidth = rect.width;
const containerHeight = rect.height;
const ctx = uni.createCanvasContext("GradeAnalysisLineChart");
// 使
const lineData = {
categories: props.bjPjList.map(
(bj) => bj.bjmc || bj.njmc + bj.bjmc
),
series: [
{
name: "班级平均分",
data: props.bjPjList.map((bj) => bj.fs),
},
],
};
//
if (!lineData.categories || lineData.categories.length === 0) {
console.error("图表数据不完整categories为空");
isLoading.value = false;
return;
}
if (!lineData.series || lineData.series.length === 0) {
console.error("图表数据不完整series为空");
isLoading.value = false;
return;
}
// series
for (let i = 0; i < lineData.series.length; i++) {
const series = lineData.series[i];
if (!series.data || series.data.length === 0) {
console.error(`图表数据不完整series[${i}].data为空`);
isLoading.value = false;
return;
}
}
console.log("图表数据:", lineData);
const newOption: any = {
...lineOption,
};
newOption.context = ctx;
newOption.width = containerWidth;
newOption.height = containerHeight;
newOption.categories = lineData.categories;
newOption.series = lineData.series;
const options = new uCharts(newOption);
try {
new uCharts(newOption);
isLoading.value = false;
} catch (error) {
console.error("图表绘制失败:", error);
isLoading.value = false;
}
} else {
console.error(
`无法获取容器 #line-chart-container 的有效尺寸:`,
rect
);
isLoading.value = false;
}
})
.exec();
});
}, 500);
};
// --- Watch ---
// bjPjList
watch(
() => props.bjPjList,
() => {
if (props.bjPjList && props.bjPjList.length > 0) {
drawLineChart();
}
},
{ deep: true }
);
// --- Lifecycle ---
onMounted(() => {
drawLineChart();
});
</script>
<style scoped lang="scss">
.km-xz-tabs {
margin-bottom: 20rpx;
}
.tb-rq {
width: 100%;
height: 350rpx;
position: relative;
margin: 8rpx 0;
border-radius: 6px;
overflow: hidden;
background-color: #ffffff;
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.05);
}
.charts {
width: 100% !important;
height: 100% !important;
position: absolute;
top: 0;
left: 0;
max-width: 100%;
max-height: 100%;
}
.tb-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #999;
font-size: 24rpx;
background-color: #ffffff;
z-index: 1;
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<view class="bj-xzq">
<picker
mode="selector"
:range="bjLb"
range-key="name"
:value="xzbjIndex"
@change="onBjChange"
:disabled="bjLb.length === 0"
>
<view class="xz-item">
<text v-if="bjLb.length > 0">{{ xzbjName || "选择班级" }}</text>
<text v-else class="no-data-text">暂无班级数据</text>
<uni-icons type="bottom" size="16" color="#666"></uni-icons>
</view>
</picker>
</view>
</template>
<script lang="ts" setup>
import { jsdJsdkbApi } from "@/api/base/server";
import { useUserStore } from "@/store/modules/user";
import { computed, onMounted, ref } from "vue";
// --- Interfaces ---
interface BjInfo {
id: string | number;
name: string;
}
// --- Props ---
interface Props {
modelValue?: string | number | null;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: null,
});
// --- Store ---
const userStore = useUserStore();
// --- Emits ---
const emit = defineEmits(["update:modelValue", "change"]);
// --- State ---
const xzbjIndex = ref<number>(0);
const bjLb = ref<BjInfo[]>([]);
// --- Computed ---
const xzbjName = computed(() => {
if (props.modelValue) {
const index = bjLb.value.findIndex((bj) => bj.id === props.modelValue);
return index >= 0 ? bjLb.value[index].name : "选择班级";
}
return bjLb.value[xzbjIndex.value]?.name || "选择班级";
});
// --- API Functions ---
const getBjLb = async () => {
try {
const jsData = userStore.getJs;
if (!jsData || !jsData.id) {
console.error("教师ID不存在");
return;
}
const res = await jsdJsdkbApi({ jsId: jsData.id });
if (res && res.resultCode === 1 && res.result) {
bjLb.value = res.result.map((bj: any) => ({
id: bj.id,
name: bj.njmc + bj.bjmc,
}));
console.log("获取班级列表成功:", bjLb.value);
//
if (bjLb.value.length > 0 && !props.modelValue) {
xzbjIndex.value = 0;
const firstBj = bjLb.value[0];
emit("update:modelValue", firstBj.id);
emit("change", firstBj.id, firstBj);
console.log(
`自动选择第一个班级: ${firstBj.id}, 班级名称: ${firstBj.name}`
);
}
} else {
bjLb.value = [];
console.log("班级列表为空");
}
} catch (error) {
console.error("获取班级列表失败:", error);
bjLb.value = [];
}
};
// --- Event Handlers ---
const onBjChange = (event: any) => {
const index = Number(event.detail.value);
xzbjIndex.value = index;
const xzbj = bjLb.value[index];
emit("update:modelValue", xzbj?.id || null);
emit("change", xzbj?.id || null, xzbj || null);
console.log(`切换到班级: ${xzbj?.id}, 班级名称: ${xzbj?.name}`);
};
// --- Lifecycle ---
onMounted(async () => {
await getBjLb();
});
</script>
<style scoped lang="scss">
.bj-xzq {
background-color: #ffffff;
padding: 10rpx 30rpx;
display: flex;
align-items: center;
height: 80rpx;
border-bottom: 1rpx solid #f0f0f0;
.xz-item {
display: flex;
align-items: center;
font-size: 30rpx;
color: #333;
font-weight: bold;
width: 100%;
uni-icons {
margin-left: 10rpx;
}
}
.no-data-text {
color: #999;
font-weight: normal;
}
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<uni-section
title="统计概览"
type="line"
titleFontSize="16px"
padding
class="mt-10"
>
<view class="tjgl-kp">
<view class="tj-gd">
<view class="tj-item">
<text class="label">班级人数</text>
<text class="value">{{ tjglData.bjrs }}</text>
</view>
<view class="tj-item">
<text class="label">年级平均总分</text>
<text class="value primary">{{ tjglData.njpjzf }}</text>
</view>
<view class="tj-item">
<text class="label">班级平均分</text>
<text class="value primary">{{ tjglData.bjpjf }}</text>
</view>
<view class="tj-item">
<text class="label">年级最高分</text>
<text class="value primary">{{ tjglData.njzgf }}</text>
</view>
<view class="tj-item">
<text class="label">优生率</text>
<text class="value success">{{ tjglData.ysl }}%</text>
</view>
<view class="tj-item">
<text class="label">班级最高分</text>
<text class="value primary">{{ tjglData.bjzgf }}</text>
</view>
<view class="tj-item">
<text class="label">优生人数</text>
<text class="value">{{ tjglData.ysrs }}</text>
</view>
<view class="tj-item">
<text class="label">及格率</text>
<text class="value success">{{ tjglData.jgl }}%</text>
</view>
<view class="tj-item">
<text class="label">及格人数</text>
<text class="value">{{ tjglData.jgrs }}</text>
</view>
</view>
</view>
</uni-section>
</template>
<script lang="ts" setup>
// --- Interfaces ---
interface TjglData {
bjrs: number;
njpjzf: number;
bjpjf: number;
njzgf: number;
ysl: number;
bjzgf: number;
ysrs: number;
jgl: number;
jgrs: number;
}
// --- Props ---
interface Props {
tjglData?: TjglData;
}
const props = withDefaults(defineProps<Props>(), {
tjglData: () => ({
bjrs: 45,
njpjzf: 82.5,
bjpjf: 85.1,
njzgf: 99,
ysl: 75.6,
bjzgf: 98,
ysrs: 34,
jgl: 95.6,
jgrs: 43,
}),
});
</script>
<style scoped lang="scss">
.tjgl-kp {
background-color: #f8f9fd;
border-radius: 6px;
padding: 20rpx;
}
.tj-gd {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 25rpx 15rpx;
}
.tj-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.label {
font-size: 24rpx;
color: #666;
margin-bottom: 6rpx;
}
.value {
font-size: 32rpx;
font-weight: bold;
color: #333;
&.primary {
color: #447ade;
}
&.success {
color: #67c23a;
}
}
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<uni-section
title="学生成绩"
type="line"
titleFontSize="16px"
padding
class="mt-10"
>
<view class="xs-cj-bg">
<view class="bg-header">
<view class="th">姓名</view>
<view class="th">得分</view>
<view class="th">班级排名</view>
</view>
<view class="bg-body">
<view class="bg-row" v-for="xs in xsCjLb" :key="xs.id">
<view class="td">{{ xs.name }}</view>
<view class="td">{{ xs.score }}</view>
<view class="td">{{ xs.bjpm }}</view>
</view>
<view v-if="!xsCjLb.length" class="bg-empty"> 暂无数据 </view>
</view>
</view>
</uni-section>
</template>
<script lang="ts" setup>
// --- Interfaces ---
interface XsCj {
id: string | number;
name: string;
score: number;
bjpm: number;
}
// --- Props ---
interface Props {
xsCjLb?: XsCj[];
}
const props = withDefaults(defineProps<Props>(), {
xsCjLb: () => [
{ id: "st-1", name: "张三", score: 95, bjpm: 1 },
{ id: "st-2", name: "李四", score: 88, bjpm: 5 },
{ id: "st-3", name: "王五", score: 92, bjpm: 3 },
{ id: "st-4", name: "赵六", score: 75, bjpm: 15 },
{ id: "st-5", name: "孙七", score: 85, bjpm: 8 },
],
});
</script>
<style scoped lang="scss">
.xs-cj-bg {
width: 100%;
font-size: 26rpx;
border: 1rpx solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
.bg-header {
display: flex;
background-color: #eaf2ff;
color: #333;
font-weight: bold;
border-bottom: 1rpx solid #e0e0e0;
.th {
flex: 1;
padding: 16rpx 10rpx;
text-align: center;
border-right: 1rpx solid #e0e0e0;
&:last-child {
border-right: none;
}
}
}
.bg-body {
.bg-row {
display: flex;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.td {
flex: 1;
padding: 16rpx 10rpx;
text-align: center;
color: #666;
border-right: 1rpx solid #f0f0f0;
&:last-child {
border-right: none;
}
}
}
.bg-empty {
text-align: center;
color: #999;
padding: 40rpx 0;
}
}
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<view class="kscc-xzq">
<picker
mode="selector"
:range="ksccLb"
range-key="name"
:value="xzksccIndex"
@change="onKsccChange"
:disabled="ksccLb.length === 0"
>
<view class="xz-item">
<text v-if="ksccLb.length > 0">{{ xzksccName || "选择考试场次" }}</text>
<text v-else class="no-data-text">暂无考试场次数据</text>
<uni-icons type="bottom" size="16" color="#666"></uni-icons>
</view>
</picker>
</view>
</template>
<script lang="ts" setup>
import { jsdKsccApi } from "@/api/base/server";
import { computed, ref, watch } from "vue";
// --- Interfaces ---
interface KsccInfo {
id: string | number;
name: string;
ksmc: string;
kskstime: string;
}
// --- Props ---
interface Props {
modelValue?: string | number | null;
bjId?: string | number | null;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: null,
bjId: null,
});
// --- Emits ---
const emit = defineEmits(["update:modelValue", "change"]);
// --- State ---
const xzksccIndex = ref<number>(0);
const ksccLb = ref<KsccInfo[]>([]);
// --- Computed ---
const xzksccName = computed(() => {
if (props.modelValue) {
const index = ksccLb.value.findIndex(
(kscc) => kscc.id === props.modelValue
);
return index >= 0 ? ksccLb.value[index].name : "选择考试场次";
}
return ksccLb.value[xzksccIndex.value]?.name || "选择考试场次";
});
// --- API Functions ---
const getKsccLb = async (bjId: string | number) => {
try {
const res = await jsdKsccApi({ bjId });
if (res && res.resultCode === 1 && res.result) {
ksccLb.value = res.result.map((kscc: any) => ({
id: kscc.id,
name: kscc.ksmc || kscc.name,
ksmc: kscc.ksmc,
kskstime: kscc.kskstime,
}));
console.log("获取考试场次列表成功:", ksccLb.value);
//
if (ksccLb.value.length > 0 && !props.modelValue) {
xzksccIndex.value = 0;
const firstKscc = ksccLb.value[0];
emit("update:modelValue", firstKscc.id);
emit("change", firstKscc.id, firstKscc);
console.log(
`自动选择第一个考试场次: ${firstKscc.id}, 考试名称: ${firstKscc.name}`
);
}
} else {
ksccLb.value = [];
console.log("考试场次列表为空");
}
} catch (error) {
console.error("获取考试场次列表失败:", error);
ksccLb.value = [];
}
};
// --- Event Handlers ---
const onKsccChange = (event: any) => {
const index = Number(event.detail.value);
xzksccIndex.value = index;
const xzkscc = ksccLb.value[index];
emit("update:modelValue", xzkscc?.id || null);
emit("change", xzkscc?.id || null, xzkscc || null);
console.log(`切换到考试场次: ${xzkscc?.id}, 考试名称: ${xzkscc?.name}`);
};
// --- Watchers ---
watch(
() => props.bjId,
async (newBjId) => {
if (newBjId) {
await getKsccLb(newBjId);
} else {
ksccLb.value = [];
}
},
{ immediate: true }
);
watch(
() => ksccLb.value,
(newKsccLb) => {
//
if (newKsccLb.length > 0 && !props.modelValue) {
xzksccIndex.value = 0;
const firstKscc = newKsccLb[0];
emit("update:modelValue", firstKscc.id);
emit("change", firstKscc.id, firstKscc);
}
}
);
</script>
<style scoped lang="scss">
.kscc-xzq {
background-color: #ffffff;
padding: 10rpx 30rpx;
display: flex;
align-items: center;
height: 80rpx;
border-bottom: 1rpx solid #f0f0f0;
.xz-item {
display: flex;
align-items: center;
font-size: 30rpx;
color: #333;
font-weight: bold;
width: 100%;
uni-icons {
margin-left: 10rpx;
}
}
.no-data-text {
color: #999;
font-weight: normal;
}
}
</style>

View File

@ -0,0 +1,186 @@
<template>
<uni-section
title="总分等级段"
type="line"
titleFontSize="16px"
padding
class="mt-10"
>
<view class="tb-rq" id="area-chart-container">
<canvas
canvas-id="GradeAnalysisAreaChart"
id="GradeAnalysisAreaChart"
class="charts"
/>
<view v-if="isLoading" class="tb-placeholder">
图表加载中或无数据...
</view>
</view>
</uni-section>
</template>
<script lang="ts" setup>
import uCharts from "@/components/charts/u-charts.js";
import { nextTick, onMounted, ref } from "vue";
import { areaOption } from "./cj.data";
// --- Interfaces ---
interface DjDuan {
name: string;
value: number;
color: string;
}
// --- Props ---
interface Props {
djDuanLb?: DjDuan[];
}
const props = withDefaults(defineProps<Props>(), {
djDuanLb: () => [
{ name: "A", value: 5, color: "#1890ff" },
{ name: "B", value: 15, color: "#2fc25b" },
{ name: "C", value: 18, color: "#facc14" },
{ name: "D", value: 10, color: "#f04864" },
{ name: "E", value: 2, color: "#999999" },
],
});
// --- State ---
const isLoading = ref(false);
// --- ---
const drawAreaChart = () => {
isLoading.value = true;
setTimeout(() => {
nextTick(() => {
uni
.createSelectorQuery()
.select("#area-chart-container")
.boundingClientRect((rect) => {
if (
rect &&
!Array.isArray(rect) &&
typeof rect.width === "number" &&
rect.width > 0 &&
typeof rect.height === "number" &&
rect.height > 0
) {
const containerWidth = rect.width;
const containerHeight = rect.height;
const ctx = uni.createCanvasContext("GradeAnalysisAreaChart");
const areaData = {
categories: props.djDuanLb.map((item) => item.name),
series: [
{
name: "人数",
data: props.djDuanLb.map((item) => item.value),
color: "#1890ff",
},
],
};
//
if (!areaData.categories || areaData.categories.length === 0) {
console.error("图表数据不完整categories为空");
isLoading.value = false;
return;
}
if (!areaData.series || areaData.series.length === 0) {
console.error("图表数据不完整series为空");
isLoading.value = false;
return;
}
if (
!areaData.series[0].data ||
areaData.series[0].data.length === 0
) {
console.error("图表数据不完整series[0].data为空");
isLoading.value = false;
return;
}
const newOption: any = {
...areaOption,
};
newOption.context = ctx;
newOption.width = containerWidth;
newOption.height = containerHeight;
newOption.categories = areaData.categories;
newOption.series = areaData.series;
//
if (newOption.yAxis && newOption.yAxis.data) {
delete newOption.yAxis.data;
}
console.log("图表数据:", areaData);
try {
new uCharts(newOption);
isLoading.value = false;
} catch (error) {
console.error("图表绘制失败:", error);
isLoading.value = false;
}
} else {
console.error(
`无法获取容器 #area-chart-container 的有效尺寸:`,
rect
);
isLoading.value = false;
}
})
.exec();
});
}, 500);
};
// --- Lifecycle ---
onMounted(() => {
drawAreaChart();
});
</script>
<style scoped lang="scss">
.tb-rq {
width: 100%;
height: 350rpx;
position: relative;
margin: 8rpx 0;
border-radius: 6px;
overflow: hidden;
background-color: #ffffff;
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.05);
}
.charts {
width: 100% !important;
height: 100% !important;
position: absolute;
top: 0;
left: 0;
max-width: 100%;
max-height: 100%;
}
.tb-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #999;
font-size: 24rpx;
background-color: #ffffff;
z-index: 1;
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<uni-section
title="总分分数段"
type="line"
titleFontSize="16px"
padding
class="mt-10"
>
<view class="tb-rq" id="donut-chart-container">
<canvas
canvas-id="GradeAnalysisDonutChart"
id="GradeAnalysisDonutChart"
class="charts"
/>
<view v-if="isLoading" class="tb-placeholder">
图表加载中或无数据...
</view>
</view>
</uni-section>
</template>
<script lang="ts" setup>
import uCharts from "@/components/charts/u-charts.js";
import { nextTick, onMounted, ref } from "vue";
import { ringOption } from "./cj.data";
// --- Interfaces ---
interface FsDuan {
name: string;
value: number;
color: string;
}
// --- Props ---
interface Props {
fsDuanLb?: FsDuan[];
}
const props = withDefaults(defineProps<Props>(), {
fsDuanLb: () => [
{ name: "240-269", value: 5, color: "#1890ff" },
{ name: "210-239", value: 12, color: "#2fc25b" },
{ name: "180-210", value: 18, color: "#facc14" },
{ name: "180以下", value: 10, color: "#f04864" },
],
});
// --- State ---
const isLoading = ref(false);
// --- ---
const drawDonutChart = () => {
isLoading.value = true;
setTimeout(() => {
nextTick(() => {
uni
.createSelectorQuery()
.select("#donut-chart-container")
.boundingClientRect((rect) => {
if (
rect &&
!Array.isArray(rect) &&
typeof rect.width === "number" &&
rect.width > 0 &&
typeof rect.height === "number" &&
rect.height > 0
) {
const containerWidth = rect.width;
const containerHeight = rect.height;
const ctx = uni.createCanvasContext("GradeAnalysisDonutChart");
const donutData = {
series: [
{
name: "总分分数段",
data: props.fsDuanLb,
},
],
};
//
if (!donutData.series || donutData.series.length === 0) {
console.error("图表数据不完整series为空");
isLoading.value = false;
return;
}
if (
!donutData.series[0].data ||
donutData.series[0].data.length === 0
) {
console.error("图表数据不完整series[0].data为空");
isLoading.value = false;
return;
}
const newOption: any = {
...ringOption,
};
newOption.context = ctx;
newOption.width = containerWidth;
newOption.height = containerHeight;
newOption.series = donutData.series;
//
if (newOption.yAxis && newOption.yAxis.data) {
delete newOption.yAxis.data;
}
console.log("图表数据:", donutData);
try {
new uCharts(newOption);
isLoading.value = false;
} catch (error) {
console.error("图表绘制失败:", error);
isLoading.value = false;
}
} else {
console.error(
`无法获取容器 #donut-chart-container 的有效尺寸:`,
rect
);
isLoading.value = false;
}
})
.exec();
});
}, 500);
};
// --- Lifecycle ---
onMounted(() => {
drawDonutChart();
});
</script>
<style scoped lang="scss">
.tb-rq {
width: 100%;
height: 350rpx;
position: relative;
margin: 8rpx 0;
border-radius: 6px;
overflow: hidden;
background-color: #ffffff;
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.05);
}
.charts {
width: 100% !important;
height: 100% !important;
position: absolute;
top: 0;
left: 0;
max-width: 100%;
max-height: 100%;
}
.tb-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #999;
font-size: 24rpx;
background-color: #ffffff;
z-index: 1;
}
</style>

View File

@ -0,0 +1,370 @@
export const ringOption = {
type: "ring",
// context: ctx,
// width: containerWidth,
// height: containerHeight,
// series: donutData.series,
animation: true,
timing: "easeOut",
duration: 1000,
rotate: false,
rotateLock: false,
background: "#FFFFFF",
color: [
"#1890FF",
"#91CB74",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3CA272",
"#FC8452",
"#9A60B4",
"#ea7ccc",
],
padding: [5, 5, 5, 5],
fontSize: 13,
fontColor: "#666666",
dataLabel: true,
dataPointShape: true,
dataPointShapeType: "solid",
touchMoveLimit: 60,
enableScroll: false,
enableMarkLine: false,
legend: {
show: true,
position: "right",
lineHeight: 25,
float: "center",
padding: 5,
margin: 5,
backgroundColor: "rgba(0,0,0,0)",
borderColor: "rgba(0,0,0,0)",
borderWidth: 0,
fontSize: 13,
fontColor: "#666666",
hiddenColor: "#CECECE",
itemGap: 10,
},
title: {
name: "",
fontSize: 15,
color: "#666666",
offsetX: 0,
offsetY: 0,
},
subtitle: {
name: "",
fontSize: 25,
color: "#7cb5ec",
offsetX: 0,
offsetY: 0,
},
extra: {
ring: {
ringWidth: 30,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: true,
borderWidth: 3,
borderColor: "#FFFFFF",
centerColor: "#FFFFFF",
customRadius: 0,
linearType: "none",
},
tooltip: {
showBox: true,
showArrow: true,
showCategory: false,
borderWidth: 0,
borderRadius: 0,
borderColor: "#000000",
borderOpacity: 0.7,
bgColor: "#000000",
bgOpacity: 0.7,
gridType: "solid",
dashLength: 4,
gridColor: "#CCCCCC",
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: "#FFFFFF",
legendShow: true,
legendShape: "auto",
splitLine: true,
horizentalLine: false,
xAxisLabel: false,
yAxisLabel: false,
labelBgColor: "#FFFFFF",
labelBgOpacity: 0.7,
labelFontColor: "#666666",
},
},
};
export const areaOption = {
type: "area",
// context: ctx,
// width: containerWidth,
// height: containerHeight,
// categories: areaData.categories,
// series: areaData.series,
animation: true,
timing: "easeOut",
duration: 1000,
rotate: false,
rotateLock: false,
background: "#FFFFFF",
color: [
"#1890FF",
"#91CB74",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3CA272",
"#FC8452",
"#9A60B4",
"#ea7ccc",
],
padding: [15, 15, 0, 15],
fontSize: 13,
fontColor: "#666666",
dataLabel: true,
dataPointShape: true,
dataPointShapeType: "solid",
touchMoveLimit: 60,
enableScroll: false,
enableMarkLine: false,
legend: {
show: false,
position: "bottom",
float: "center",
padding: 5,
margin: 5,
backgroundColor: "rgba(0,0,0,0)",
borderColor: "rgba(0,0,0,0)",
borderWidth: 0,
fontSize: 13,
fontColor: "#666666",
lineHeight: 11,
hiddenColor: "#CECECE",
itemGap: 10,
},
xAxis: {
disableGrid: true,
disabled: false,
axisLine: true,
axisLineColor: "#CCCCCC",
calibration: false,
fontColor: "#666666",
fontSize: 13,
lineHeight: 20,
marginTop: 0,
rotateLabel: false,
rotateAngle: 45,
itemCount: 5,
boundaryGap: "center",
splitNumber: 5,
gridColor: "#CCCCCC",
gridType: "solid",
dashLength: 4,
gridEval: 1,
scrollShow: false,
scrollAlign: "left",
scrollColor: "#A6A6A6",
scrollBackgroundColor: "#EFEBEF",
title: "",
titleFontSize: 13,
titleOffsetY: 0,
titleOffsetX: 0,
titleFontColor: "#666666",
formatter: "",
},
yAxis: {
gridType: "dash",
dashLength: 2,
disabled: false,
disableGrid: false,
splitNumber: 5,
gridColor: "#CCCCCC",
padding: 10,
showTitle: false,
data: [],
},
extra: {
area: {
type: "straight",
opacity: 0.2,
addLine: true,
width: 2,
gradient: false,
activeType: "hollow",
},
tooltip: {
showBox: true,
showArrow: true,
showCategory: false,
borderWidth: 0,
borderRadius: 0,
borderColor: "#000000",
borderOpacity: 0.7,
bgColor: "#000000",
bgOpacity: 0.7,
gridType: "solid",
dashLength: 4,
gridColor: "#CCCCCC",
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: "#FFFFFF",
legendShow: true,
legendShape: "auto",
splitLine: true,
horizentalLine: false,
xAxisLabel: false,
yAxisLabel: false,
labelBgColor: "#FFFFFF",
labelBgOpacity: 0.7,
labelFontColor: "#666666",
},
markLine: {
type: "solid",
dashLength: 4,
data: [],
},
},
};
export const lineOption = {
type: "line",
// context: ctx,
// width: containerWidth,
// height: containerHeight,
// categories: lineData.categories,
// series: lineData.series,
animation: true,
timing: "easeOut",
duration: 1000,
rotate: false,
rotateLock: false,
background: "#FFFFFF",
color: [
"#1890FF",
"#91CB74",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3CA272",
"#FC8452",
"#9A60B4",
"#ea7ccc",
],
padding: [15, 10, 0, 15],
fontSize: 13,
fontColor: "#666666",
dataLabel: true,
dataPointShape: true,
dataPointShapeType: "solid",
touchMoveLimit: 60,
enableScroll: false,
enableMarkLine: false,
legend: {
show: false,
position: "bottom",
float: "center",
padding: 5,
margin: 5,
backgroundColor: "rgba(0,0,0,0)",
borderColor: "rgba(0,0,0,0)",
borderWidth: 0,
fontSize: 13,
fontColor: "#666666",
lineHeight: 11,
hiddenColor: "#CECECE",
itemGap: 10,
},
xAxis: {
disableGrid: true,
disabled: false,
axisLine: true,
axisLineColor: "#CCCCCC",
calibration: false,
fontColor: "#666666",
fontSize: 13,
lineHeight: 20,
marginTop: 0,
rotateLabel: false,
rotateAngle: 45,
itemCount: 5,
boundaryGap: "center",
splitNumber: 5,
gridColor: "#CCCCCC",
gridType: "solid",
dashLength: 4,
gridEval: 1,
scrollShow: false,
scrollAlign: "left",
scrollColor: "#A6A6A6",
scrollBackgroundColor: "#EFEBEF",
title: "",
titleFontSize: 13,
titleOffsetY: 0,
titleOffsetX: 0,
titleFontColor: "#666666",
formatter: "",
},
yAxis: {
gridType: "dash",
dashLength: 2,
disabled: false,
disableGrid: false,
splitNumber: 5,
gridColor: "#CCCCCC",
padding: 10,
showTitle: false,
data: [],
},
extra: {
line: {
type: "straight",
width: 2,
activeType: "hollow",
linearType: "none",
onShadow: false,
animation: "vertical",
},
tooltip: {
showBox: true,
showArrow: true,
showCategory: false,
borderWidth: 0,
borderRadius: 0,
borderColor: "#000000",
borderOpacity: 0.7,
bgColor: "#000000",
bgOpacity: 0.7,
gridType: "solid",
dashLength: 4,
gridColor: "#CCCCCC",
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: "#FFFFFF",
legendShow: true,
legendShape: "auto",
splitLine: true,
horizentalLine: false,
xAxisLabel: false,
yAxisLabel: false,
labelBgColor: "#FFFFFF",
labelBgOpacity: 0.7,
labelFontColor: "#666666",
},
markLine: {
type: "solid",
dashLength: 4,
data: [],
},
},
};

View File

@ -0,0 +1,228 @@
<template>
<BasicLayout :show-nav-bar="true" :nav-bar-props="{ title: 'API测试' }">
<view class="api-test-page">
<!-- 测试教师授课班级接口 -->
<view class="test-section">
<view class="section-title">测试教师授课班级接口</view>
<button @click="testJsdkb" class="test-btn">
测试获取教师授课班级
</button>
<view v-if="jsdkbResult" class="result-section">
<text class="result-title">结果</text>
<text class="result-text">{{
JSON.stringify(jsdkbResult, null, 2)
}}</text>
</view>
</view>
<!-- 测试考试场次接口 -->
<view class="test-section">
<view class="section-title">测试考试场次接口</view>
<input
v-model="testBjId"
placeholder="输入班级ID"
class="input-field"
/>
<button @click="testKscc" class="test-btn">测试获取考试场次</button>
<view v-if="ksccResult" class="result-section">
<text class="result-title">结果</text>
<text class="result-text">{{
JSON.stringify(ksccResult, null, 2)
}}</text>
</view>
</view>
<!-- 测试成绩接口 -->
<view class="test-section">
<view class="section-title">测试成绩接口</view>
<input
v-model="testBjId2"
placeholder="输入班级ID"
class="input-field"
/>
<input
v-model="testKsccId"
placeholder="输入考试场次ID"
class="input-field"
/>
<button @click="testCjData" class="test-btn">测试获取成绩数据</button>
<view v-if="cjDataResult" class="result-section">
<text class="result-title">结果</text>
<text class="result-text">{{
JSON.stringify(cjDataResult, null, 2)
}}</text>
</view>
</view>
<!-- 测试缓存机制 -->
<view class="test-section">
<view class="section-title">测试缓存机制</view>
<button @click="testCache" class="test-btn">测试缓存机制</button>
<view v-if="cacheResult" class="result-section">
<text class="result-title">缓存测试结果</text>
<text class="result-text">{{ cacheResult }}</text>
</view>
</view>
</view>
</BasicLayout>
</template>
<script lang="ts" setup>
import { jsdBjKscjApi, jsdJsdkbApi, jsdKsccApi } from "@/api/base/server";
import { useCommonStore } from "@/store/modules/common";
import { useUserStore } from "@/store/modules/user";
import { ref } from "vue";
const commonStore = useCommonStore();
const userStore = useUserStore();
const jsdkbResult = ref<any>(null);
const ksccResult = ref<any>(null);
const cjDataResult = ref<any>(null);
const cacheResult = ref<string>("");
const testBjId = ref<string>("");
const testBjId2 = ref<string>("");
const testKsccId = ref<string>("");
const testJsdkb = async () => {
try {
const jsData = userStore.getJs();
if (!jsData || !jsData.id) {
uni.showToast({ title: "教师信息不存在", icon: "none" });
return;
}
const response = await jsdJsdkbApi({ jsId: jsData.id });
jsdkbResult.value = response;
console.log("教师授课班级测试结果:", response);
} catch (error) {
console.error("测试教师授课班级接口出错:", error);
uni.showToast({ title: "测试失败", icon: "none" });
}
};
const testKscc = async () => {
try {
if (!testBjId.value) {
uni.showToast({ title: "请输入班级ID", icon: "none" });
return;
}
const response = await jsdKsccApi({ bjId: testBjId.value });
ksccResult.value = response;
console.log("考试场次测试结果:", response);
} catch (error) {
console.error("测试考试场次接口出错:", error);
uni.showToast({ title: "测试失败", icon: "none" });
}
};
const testCjData = async () => {
try {
if (!testBjId2.value || !testKsccId.value) {
uni.showToast({ title: "请输入班级ID和考试场次ID", icon: "none" });
return;
}
const response = await jsdBjKscjApi({
bjId: testBjId2.value,
ksccId: testKsccId.value,
});
cjDataResult.value = response;
console.log("成绩数据测试结果:", response);
} catch (error) {
console.error("测试成绩数据接口出错:", error);
uni.showToast({ title: "测试失败", icon: "none" });
}
};
const testCache = async () => {
try {
const jsData = userStore.getJs();
if (!jsData || !jsData.id) {
uni.showToast({ title: "教师信息不存在", icon: "none" });
return;
}
// API
const startTime1 = Date.now();
const response1 = await commonStore.getJsdkb({ jsId: jsData.id });
const time1 = Date.now() - startTime1;
//
const startTime2 = Date.now();
const response2 = await commonStore.getJsdkb({ jsId: jsData.id });
const time2 = Date.now() - startTime2;
cacheResult.value = `第一次调用耗时: ${time1}ms, 第二次调用耗时: ${time2}ms, 缓存是否生效: ${
time2 < time1
}`;
console.log("缓存测试结果:", cacheResult.value);
} catch (error) {
console.error("测试缓存机制出错:", error);
uni.showToast({ title: "缓存测试失败", icon: "none" });
}
};
</script>
<style scoped>
.api-test-page {
padding: 20rpx;
}
.test-section {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.test-btn {
background: #1890ff;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.input-field {
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
}
.result-section {
background: #f8f9fa;
border-radius: 8rpx;
padding: 20rpx;
margin-top: 20rpx;
}
.result-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
display: block;
}
.result-text {
font-size: 24rpx;
color: #666;
word-break: break-all;
white-space: pre-wrap;
}
</style>

View File

@ -1,11 +1,12 @@
import { defineStore } from "pinia";
import { import {
njFindAll,
bjFindByNjId, bjFindByNjId,
jsFindAll, jsFindAll,
njFindAll,
zwFindAllApi, zwFindAllApi,
zwGetListByLxApi, zwGetListByLxApi,
} from "@/api/base/common"; } from "@/api/base/common";
import { jsdJsdkbApi, jsdKsccApi } from "@/api/base/server";
import { defineStore } from "pinia";
interface CommonState { interface CommonState {
data: any; data: any;
@ -15,12 +16,12 @@ export const useCommonStore = defineStore({
id: "app-common", id: "app-common",
state: (): CommonState => ({ state: (): CommonState => ({
// 字典数据 // 字典数据
data: {} data: {},
}), }),
getters: { getters: {
getData(): any { getData(): any {
return this.data; return this.data;
} },
}, },
actions: { actions: {
setData(data: any) { setData(data: any) {
@ -36,6 +37,7 @@ export const useCommonStore = defineStore({
// 根据年级查询班级 // 根据年级查询班级
async getBjListByNj(params: any): Promise<any> { async getBjListByNj(params: any): Promise<any> {
if (!this.data.bj || !this.data.bj[params.njId]) { if (!this.data.bj || !this.data.bj[params.njId]) {
this.data.bj = this.data.bj || {};
this.data.bj[params.njId] = await bjFindByNjId(params); this.data.bj[params.njId] = await bjFindByNjId(params);
} }
return Promise.resolve(this.data.bj[params.njId]); return Promise.resolve(this.data.bj[params.njId]);
@ -62,10 +64,38 @@ export const useCommonStore = defineStore({
} }
return Promise.resolve(this.data.zw[params.zwlx]); return Promise.resolve(this.data.zw[params.zwlx]);
}, },
// 获取教师授课班级列表
async getJsdkb(params: any): Promise<any> {
if (!this.data.jsdkb || !this.data.jsdkb[params.jsId]) {
this.data.jsdkb = this.data.jsdkb || {};
this.data.jsdkb[params.jsId] = await jsdJsdkbApi(params);
}
return Promise.resolve(this.data.jsdkb[params.jsId]);
},
// 获取班级考试场次列表(带缓存机制)
async getKscc(params: any): Promise<any> {
if (!this.data.kscc || !this.data.kscc[params.bjId]) {
this.data.kscc = this.data.kscc || {};
this.data.kscc[params.bjId] = await jsdKsccApi(params);
}
return Promise.resolve(this.data.kscc[params.bjId]);
},
// 清除班级考试场次缓存
clearKsccCache(bjId?: string) {
if (bjId) {
// 清除指定班级的缓存
if (this.data.kscc && this.data.kscc[bjId]) {
delete this.data.kscc[bjId];
}
} else {
// 清除所有考试场次缓存
this.data.kscc = {};
}
},
}, },
persist: { persist: {
enabled: true, enabled: true,
detached: true, detached: true,
H5Storage: localStorage H5Storage: localStorage,
}, },
}); });