学生实践

This commit is contained in:
hebo 2026-02-14 11:28:51 +08:00
parent 99e7e43e9d
commit 0562efd6e8
5 changed files with 412 additions and 559 deletions

View File

@ -74,6 +74,13 @@ export const xsKsccApi = async (params: any) => {
export const xsKscjApi = async (params: any) => {
return await get("/mobile/jz/kscj", params);
};
/**
* ID和学生ID查询评价
*/
export const ksccPjFindByKsccAndXsApi = async (params: { ksccId: string; xsId: string }) => {
return await get("/api/ksccPj/findByKsccAndXs", params);
};
/**
*
*/

View File

@ -111,14 +111,15 @@
</view>
<view class="file-info">
<text class="file-name">{{ file.originalName || file.name }}</text>
<text class="file-size" v-if="file.size">{{ formatFileSize(file.size) }}</text>
<text class="file-type">{{ file.extension?.toUpperCase() || 'FILE' }}</text>
</view>
</view>
<view class="file-actions">
<view class="delete-btn" @click="removeFile(index)">
<view class="delete-btn" @click.stop="removeFile(index)">
<uni-icons type="close" size="16" color="#fff"></uni-icons>
</view>
<view class="download-btn" @click.stop="downloadFileItem(index)">
<uni-icons type="download" size="16" color="#fff"></uni-icons>
</view>
</view>
</view>
@ -140,7 +141,8 @@ import { ref, computed, watch } from 'vue'
import { imagUrl } from '@/utils'
import {
previewFile as previewFileUtil,
previewVideo as previewVideoUtil
previewVideo as previewVideoUtil,
downloadFile as downloadFileUtil
} from '@/utils/filePreview'
//
@ -736,6 +738,24 @@ const previewFile = (index: number) => {
}
}
// H5 location.hrefAPP/ uni.downloadFile
const downloadFileItem = (index: number) => {
const file = fileList.value[index]
if (!file) return
const pathOrUrl = (file as any).url ?? (file as any).filePath
if (!pathOrUrl) {
showToast({ title: '文件未上传完成,无法下载', icon: 'none' })
return
}
const fileUrl = /^https?:\/\//i.test(pathOrUrl) ? pathOrUrl : imagUrl(pathOrUrl)
const baseName = file.originalName || file.name || '下载文件'
const ext = file.extension || String(pathOrUrl).split('.').pop() || ''
const fileName = ext && !baseName.includes('.') ? `${baseName}.${ext}` : baseName
downloadFileUtil(fileUrl, fileName).catch(() => {
showToast({ title: '下载失败', icon: 'none' })
})
}
//
const removeFile = (index: number) => {
fileList.value.splice(index, 1)
@ -1253,13 +1273,13 @@ defineExpose({
border-radius: 8rpx;
padding: 16rpx;
border: 1rpx solid #e9ecef;
position: relative;
}
.file-preview {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
cursor: pointer;
}
@ -1289,38 +1309,37 @@ defineExpose({
font-size: 28rpx;
color: #333;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: normal;
margin-bottom: 4rpx;
}
.file-size {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 2rpx;
}
.file-type {
font-size: 20rpx;
color: #666;
display: block;
}
/* 右侧操作区单独一列,与文件名不重叠 */
.file-actions {
position: absolute;
top: 8rpx;
right: 8rpx;
flex-shrink: 0;
margin-left: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.file-actions .download-btn,
.file-actions .delete-btn {
width: 32rpx;
height: 32rpx;
background: rgba(255, 59, 48, 0.8);
border-radius: 50%;
width: 56rpx;
height: 56rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.file-actions .download-btn {
background: rgba(0, 122, 255, 0.9);
}
.file-actions .delete-btn {
background: rgba(255, 59, 48, 0.9);
}
</style>

View File

@ -1,34 +1,39 @@
<template>
<view class="grades-page flex flex-col">
<!-- 顶部蓝色背景 -->
<!-- 数据加载遮罩 -->
<view v-if="pageLoading" class="page-loading-mask">
<view class="page-loading-box">
<view class="page-loading-spinner"></view>
<text class="page-loading-text">加载中...</text>
</view>
</view>
<!-- 顶部蓝色背景与教师端一致 -->
<view class="blue-header">
<view class="header-content">
<text class="title">{{ kscc.ksmc }}</text>
<text class="subtitle">教学质量监测成绩报告</text>
<view class="color-bar"></view>
<text class="total-score">{{ksccKmList.length}}满分{{ totalKmFs }}</text>
<text class="grade">{{ curKsdj.dj }}</text>
<view class="grade-info">
<text class="grade-explanation" @click="showGradeInfo">等级说明</text>
<view class="grade-box">
<text class="grade-label">区域等级</text>
<text class="grade-value" :style="{ color: curKsdj.djclr }">{{ curKsdj.djBx }}</text>
<text class="exam-title">{{ kscc.ksmc || '考试场次' }}</text>
<view class="student-info-section">
<text class="student-name">{{ curXs.xm || curXs.name || '学生姓名' }}</text>
<text class="student-class" v-if="curXs.njmc || curXs.bjmc">
{{ [curXs.njmc, curXs.bjmc].filter(Boolean).join(' ') }}
</text>
</view>
<view class="score-info-section">
<view class="score-left">
<text class="total-score">{{ ksccKmList.length }}</text>
<text class="full-score">满分{{ totalKmFs }}</text>
</view>
<text class="grade">{{ curKsdj.dj || '-' }}</text>
</view>
</view>
</view>
<!-- 白色内容区 -->
<view class="flex-1" style="background-color: #f6f6f6; position: relative">
<view style="background-color: #f6f6f6;">
<view class="content-area">
<!-- 选项卡 -->
<!-- 选项卡去掉分数趋势 -->
<view class="tabs">
<view
v-if="ksccKmList.length > 2"
v-if="ksccKmList && ksccKmList.length > 2"
class="tab-item"
:class="{ active: activeTab === 'diagnosis' }"
@click="switchTab('diagnosis')"
@ -42,52 +47,38 @@
>
<text>学科成绩</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'trend' }"
@click="switchTab('trend')"
>
<text>分数趋势</text>
</view>
</view>
<!-- 学科成绩视图 -->
<view class="score-view flex-1 po-re" v-if="activeTab === 'scores'">
<view class="po-ab inset-0 px-15" style="overflow: auto" v-if="ksccKscjList.length">
<!-- 学科成绩视图与教师端布局一致分数按 sfXsFs 显示 -->
<view class="score-view" v-if="activeTab === 'scores'">
<view v-if="ksccKscjList.length > 0" class="score-content">
<view
class="subject-item"
v-for="(kscj, index) in ksccKscjList"
:key="index"
>
<view class="subject-header">
<text class="subject-name">{{ kscj.km.kmmc }}</text>
<text v-if="kscc.sfXsFs && kscj.dj">分数{{ kscj.ksfs }}</text>
<text class="subject-name">{{ kscj.km?.kmmc || kscj.kmmc || '未知科目' }}</text>
<text class="detail-btn" :style="{ color: kscj.djclr || '#909399' }" v-if="kscj.dj">{{ kscj.djBx }}</text>
</view>
<view class="subject-body">
<text class="subject-grade">{{ kscj.dj }}</text>
<text class="detail-btn" :style="{ color: kscj.djclr }" v-if="kscj.dj">{{ kscj.djBx }}</text>
<text class="subject-grade" :style="{ color: kscj.djclr || '#303133' }">{{ kscj.dj || '-' }}</text>
<view class="grade-info-wrapper">
<text v-if="kscc.sfXsFs && kscj.ksfs" class="subject-score">{{ kscj.ksfs }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 分数趋势视图 -->
<view class="trend-view flex-1 po-re" v-if="activeTab === 'trend'">
<view class="tab-placeholder">
<!-- 选项卡 -->
<BasicTabs
ref="tabsRef" :list="xqKmmcList" bar-width="60px" scroll-count="4"
:current="curKmIndex" @change="switchKm"
/>
<view v-else class="empty-scores">
<text>暂无成绩数据</text>
</view>
<view style="flex: 1 0 1px; overflow: auto">
<view class="radar-placeholder" id="chart-container1">
<canvas
style="width: 100%; height: 100%"
canvas-id="trendCanvas"
id="trendCanvas"
class="charts"
/>
<!-- 班主任寄语家长端只读展示 -->
<view class="comment-section">
<view class="comment-header">
<text class="comment-title">班主任寄语</text>
</view>
<view class="comment-text-wrap">
<text class="comment-text">{{ pjBzr || '暂无寄语' }}</text>
</view>
</view>
</view>
@ -103,57 +94,9 @@
class="charts"
/>
</view>
<!-- 诊断评语 -->
<!-- <view class="diagnosis-comment">
<text class="comment-title">尊敬的家长您好</text>
<text class="comment-text"
>向您祝贺朱信权同学本次考试成绩位于较优秀之列</text
>
<text class="comment-text"
>这次考试中道德与法治学科表现最优秀继续保持学科优势相对来说英语学科表现最弱不过也有27道题的答题表现超出同层次的平均</text
>
</view> -->
</view>
</view>
</view>
<!-- 等级说明弹窗 -->
<u-popup
:show="gradeInfoPopupVisible"
mode="center"
round="10"
:closeable="true"
@close="hideGradeInfo"
>
<view class="grade-info-popup">
<view class="popup-header">
<text class="popup-title">等级说明</text>
<u-icon
name="close"
size="20"
color="#999"
@click="hideGradeInfo"
></u-icon>
</view>
<view class="popup-content">
<view
class="grade-item"
v-for="(item, index) in gradeInfoList"
:key="index"
>
<view
class="grade-label"
:style="{ backgroundColor: item.color }"
>{{ item.grade }}</view
>
<view class="grade-desc">
<text class="grade-title">{{ item.desc }}</text>
<text class="grade-detail">{{ item.description }}</text>
</view>
</view>
</view>
</view>
</u-popup>
</view>
</view>
</template>
@ -162,7 +105,7 @@
import { ref, onMounted, watch, nextTick } from "vue";
import uCharts from "@/components/charts/u-charts.js";
import dayjs from "dayjs";
import { xsKscjApi } from "@/api/base/server";
import { xsKscjApi, ksccPjFindByKsccAndXsApi } from "@/api/base/server";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
const { getCurXs } = useUserStore();
@ -173,19 +116,18 @@ const kmTabsRef = ref<any>(null)
const fsScale = 100;
const curXs = ref<any>({})
const curXs = ref<any>({})
const kscc = ref<any>({})
const kscjList = ref<any>([])
const curKsdj = ref<any>({})
const xqKmList = ref<any>([])
const curKm = ref<any>({})
const curKmIndex = ref<number>(0)
const xqKmmcList = ref<string[]>([])
//
const activeTab = ref("scores");
//
const pageLoading = ref<boolean>(false);
//
const pjBzr = ref<string>("");
//
const switchTab = (tab: string) => {
@ -195,12 +137,13 @@ const switchTab = (tab: string) => {
type ColorMapType = {
[key: string]: string;
};
//
const colorMap: ColorMapType = {
"A": "#FFD700", // -
"B": "#00FF00", // - 绿
"C": "#FF8C00", // -
"D": "#666666", // -
"E": "#FF0000", // -
"A": "#4bb604", // - 绿
"B": "#FF8C00", // -
"C": "#FFD700", // -
"D": "#FF0000", // -
"E": "#666666", // -
};
//
@ -209,17 +152,6 @@ const radarData = {
series: [{ name: "分数", data: [85, 90, 88, 92, 86, 91, 89] }],
};
//
let trendData = {
categories: ["01-04", "02-04", "03-04", "04-04", "05-04"],
series: [
{
name: "成绩趋势",
data: [84, 81, 89, 92, 99],
},
],
};
//
const sysKmList = ref<any>([]);
//
@ -233,26 +165,6 @@ const kmDjList = ref<any>([]);
const djList = ref<any>([]);
//
const ksccKscjList = ref<any>([]);
//
const xqKscjList = ref<any>([]);
const switchKm = (index : number) => {
curKmIndex.value = index;
curKm.value = xqKmList.value[index];
trendData = {
categories: curKm.value.rqList,
series: [
{
name: "成绩趋势",
data: curKm.value.fsList,
},
],
};
if (activeTab.value === "trend") {
//
drawTrendChart();
}
};
//
const drawRadarChart = () => {
@ -310,85 +222,10 @@ const drawRadarChart = () => {
});
};
//
const drawTrendChart = () => {
nextTick(() => {
try {
//
const query = uni.createSelectorQuery();
query
.select("#chart-container1")
.boundingClientRect((data) => {
if (!data) {
console.error("未找到趋势图容器");
return;
}
//
const rect = data as any;
const canvasWidth = rect.width || 300;
const canvasHeight = rect.height || 300;
const ctx = uni.createCanvasContext("trendCanvas");
// 线
const options = {
type: "line",
context: ctx,
width: canvasWidth,
height: canvasHeight,
categories: trendData.categories,
series: trendData.series,
animation: true,
background: "#FFFFFF",
padding: [15, 15, 0, 15],
dataLabel: kscc.value.sfXsFs, //
dataPointShape: true,
enableScroll: false,
legend: {
show: false,
},
xAxis: {
disableGrid: true,
axisLine: true,
axisLineColor: "#CCCCCC",
},
yAxis: {
gridType: "dash",
dashLength: 2,
data: [
{
min: 0,
max: 100,
},
],
format: (val : number) => {
return val == 0 || val == 100 ? val : '';
},
},
extra: {
line: {
type: "straight",
width: 2,
},
},
};
new uCharts(options);
})
.exec();
} catch (error) {
console.error("绘制趋势图出错:", error);
}
});
};
//
watch(activeTab, (newValue) => {
if (newValue === "diagnosis") {
setTimeout(drawRadarChart, 50);
} else if (newValue === "trend") {
setTimeout(drawTrendChart, 50);
}
});
@ -418,38 +255,6 @@ const initKsdj = () => {
singleFlag = maxDj < maxKmDj + 100;
};
// { id{ } }
const buildXqKmKscjList = () => {
const xqKscjMap = new Map<string, any>();
const len = xqKscjList.value.length;
for (let i = 0; i < len; i++) {
const exam = xqKscjList.value[i];
exam.ksrq = dayjs(exam.kskstime).format('MM-DD');
if (!xqKscjMap.has(exam.kmId)) {
xqKscjMap.set(exam.kmId, {});
}
xqKscjMap.get(exam.kmId)[exam.ksrq] = exam.ksfs;
}
xqKmList.value = [];
// map
const keys = Array.from(xqKscjMap.keys());
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = xqKscjMap.get(key);
// valueObjectmap
const dataMap = new Map(Object.entries(value));
let km = {
id: key,
kmmc: sysKmList.value.find((item: any) => item.id === key)?.kmmc,
rqList: Array.from(dataMap.keys()).map(String),
fsList: Array.from(dataMap.values()).map(Number)
};
xqKmList.value.push(km);
}
xqKmmcList.value = xqKmList.value.map((item: any) => item.kmmc);
switchKm(0);
}
const rebuildData = () => {
let ksfsList: any[] = [];
let totalFs = 0.00;
@ -474,100 +279,101 @@ const rebuildData = () => {
totalFs = totalFs / ksccKmList.value.length;
}
curKsdj.value = djList.value.find((item: any) => item.zdf <= totalFs && item.zgf >= totalFs) || {};
console.log("考试场次等级", curKsdj.value);
//
buildXqKmKscjList();
}
onMounted(async () => {
curXs.value = getCurXs;
kscc.value = getData;
const res = await xsKscjApi({
xsId: getCurXs.id,
ksccId: getData.id,
njmcId: getData.njmcId
});
if (res.resultCode == 1) {
sysKmList.value = res.result.kmList;
ksccKmList.value = res.result.ksccKmList;
kmDjList.value = res.result.kmDjList;
djList.value = res.result.djList;
ksccKscjList.value = res.result.ksccKscjList;
xqKscjList.value = res.result.xqKscjList;
//
initKsdj();
//
rebuildData();
}
//
// DOM
setTimeout(() => {
if (activeTab.value === "diagnosis") {
drawRadarChart();
} else if (activeTab.value === "trend") {
drawTrendChart();
pageLoading.value = true;
try {
curXs.value = getCurXs;
kscc.value = getData;
const res = await xsKscjApi({
xsId: getCurXs.id,
ksccId: getData.id,
njmcId: getData.njmcId
});
if (res.resultCode == 1) {
sysKmList.value = res.result.kmList || [];
ksccKmList.value = res.result.ksccKmList || [];
kmDjList.value = res.result.kmDjList || [];
djList.value = res.result.djList || [];
ksccKscjList.value = res.result.ksccKscjList || [];
initKsdj();
rebuildData();
}
}, 300);
//
try {
const pjRes = await ksccPjFindByKsccAndXsApi({
ksccId: getData.id,
xsId: getCurXs.id,
});
if (pjRes && pjRes.resultCode === 1 && pjRes.result && pjRes.result.pjBzr) {
pjBzr.value = pjRes.result.pjBzr;
}
} catch (_) {}
setTimeout(() => {
if (activeTab.value === "diagnosis") {
drawRadarChart();
}
}, 300);
} finally {
pageLoading.value = false;
}
});
//
const gradeInfoPopupVisible = ref(false);
//
const gradeInfoList = ref([
{
grade: "A+",
color: "#FF6B6B",
desc: "优异",
description: "超过90%同学,处于年级前列",
},
{
grade: "A",
color: "#4D96FF",
desc: "相对较好",
description: "超过80%同学,表现良好",
},
{
grade: "B",
color: "#6BCB77",
desc: "良好",
description: "超过60%同学,达到年级中上水平",
},
{
grade: "C",
color: "#FFD93D",
desc: "一般",
description: "达到年级平均水平",
},
{
grade: "D",
color: "#B8B8B8",
desc: "待提高",
description: "未达到年级平均水平,需要加强",
},
]);
//
function showGradeInfo() {
gradeInfoPopupVisible.value = true;
}
//
function hideGradeInfo() {
gradeInfoPopupVisible.value = false;
}
</script>
<style lang="scss" scoped>
.grades-page {
background-color: #f8f8f8;
height: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
padding-bottom: 30px;
}
/* 顶部蓝色背景 */
/* 数据加载遮罩 */
.page-loading-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.page-loading-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 32px;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
}
.page-loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #e8e8e8;
border-top-color: #2672ff;
border-radius: 50%;
animation: page-loading-spin 0.8s linear infinite;
}
.page-loading-text {
margin-top: 12px;
font-size: 14px;
color: #666;
}
@keyframes page-loading-spin {
to {
transform: rotate(360deg);
}
}
/* 顶部蓝色背景(与教师端一致) */
.blue-header {
background-color: #2672ff;
padding: 15px 15px 50px 15px;
padding: 15px 15px 20px 15px;
position: relative;
color: white;
box-sizing: border-box;
@ -585,110 +391,88 @@ function hideGradeInfo() {
}
.header-content {
padding-top: 30px;
padding-top: 15px;
padding-bottom: 15px;
display: flex;
flex-direction: column;
align-items: center;
.title {
font-size: 20px;
.exam-title {
font-size: 18px;
font-weight: 500;
text-align: center;
}
.subtitle {
font-size: 20px;
font-weight: 500;
margin-bottom: 15px;
text-align: center;
}
.color-bar {
width: 180px;
height: 15px;
background: linear-gradient(
to right,
#29b6f6,
#4caf50,
#ff9800,
#f57c00,
#ffab91,
#ffe0b2
);
border-radius: 15px;
margin-bottom: 15px;
}
.total-score {
font-size: 16px;
margin-bottom: 10px;
opacity: 0.95;
}
.grade {
font-size: 50px;
font-weight: bold;
margin-bottom: 15px;
}
.grade-info {
.student-info-section {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-bottom: 10px;
width: 100%;
gap: 8px;
.student-name {
font-size: 22px;
font-weight: 600;
text-align: center;
}
.student-class {
font-size: 15px;
text-align: center;
opacity: 0.9;
padding: 4px 12px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 12px;
}
}
.score-info-section {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
padding: 0 20px;
gap: 30px;
.grade-explanation {
font-size: 14px;
margin-right: 15px;
position: relative;
padding-bottom: 2px;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 1px;
background-color: rgba(255, 255, 255, 0.7);
}
}
.grade-box {
background-color: rgba(255, 255, 255, 0.2);
padding: 8px 20px;
border-radius: 6px;
.score-left {
display: flex;
flex-direction: column;
align-items: center;
.grade-label {
.total-score {
font-size: 14px;
margin-bottom: 5px;
opacity: 0.9;
}
.grade-value {
font-size: 16px;
font-weight: 500;
.full-score {
font-size: 14px;
opacity: 0.9;
}
}
.grade {
font-size: 48px;
font-weight: bold;
line-height: 1;
}
}
}
}
/* 内容区域 */
/* 内容区域(与教师端一致) */
.content-area {
background-color: white;
padding: 20px 20px 40px 20px;
margin-top: -20px;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
background-color: white;
overflow: hidden;
position: absolute;
z-index: 3;
top: -70rpx;
left: 30rpx;
right: 30rpx;
bottom: 30rpx;
display: flex;
flex-direction: column;
min-height: calc(100vh - 200px);
/* 选项卡 */
.tabs {
@ -720,18 +504,41 @@ function hideGradeInfo() {
}
}
/* 学科成绩视图 */
/* 学科成绩视图(与教师端一致) */
.score-view {
padding: 15px;
margin-top: 15px;
display: flex;
flex-direction: column;
.score-content {
padding: 0;
flex: 1;
}
.subject-item {
padding-top: 30rpx;
padding: 15px 0;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
.subject-header {
margin-bottom: 5px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
.subject-name {
font-size: 15px;
color: #606266;
font-weight: 500;
}
.detail-btn {
font-size: 15px;
font-weight: 500;
color: #909399;
}
}
@ -739,48 +546,67 @@ function hideGradeInfo() {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #ebeef5;
padding-bottom: 15px;
.subject-grade {
font-size: 24px;
font-weight: bold;
color: #303133;
}
.detail-btn {
font-size: 14px;
color: #909399;
.grade-info-wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
.subject-score {
font-size: 24px;
font-weight: bold;
color: #303133;
line-height: 1;
}
}
}
}
.empty-scores {
padding: 50px 15px;
text-align: center;
color: #999;
font-size: 14px;
}
/* 班主任寄语(家长端只读) */
.comment-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
background-color: #fff;
.comment-header {
margin-bottom: 10px;
}
.comment-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
.comment-text-wrap {
padding: 12px 0;
}
.comment-text {
font-size: 14px;
color: #606266;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
}
}
/* 分数趋势视图 */
.trend-view {
padding: 15px;
height: 100%;
display: flex;
flex-direction: column;
.tab-placeholder {
}
.km-info {
margin-bottom: 10px;
display: flex;
align-items: center;
.km-mc {
flex: 1 0 1px;
}
.switch-btn {
padding: 4px 12px;
background-color: rgba(25, 118, 210, 0.8);
color: #fff;
border-radius: 20px;
font-size: 13px;
}
}
}
.radar-placeholder {
height: 300px;
background-color: #f8f8f8;
@ -824,84 +650,6 @@ function hideGradeInfo() {
}
}
/* 等级说明弹窗 */
.grade-info-popup {
width: 280px;
background-color: #fff;
border-radius: 10px;
overflow: hidden;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f2f2f2;
.popup-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
}
.popup-content {
padding: 10px 15px 20px;
max-height: 60vh;
overflow-y: auto;
.grade-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
padding: 8px;
border-radius: 6px;
transition: all 0.3s;
&:last-child {
margin-bottom: 0;
}
&:hover {
background-color: #f9f9f9;
}
.grade-label {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 14px;
font-weight: bold;
margin-right: 12px;
flex-shrink: 0;
}
.grade-desc {
flex: 1;
.grade-title {
font-size: 14px;
font-weight: 500;
color: #333;
display: block;
margin-bottom: 4px;
}
.grade-detail {
font-size: 12px;
color: #666;
line-height: 1.4;
}
}
}
}
}
/* 学生选择器弹窗样式 */
.student-selector {
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);

View File

@ -166,7 +166,7 @@ const menuItems = ref([
permissionKey: "school-bjkb", //
},
{
title: "成绩查询",
title: "学业监测",
icon: "/static/base/home/file-search-line.png",
path: "/pages/base/grades/list",
permissionKey: "school-cjcx", //

View File

@ -131,30 +131,109 @@ export const previewFile = (fileUrl: string, fileName: string, fileType: string)
});
};
// 下载文件
const downloadFile = (fileUrl: string, fileName: string) => {
return new Promise((resolve, reject) => {
uni.downloadFile({
url: fileUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.showToast({ title: '文件已保存', icon: 'success' });
resolve(saveRes);
},
fail: reject
});
// 判断是否为浏览器环境含微信内置浏览器、手Q 等 H5
const isBrowserEnv = () =>
typeof window !== 'undefined' && typeof window.document !== 'undefined';
// 下载文件(导出给 ImageVideoUpload 等组件使用)
export const downloadFile = (fileUrl: string, fileName: string) => {
// 微信/手Q 等内置浏览器中 platform 可能非 'web',统一按浏览器用 H5 下载,避免走原生路径触发“不支持文件系统管理器”
if (isBrowserEnv() || uni.getSystemInfoSync().platform === 'web') {
return downloadForWeb(fileUrl, fileName);
}
return downloadForNative(fileUrl, fileName);
};
// H5 平台下载(与选项统计导出一致:直接跳转真实 URL支持微信内、外部浏览器打开后下载
const downloadForWeb = (url: string, _filename: string) => {
return new Promise<void>((resolve, reject) => {
try {
uni.showToast({ title: "正在下载...", icon: "success", duration: 1500 });
setTimeout(() => {
if (typeof window !== 'undefined' && window.location) {
window.location.href = url;
resolve();
} else {
uni.showToast({ title: "下载失败", icon: "none" });
reject(new Error("非浏览器环境"));
}
}, 100);
} catch (err) {
console.error("H5下载失败:", err);
uni.showToast({ title: "下载失败", icon: "none" });
reject(err);
}
});
};
// 原生平台下载APP/小程序)
const downloadForNative = (url: string, filename: string) => {
return new Promise((resolve, reject) => {
uni.showLoading({ title: "下载中..." });
uni.downloadFile({
url: url,
success: async (res) => {
if (res.statusCode === 200) {
try {
const saved = await saveFile(res.tempFilePath, filename);
if (saved) {
uni.openDocument({
filePath: saved,
success: () => {
uni.showToast({ title: "文件已打开", icon: "success" });
resolve(true);
},
fail: () => {
uni.showToast({ title: "文件已下载,但无法打开", icon: "none" });
resolve(true);
}
});
} else {
reject(new Error('保存失败'));
}
} catch (error) {
console.error('处理下载文件时出错:', error);
reject(error);
}
} else {
uni.showToast({ title: "下载失败", icon: "none" });
reject(new Error('下载失败'));
}
},
fail: reject
fail: (err) => {
console.error("下载失败:", err);
uni.showToast({ title: "下载失败", icon: "none" });
reject(err);
},
complete: () => uni.hideLoading()
});
});
};
// 保存文件到本地(仅原生端使用)
const saveFile = (tempPath: string, filename: string): Promise<string | null> => {
return new Promise((resolve) => {
try {
if (typeof uni.getFileSystemManager === 'function') {
const fs = uni.getFileSystemManager();
const appPath = `${filename}`;
fs.rename({
oldPath: tempPath,
newPath: appPath,
success: () => resolve(appPath),
fail: () => resolve(tempPath)
});
} else {
resolve(tempPath);
}
} catch (error) {
console.error('保存文件时出错:', error);
uni.showToast({ title: "保存失败", icon: "none" });
resolve(null);
}
});
};
// 视频预览
export const previewVideo = (videoUrl: string, videoTitle: string) => {
console.log('=== 视频预览调试信息 ===');