feat: 老师端top10

This commit is contained in:
李梦 2026-02-15 08:39:56 +08:00
parent 6fda3ac190
commit 6824b4fd28
10 changed files with 564 additions and 28 deletions

4
.env
View File

@ -1,5 +1,5 @@
#VITE_HOST = http://192.168.0.114:9053 #VITE_HOST = http://192.168.0.114:9053
#VITE_HOST = http://43.136.52.196:9053 VITE_HOST = http://43.136.52.196:9053
VITE_HOST= https://ai.xuexiaole.com #VITE_HOST= https://ai.xuexiaole.com
VITE_OSS_HOST = https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com VITE_OSS_HOST = https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com
VITE_WS_URL = wss://test.qiaoying.vip/wss/websocket VITE_WS_URL = wss://test.qiaoying.vip/wss/websocket

View File

@ -203,3 +203,53 @@ export const getClassHomeworkReport = (params: {
method: 'GET', method: 'GET',
}); });
}; };
// ---------- 薄弱知识点 Top10 school/report/weakKnowledge/top10 ----------
export interface WeakKnowledgeItem {
knowledgeId: number | string;
knowledgeName: string;
subjectId: number | string;
subjectName: string;
errorCount: number;
titleCount: number;
[key: string]: any;
}
export interface WeakKnowledgeTop10Data {
classId: number | string;
className: string;
topWeakKnowledgeList: WeakKnowledgeItem[];
}
/** 薄弱知识点 Top10GET参数 form 风格 */
export const getWeakKnowledgeTop10 = (params: {
classId: string | number;
subjectIds?: number[];
startTime?: string;
endTime?: string;
schoolId?: string;
}) => {
const formEncode = (v: string) => encodeURIComponent(v).replace(/%20/g, '+');
const query: string[] = [];
query.push('classId=' + formEncode(String(params.classId)));
if (params.schoolId) {
query.push('schoolId=' + formEncode(String(params.schoolId)));
}
if (params.subjectIds && params.subjectIds.length > 0) {
params.subjectIds.forEach((id) => {
query.push('subjectIds=' + formEncode(String(id)));
});
}
if (params.startTime != null && params.startTime !== '' && String(params.startTime).trim() !== '') {
const st = String(params.startTime).trim();
query.push('startTime=' + formEncode(st.includes(' ') ? st : st + ' 00:00:00'));
}
if (params.endTime != null && params.endTime !== '' && String(params.endTime).trim() !== '') {
const et = String(params.endTime).trim();
query.push('endTime=' + formEncode(et.includes(' ') ? et : et + ' 23:59:59'));
}
return request({
url: '/school/report/weakKnowledge/top10?' + query.join('&'),
method: 'GET',
});
};

View File

@ -0,0 +1,100 @@
<template>
<view class="qy-loading">
<view class="qy-loading-inner">
<image
v-if="!loadError"
class="qy-loading-img"
:style="imgStyle"
:src="loadingSrc"
mode="aspectFit"
@error="onImageError"
/>
<view v-else class="qy-loading-fallback">
<view class="qy-loading-placeholder"></view>
</view>
<text v-if="text" class="qy-loading-text">{{ text }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
// static loading.webp
import defaultLoadingImg from '@/static/loading.webp';
const props = withDefaults(
defineProps<{
/** 提示文字,不传则不显示 */
text?: string;
/** 尺寸,单位 rpx默认 120 */
size?: number;
/** 自定义图片路径,不传则使用 static/loading.webp */
src?: string;
}>(),
{
text: '',
size: 120,
src: '',
},
);
const loadError = ref(false);
const loadingSrc = computed(() => {
if (props.src) return props.src;
return defaultLoadingImg;
});
const imgStyle = computed(() => ({
width: props.size + 'rpx',
height: props.size + 'rpx',
}));
function onImageError() {
loadError.value = true;
}
</script>
<style lang="scss" scoped>
.qy-loading {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 120rpx;
padding: 24rpx 0;
}
.qy-loading-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.qy-loading-img {
flex-shrink: 0;
}
.qy-loading-fallback {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.qy-loading-placeholder {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
}
.qy-loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #fff;
text-align: center;
}
</style>

View File

@ -140,6 +140,19 @@
"pageOrientation": "landscape" "pageOrientation": "landscape"
} }
} }
},
{
"path": "weakKnowledge/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "薄弱知识点 Top10",
"disableScroll": true,
"pageOrientation": "landscape",
"allowsBounceVertical": "NO",
"mp-weixin": {
"pageOrientation": "landscape"
}
}
} }
] ]
} }

