451 lines
12 KiB
Vue
451 lines
12 KiB
Vue
|
|
<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>
|