feat: 老师端

This commit is contained in:
李梦 2026-02-09 18:07:15 +08:00
parent d1765d936c
commit da364bc97d
11 changed files with 1618 additions and 366 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

@ -55,6 +55,8 @@ export const getPaperReleaseRecordList = (params: {
schoolId: string; schoolId: string;
current: number; current: number;
size: number; size: number;
classId?: string;
subjectId?: string;
}) => { }) => {
return request({ return request({
url: '/school/paperReleaseRecord/list', url: '/school/paperReleaseRecord/list',
@ -62,3 +64,81 @@ export const getPaperReleaseRecordList = (params: {
params, params,
}); });
}; };
// 班级列表(按教师)- 接口 org/class/listByTeacher可选年级筛选
export const getClassListByTeacher = (params?: { gradeId?: string; gradeName?: string }) => {
return request({
url: '/org/class/listByTeacher',
method: 'GET',
params: params || {},
});
};
// 根据班级ID获取学科列表
export const getSubjectListByClassId = (classId: string) => {
return request({
url: '/org/class/listSubjectInfoByClassId',
method: 'GET',
params: { classId },
});
};
// ---------- 班级作业完成情况报表 school/report/classHomework/list ----------
export interface ClassHomeworkSubjectStat {
subjectId: number;
subjectName: string;
homeworkCount: number;
completedCount: number;
uncompletedCount: number;
}
export interface ClassHomeworkStudentItem {
userId: number;
userName: string;
totalCount: number;
completedCount: number;
uncompletedCount: number;
subjectStats: ClassHomeworkSubjectStat[];
}
export interface ClassHomeworkReportData {
classId: number;
className: string;
subjects: { subjectId: number; subjectName: string }[];
studentList: ClassHomeworkStudentItem[];
}
/** 班级作业报表GET 请求,参数以 form 表单风格放在 querysubjectIds 为多个同名参数) */
export const getClassHomeworkReport = (params: {
classId: string | number;
/** 学科ID列表可多选不传或空则查全部学科 */
subjectIds?: number[];
startTime?: string;
endTime?: string;
}) => {
// form 表单编码:空格用 +,特殊字符用 encodeURIComponent 后把 %20 替换为 +
const formEncode = (v: string) => encodeURIComponent(v).replace(/%20/g, '+');
const query: string[] = [];
query.push('classId=' + formEncode(String(params.classId)));
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() !== '') {
// 后端 Date 类型需要带时分秒picker 只返回 yyyy-MM-dd补 00:00:00
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() !== '') {
// 结束时间补 23:59:59保证包含当天
const et = String(params.endTime).trim();
query.push('endTime=' + formEncode(et.includes(' ') ? et : et + ' 23:59:59'));
}
const queryString = query.join('&');
return request({
url: '/school/report/classHomework/list?' + queryString,
method: 'GET',
});
};

View File