View File

@ -77,10 +77,7 @@
<!-- 加载中 --> <!-- 加载中 -->
<view v-else class="loading-page"> <view v-else class="loading-page">
<qy-Empty <qy-Loading :text="reportFlag ? '正在加载报告中...' : '正在精选题目中...'" />
type="loading"
:content="reportFlag ? '正在加载报告中...' : '正在精选题目中...'"
/>
</view> </view>
</view> </view>
</template> </template>

View File

@ -90,7 +90,7 @@
<text>重置</text> <text>重置</text>
</view> </view>
<view class="btn-confirm" :class="{ disabled: !selectedClassId }" @click="handleConfirm"> <view class="btn-confirm" :class="{ disabled: !selectedClassId }" @click="handleConfirm">
<text>查询</text> <text>{{ confirmButtonText }}</text>
</view> </view>
</view> </view>
</view> </view>
@ -98,13 +98,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app'; import { onLoad, onShow } from '@dcloudio/uni-app';
import { getClassListByTeacher, getSubjectListByClassId } from '@/api/teacher'; import { getClassListByTeacher, getSubjectListByClassId } from '@/api/teacher';
import { teacher } from '@/store/teacher'; import { teacher } from '@/store/teacher';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
const teacherStore = teacher(); const teacherStore = teacher();
const { schoolId, teacherInfo } = storeToRefs(teacherStore); const { schoolId, teacherInfo } = storeToRefs(teacherStore);
// target=weakKnowledge
const target = ref('');
onLoad((options: any) => {
if (options?.target) target.value = options.target;
});
const confirmButtonText = computed(() =>
target.value === 'weakKnowledge' ? '查看薄弱知识点' : '查询',
);
// + // +
const topPaddingRightRpx = ref(0); const topPaddingRightRpx = ref(0);
const safeAreaLeftRpx = ref(0); const safeAreaLeftRpx = ref(0);
@ -233,24 +242,28 @@ function handleConfirm() {
uni.showToast({ title: '请先选择班级', icon: 'none' }); uni.showToast({ title: '请先选择班级', icon: 'none' });
return; return;
} }
// const classIdEnc = encodeURIComponent(selectedClassId.value);
const cls = classList.value.find((c) => c.id === selectedClassId.value); const params: string[] = ['classId=' + classIdEnc];
const className = cls?.name || '';
//
const params: string[] = [];
params.push('classId=' + encodeURIComponent(selectedClassId.value));
params.push('className=' + encodeURIComponent(className));
if (selectedSubjectIds.value.length > 0) { if (selectedSubjectIds.value.length > 0) {
params.push('subjectIds=' + encodeURIComponent(selectedSubjectIds.value.join(','))); params.push('subjectIds=' + encodeURIComponent(selectedSubjectIds.value.join(',')));
} }
if (startTime.value) { if (startTime.value) params.push('startTime=' + encodeURIComponent(startTime.value));
params.push('startTime=' + encodeURIComponent(startTime.value)); if (endTime.value) params.push('endTime=' + encodeURIComponent(endTime.value));
const query = params.join('&');
if (target.value === 'weakKnowledge') {
uni.navigateTo({ url: '/pages/teacher/weakKnowledge/index?' + query });
} else {
const cls = classList.value.find((c) => c.id === selectedClassId.value);
const className = cls?.name || '';
const homeworkParams = ['classId=' + classIdEnc, 'className=' + encodeURIComponent(className)];
if (selectedSubjectIds.value.length > 0) {
homeworkParams.push('subjectIds=' + encodeURIComponent(selectedSubjectIds.value.join(',')));
} }
if (endTime.value) { if (startTime.value) homeworkParams.push('startTime=' + encodeURIComponent(startTime.value));
params.push('endTime=' + encodeURIComponent(endTime.value)); if (endTime.value) homeworkParams.push('endTime=' + encodeURIComponent(endTime.value));
uni.navigateTo({ url: '/pages/teacher/homework/index?' + homeworkParams.join('&') });
} }
const url = '/pages/teacher/homework/index?' + params.join('&');
uni.navigateTo({ url });
} }
const fetchClassList = async () => { const fetchClassList = async () => {

View File

@ -47,8 +47,7 @@
<view class="main-body"> <view class="main-body">
<!-- 加载中 --> <!-- 加载中 -->
<view v-if="loading" class="status-box"> <view v-if="loading" class="status-box">
<view class="status-icon"></view> <qy-Loading text="加载中..." />
<text class="status-text">加载中...</text>
</view> </view>
<!-- 加载失败 --> <!-- 加载失败 -->
<view v-else-if="error" class="status-box error"> <view v-else-if="error" class="status-box error">

View File

@ -25,6 +25,15 @@
</view> </view>
</picker> </picker>
<!-- 薄弱知识点 Top10点击后先到筛选页选条件再进薄弱知识点页 -->
<view class="weak-knowledge-wrap">
<view class="weak-knowledge-btn" @click="goWeakKnowledge">
<text class="weak-knowledge-icon">📌</text>
<text class="weak-knowledge-label">薄弱知识点 Top10</text>
</view>
</view>
<!-- 学情报告等入口横向排版 -->
<view class="left-actions"> <view class="left-actions">
<view class="action-item" @click="handleNavigate('/pages/teacher/homework/filter')"> <view class="action-item" @click="handleNavigate('/pages/teacher/homework/filter')">
<text class="action-icon">📊</text> <text class="action-icon">📊</text>
@ -80,7 +89,7 @@
<scroll-view class="dashboard-body" scroll-y enhanced :show-scrollbar="false"> <scroll-view class="dashboard-body" scroll-y enhanced :show-scrollbar="false">
<!-- 加载 / 空状态 --> <!-- 加载 / 空状态 -->
<view v-if="loading" class="status-box"> <view v-if="loading" class="status-box">
<text class="status-text">加载中...</text> <qy-Loading text="加载中..." />
</view> </view>
<view v-else-if="!selectedClassId" class="status-box"> <view v-else-if="!selectedClassId" class="status-box">
<text class="status-text">请选择班级查看数据</text> <text class="status-text">请选择班级查看数据</text>
@ -229,6 +238,18 @@ const handleNavigate = (url: string) => {
} }
uni.navigateTo({ url }); uni.navigateTo({ url });
}; };
/** 跳转筛选条件页面,选择完筛选项后进入薄弱知识点页面 */
function goWeakKnowledge() {
if (!token.value) {
uni.navigateTo({
url: `/pages/login/index?redirect=${encodeURIComponent('/pages/teacher/homework/filter?target=weakKnowledge')}`,
});
return;
}
uni.navigateTo({ url: '/pages/teacher/homework/filter?target=weakKnowledge' });
}
const goLogin = () => uni.navigateTo({ url: '/pages/login/index' }); const goLogin = () => uni.navigateTo({ url: '/pages/login/index' });
const handleLogout = () => { const handleLogout = () => {
uni.showModal({ uni.showModal({
@ -516,23 +537,67 @@ $teacher-bg: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/main_bg.s
&:active { opacity: 0.85; } &:active { opacity: 0.85; }
} }
// Top10
.weak-knowledge-wrap {
width: 100%;
margin-top: 8rpx;
}
.weak-knowledge-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx;
padding: 8rpx 6rpx;
background: linear-gradient(135deg, #8f9df7 0%, #cab5ff 100%);
border-radius: 8rpx;
border: 1rpx solid rgba(143, 157, 247, 0.5);
.weak-knowledge-icon {
font-size: 14rpx;
flex-shrink: 0;
}
.weak-knowledge-label {
font-family: $font-special;
font-size: 11rpx;
color: #fff;
font-weight: 600;
text-align: center;
line-height: 1.2;
}
&.disabled {
background: rgba(255, 255, 255, 0.5);
border-color: rgba(196, 181, 255, 0.3);
.weak-knowledge-label { color: #999; }
}
&:active:not(.disabled) { opacity: 0.85; }
}
//
.left-actions { .left-actions {
margin-top: auto; margin-top: auto;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
flex-wrap: wrap;
gap: 6rpx; gap: 6rpx;
justify-content: center;
} }
.action-item { .action-item {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: center; align-items: center;
justify-content: center;
gap: 4rpx;
padding: 8rpx 4rpx; padding: 8rpx 4rpx;
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
border-radius: 8rpx; border-radius: 8rpx;
border: 1rpx solid rgba(196, 181, 255, 0.2); border: 1rpx solid rgba(196, 181, 255, 0.2);
margin-top: 8rpx;
width: 100%;
.action-icon { .action-icon {
font-size: 18rpx; font-size: 18rpx;
margin-bottom: 2rpx; margin-bottom: 2rpx;
@ -546,6 +611,7 @@ $teacher-bg: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/main_bg.s
&.logout { &.logout {
background: rgba(255, 255, 255, 0.6); background: rgba(255, 255, 255, 0.6);
.action-label { color: #999; } .action-label { color: #999; }
width: 100%;
} }
&:active { opacity: 0.8; } &:active { opacity: 0.8; }

View File

@ -0,0 +1,298 @@
<template>
<view class="weak-page" :style="safeAreaStyle">
<!-- 顶部栏 -->
<view class="top-bar" :style="topRightPaddingStyle">
<qy-BackBar leftText="薄弱知识点 Top10" />
</view>
<!-- 主体 -->
<view class="main-body">
<view v-if="loading" class="status-box">
<qy-Loading text="加载中..." />
</view>
<view v-else-if="errorMsg" class="status-box error">
<text class="status-text">{{ errorMsg }}</text>
</view>
<view v-else-if="!classId" class="status-box">
<text class="status-text">缺少班级参数</text>
</view>
<scroll-view v-else class="list-scroll" scroll-y enhanced :show-scrollbar="false">
<view v-if="listData" class="list-content">
<view class="class-header">
<text class="class-name">{{ listData.className }}</text>
<text class="class-desc">薄弱知识点 Top10</text>
</view>
<view v-if="!listData.topWeakKnowledgeList || listData.topWeakKnowledgeList.length === 0" class="empty-hint">
<text>暂无薄弱知识点数据</text>
</view>
<view v-else class="knowledge-list">
<view
v-for="(item, idx) in listData.topWeakKnowledgeList"
:key="item.knowledgeId + '-' + idx"
class="knowledge-item"
>
<view class="knowledge-rank" :class="'rank-' + (idx + 1)">{{ idx + 1 }}</view>
<view class="knowledge-main">
<text class="knowledge-name">{{ item.knowledgeName || '未命名知识点' }}</text>
<view class="knowledge-meta">
<text v-if="item.subjectName" class="knowledge-subject">{{ item.subjectName }}</text>
<text class="knowledge-count">{{ item.errorCount ?? 0 }}</text>
<text class="knowledge-titles">涉及{{ item.titleCount ?? 0 }}</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import { teacher } from '@/store/teacher';
import { getWeakKnowledgeTop10 } from '@/api/teacher';
import type { WeakKnowledgeTop10Data } from '@/api/teacher';
const teacherStore = teacher();
const { schoolId } = storeToRefs(teacherStore);
const classId = ref('');
const startTime = ref('');
const endTime = ref('');
const subjectIds = ref<number[]>([]);
const loading = ref(false);
const errorMsg = ref('');
const listData = ref<WeakKnowledgeTop10Data | null>(null);
//
const topPaddingRightRpx = ref(0);
const safeAreaLeftRpx = ref(0);
const topRightPaddingStyle = computed(() =>
topPaddingRightRpx.value > 0 ? { paddingRight: topPaddingRightRpx.value + 'rpx' } : {},
);
const safeAreaStyle = computed(() => {
const style: Record<string, string> = {};
if (safeAreaLeftRpx.value > 0) {
style.paddingLeft = safeAreaLeftRpx.value + 'rpx';
}
return style;
});
function getCapsulePadding() {
try {
// #ifdef MP-WEIXIN
const menuButton = uni.getMenuButtonBoundingClientRect();
const sys = uni.getSystemInfoSync();
if (menuButton && sys?.windowWidth) {
const r = 750 / sys.windowWidth;
topPaddingRightRpx.value = Math.ceil((sys.windowWidth - menuButton.left) * r) + 24;
} else {
topPaddingRightRpx.value = 140;
}
if (sys?.safeArea && sys.safeArea.left > 0) {
safeAreaLeftRpx.value = Math.ceil(sys.safeArea.left * (750 / (sys.windowWidth || sys.screenWidth))) + 4;
}
// #endif
} catch {
topPaddingRightRpx.value = 140;
}
}
onLoad((options: any) => {
if (options?.classId) classId.value = options.classId;
if (options?.startTime) startTime.value = options.startTime;
if (options?.endTime) endTime.value = options.endTime;
if (options?.subjectIds) {
const raw = options.subjectIds;
if (typeof raw === 'string') {
subjectIds.value = raw.split(',').map((s: string) => Number(s.trim())).filter((n: number) => !isNaN(n));
} else if (Array.isArray(raw)) {
subjectIds.value = raw.map((s: any) => Number(s)).filter((n: number) => !isNaN(n));
}
}
});
async function fetchList() {
if (!classId.value) return;
loading.value = true;
errorMsg.value = '';
listData.value = null;
try {
const params: any = { classId: classId.value };
if (schoolId.value) params.schoolId = schoolId.value;
if (subjectIds.value.length > 0) params.subjectIds = subjectIds.value;
if (startTime.value?.trim()) params.startTime = startTime.value.trim();
if (endTime.value?.trim()) params.endTime = endTime.value.trim();
const res = await getWeakKnowledgeTop10(params);
listData.value = (res?.data ?? res) as WeakKnowledgeTop10Data;
} catch (e) {
console.error('薄弱知识点 Top10 请求失败', e);
errorMsg.value = '加载失败,请重试';
} finally {
loading.value = false;
}
}
onMounted(() => {
getCapsulePadding();
if (classId.value) fetchList();
});
</script>
<style lang="scss" scoped>
$teacher-bg: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/main_bg.svg';
.weak-page {
height: 100vh;
display: flex;
flex-direction: column;
background-image: url($teacher-bg);
background-size: cover;
background-position: center;
box-sizing: border-box;
padding: 0 16rpx 0 16rpx;
overflow: hidden;
}
.top-bar {
padding: 12rpx 0 8rpx;
flex-shrink: 0;
}
.main-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.status-box {
display: flex;
align-items: center;
justify-content: center;
padding: 60rpx 20rpx;
.status-text {
font-size: 16rpx;
color: #fff;
text-shadow: 0 1rpx 4rpx rgba(143, 157, 247, 0.5);
}
&.error .status-text {
color: #ffb3b3;
}
}
.list-scroll {
flex: 1;
height: 0;
}
.list-content {
padding-bottom: 24rpx;
}
.class-header {
margin-bottom: 16rpx;
padding: 12rpx 16rpx;
background: rgba(255, 255, 255, 0.95);
border-radius: 12rpx;
border: 1rpx solid rgba(196, 181, 255, 0.25);
.class-name {
font-family: $font-special;
font-size: 20rpx;
font-weight: 700;
color: $font-color;
display: block;
}
.class-desc {
font-size: 13rpx;
color: #888;
margin-top: 4rpx;
}
}
.empty-hint {
padding: 40rpx 0;
text {
font-size: 15rpx;
color: #999;
}
}
.knowledge-list {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.knowledge-item {
display: flex;
align-items: flex-start;
gap: 12rpx;
padding: 12rpx 14rpx;
background: rgba(255, 255, 255, 0.95);
border-radius: 10rpx;
border: 1rpx solid rgba(196, 181, 255, 0.18);
}
.knowledge-rank {
width: 32rpx;
height: 32rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-family: $font-special;
font-size: 14rpx;
font-weight: 700;
color: #fff;
flex-shrink: 0;
background: #cab5ff;
&.rank-1 { background: linear-gradient(135deg, #ff6b6b, #ee5a24); }
&.rank-2 { background: linear-gradient(135deg, #f7a635, #f59f00); }
&.rank-3 { background: linear-gradient(135deg, #5b8def, #4a7bdf); }
}
.knowledge-main {
flex: 1;
min-width: 0;
}
.knowledge-name {
font-family: $font-special;
font-size: 15rpx;
font-weight: 600;
color: $font-color;
@include single-ellipsis;
display: block;
}
.knowledge-meta {
display: flex;
align-items: center;
gap: 12rpx;
margin-top: 6rpx;
}
.knowledge-subject {
font-size: 12rpx;
color: #8f9df7;
padding: 2rpx 8rpx;
background: rgba(143, 157, 247, 0.12);
border-radius: 4rpx;
}
.knowledge-count {
font-size: 12rpx;
color: #e05040;
}
.knowledge-titles {
font-size: 12rpx;
color: #999;
}
</style>

BIN
src/static/loading.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB