zhxy-jsd/src/pages/view/routine/da/xsda/studentArchive.vue
2026-02-23 17:29:36 +08:00

451 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="student-archive-page">
<!-- 顶部区域可选年级班级学生 + 按学生姓名搜索 jzda 一致 -->
<view class="top-section">
<view class="selector-picker" @click="openStudentPicker">
<text :class="{ placeholder: !pickerDisplayText }">{{ pickerDisplayText || '可选年级、班级或学生筛选' }}</text>
<view class="selector-actions">
<view v-if="queryScope" class="clear-btn" @click.stop="clearQueryScope">×</view>
<uni-icons type="right" size="16" color="#666"></uni-icons>
</view>
</view>
<view class="search-section">
<view class="search-box">
<BasicSearch
placeholder="按学生姓名搜索"
v-model="searchKeyword"
@search="onSearch"
/>
<u-button text="搜索" type="primary" size="small" class="search-btn" @click="onSearch" />
</view>
</view>
</view>
<!-- 学生档案统计 -->
<view class="section" v-if="queryScope || searchKeyword.trim()">
<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="queryScope || searchKeyword.trim()">
<view class="section-title">{{ getListTitle() }} ({{ filteredStudentList.length }})</view>
<view v-if="loading" class="loading-text">加载中...</view>
<view v-else-if="!filteredStudentList.length" class="empty-text">暂无学生档案数据</view>
<view v-else 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"
/>
</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="arrow-icon-container" @click.stop="viewStudentDetail(student)">
<image class="arrow-icon" src="/static/base/view/more.png" mode="aspectFit" />
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态未选择未搜索 -->
<view class="empty-state" v-if="!queryScope && !searchKeyword.trim()">
<view class="empty-icon">📚</view>
<view class="empty-text">请输入学生姓名搜索或选择年级班级学生</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import BasicSearch from '@/components/BasicSearch/Search.vue'
import { onLoad } from '@dcloudio/uni-app'
import { findStudentArchiveApi } from '@/api/analysis/xs'
import { imagUrl } from '@/utils'
import { useDataStore } from '@/store/modules/data'
interface QueryScope {
njIds?: string
bjIds?: string
xsIds?: string
displayText: string
}
interface StudentInfo {
xsId: string
xsxm: string
xstx?: string
njId: string
njmcId: string
njmc: string
bc: string
bjId: string
bjmc: string
jzIds?: string
jzxm?: string
xb?: string
sfzh?: string
cstime?: string
}
const queryScope = ref<QueryScope | null>(null)
const pickerDisplayText = computed(() => queryScope.value?.displayText || '')
const studentList = ref<StudentInfo[]>([])
const searchKeyword = ref('')
const loading = ref(false)
const selectedStatType = ref<string>('all')
const { setXs } = useDataStore()
const followedCount = computed(() => studentList.value.filter((s) => s.jzxm).length)
const unfollowedCount = computed(() => studentList.value.filter((s) => !s.jzxm).length)
const filteredStudentList = computed(() => {
let list = studentList.value
if (selectedStatType.value === 'followed') list = list.filter((s) => s.jzxm)
else if (selectedStatType.value === 'unfollowed') list = list.filter((s) => !s.jzxm)
return list
})
// 打开学生选择器:统一传递 njIds、bjIds、xsIds 供后端多条件查询
const openStudentPicker = () => {
uni.$once('studentPickerConfirm', (students: any[]) => {
if (students && students.length > 0) {
const njIdSet = [...new Set(students.map((s: any) => s.njId).filter(Boolean))]
const bjIdSet = [...new Set(students.map((s: any) => s.bjId).filter(Boolean))]
const xsIdList = students.map((s: any) => s.xsId || s.id).filter(Boolean)
let displayText: string
if (bjIdSet.length === 1 && njIdSet.length === 1) {
displayText = students[0]?.bc || [students[0]?.njmc, students[0]?.bjmc].filter(Boolean).join('') || '已选班级'
} else if (njIdSet.length === 1) {
displayText = bjIdSet.length > 1 ? `已选 ${bjIdSet.length} 个班级` : (students[0]?.njmc || '已选年级')
} else if (students.length <= 5) {
displayText = students.map((s: any) => s.xsxm || s.xm).join('、')
} else {
displayText = `已选 ${students.length}`
}
queryScope.value = {
njIds: njIdSet.length > 0 ? njIdSet.join(',') : undefined,
bjIds: bjIdSet.length > 0 ? bjIdSet.join(',') : undefined,
xsIds: xsIdList.length > 0 ? xsIdList.join(',') : undefined,
displayText
}
searchKeyword.value = ''
loadStudents()
}
})
uni.navigateTo({
url: '/pages/components/StudentPicker/index?showGrade=true&showClass=true&showStudent=true&multiple=true'
})
}
const clearQueryScope = () => {
queryScope.value = null
studentList.value = []
}
const onSearch = () => {
loadStudents()
}
const loadStudents = async () => {
const scope = queryScope.value
const njIds = scope?.njIds
const bjIds = scope?.bjIds
const xsIds = scope?.xsIds
const xsxm = searchKeyword.value?.trim() || undefined
if (!njIds && !bjIds && !xsIds && !xsxm) {
studentList.value = []
return
}
loading.value = true
try {
const res: any = await findStudentArchiveApi({ njIds, bjIds, xsIds, xsxm })
const list = res?.result || res?.data || []
studentList.value = Array.isArray(list) ? list : []
} catch (error) {
console.error('查询学生档案失败:', error)
uni.showToast({ title: '查询失败', icon: 'none' })
studentList.value = []
} finally {
loading.value = false
}
}
const onStatItemClick = (statType: string) => {
selectedStatType.value = statType
}
const getListTitle = () => {
switch (selectedStatType.value) {
case 'followed': return '已关注学生'
case 'unfollowed': return '未关注学生'
default: return '学生档案列表'
}
}
const getFollowStatusClass = (jzxm?: string) => (jzxm ? 'status-followed' : 'status-unfollowed')
const viewStudentDetail = (student: StudentInfo) => {
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,
xb: student.xb,
sfzh: student.sfzh,
cstime: student.cstime
})
uni.navigateTo({ url: '/pages/view/homeSchool/parentAddressBook/detail' })
}
onLoad(() => {})
</script>
<style lang="scss" scoped>
.student-archive-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.top-section {
background-color: #fff;
padding: 24rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-section {
margin-top: 20rpx;
}
.search-section .search-box {
display: flex;
gap: 20rpx;
align-items: center;
& > *:first-child { flex: 1; min-width: 0; }
.search-btn { flex-shrink: 0; width: 140rpx !important; }
}
.selector-picker {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 20rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
&.placeholder { color: #999; }
.selector-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.clear-btn {
font-size: 36rpx;
color: #999;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
background: #eee;
border-radius: 50%;
}
}
.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;
}
.stats-container {
display: flex;
justify-content: space-around;
margin-bottom: 20rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
gap: 60rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120rpx;
padding: 20rpx 10rpx;
border-radius: 12rpx;
&.active {
background-color: #e6f7ff;
border: 2rpx solid #007aff;
}
}
.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-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.student-item {
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
flex-shrink: 0;
}
.student-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
}
.status-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
}
.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; }
.arrow-icon-container {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.arrow-icon {
width: 35rpx;
height: 35rpx;
opacity: 0.6;
}
.loading-text, .empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
padding: 40rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 40rpx;
.empty-icon { font-size: 120rpx; margin-bottom: 20rpx; }
.empty-text { font-size: 32rpx; color: #666; }
}
</style>