选课调整

This commit is contained in:
hebo 2025-09-09 21:55:09 +08:00
parent 073e462d71
commit 1598d8e886
16 changed files with 3589 additions and 234 deletions

19
src/api/analysis/xk.ts Normal file
View File

@ -0,0 +1,19 @@
import { get, post } from '@/utils/request'
// 获取选课列表
export const getXkListApi = () => {
return get('/api/xk/findPage', {
page: 1,
rows: 100,
status: 'A'
})
}
// 获取兴趣课选课列表
export const getXkCourseListApi = (njId: string, bjId: string, xkId?: string) => {
const params: any = { njId, bjId }
if (xkId) {
params.xkId = xkId
}
return get('/api/xk/getXkCourseList', params)
}

10
src/api/analysis/xkDm.ts Normal file
View File

@ -0,0 +1,10 @@
import { get } from '@/utils/request'
// 获取选课点名统计
export const getXkDmStatisticsApi = (xkId: string, startTime?: string, endTime?: string) => {
const params: any = { xkId }
if (startTime) params.startTime = startTime
if (endTime) params.endTime = endTime
return get('/api/xkDm/getXkDmStatistics', params)
}

10
src/api/analysis/xs.ts Normal file
View File

@ -0,0 +1,10 @@
import { get, post } from "@/utils/request";
// 学生档案相关API接口
/**
* ID和班级ID查询学生及家长信息
*/
export function findStudentInfoByNjAndBjSimpleApi(njId: string, bjId: string) {
return get('/api/xs/findStudentInfoByNjAndBjSimple', { njId, bjId });
}

View File

