1、拆分点名页面

2、默认加载班主任和副班主任陪餐
This commit is contained in:
ywyonui 2025-09-12 19:44:12 +08:00
parent 3fe5dc5b14
commit 8c79a8e745
10 changed files with 1169 additions and 950 deletions

7
src/api/base/jsApi.ts Normal file
View File

@ -0,0 +1,7 @@
// 食堂巡查相关API接口
import { get, post } from "@/utils/request";
// 查询某个班级的班主任和副班主任
export const findNjJsListApi = async (params: any) => {
return await get("/api/bj/findNjJsList", params);
};

View File

@ -8,12 +8,12 @@
<view v-else class="js-selected" @click="showPicker">
<view class="js-selected-name">
<text class="data" v-if="selectedList && selectedList.length">{{ getShowSelectedName() }}</text>
<text class="data" style="color: #999;" v-else>{{ placeholder }}</text>
<text class="data" style="color: #999;" v-else>{{ placeholder }}</text>
</view>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
<u-popup :show="showPopup" @close="showPopup=false">
<u-popup :show="showPopup" @close="showPopup = false">
<view class="js-picker-popup">
<view class="js-picker-header">
<view class="js-cancel-btn" @click="handleCancel">取消</view>
@ -21,12 +21,12 @@
<view class="js-ok-btn" @click="handleOk">确定</view>
</view>
<view class="js-picker-search">
<BasicSearch @change="handleSearch" :showAction="false" height="36" :placeholder="searchPlaceholder"/>
<BasicSearch @change="handleSearch" :showAction="false" height="36" :placeholder="searchPlaceholder" />
</view>
<view class="js-list">
<scroll-view scroll-y style="max-height: 60vh" :scroll-top="targetScrollTop">
<view class="js-item" v-for="(item, index) in jsList" :key="index"
:class="{ selected: item.selected }" @click="handleSelect(item)">
<scroll-view scroll-y style="max-height: 60vh" :scroll-top="targetScrollTop">
<view class="js-item" v-for="(item, index) in jsList" :key="index" :class="{ selected: item.selected }"
@click="handleSelect(item)">
<view class="js-name">{{ item.label }}</view>
<BasicIcon type="checkmarkempty" v-if="item.selected" color="#333" />
</view>
@ -44,7 +44,7 @@ const { getAllJsBasicInfoVo } = useCommonStore();
//
const props = withDefaults(defineProps<{
defualtValue?: any,
defaultValue?: any,
parentData?: any,
multiple?: boolean,
// id
@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
//
customTrigger?: boolean
}>(), {
defualtValue: null,
defaultValue: null,
parentData: null,
multiple: false,
excludeIds: [],
@ -79,13 +79,16 @@ let searchKey = "";
const selectedList = ref<any>([]);
const getShowSelectedName = () => {
return selectedList.value.map((item: any) => item.label).join(",");
if (selectedList.value && selectedList.value.length > 0) {
return selectedList.value.map((item: any) => item.label).join(",");
}
return '';
};
const handleSearch = (value: any) => {
searchKey = value;
rebuildJsList();
return
return
};
const handleSelect = (item: any) => {
@ -134,47 +137,81 @@ const showPicker = () => {
});
};
const rebuildJsList = () => {
const rebuildJsList = () => {
jsList.value = [];
if (!jsListAll.value || !Array.isArray(jsListAll.value)) {
return;
}
// 使for...of
for (const item of jsListAll.value) {
// item
if (!item || !item.id || !item.jsxm) {
continue;
}
//
if (props.excludeIds && Array.isArray(props.excludeIds) && props.excludeIds.includes(item.id)) {
continue;
}
// jsxm
if (!searchKey || item.jsxm.includes(searchKey)) {
const isSelected = selectedList.value.some((selected: any) => selected.id === item.id);
jsList.value.push({
...item,
label: item.jsxm,
value: item.id,
selected: false
selected: isSelected,
});
}
}
};
//
defineExpose({
showPicker
});
const clearValue = () => {
selectedList.value = [];
for (const item of jsList.value) {
item.selected = false;
}
};
const setValue = (val: any) => {
//
if (val) {
if (props.multiple) {
if (Array.isArray(val) && val.length > 0) {
selectedList.value = [];
for (const item of jsList.value) {
if (val.includes(item.value)) {
item.selected = true;
selectedList.value.push(item);
} else {
item.selected = false;
}
}
} else {
clearValue();
}
} else {
for (const item of jsList.value) {
if (item.value === val) {
item.selected = true;
selectedList.value = [item];
} else {
item.selected = false;
}
}
}
} else {
clearValue();
}
};
onMounted(async () => {
try {
const res = await getAllJsBasicInfoVo()
if (res && res.result && Array.isArray(res.result)) {
jsListAll.value = res.result;
// rebuildJsList
await new Promise<void>((resolve) => {
rebuildJsList();
@ -183,34 +220,7 @@ onMounted(async () => {
resolve();
});
});
//
if (props.defualtValue) {
if (props.multiple) {
if (Array.isArray(props.defualtValue) && props.defualtValue.length > 0) {
selectedList.value = [];
// 使for...of
for (const item of jsList.value) {
if (props.defualtValue.includes(item.value)) {
item.selected = true;
selectedList.value.push(item);
} else {
item.selected = false;
}
}
}
} else {
// 使for...of
for (const item of jsList.value) {
if (item.value === props.defualtValue) {
item.selected = true;
selectedList.value = [item];
} else {
item.selected = false;
}
}
}
}
setValue(props.defaultValue);
} else {
console.warn('JsPicker: 获取教师数据失败或数据格式不正确');
}
@ -218,11 +228,18 @@ onMounted(async () => {
console.error('JsPicker初始化失败:', error);
}
});
//
defineExpose({
showPicker,
setValue
});
</script>
<style lang="scss" scoped>
.js-picker {
flex: 1;
.js-selected {
display: flex;
align-items: center;
@ -232,15 +249,15 @@ onMounted(async () => {
border-radius: 12rpx;
border: 1rpx solid #e9ecef;
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.js-selected-name {
flex: 1;
.data {
font-size: 30rpx;
color: #333;
@ -248,47 +265,58 @@ onMounted(async () => {
}
}
}
.js-picker-popup {
min-height: 50vh;
max-height: 90vh;
padding: 10px;
display: flex;
flex-direction: column;
.js-picker-header {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
.js-cancel-btn {
flex: 0 0 50;
padding: 5px 10px;
}
.js-ok-btn {
flex: 0 0 50;
padding: 5px 10px;
color: #3c9cff;
}
.js-picker-title {
.js-picker-title {
flex: 1 0 1px;
text-align: center;
font-size: 20px;
}
}
.js-picker-search {
margin: 10px 0;
}
.js-list {
flex: 1 0 1px;
.js-item {
display: flex;
padding: 10px 15px;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
&.selected {
background-color: beige;
}
.js-name {
flex: 1;
}

View File

@ -10,7 +10,7 @@
:customTrigger="true"
:multiple="true"
:excludeIds="getExcludeApproverIds()"
:defualtValue="[]"
:defaultValue="[]"
title="选择审批人"
placeholder="请选择审批人"
searchPlaceholder="搜索审批人"
@ -51,7 +51,7 @@
:customTrigger="true"
:multiple="true"
:excludeIds="getExcludeCcIds()"
:defualtValue="[]"
:defaultValue="[]"
title="选择抄送人"
placeholder="请选择抄送人"
searchPlaceholder="搜索抄送人"

View File

@ -26,7 +26,7 @@
<BasicJsPicker
@change="changeJsByTy"
:parent-data="tyDk"
:defualtValue="tyDk.dkJsId"
:defaultValue="tyDk.dkJsId"
:multiple="false"
:excludeIds="excludeIds"
/>
@ -48,7 +48,7 @@
<BasicJsPicker
@change="changeJsByKm"
:parent-data="item"
:defualtValue="item.dkJsId"
:defaultValue="item.dkJsId"
:multiple="false"
:excludeIds="excludeIds"
/>
@ -84,7 +84,7 @@
<BasicJsPicker
@change="changeJs"
:parent-data="item"
:defualtValue="item.dkJsId"
:defaultValue="item.dkJsId"
:multiple="false"
:excludeIds="excludeIds"
/>

View File

@ -60,10 +60,7 @@ const [register, { reload, setParam }] = useLayout({
total.value = 0;
return { rows: [], total: 0 };
}
},
componentProps: {
auto: false,
},
}
});
const goToBz = (bz: any) => {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,278 @@
<template>
<view class="section">
<view class="section-title">陪餐教师</view>
<BasicJsPicker
v-if="!isLoading"
:defaultValue="jsIdList"
:multiple="true"
@change="onChangeJs"
placeholder="请选择陪餐教师"
ref="jsPickerRef"
/>
<view class="teacher-list" v-if="jsList.length > 0">
<view class="teacher-grid">
<view
v-for="teacher in jsList"
:key="teacher.value"
class="teacher-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<!-- <view class="avatar-container mr-8">
<image
class="teacher-avatar"
:src="imagUrl(teacher.headPic) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view> -->
<view class="flex-1 overflow-hidden">
<view class="teacher-name">
<text class="font-14 cor-333">{{ teacher.label }}</text>
</view>
</view>
<BasicIcon type="clear" size="24" color="#ff4d4f" @click="removeJs(teacher.value)" class="remove-btn" />
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import BasicJsPicker from '@/components/BasicJsPicker/Picker.vue'
import BasicIcon from '@/components/BasicIcon/Icon.vue'
import { imagUrl } from "@/utils";
import { findNjJsListApi } from '@/api/base/jsApi'
//
const props = withDefaults(defineProps<{
bjId?: string
}>(), {
bjId: ''
});
const isLoading = ref(false);
const jsPickerRef = ref<any>(null);
const jsList = ref<any>([]);
const jsIdList = ref<string[]>([]);
//
const onChangeJs = (teachers: any[]) => {
jsList.value = teachers.map(teacher => ({
...teacher
}))
}
//
const removeJs = (jsId: string) => {
jsList.value = jsList.value.filter((js: any) => js.id !== jsId);
jsIdList.value = jsList.value.map((js: any) => js.id);
jsPickerRef.value.setValue(jsIdList.value);
}
const loadJsByBj = async () => {
const res = await findNjJsListApi({bjId: props.bjId});
jsList.value = (res.result || []).map((js:any) => {
return {
...js,
value: js.id,
label: js.jsxm
}
});
jsIdList.value = res.result.map((js:any) => js.id);
if (jsPickerRef.value) {
jsPickerRef.value.setValue(jsIdList.value);
}
}
const getDmJsList = () => {
return jsList.value.map((js:any) => ({
jsId: js.id || js.value, // ID
jsXm: js.jsxm || js.label, //
tx: js.headPic, //
pcZt: 'A'
}))
}
//
watch(() => props.bjId, (newVal) => {
if (newVal) {
loadJsByBj();
}
}, { deep: true, immediate: true })
defineExpose({
getDmJsList,
})
</script>
<style lang="scss" scoped>
.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;
}
.teacher-list {
margin-top: 20rpx;
}
.teacher-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.teacher-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
.remove-btn {
position: absolute;
right: 6rpx;
top: 6rpx;
cursor: pointer;
}
}
.teacher-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.avatar-container {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
padding: 6rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.teacher-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: #f5f5f5;
}
.status-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
gap: 4rpx;
&.clickable {
cursor: pointer;
transition: all 0.2s;
&:hover {
opacity: 0.8;
transform: scale(1.05);
}
}
&.readonly {
opacity: 0.8;
cursor: not-allowed;
}
.status-arrow {
font-size: 16rpx;
opacity: 0.8;
}
}
.status-normal {
background-color: #e6f7ff;
color: #52c41a;
}
.status-leave {
background-color: #fff7e6;
color: #faad14;
}
.status-absent {
background-color: #fff2f0;
color: #ff4d4f;
}
.status-unpaid {
background-color: #f9f0ff;
color: #722ed1;
}
.status-unregistered {
background-color: #fff0f6;
color: #eb2f96;
}
.flex-row {
display: flex;
flex-direction: row;
}
.items-center {
align-items: center;
}
.overflow-hidden {
overflow: hidden;
}
.mr-8 {
margin-right: 16rpx;
}
.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;
}
.remove-btn {
margin-left: 12rpx;
font-size: 28rpx;
color: #ff4d4f;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,588 @@
<template>
<view class="section">
<view class="section-title">
学生状态列表
<text class="refresh-btn" @click="loadXsList">刷新</text>
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view class="stat-item">
<text class="stat-number">{{ rsData.zrs }}</text>
<text class="stat-label">总人数</text>
</view>
<view class="stat-item">
<text class="stat-number unregistered">{{ rsData.bmRs }}</text>
<text class="stat-label">报名就餐</text>
</view>
<view class="stat-item">
<text class="stat-number unregistered">{{ rsData.unBmRs }}</text>
<text class="stat-label">未报名就餐</text>
</view>
<view class="stat-item">
<text class="stat-number normal">{{ hqZtSl('A') }}</text>
<text class="stat-label">正常</text>
</view>
<view class="stat-item">
<text class="stat-number leave">{{ hqZtSl('B') }}</text>
<text class="stat-label">请假</text>
</view>
<view class="stat-item">
<text class="stat-number absent">{{ hqZtSl('C') }}</text>
<text class="stat-label">缺勤</text>
</view>
</view>
<!-- 学生列表 - 改为card形式 -->
<view class="xs-list">
<!-- 已缴费学生列表 -->
<view v-if="bmXsList.length > 0" class="xs-section">
<view class="section-subtitle">已报名学生 ({{ bmXsList.length }})</view>
<view class="xs-grid">
<view
v-for="xs in bmXsList"
:key="xs.id"
class="xs-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="xs-avatar"
:src="imagUrl(xs.xstx) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="xs-name mb-8">
<text class="font-14 cor-333">{{ xs.xm }}</text>
</view>
<view class="flex-row">
<!-- 已缴费学生可以切换状态 -->
<view
class="status-tag clickable"
:class="getStatusClass(xs.jcZt)"
@click="dkZtXz(xs)"
>
{{ hqZtWz(xs.jcZt) }}
<text class="status-arrow"></text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 未缴费/未报名学生列表 -->
<view v-if="unBmXsList.length > 0" class="xs-section">
<view class="section-subtitle">未报名学生 ({{ unBmXsList.length }})</view>
<view class="xs-grid">
<view
v-for="xs in unBmXsList"
:key="xs.id"
class="xs-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="xs-avatar"
:src="imagUrl(xs.xstx) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="xs-name mb-8">
<text class="font-12 cor-333">{{ xs.xm }}</text>
</view>
<view class="flex-row">
<!-- 未缴费/未报名学生只显示状态不能切换 -->
<view
class="status-tag readonly"
:class="getStatusClass(xs.jcZt)"
>
{{ hqZtWz(xs.jcZt) }}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-if="bmXsList.length === 0 && unBmXsList.length === 0" class="empty-tip">
暂无学生数据
</view>
</view>
<!-- 状态选择弹窗 -->
<u-picker
:defaultIndex="mqXz"
:show="ztVisible"
:columns="[ztList]"
@confirm="onChangeZt"
@cancel="ztVisible = false"
></u-picker>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { findPageByNoJc } from "@/api/base/xsApi";
import { jcQdFindPageApi } from '@/api/base/jcApi'
import { imagUrl } from "@/utils";
import { sortChinese } from "@/utils/pinyinUtil"
//
const props = withDefaults(defineProps<{
bzId?: string
njId?: string
bjId?: string
}>(), {
bzId: '',
njId: '',
bjId: '',
});
//
const rsData = ref({
zrs: 0,
bmRs: 0,
unBmRs: 0
});
const bmXsList = ref<any>([]);
const unBmXsList = ref<any>([]);
const jzZt = ref(false) //
//
const ztVisible = ref(false) //
const ztList = ref<Array<{ text: string, value: string }>>([
{ text: '正常', value: 'A' },
{ text: '请假', value: 'B' },
{ text: '缺勤', value: 'C' }
])
const dqXs = ref<any>(null) //
const mqXz = ref<any>([0]) //
//
const loadXsList = async () => {
if (!props.bzId || !props.bjId) return
jzZt.value = true
try {
const params = {
bzId: props.bzId,
bjId: props.bjId,
njId: props.njId,
pageNo: 1,
rows: 1000
}
const resQd = await jcQdFindPageApi(params);
bmXsList.value = (resQd.rows || []).map((item: any) => {
return {
...item,
jcZt: 'A',
xm: (item.xsxm || item.xm || '').trim()
}
});
bmXsList.value = sortChinese(bmXsList.value, 'xm');
const resUn = await findPageByNoJc(params);
unBmXsList.value = (resUn.rows || []).map((item: any) => {
return {
...item,
jcZt: 'E',
xm: (item.xsxm || item.xm || '').trim()
}
});
unBmXsList.value = sortChinese(unBmXsList.value, 'xm');
rsData.value = {
zrs: (resQd.records || 0) + (resUn.records || 0),
bmRs: (resQd.records || 0),
unBmRs: (resUn.records || 0),
};
} catch (error) {
console.error('加载学生列表失败:', error)
uni.showToast({
title: '加载学生列表失败',
icon: 'none'
})
} finally {
jzZt.value = false
}
}
// ID
watch(() => props.bjId, () => {
if (props.bzId && props.bjId) {
loadXsList()
}
}, { immediate: true })
const hqZtWz = (status: string) => {
switch (status) {
case 'A':
return '正常'
case 'B':
return '请假'
case 'C':
return '缺勤'
case 'D':
return '未缴费'
case 'E':
return '未报名'
default:
return '正常'
}
}
const hqZtSl = (status: string) => {
return bmXsList.value.filter((s:any) => s.jcZt === status).length
}
//
const getStatusClass = (status: string) => {
switch (status) {
case 'A':
return 'status-normal'
case 'B':
return 'status-leave'
case 'C':
return 'status-absent'
case 'D':
return 'status-unpaid'
case 'E':
return 'status-unregistered'
default:
return 'status-normal'
}
}
//
const dkZtXz = (xs: any) => {
dqXs.value = xs;
//
const currentIndex = ztList.value.findIndex(option => option.value === xs.jcZt)
mqXz.value = [currentIndex >= 0 ? currentIndex : 0]
ztVisible.value = true
}
//
const onChangeZt = (e: any) => {
if (dqXs.value && e.value && e.value[0]) {
const selectedStatus = ztList.value.find(
(option: any) => option.value === e.value[0].value
)
if (selectedStatus) {
//
dqXs.value.jcZt = selectedStatus.value
}
}
ztVisible.value = false
}
const getDmXsList = () => {
let retList: any = [];
for(let qd of bmXsList.value) {
retList.push({
xsId: qd.xsId,
xsXm: qd.xm, //
jcZt: qd.jcZt,
jcQdId: qd.id,
jcBzId: qd.bzId,
jzId: qd.jzId,
tx: qd.xstx //
});
}
for(let xs of unBmXsList.value) {
retList.push({
xsId: xs.id,
xsXm: xs.xm, //
jcZt: xs.jcZt,
jcQdId: '',
jcBzId: props.bzId,
jzId: '',
tx: xs.xstx //
});
}
return retList;
}
defineExpose({
loadXsList,
getDmXsList
})
</script>
<style lang="scss" scoped>
.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;
}
.section-subtitle {
font-size: 28rpx;
font-weight: bold;
color: #666;
margin: 20rpx 0 15rpx 0;
padding: 10rpx 0;
border-bottom: 1px solid #f0f0f0;
}
.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;
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
&.normal {
color: #52c41a;
}
&.leave {
color: #faad14;
}
&.absent {
color: #ff4d4f;
}
&.unpaid {
color: #722ed1;
}
&.unregistered {
color: #eb2f96;
}
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.xs-section {
margin-bottom: 30rpx;
}
.xs-list {
margin-bottom: 30rpx;
}
.xs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.xs-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.xs-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.avatar-container {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
padding: 6rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.xs-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: #f5f5f5;
}
.status-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
gap: 4rpx;
&.clickable {
cursor: pointer;
transition: all 0.2s;
&:hover {
opacity: 0.8;
transform: scale(1.05);
}
}
&.readonly {
opacity: 0.8;
cursor: not-allowed;
}
.status-arrow {
font-size: 16rpx;
opacity: 0.8;
}
}
.status-normal {
background-color: #e6f7ff;
color: #52c41a;
}
.status-leave {
background-color: #fff7e6;
color: #faad14;
}
.status-absent {
background-color: #fff2f0;
color: #ff4d4f;
}
.status-unpaid {
background-color: #f9f0ff;
color: #722ed1;
}
.status-unregistered {
background-color: #fff0f6;
color: #eb2f96;
}
.xs-type {
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: inline-block;
}
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 120rpx;
}
.cor-success {
color: #52c41a;
}
.cor-warning {
color: #faad14;
}
.cor-666 {
color: #666;
}
.cor-333 {
color: #333;
}
.flex-row {
display: flex;
flex-direction: row;
}
.justify-end {
justify-content: flex-end;
}
.flex-1 {
flex: 1;
}
.items-center {
align-items: center;
}
.overflow-hidden {
overflow: hidden;
}
.mr-8 {
margin-right: 16rpx;
}
.mb-8 {
margin-bottom: 16rpx;
}
.font-14 {
font-size: 28rpx;
}
.font-12 {
font-size: 24rpx;
}
.font-bold {
font-weight: bold;
}
.bg-white {
background-color: #fff;
}
.r-md {
border-radius: 16rpx;
}
.p-12 {
padding: 24rpx;
}
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
</style>

View File

@ -41,25 +41,17 @@
class="teacher-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<!-- <view class="avatar-container mr-8">
<image
class="teacher-avatar"
:src="imagUrl(teacher.tx) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
</view> -->
<view class="flex-1 overflow-hidden">
<view class="teacher-name mb-8">
<view class="teacher-name">
<text class="font-14 cor-333">{{ teacher.jsXm }}</text>
</view>
<view class="flex-row">
<view
class="status-tag readonly"
:class="getTeacherStatusClass(teacher.pcZt)"
>
{{ getTeacherStatusText(teacher.pcZt) }}
</view>
</view>
</view>
</view>
</view>
@ -151,7 +143,7 @@
<view class="avatar-container mr-8">
<image
class="xs-avatar"
:src="imagUrl(xs.xstx) || '/static/images/default-avatar.png'"
:src="imagUrl(xs.tx) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
@ -226,13 +218,23 @@ const loadDetail = async () => {
if (response.result) {
dmDetail.value = response.result;
dmXsList.value = response.result.dmXsList || [];
dmXsList.value = sortChinese(dmXsList.value, 'xsXm');
unBmXsList.value = response.result.unBmXsList || [];
unBmXsList.value = sortChinese(unBmXsList.value, 'xm');
let srcList = response.result.dmXsList || [];
srcList = sortChinese(srcList, 'xsXm');
dmXsList.value = [];
unBmXsList.value = [];
for (let i = 0; i < srcList.length; i++) {
const xs = srcList[i];
switch (xs.jcZt) {
case 'E': {
unBmXsList.value.push(xs);
} break;
default: {
dmXsList.value.push(xs);
}
}
}
rsData.value = {
zrs: unBmXsList.value.length + dmXsList.value.length,
zrs: srcList.length,
bmRs: dmXsList.value.length,
unBmRs: unBmXsList.value.length,
}
@ -474,7 +476,13 @@ onMounted(() => {
margin-bottom: 30rpx;
}
.teacher-grid, .xs-grid {
.teacher-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.xs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;

107
src/utils/debounce.ts Normal file
View File

@ -0,0 +1,107 @@
// src/utils/debounce.ts
import { ref } from 'vue';
/**
*
* @param func
* @param delay
* @param immediate
* @returns
*/
export function debounce(func: Function, wait: number, immediate: boolean = false) {
let timeout: NodeJS.Timeout | null;
return function (this: any, ...args: any[]) {
const context = this;
const later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
/**
*
*
*/
export class DebounceManager {
private states: Map<string, boolean> = new Map();
/**
*
* @param key
* @param value
* @param duration
*/
setState(key: string, value: boolean, duration?: number): void {
this.states.set(key, value);
if (value && duration) {
setTimeout(() => {
this.states.set(key, false);
}, duration);
}
}
/**
*
* @param key
* @returns
*/
getState(key: string): boolean {
return this.states.get(key) || false;
}
/**
*
*/
reset(): void {
this.states.clear();
}
}
/**
* Vue组合式API防抖函数
* @param delay
* @returns
*/
export function useDebounce(delay: number = 1000) {
const isProcessing = ref(false);
const debounce = <T extends (...args: any[]) => Promise<any>>(
func: T
): ((...args: Parameters<T>) => Promise<ReturnType<T> | void>) => {
return async (...args: Parameters<T>): Promise<ReturnType<T> | void> => {
// 如果正在处理中,则阻止新的调用
if (isProcessing.value) {
return;
}
isProcessing.value = true;
try {
const result = await func(...args);
return result;
} finally {
// 延迟重置状态,防止快速重复点击
setTimeout(() => {
isProcessing.value = false;
}, delay);
}
};
};
const reset = () => {
isProcessing.value = false;
};
return {
isProcessing,
debounce,
reset
};
}