@ -1,12 +1,15 @@
<template> <template>
<view class="select-wrap"> <view class="select-wrap">
<view class="select-btn" @click="showPicker = true"> <view class="select-btn" @click="openPopup">
<text class="select-text">{{ currentLabel }}</text> <text class="select-text">{{ currentLabel }}</text>
<image class="select-arrow" :src="`${OSS_URL}/icon/icon_arrow_down.svg`" mode="aspectFit" /> <image class="select-arrow" :src="`${OSS_URL}/icon/icon_arrow_down.svg`" mode="aspectFit" />
</view> </view>
<uni-popup ref="popupRef" type="bottom" @change="onPopupChange"> <uni-popup ref="popupRef" type="bottom" @change="onPopupChange">
<view class="picker-content"> <view
class="picker-content"
:style="{ paddingRight: capsulePaddingRightRpx + 'rpx' }"
>
<view class="picker-header"> <view class="picker-header">
<text class="cancel" @click="handleCancel">取消</text> <text class="cancel" @click="handleCancel">取消</text>
<text class="title">请选择</text> <text class="title">请选择</text>
@ -33,10 +36,39 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com'; const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
//
const capsulePaddingRightRpx = ref(0);
const getCapsulePadding = () => {
try {
// #ifdef MP-WEIXIN
const menuButton = uni.getMenuButtonBoundingClientRect();
const systemInfo = uni.getSystemInfoSync();
if (menuButton && systemInfo?.windowWidth) {
const pxToRpx = 750 / systemInfo.windowWidth;
// +
const rightPx = systemInfo.windowWidth - menuButton.left;
capsulePaddingRightRpx.value = Math.ceil(rightPx * pxToRpx) + 24;
} else {
capsulePaddingRightRpx.value = 140;
}
// #endif
// #ifndef MP-WEIXIN
capsulePaddingRightRpx.value = 0;
// #endif
} catch {
capsulePaddingRightRpx.value = 140;
}
};
onMounted(() => {
getCapsulePadding();
});
interface SelectOption { interface SelectOption {
label: string; label: string;
value: string | number; value: string | number;
@ -76,6 +108,10 @@ watch(
{ immediate: true }, { immediate: true },
); );
const openPopup = () => {
showPicker.value = true;
};
watch(showPicker, (val) => { watch(showPicker, (val) => {
if (val) { if (val) {
popupRef.value?.open(); popupRef.value?.open();

View File

@ -115,6 +115,19 @@
} }
} }
}, },
{
"path": "homework/filter",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "筛选条件",
"disableScroll": true,
"pageOrientation": "landscape",
"allowsBounceVertical": "NO",
"mp-weixin": {
"pageOrientation": "landscape"
}
}
},
{ {
"path": "homework/index", "path": "homework/index",
"style": { "style": {

View File

@ -210,6 +210,13 @@
<rich-text :nodes="filterTitle(typeof data.aiAnswer === 'string' ? data.aiAnswer : String(data.aiAnswer))" /> <rich-text :nodes="filterTitle(typeof data.aiAnswer === 'string' ? data.aiAnswer : String(data.aiAnswer))" />
</view> </view>
</view> </view>
<!-- AI点评 -->
<view v-if="data?.aiAnalyze != null " class="answer-box">
<view class="answer-box-title">AI点评</view>
<view class="answer-box-main analyze">
<rich-text :nodes="filterTitle(typeof data.aiAnalyze === 'string' ? data.aiAnalyze : String(data.aiAnalyze))" />
</view>
</view>
</template> </template>
</scroll-view> </scroll-view>

View File

@ -26,7 +26,9 @@
<view class="book-box"> <view class="book-box">
<view class="book"> <view class="book">
<template v-if="paperList.length && paperList[activeIdx]"> <template v-if="paperList.length && paperList[activeIdx]">
<!-- key 随题目变化切换题目时强制重新挂载重新加载题目内容含公式 SVG 避免网络差时图片加载失败不重试 -->
<Question <Question
:key="`question-${activeIdx}-${paperList[activeIdx]?.id ?? ''}`"
:data="paperList[activeIdx]" :data="paperList[activeIdx]"
:idx="activeIdx + 1" :idx="activeIdx + 1"
:total="paperList.length" :total="paperList.length"

View File

@ -0,0 +1,475 @@
<template>
<view class="filter-page" :style="safeAreaStyle">
<!-- 顶部标题 -->
<view class="header" :style="topRightPaddingStyle">
<qy-BackBar leftText="筛选条件" />
</view>
<!-- 筛选区域 -->
<scroll-view class="filter-body" scroll-y>
<!-- 班级选择 -->
<view class="filter-section">
<view class="section-title">
<text class="section-title-text">选择班级</text>
<text class="section-required">*必选</text>
</view>
<view class="option-grid">
<view
v-for="cls in classList"
:key="cls.id"
class="option-item"
:class="{ active: selectedClassId === cls.id }"
@click="selectedClassId = cls.id"
>
<text class="option-text">{{ cls.name }}</text>
<view v-if="selectedClassId === cls.id" class="option-check"></view>
</view>
</view>
<view v-if="classList.length === 0" class="empty-hint">
<text>{{ classLoading ? '加载中...' : '暂无班级数据' }}</text>
</view>
</view>
<!-- 学科选择 -->
<view class="filter-section">
<view class="section-title">
<text class="section-title-text">选择学科</text>
<text class="section-hint">可多选不选默认全部</text>
</view>
<view v-if="!selectedClassId" class="empty-hint">
<text>请先选择班级</text>
</view>
<view v-else-if="subjectLoading" class="empty-hint">
<text>加载学科中...</text>
</view>
<view v-else-if="subjectList.length === 0" class="empty-hint">
<text>该班级暂无学科数据</text>
</view>
<view v-else class="option-grid">
<view
v-for="sub in subjectList"
:key="sub.value"
class="option-item"
:class="{ active: selectedSubjectIds.includes(sub.value) }"
@click="toggleSubject(sub.value)"
>
<text class="option-text">{{ sub.label }}</text>
<view v-if="selectedSubjectIds.includes(sub.value)" class="option-check"></view>
</view>
</view>
</view>
<!-- 时间范围 -->
<view class="filter-section">
<view class="section-title">
<text class="section-title-text">时间范围</text>
<text class="section-hint">可选</text>
</view>
<view class="date-row">
<picker mode="date" :value="startTime" @change="onStartChange">
<view class="date-picker-box" :class="{ filled: !!startTime }">
<text class="date-picker-text">{{ startTime || '开始日期' }}</text>
</view>
</picker>
<text class="date-sep"></text>
<picker mode="date" :value="endTime" @change="onEndChange">
<view class="date-picker-box" :class="{ filled: !!endTime }">
<text class="date-picker-text">{{ endTime || '结束日期' }}</text>
</view>
</picker>
<view v-if="startTime || endTime" class="date-clear" @click="clearDate">
<text>清除</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="bottom-bar">
<view class="btn-reset" @click="handleReset">
<text>重置</text>
</view>
<view class="btn-confirm" :class="{ disabled: !selectedClassId }" @click="handleConfirm">
<text>查询</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { getClassListByTeacher, getSubjectListByClassId } from '@/api/teacher';
import { teacher } from '@/store/teacher';
import { storeToRefs } from 'pinia';
const teacherStore = teacher();
const { schoolId } = storeToRefs(teacherStore);
// +
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 systemInfo = uni.getSystemInfoSync();
if (menuButton && systemInfo?.windowWidth) {
const pxToRpx = 750 / systemInfo.windowWidth;
const rightPx = systemInfo.windowWidth - menuButton.left;
topPaddingRightRpx.value = Math.ceil(rightPx * pxToRpx) + 24;
} else {
topPaddingRightRpx.value = 140;
}
//
if (systemInfo?.safeArea && systemInfo.safeArea.left > 0) {
const pxToRpx = 750 / (systemInfo.windowWidth || systemInfo.screenWidth);
safeAreaLeftRpx.value = Math.ceil(systemInfo.safeArea.left * pxToRpx) + 4;
}
// #endif
// #ifndef MP-WEIXIN
topPaddingRightRpx.value = 0;
// #endif
} catch {
topPaddingRightRpx.value = 140;
}
}
//
const classList = ref<{ id: string; name: string }[]>([]);
const classLoading = ref(false);
const selectedClassId = ref('');
//
const subjectList = ref<{ value: number; label: string }[]>([]);
const subjectLoading = ref(false);
const selectedSubjectIds = ref<number[]>([]);
//
watch(() => selectedClassId.value, async (newClassId) => {
//
selectedSubjectIds.value = [];
subjectList.value = [];
if (!newClassId) return;
subjectLoading.value = true;
try {
const res = await getSubjectListByClassId(newClassId);
const data = res?.data ?? res;
//
if (Array.isArray(data)) {
subjectList.value = data.map((item: any) => ({
value: item.subjectId ?? item.id ?? item.value,
label: item.subjectName ?? item.name ?? item.label ?? '',
}));
} else {
subjectList.value = [];
}
} catch (e) {
console.error('获取学科列表失败', e);
subjectList.value = [];
} finally {
subjectLoading.value = false;
}
});
//
function getTodayStr() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
const today = getTodayStr();
const startTime = ref(today);
const endTime = ref(today);
function toggleSubject(value: number) {
const idx = selectedSubjectIds.value.indexOf(value);
if (idx >= 0) {
selectedSubjectIds.value = selectedSubjectIds.value.filter((v) => v !== value);
} else {
selectedSubjectIds.value = [...selectedSubjectIds.value, value];
}
}
function onStartChange(e: any) {
startTime.value = e.detail?.value || '';
}
function onEndChange(e: any) {
endTime.value = e.detail?.value || '';
}
function clearDate() {
startTime.value = '';
endTime.value = '';
}
function handleReset() {
selectedClassId.value = '';
selectedSubjectIds.value = [];
startTime.value = '';
endTime.value = '';
}
function handleConfirm() {
if (!selectedClassId.value) {
uni.showToast({ title: '请先选择班级', icon: 'none' });
return;
}
//
const cls = classList.value.find((c) => c.id === selectedClassId.value);
const className = cls?.name || '';
//
const params: string[] = [];
params.push('classId=' + encodeURIComponent(selectedClassId.value));
params.push('className=' + encodeURIComponent(className));
if (selectedSubjectIds.value.length > 0) {
params.push('subjectIds=' + encodeURIComponent(selectedSubjectIds.value.join(',')));
}
if (startTime.value) {
params.push('startTime=' + encodeURIComponent(startTime.value));
}
if (endTime.value) {
params.push('endTime=' + encodeURIComponent(endTime.value));
}
const url = '/pages/teacher/homework/index?' + params.join('&');
uni.navigateTo({ url });
}
const fetchClassList = async () => {
if (!schoolId.value) return;
classLoading.value = true;
try {
const res = await getClassListByTeacher();
const data = res?.data ?? res;
classList.value = Array.isArray(data) ? data : data?.list ?? data?.rows ?? [];
} catch (e) {
console.error('获取班级列表失败', e);
classList.value = [];
} finally {
classLoading.value = false;
}
};
onMounted(() => {
getCapsulePadding();
if (schoolId.value) fetchClassList();
});
onShow(async () => {
if (!schoolId.value) {
try {
await teacherStore.getLoginUser();
} catch (err) {
console.error('刷新老师信息失败', err);
}
}
if (schoolId.value && classList.value.length === 0) {
fetchClassList();
}
});
</script>
<style lang="scss" scoped>
.filter-page {
height: 100vh;
display: flex;
flex-direction: column;
background-image: url('https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/main_bg.svg');
background-size: cover;
background-position: center;
box-sizing: border-box;
overflow: hidden;
padding-left: 16rpx;
padding-right: 16rpx;
}
.header {
padding: 4rpx 12rpx 0;
flex-shrink: 0;
}
//
.filter-body {
flex: 1;
height: 0;
padding: 4rpx 12rpx 6rpx;
}
.filter-section {
background: linear-gradient(180deg, #fff 0%, #f8f6ff 100%);
border-radius: 8rpx;
padding: 6rpx 10rpx;
margin-bottom: 6rpx;
box-shadow: 0 1rpx 6rpx rgba(143, 157, 247, 0.1);
border: 1rpx solid rgba(196, 181, 255, 0.2);
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 5rpx;
gap: 4rpx;
}
.section-title-text {
font-family: $font-special;
font-size: 15rpx;
font-weight: 600;
color: $font-color;
}
.section-required {
font-size: 11rpx;
color: #e05040;
}
.section-hint {
font-size: 11rpx;
color: #999;
}
//
.option-grid {
display: flex;
flex-wrap: wrap;
gap: 5rpx;
}
.option-item {
display: flex;
align-items: center;
gap: 3rpx;
padding: 4rpx 8rpx;
background: rgba(196, 181, 255, 0.1);
border: 1rpx solid rgba(196, 181, 255, 0.25);
border-radius: 6rpx;
transition: all 0.2s;
.option-text {
font-family: $font-special;
font-size: 13rpx;
color: #666;
}
.option-check {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: linear-gradient(135deg, #8f9df7 0%, #92fc90 100%);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 10rpx;
font-weight: 700;
}
&.active {
background: linear-gradient(135deg, rgba(143, 157, 247, 0.15) 0%, rgba(202, 181, 255, 0.15) 100%);
border-color: #8f9df7;
.option-text {
color: #8f9df7;
font-weight: 600;
}
}
}
.empty-hint {
padding: 8rpx 0;
text {
font-size: 12rpx;
color: #999;
}
}
//
.date-row {
display: flex;
align-items: center;
gap: 6rpx;
}
.date-picker-box {
padding: 4rpx 10rpx;
background: rgba(196, 181, 255, 0.1);
border: 1rpx solid rgba(196, 181, 255, 0.25);
border-radius: 6rpx;
min-width: 90rpx;
text-align: center;
.date-picker-text {
font-size: 12rpx;
color: #999;
}
&.filled {
border-color: #8f9df7;
.date-picker-text {
color: #8f9df7;
font-weight: 500;
}
}
}
.date-sep {
font-size: 12rpx;
color: #999;
}
.date-clear {
padding: 2rpx 6rpx;
text {
font-size: 12rpx;
color: #e05040;
}
}
//
.bottom-bar {
flex-shrink: 0;
display: flex;
gap: 10rpx;
padding: 0 12rpx;
padding-bottom: calc(6rpx + env(safe-area-inset-bottom));
}
.btn-reset {
flex: 1;
padding: 7rpx 0;
border-radius: 8rpx;
background: rgba(255, 255, 255, 0.9);
border: 1rpx solid rgba(196, 181, 255, 0.3);
text-align: center;
text {
font-family: $font-special;
font-size: 13rpx;
color: #666;
}
}
.btn-confirm {
flex: 2;
padding: 7rpx 0;
border-radius: 8rpx;
background: linear-gradient(269deg, #adadff 2%, #cab5ff 52%, #8f9df7 100%);
box-shadow: 0 2rpx 8rpx rgba(143, 157, 247, 0.3);
text-align: center;
text {
font-family: $font-special;
font-size: 13rpx;
color: #fff;
font-weight: 600;
}
&.disabled {
opacity: 0.5;
}
&:active:not(.disabled) {
opacity: 0.85;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@
<!-- 右侧功能入口 --> <!-- 右侧功能入口 -->
<view class="right-section"> <view class="right-section">
<view class="content"> <view class="content">
<view class="entry-card" @click="handleNavigate('/pages/teacher/homework/index')"> <view class="entry-card" @click="handleNavigate('/pages/teacher/homework/filter')">
<view class="card-icon homework-icon"> <view class="card-icon homework-icon">
<image :src="`${OSS_URL}/icon/homeWork_hisJob.svg`" mode="aspectFit" /> <image :src="`${OSS_URL}/icon/homeWork_hisJob.svg`" mode="aspectFit" />
</view> </view>

View File

@ -1,3 +1,4 @@
export * from './tool'; export * from './tool';
export * from './map'; export * from './map';
export * from './getStatic'; export * from './getStatic';
export * from './subject';

36
src/utils/subject.ts Normal file
View File

@ -0,0 +1,36 @@
/**
* admin playground store/basic-data/subject
*/
export interface SubjectOption {
value: number;
label: string;
}
const subjects: SubjectOption[] = [
{ value: 2, label: '语文' },
{ value: 3, label: '数学' },
{ value: 4, label: '英语' },
{ value: 5, label: '科学' },
{ value: 6, label: '物理' },
{ value: 7, label: '化学' },
{ value: 8, label: '历史' },
{ value: 9, label: '道德与法治' },
{ value: 10, label: '地理' },
{ value: 11, label: '生物' },
{ value: 12, label: '政治' },
{ value: 13, label: '信息' },
{ value: 14, label: '通用' },
{ value: 15, label: '日语' },
];
/** 学科下拉选项(用于筛选等),与 admin 教材选择弹框学科数据一致 */
export function getSubjectOptions(): SubjectOption[] {
return subjects;
}
/** 学科 value 转 label */
export function getSubjectLabel(value: number | string): string {
const v = Number(value);
const item = subjects.find((s) => s.value === v);
return item?.label ?? '';
}