@ -560,6 +560,41 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/analysis/xs/studentArchive",
"style": {
"navigationBarTitleText": "学生档案",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/analysis/xk/xkCourse",
"style": {
"navigationBarTitleText": "课程明单",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/analysis/xk/xkList",
"style": {
"navigationBarTitleText": "选课清单",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/analysis/xk/dmStatistics",
"style": {
"navigationBarTitleText": "点名统计",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/analysis/xk/dmXkList",
"style": {
"navigationBarTitleText": "点名选课列表",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/qd/index",
"style": {

View File

@ -320,6 +320,30 @@ const sections = reactive<Section[]>([
permissionKey: "routine-bjjl", //
path: "/pages/view/notice/index",
},
{
id: "hs4",
icon: "xsda",
text: "学生档案",
show: true,
permissionKey: "home-xsda", //
path: "/pages/view/analysis/xs/studentArchive",
},
{
id: "hs5",
icon: "xkqd",
text: "选课清单",
show: true,
permissionKey: "home-xkqd", //
path: "/pages/view/analysis/xk/xkList",
},
{
id: "hs6",
icon: "dmtj",
text: "点名统计",
show: true,
permissionKey: "home-dmtj", //
path: "/pages/view/analysis/xk/dmXkList",
},
],
},
{

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,436 @@
<template>
<view class="xk-list-container">
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">选课列表</text>
</view>
<!-- 选课列表 -->
<view class="section" v-if="xkList.length > 0">
<view class="section-title">
选课信息 ({{ xkList.length }}个选课)
</view>
<!-- 选课列表 -->
<view class="xk-list">
<view
v-for="xk in xkList"
:key="xk.id"
class="xk-item bg-white r-md p-12"
@click="goToXkCourse(xk)"
>
<view class="xk-header">
<view class="xk-title">{{ xk.xkmc }}</view>
<view class="xk-actions">
<view v-if="xk.xkStatus !== '已结束'" class="xk-status" :class="getStatusClass(xk.xkStatus)">
{{ xk.xkStatus }}
</view>
<view v-else class="more-btn" @click.stop="showMoreOptions(xk)">
<image src="/static/base/view/more.png" class="more-icon" />
</view>
</view>
</view>
<view class="xk-info">
<view class="info-row">
<text class="info-label">选课类型</text>
<text class="info-value">{{ xk.xklxName || '未知' }}</text>
</view>
<view class="info-row">
<text class="info-label">学期名称</text>
<text class="info-value">{{ xk.xqmc || '未知' }}</text>
</view>
<view class="info-row">
<text class="info-label">年级范围</text>
<text class="info-value">{{ xk.njmc || '全部' }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && xkList.length === 0 && hasSearched">
<view class="empty-icon">📚</view>
<view class="empty-text">暂无选课数据</view>
<view class="empty-tip">请联系管理员添加选课信息</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-text">正在加载...</view>
</view>
<!-- 滚动加载更多 -->
<view class="load-more" v-if="hasMore && !loading">
<view class="load-more-text">上拉加载更多</view>
</view>
<!-- 没有更多数据 -->
<view class="no-more" v-if="!hasMore && hasSearched && xkList.length > 0">
<view class="no-more-text">没有更多数据了</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getXkListApi } from '@/api/analysis/xk'
//
interface XkInfo {
id: string
xkmc: string
xklxId: string
xklxName: string
njId: string
njmc: string
xqId: string
xqmc: string
xkkstime: string
xkjstime: string
xkzt: number
xkStatus: string
allXkNum: number
yfXkNum: number
wfXkNum: number
status: string
createdTime: string
}
//
const xkList = ref<XkInfo[]>([])
const loading = ref(false)
const hasSearched = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
//
onLoad(() => {
console.log('选课列表页面加载')
loadXkList()
})
//
const loadXkList = async (isLoadMore = false) => {
if (loading.value) return
loading.value = true
hasSearched.value = true
try {
//
const response = await getXkListApi()
console.log('API返回结果:', response)
if (response && response.rows) {
const newData = response.rows || []
if (isLoadMore) {
//
xkList.value = [...xkList.value, ...newData]
} else {
//
xkList.value = newData
}
//
hasMore.value = newData.length >= pageSize.value
} else {
throw new Error('查询失败')
}
} catch (error) {
console.error('查询选课列表失败:', error)
uni.showToast({
title: '查询失败,请重试',
icon: 'none'
})
if (!isLoadMore) {
xkList.value = []
}
} finally {
loading.value = false
}
}
//
const loadMore = () => {
if (loading.value || !hasMore.value) {
return
}
currentPage.value++
loadXkList(true)
}
//
const onReachBottom = () => {
loadMore()
}
//
const goToXkCourse = (xk: XkInfo) => {
console.log('跳转到点名统计:', xk)
uni.navigateTo({
url: `/pages/view/analysis/xk/dmStatistics?xkId=${xk.id}`
})
}
// -
const showMoreOptions = (xk: XkInfo) => {
console.log('点击更多按钮,直接跳转到点名统计:', xk)
goToXkCourse(xk)
}
//
const getStatusClass = (status: string) => {
switch (status) {
case '未开始':
return 'status-pending'
case '进行中':
return 'status-active'
case '已结束':
return 'status-ended'
default:
return 'status-unknown'
}
}
</script>
<style lang="scss" scoped>
.xk-list-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.page-header {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.page-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.xk-list {
margin-bottom: 30rpx;
}
.xk-item {
margin-bottom: 20rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
position: relative;
cursor: pointer;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
}
.xk-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.xk-actions {
display: flex;
align-items: center;
}
.xk-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.xk-status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-weight: bold;
&.status-pending {
background-color: #fff7e6;
color: #fa8c16;
border: 1rpx solid #ffd591;
}
&.status-active {
background-color: #f6ffed;
color: #52c41a;
border: 1rpx solid #b7eb8f;
}
&.status-ended {
background-color: #fff2f0;
color: #ff4d4f;
border: 1rpx solid #ffccc7;
}
&.status-unknown {
background-color: #f5f5f5;
color: #999;
border: 1rpx solid #d9d9d9;
}
}
.more-btn {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: #f5f5f5;
transition: all 0.2s ease;
cursor: pointer;
&:hover {
background-color: #e6f7ff;
transform: scale(1.1);
}
&:active {
background-color: #d9d9d9;
transform: scale(0.95);
}
}
.more-icon {
width: 32rpx;
height: 32rpx;
}
.xk-info {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
margin-bottom: 12rpx;
font-size: 28rpx;
}
.info-label {
color: #666;
margin-right: 10rpx;
min-width: 140rpx;
}
.info-value {
color: #333;
flex: 1;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
.empty-icon {
font-size: 120rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 28rpx;
color: #999;
}
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx;
.loading-text {
font-size: 32rpx;
color: #666;
}
}
.bg-white {
background-color: #fff;
}
.r-md {
border-radius: 16rpx;
}
.p-12 {
padding: 24rpx;
}
/* 滚动加载样式 */
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
.load-more-text {
font-size: 28rpx;
color: #999;
}
}
.no-more {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
.no-more-text {
font-size: 28rpx;
color: #ccc;
}
}
</style>

View File

@ -0,0 +1,944 @@
<template>
<view class="xk-course-container">
<!-- 班级选择器 -->
<view class="section">
<view class="section-title">选择班级</view>
<BasicNjBjPicker
v-model="classInfo"
placeholder="请选择年级班级"
@change="onClassChange"
icon-arrow="right"
:customStyle="{ backgroundColor: '#fff', borderRadius: '0', padding: '12px 15px' }"
/>
<!-- 班级选择提示 -->
<view v-if="!classInfo" class="class-tip">
<text class="tip-icon"></text>
<text class="tip-text">请先选择班级</text>
</view>
</view>
<!-- 选课统计 -->
<view class="section" v-if="classInfo">
<view class="section-title">
选课统计
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view
class="stat-item"
:class="{ active: selectedStatType === 'registered' }"
@click="onStatItemClick('registered')"
>
<text class="stat-number registered">{{ registeredCount }}</text>
<text class="stat-label">已报名</text>
</view>
<view
class="stat-item"
:class="{ active: selectedStatType === 'unregistered' }"
@click="onStatItemClick('unregistered')"
>
<text class="stat-number unregistered">{{ unregisteredCount }}</text>
<text class="stat-label">未报名</text>
</view>
<view
class="stat-item"
:class="{ active: selectedStatType === 'all' }"
@click="onStatItemClick('all')"
>
<text class="stat-number">{{ totalStudents }}</text>
<text class="stat-label">总人数</text>
</view>
</view>
</view>
<!-- 课程列表或学生列表 -->
<view class="section" v-if="classInfo && (courseList.length > 0 || studentList.length > 0)">
<view class="section-title">
{{ getListTitle() }} ({{ getListCount() }})
</view>
<!-- 课程列表 -->
<view v-if="selectedStatType === 'registered'" class="course-list">
<view
v-for="course in filteredCourseList"
:key="course.id"
class="course-item bg-white r-md p-12"
>
<view class="course-header">
<view class="course-title">{{ course.kcmc }}</view>
<view class="course-student-count">{{ course.studentCount }}</view>
</view>
<view class="course-info">
<view class="info-row">
<text class="info-label">上课时间</text>
<text class="info-value">{{ course.studyTime }}</text>
</view>
<view class="info-row">
<text class="info-label">上课地点</text>
<text class="info-value">{{ course.kcdd }}</text>
</view>
<view class="info-row">
<text class="info-label">上课老师</text>
<text class="info-value">{{ course.jsName }}</text>
</view>
</view>
<!-- 学生列表 -->
<view class="student-list" v-if="course.students && course.students.length > 0">
<view class="student-list-title">报名学生</view>
<view class="student-tags">
<view
v-for="student in course.students"
:key="student.xsId"
class="student-tag"
>
{{ student.xm }}
</view>
</view>
</view>
</view>
</view>
<!-- 学生列表 -->
<view v-if="selectedStatType === 'unregistered' || selectedStatType === 'all'" class="student-list">
<view class="student-grid">
<view
v-for="student in filteredStudentList"
:key="student.xsId"
class="student-item bg-white r-md p-12"
@click="viewStudentDetail(student)"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="student-avatar"
:src="imagUrl(student.xstx || '')"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="student-name mb-8">
<text class="font-14 cor-333">{{ student.xsxm }}</text>
</view>
<view class="flex-row">
<!-- 报名状态标签 -->
<view
class="status-tag"
:class="getRegistrationStatusClass(student)"
>
{{ getRegistrationStatus(student) }}
</view>
</view>
</view>
<!-- 更多图标 -->
<view class="more-icon-container" @click.stop="showMoreOptions(student)">
<image
class="more-icon"
src="/static/base/view/more.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && courseList.length === 0 && hasSearched">
<view class="empty-icon">📚</view>
<view class="empty-text">暂无选课数据</view>
<view class="empty-tip">请选择年级班级后查询</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-text">正在加载...</view>
</view>
<!-- 滚动加载更多 -->
<view class="load-more" v-if="hasMore && !loading">
<view class="load-more-text">上拉加载更多</view>
</view>
<!-- 没有更多数据 -->
<view class="no-more" v-if="!hasMore && hasSearched && (courseList.length > 0 || studentList.length > 0)">
<view class="no-more-text">没有更多数据了</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { BasicNjBjPicker } from '@/components/BasicNjBjPicker'
import { onLoad } from '@dcloudio/uni-app'
import { getXkCourseListApi } from '@/api/analysis/xk'
import { findStudentInfoByNjAndBjSimpleApi } from '@/api/analysis/xs'
import { imagUrl } from "@/utils"
import { useDataStore } from '@/store/modules/data'
//
interface ClassInfo {
njId: string
bjId: string
nj: any
bj: any
}
interface CourseInfo {
id: string
kcmc: string
studyTime: string
kcdd: string
jsName: string
totalStudents: number
totalRegisteredCount: number
studentCount: number
studentNames?: string
students: StudentInfo[]
}
interface StudentInfo {
xsId: string
xm: string
xsxm: string
xstx?: string
njId: string
njmcId: string
njmc: string
bc: string
bjId: string
bjmc: string
jzIds?: string
jzxm?: string
}
//
const classInfo = ref<ClassInfo | null>(null)
const courseList = ref<CourseInfo[]>([])
const studentList = ref<StudentInfo[]>([])
const loading = ref(false)
const hasSearched = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
const selectedStatType = ref<string>('registered') //
const xkId = ref<string>('') // ID
const xkmc = ref<string>('') //
// 使store
const { setXs } = useDataStore()
//
const totalStudents = computed(() => {
//
if (courseList.value.length > 0) {
return courseList.value[0].totalStudents || 0
}
return 0
})
const registeredCount = computed(() => {
// 使
if (courseList.value.length > 0) {
return courseList.value[0].totalRegisteredCount || 0
}
return 0
})
const unregisteredCount = computed(() => {
// - =
return Math.max(0, totalStudents.value - registeredCount.value)
})
//
const filteredCourseList = computed(() => {
if (selectedStatType.value === 'all') {
return courseList.value
} else if (selectedStatType.value === 'registered') {
return courseList.value.filter(course => course.studentCount > 0)
} else if (selectedStatType.value === 'unregistered') {
return courseList.value.filter(course => course.studentCount === 0)
}
return courseList.value
})
//
const filteredStudentList = computed(() => {
if (selectedStatType.value === 'all') {
return studentList.value
} else if (selectedStatType.value === 'unregistered') {
//
const registeredStudentNames = new Set()
courseList.value.forEach(course => {
if (course.studentNames) {
// Set
const names = course.studentNames.split(',').map((name: string) => name.trim()).filter((name: string) => name)
names.forEach((name: string) => registeredStudentNames.add(name))
}
})
console.log('已报名学生姓名:', Array.from(registeredStudentNames))
console.log('所有学生数量:', studentList.value.length)
const unregisteredStudents = studentList.value.filter(student => !registeredStudentNames.has(student.xsxm))
console.log('未报名学生数量:', unregisteredStudents.length)
return unregisteredStudents
}
return studentList.value
})
// -
const onClassChange = async (nj: any, bj: any) => {
console.log('年级班级选择变化:', nj, bj)
classInfo.value = {
njId: nj.key,
bjId: bj.key,
nj: nj,
bj: bj
}
//
currentPage.value = 1
hasMore.value = true
courseList.value = []
studentList.value = []
//
await Promise.all([searchCourses(), searchStudents()])
}
//
const searchCourses = async () => {
if (!classInfo.value || !classInfo.value.njId || !classInfo.value.bjId) {
return
}
loading.value = true
hasSearched.value = true
try {
//
const response = await getXkCourseListApi(classInfo.value!.njId, classInfo.value!.bjId, xkId.value)
console.log('API返回结果:', response)
if (response && response.resultCode === 1) {
courseList.value = response.result || []
} else {
throw new Error(response?.message || '查询失败')
}
} catch (error) {
console.error('查询选课数据失败:', error)
uni.showToast({
title: '查询失败,请重试',
icon: 'none'
})
courseList.value = []
} finally {
loading.value = false
}
}
//
const searchStudents = async () => {
if (!classInfo.value || !classInfo.value.njId || !classInfo.value.bjId) {
return
}
try {
//
const response = await findStudentInfoByNjAndBjSimpleApi(classInfo.value!.njId, classInfo.value!.bjId)
console.log('学生API返回结果:', response)
if (response && response.resultCode === 1) {
studentList.value = response.result || []
} else {
throw new Error(response?.message || '查询学生失败')
}
} catch (error) {
console.error('查询学生数据失败:', error)
studentList.value = []
}
}
//
const refreshCourseList = () => {
currentPage.value = 1
hasMore.value = true
courseList.value = []
studentList.value = []
Promise.all([searchCourses(), searchStudents()])
}
//
const loadMore = async () => {
if (loading.value || !hasMore.value) {
return
}
currentPage.value++
await Promise.all([searchCourses(), searchStudents()])
}
//
const onReachBottom = () => {
loadMore()
}
//
const onStatItemClick = (statType: string) => {
selectedStatType.value = statType
console.log('选中统计类型:', statType)
}
//
const getListTitle = () => {
switch (selectedStatType.value) {
case 'registered':
return '已报名课程'
case 'unregistered':
return '未报名学生'
case 'all':
return '总人数学生'
default:
return '已报名课程'
}
}
//
const getListCount = () => {
switch (selectedStatType.value) {
case 'registered':
return `${filteredCourseList.value.length}门课程`
case 'unregistered':
return `${filteredStudentList.value.length}`
case 'all':
return `${filteredStudentList.value.length}`
default:
return `${filteredCourseList.value.length}门课程`
}
}
//
const showMoreOptions = (student: StudentInfo) => {
console.log('显示更多选项:', student)
//
uni.showActionSheet({
itemList: ['查看详情', '编辑信息', '联系家长', '删除学生'],
success: (res) => {
const tapIndex = res.tapIndex
switch (tapIndex) {
case 0:
//
viewStudentDetail(student)
break
case 1:
//
uni.showToast({
title: '编辑功能开发中',
icon: 'none'
})
break
case 2:
//
if (student.jzxm) {
uni.showToast({
title: '联系家长功能开发中',
icon: 'none'
})
} else {
uni.showToast({
title: '该学生暂无家长信息',
icon: 'none'
})
}
break
case 3:
//
uni.showModal({
title: '确认删除',
content: `确定要删除学生 ${student.xsxm} 吗?`,
success: (modalRes) => {
if (modalRes.confirm) {
uni.showToast({
title: '删除功能开发中',
icon: 'none'
})
}
}
})
break
}
}
})
}
//
const getRegistrationStatus = (student: StudentInfo) => {
//
const registeredStudentNames = new Set()
courseList.value.forEach(course => {
if (course.studentNames) {
const names = course.studentNames.split(',').map((name: string) => name.trim()).filter((name: string) => name)
names.forEach((name: string) => registeredStudentNames.add(name))
}
})
return registeredStudentNames.has(student.xsxm) ? '已报名' : '未报名'
}
//
const getRegistrationStatusClass = (student: StudentInfo) => {
const registeredStudentNames = new Set()
courseList.value.forEach(course => {
if (course.studentNames) {
const names = course.studentNames.split(',').map((name: string) => name.trim()).filter((name: string) => name)
names.forEach((name: string) => registeredStudentNames.add(name))
}
})
return registeredStudentNames.has(student.xsxm) ? 'status-registered' : 'status-unregistered'
}
//
const viewStudentDetail = (student: StudentInfo) => {
console.log('查看学生详情:', student)
// store
setXs({
xsId: student.xsId,
id: student.xsId, //
xsxm: student.xsxm,
xm: student.xsxm, //
xstx: student.xstx,
avatar: student.xstx, //
njId: student.njId,
njmc: student.njmc,
njmcName: student.njmc, //
bjId: student.bjId,
bjmc: student.bjmc,
jzIds: student.jzIds,
jzxm: student.jzxm
})
//
uni.navigateTo({
url: '/pages/view/homeSchool/parentAddressBook/detail'
})
}
//
onLoad((options?: any) => {
console.log('兴趣课选课页面加载', options)
//
if (options?.xkId) {
xkId.value = options.xkId
}
if (options?.xkmc) {
xkmc.value = decodeURIComponent(options.xkmc)
}
// ID
if (xkmc.value) {
uni.setNavigationBarTitle({
title: xkmc.value
})
}
})
</script>
<style lang="scss" scoped>
.xk-course-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.refresh-btn {
font-size: 24rpx;
color: #007aff;
font-weight: normal;
}
.stats-container {
display: flex;
justify-content: space-around;
margin-bottom: 30rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
flex-wrap: wrap;
gap: 60rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120rpx;
padding: 20rpx 10rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
cursor: pointer;
&.active {
background-color: #e6f7ff;
border: 2rpx solid #007aff;
transform: scale(1.05);
}
&:hover {
background-color: #f0f9ff;
transform: scale(1.02);
}
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
&.registered {
color: #52c41a;
}
&.unregistered {
color: #ff4d4f;
}
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.course-list {
margin-bottom: 30rpx;
}
.course-item {
margin-bottom: 20rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
}
.course-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.course-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.course-student-count {
font-size: 24rpx;
color: #007aff;
background-color: #e6f7ff;
padding: 8rpx 16rpx;
border-radius: 20rpx;
}
.course-info {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
margin-bottom: 12rpx;
font-size: 28rpx;
}
.info-label {
color: #666;
margin-right: 10rpx;
min-width: 140rpx;
}
.info-value {
color: #333;
flex: 1;
}
.student-list {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
}
.student-list-title {
font-size: 28rpx;
color: #666;
margin-bottom: 15rpx;
}
.student-tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.student-tag {
background-color: #f0f9ff;
color: #007aff;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
border: 1rpx solid #e6f7ff;
}
.class-tip {
display: flex;
align-items: center;
margin-top: 20rpx;
padding: 15rpx 20rpx;
background-color: #fffbe6;
border: 1rpx solid #ffe58f;
border-radius: 12rpx;
color: #faad14;
font-size: 28rpx;
font-weight: bold;
.tip-icon {
margin-right: 10rpx;
font-size: 32rpx;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
.empty-icon {
font-size: 120rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 28rpx;
color: #999;
}
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx;
.loading-text {
font-size: 32rpx;
color: #666;
}
}
.bg-white {
background-color: #fff;
}
.r-md {
border-radius: 16rpx;
}
.p-12 {
padding: 24rpx;
}
/* 学生列表样式 */
.student-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.student-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
min-height: 120rpx;
}
.student-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
padding: 4rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
flex-shrink: 0;
}
.student-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: #f5f5f5;
}
.status-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
gap: 4rpx;
}
.status-registered {
background-color: #e6f7ff;
color: #52c41a;
}
.status-unregistered {
background-color: #fff2f0;
color: #ff4d4f;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-1 {
flex: 1;
}
.items-center {
align-items: center;
}
.overflow-hidden {
overflow: hidden;
}
.mr-8 {
margin-right: 12rpx;
}
.mb-8 {
margin-bottom: 16rpx;
}
.font-14 {
font-size: 28rpx;
}
.cor-333 {
color: #333;
}
.more-icon-container {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8rpx;
border-radius: 50%;
transition: all 0.2s ease;
flex-shrink: 0;
&:hover {
background-color: #f0f0f0;
}
&:active {
background-color: #e0e0e0;
transform: scale(0.95);
}
}
.more-icon {
width: 35rpx;
height: 35rpx;
opacity: 0.6;
transition: opacity 0.2s ease;
}
.more-icon-container:hover .more-icon {
opacity: 1;
}
/* 滚动加载样式 */
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
.load-more-text {
font-size: 28rpx;
color: #999;
}
}
.no-more {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
.no-more-text {
font-size: 28rpx;
color: #ccc;
}
}
</style>

View File

@ -0,0 +1,436 @@
<template>
<view class="xk-list-container">
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">选课列表</text>
</view>
<!-- 选课列表 -->
<view class="section" v-if="xkList.length > 0">
<view class="section-title">
选课信息 ({{ xkList.length }}个选课)
</view>
<!-- 选课列表 -->
<view class="xk-list">
<view
v-for="xk in xkList"
:key="xk.id"
class="xk-item bg-white r-md p-12"
@click="goToXkCourse(xk)"
>
<view class="xk-header">
<view class="xk-title">{{ xk.xkmc }}</view>
<view class="xk-actions">
<view v-if="xk.xkStatus !== '已结束'" class="xk-status" :class="getStatusClass(xk.xkStatus)">
{{ xk.xkStatus }}
</view>
<view v-else class="more-btn" @click.stop="showMoreOptions(xk)">
<image src="/static/base/view/more.png" class="more-icon" />
</view>
</view>
</view>
<view class="xk-info">
<view class="info-row">
<text class="info-label">选课类型</text>
<text class="info-value">{{ xk.xklxName || '未知' }}</text>
</view>
<view class="info-row">
<text class="info-label">学期名称</text>
<text class="info-value">{{ xk.xqmc || '未知' }}</text>
</view>
<view class="info-row">
<text class="info-label">年级范围</text>
<text class="info-value">{{ xk.njmc || '全部' }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && xkList.length === 0 && hasSearched">
<view class="empty-icon">📚</view>
<view class="empty-text">暂无选课数据</view>
<view class="empty-tip">请联系管理员添加选课信息</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-text">正在加载...</view>
</view>
<!-- 滚动加载更多 -->
<view class="load-more" v-if="hasMore && !loading">
<view class="load-more-text">上拉加载更多</view>
</view>
<!-- 没有更多数据 -->
<view class="no-more" v-if="!hasMore && hasSearched && xkList.length > 0">
<view class="no-more-text">没有更多数据了</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getXkListApi } from '@/api/analysis/xk'
//
interface XkInfo {
id: string
xkmc: string
xklxId: string
xklxName: string
njId: string
njmc: string
xqId: string
xqmc: string
xkkstime: string
xkjstime: string
xkzt: number
xkStatus: string
allXkNum: number
yfXkNum: number
wfXkNum: number
status: string
createdTime: string
}
//
const xkList = ref<XkInfo[]>([])
const loading = ref(false)
const hasSearched = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
//
onLoad(() => {
console.log('选课列表页面加载')
loadXkList()
})
//
const loadXkList = async (isLoadMore = false) => {
if (loading.value) return
loading.value = true
hasSearched.value = true
try {
//
const response = await getXkListApi()
console.log('API返回结果:', response)
if (response && response.rows) {
const newData = response.rows || []
if (isLoadMore) {
//
xkList.value = [...xkList.value, ...newData]
} else {
//
xkList.value = newData
}
//
hasMore.value = newData.length >= pageSize.value
} else {
throw new Error('查询失败')
}
} catch (error) {
console.error('查询选课列表失败:', error)
uni.showToast({
title: '查询失败,请重试',
icon: 'none'
})
if (!isLoadMore) {
xkList.value = []
}
} finally {
loading.value = false
}
}
//
const loadMore = () => {
if (loading.value || !hasMore.value) {
return
}
currentPage.value++
loadXkList(true)
}
//
const onReachBottom = () => {
loadMore()
}
//
const goToXkCourse = (xk: XkInfo) => {
console.log('跳转到选课详情:', xk)
uni.navigateTo({
url: `/pages/view/analysis/xk/xkCourse?xkId=${xk.id}&xkmc=${encodeURIComponent(xk.xkmc)}`
})
}
// -
const showMoreOptions = (xk: XkInfo) => {
console.log('点击更多按钮,直接跳转到课程详情:', xk)
goToXkCourse(xk)
}
//
const getStatusClass = (status: string) => {
switch (status) {
case '未开始':
return 'status-pending'
case '进行中':
return 'status-active'
case '已结束':
return 'status-ended'
default:
return 'status-unknown'
}
}
</script>
<style lang="scss" scoped>
.xk-list-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.page-header {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.page-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.xk-list {
margin-bottom: 30rpx;
}
.xk-item {
margin-bottom: 20rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
position: relative;
cursor: pointer;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
}
.xk-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.xk-actions {
display: flex;
align-items: center;
}
.xk-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.xk-status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-weight: bold;
&.status-pending {
background-color: #fff7e6;
color: #fa8c16;
border: 1rpx solid #ffd591;
}
&.status-active {
background-color: #f6ffed;
color: #52c41a;
border: 1rpx solid #b7eb8f;
}
&.status-ended {
background-color: #fff2f0;
color: #ff4d4f;
border: 1rpx solid #ffccc7;
}
&.status-unknown {
background-color: #f5f5f5;
color: #999;
border: 1rpx solid #d9d9d9;
}
}
.more-btn {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: #f5f5f5;
transition: all 0.2s ease;
cursor: pointer;
&:hover {
background-color: #e6f7ff;
transform: scale(1.1);
}
&:active {
background-color: #d9d9d9;
transform: scale(0.95);
}
}
.more-icon {
width: 32rpx;
height: 32rpx;
}
.xk-info {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
margin-bottom: 12rpx;
font-size: 28rpx;
}
.info-label {
color: #666;
margin-right: 10rpx;
min-width: 140rpx;
}
.info-value {
color: #333;
flex: 1;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
.empty-icon {
font-size: 120rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 28rpx;
color: #999;
}
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx;
.loading-text {
font-size: 32rpx;
color: #666;
}
}
.bg-white {
background-color: #fff;
}
.r-md {
border-radius: 16rpx;
}
.p-12 {
padding: 24rpx;
}
/* 滚动加载样式 */
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
.load-more-text {
font-size: 28rpx;
color: #999;
}
}
.no-more {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
.no-more-text {
font-size: 28rpx;
color: #ccc;
}
}
</style>

View File

@ -0,0 +1,630 @@
<template>
<view class="student-archive-container">
<!-- 班级选择器 -->
<view class="section">
<view class="section-title">选择班级</view>
<BasicNjBjPicker
v-model="classInfo"
placeholder="请选择年级班级"
@change="onClassChange"
icon-arrow="right"
:customStyle="{ backgroundColor: '#fff', borderRadius: '0', padding: '12px 15px' }"
/>
<!-- 班级选择提示 -->
<view v-if="!classInfo" class="class-tip">
<text class="tip-icon"></text>
<text class="tip-text">请先选择班级</text>
</view>
</view>
<!-- 学生档案统计 -->
<view class="section" v-if="classInfo">
<view class="section-title">
学生档案统计
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view
class="stat-item"
:class="{ active: selectedStatType === 'all' }"
@click="onStatItemClick('all')"
>
<text class="stat-number">{{ studentList.length }}</text>
<text class="stat-label">总人数</text>
</view>
<view
class="stat-item"
:class="{ active: selectedStatType === 'followed' }"
@click="onStatItemClick('followed')"
>
<text class="stat-number followed">{{ followedCount }}</text>
<text class="stat-label">已关注</text>
</view>
<view
class="stat-item"
:class="{ active: selectedStatType === 'unfollowed' }"
@click="onStatItemClick('unfollowed')"
>
<text class="stat-number unfollowed">{{ unfollowedCount }}</text>
<text class="stat-label">未关注</text>
</view>
</view>
</view>
<!-- 学生档案列表 -->
<view class="section" v-if="classInfo && studentList.length > 0">
<view class="section-title">
{{ getListTitle() }} ({{ filteredStudentList.length }})
</view>
<!-- 学生列表 - 改为card形式 -->
<view class="student-list">
<view class="student-grid">
<view
v-for="student in filteredStudentList"
:key="student.xsId"
class="student-item bg-white r-md p-12"
@click="viewStudentDetail(student)"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="student-avatar"
:src="imagUrl(student.xstx || '') || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="student-name mb-8">
<text class="font-14 cor-333">{{ student.xsxm }}</text>
</view>
<view class="flex-row">
<!-- 关注状态标签 -->
<view
class="status-tag"
:class="getFollowStatusClass(student.jzxm)"
>
{{ student.jzxm ? '已关注' : '未关注' }}
</view>
</view>
</view>
<!-- 更多图标 -->
<view class="more-icon-container" @click.stop="showMoreOptions(student)">
<image
class="more-icon"
src="/static/base/view/more.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && studentList.length === 0 && hasSearched">
<view class="empty-icon">📚</view>
<view class="empty-text">暂无学生档案数据</view>
<view class="empty-tip">请选择年级班级后查询</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-text">正在加载...</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { BasicNjBjPicker } from '@/components/BasicNjBjPicker'
import { onLoad } from '@dcloudio/uni-app'
import { findStudentInfoByNjAndBjSimpleApi } from '@/api/analysis/xs'
import { imagUrl } from "@/utils"
import { useDataStore } from '@/store/modules/data'
//
interface ClassInfo {
njId: string
bjId: string
nj: any
bj: any
}
interface StudentInfo {
xsId: string
xsxm: string
xstx?: string
njId: string
njmcId: string
njmc: string
bc: string
bjId: string
bjmc: string
jzIds?: string
jzxm?: string
}
interface ApiResponse {
code: number
data: StudentInfo[]
message?: string
}
//
const classInfo = ref<ClassInfo | null>(null)
const studentList = ref<StudentInfo[]>([])
const loading = ref(false)
const hasSearched = ref(false)
const selectedStatType = ref<string>('all') //
// 使store
const { setXs } = useDataStore()
//
const followedCount = computed(() => {
return studentList.value.filter(student => student.jzxm).length
})
const unfollowedCount = computed(() => {
return studentList.value.filter(student => !student.jzxm).length
})
//
const filteredStudentList = computed(() => {
if (selectedStatType.value === 'all') {
return studentList.value
} else if (selectedStatType.value === 'followed') {
return studentList.value.filter(student => student.jzxm)
} else if (selectedStatType.value === 'unfollowed') {
return studentList.value.filter(student => !student.jzxm)
}
return studentList.value
})
// -
const onClassChange = async (nj: any, bj: any) => {
console.log('年级班级选择变化:', nj, bj)
classInfo.value = {
njId: nj.key,
bjId: bj.key,
nj: nj,
bj: bj
}
//
await searchStudents()
}
//
const searchStudents = async () => {
if (!classInfo.value || !classInfo.value.njId || !classInfo.value.bjId) {
return
}
loading.value = true
hasSearched.value = true
try {
//
const response = await findStudentInfoByNjAndBjSimpleApi(classInfo.value!.njId, classInfo.value!.bjId)
console.log('API返回结果:', response)
if (response && response.resultCode === 1) {
studentList.value = response.result || []
uni.showToast({
title: `查询到${studentList.value.length}名学生`,
icon: 'success'
})
} else {
throw new Error(response?.message || '查询失败')
}
} catch (error) {
console.error('查询学生档案失败:', error)
uni.showToast({
title: '查询失败,请重试',
icon: 'none'
})
studentList.value = []
} finally {
loading.value = false
}
}
//
const refreshStudentList = () => {
searchStudents()
}
//
const onStatItemClick = (statType: string) => {
selectedStatType.value = statType
console.log('选中统计类型:', statType)
}
//
const getListTitle = () => {
switch (selectedStatType.value) {
case 'followed':
return '已关注学生'
case 'unfollowed':
return '未关注学生'
default:
return '学生档案列表'
}
}
//
const showMoreOptions = (student: StudentInfo) => {
console.log('显示更多选项:', student)
//
uni.showActionSheet({
itemList: ['查看详情', '编辑信息', '联系家长', '删除学生'],
success: (res) => {
const tapIndex = res.tapIndex
switch (tapIndex) {
case 0:
//
viewStudentDetail(student)
break
case 1:
//
uni.showToast({
title: '编辑功能开发中',
icon: 'none'
})
break
case 2:
//
if (student.jzxm) {
uni.showToast({
title: '联系家长功能开发中',
icon: 'none'
})
} else {
uni.showToast({
title: '该学生暂无家长信息',
icon: 'none'
})
}
break
case 3:
//
uni.showModal({
title: '确认删除',
content: `确定要删除学生 ${student.xsxm} 吗?`,
success: (modalRes) => {
if (modalRes.confirm) {
uni.showToast({
title: '删除功能开发中',
icon: 'none'
})
}
}
})
break
}
}
})
}
//
const getFollowStatusClass = (jzxm?: string) => {
return jzxm ? 'status-followed' : 'status-unfollowed'
}
//
const viewStudentDetail = (student: StudentInfo) => {
console.log('查看学生详情:', student)
// store
setXs({
xsId: student.xsId,
id: student.xsId, //
xsxm: student.xsxm,
xm: student.xsxm, //
xstx: student.xstx,
avatar: student.xstx, //
njId: student.njId,
njmc: student.njmc,
njmcName: student.njmc, //
bjId: student.bjId,
bjmc: student.bjmc,
jzIds: student.jzIds,
jzxm: student.jzxm
})
//
uni.navigateTo({
url: '/pages/view/homeSchool/parentAddressBook/detail'
})
}
//
onLoad(() => {
console.log('学生档案页面加载')
})
</script>
<style lang="scss" scoped>
.student-archive-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.refresh-btn {
font-size: 24rpx;
color: #007aff;
font-weight: normal;
}
.stats-container {
display: flex;
justify-content: space-around;
margin-bottom: 30rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
flex-wrap: wrap;
gap: 60rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120rpx;
padding: 20rpx 10rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
cursor: pointer;
&.active {
background-color: #e6f7ff;
border: 2rpx solid #007aff;
transform: scale(1.05);
}
&:hover {
background-color: #f0f9ff;
transform: scale(1.02);
}
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
&.followed {
color: #52c41a;
}
&.unfollowed {
color: #ff4d4f;
}
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.student-list {
margin-bottom: 30rpx;
}
.student-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.student-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
min-height: 120rpx; //
}
.student-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
padding: 4rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
flex-shrink: 0; //
}
.student-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: #f5f5f5;
}
.status-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
gap: 4rpx;
}
.status-followed {
background-color: #e6f7ff;
color: #52c41a;
}
.status-unfollowed {
background-color: #fff2f0;
color: #ff4d4f;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-1 {
flex: 1;
}
.items-center {
align-items: center;
}
.overflow-hidden {
overflow: hidden;
}
.mr-8 {
margin-right: 12rpx;
}
.mb-8 {
margin-bottom: 16rpx;
}
.font-14 {
font-size: 28rpx;
}
.cor-333 {
color: #333;
}
.bg-white {
background-color: #fff;
}
.r-md {
border-radius: 16rpx;
}
.p-12 {
padding: 24rpx;
}
.more-icon-container {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8rpx;
border-radius: 50%;
transition: all 0.2s ease;
flex-shrink: 0; //
&:hover {
background-color: #f0f0f0;
}
&:active {
background-color: #e0e0e0;
transform: scale(0.95);
}
}
.more-icon {
width: 35rpx;
height: 35rpx;
opacity: 0.6;
transition: opacity 0.2s ease;
}
.more-icon-container:hover .more-icon {
opacity: 1;
}
.class-tip {
display: flex;
align-items: center;
margin-top: 20rpx;
padding: 15rpx 20rpx;
background-color: #fffbe6;
border: 1rpx solid #ffe58f;
border-radius: 12rpx;
color: #faad14;
font-size: 28rpx;
font-weight: bold;
.tip-icon {
margin-right: 10rpx;
font-size: 32rpx;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
.empty-icon {
font-size: 120rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 28rpx;
color: #999;
}
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx;
.loading-text {
font-size: 32rpx;
color: #666;
}
}
</style>

View File

@ -56,6 +56,7 @@
v-for="jz in jzList"
:key="jz.id"
class="parent-card-item"
@click="callParent(jz.jzsj)"
>
<view class="parent-avatar-container">
<!-- 如果有头像则显示图片否则显示文字头像 -->
@ -68,7 +69,7 @@
<view v-else class="parent-avatar-text">
{{ jz.jzxm }}
</view>
<view class="call-icon-overlay" @click="callParent(jz.jzsj)">
<view class="call-icon-overlay">
<uni-icons
type="phone-filled"
size="16"
@ -352,11 +353,18 @@ const getParentAvatar = (avatarUrl: string) => {
padding: 8px; //
border-radius: 8px; //
transition: all 0.2s ease; //
cursor: pointer; //
position: relative; //
&:hover {
background-color: rgba(102, 126, 234, 0.05); //
transform: translateY(-2px); //
}
&:active {
background-color: rgba(102, 126, 234, 0.1); //
transform: translateY(0); //
}
}
.parent-avatar-container {
@ -404,13 +412,10 @@ const getParentAvatar = (avatarUrl: string) => {
align-items: center;
justify-content: center;
border: 1px solid #fff; //
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); //
pointer-events: none; //
&:active {
opacity: 0.8;
transform: scale(0.95);
}
// &:active
}
}

View File

@ -1,228 +0,0 @@
<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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB