学生请假

This commit is contained in:
hebo 2025-10-24 21:29:01 +08:00
parent e898691ce5
commit 2dbf09df94
16 changed files with 1669 additions and 118 deletions

View File

@ -149,6 +149,11 @@ export const xsQjFindByIdApi = async (params: any) => {
return await get("/api/xsQj/getDetail", params); return await get("/api/xsQj/getDetail", params);
}; };
// 学生请假确认放行
export const xsQjConfirmReleaseApi = async (params: any) => {
return await post("/api/xsQj/confirmRelease", params);
};
// 学生请假审批 // 学生请假审批
export const xsQjSpApi = async (params: any) => { export const xsQjSpApi = async (params: any) => {
return await post("/api/xsQj/sp", params); return await post("/api/xsQj/sp", params);

61
src/api/base/xsQjApi.ts Normal file
View File

@ -0,0 +1,61 @@
import { get, post } from "@/utils/request";
/**
*
*/
export const findXsQjListApi = async (params: any) => {
return await get("/api/xsQj/findPage", params);
};
/**
* ID获取学生请假详情
*/
export const findXsQjByIdApi = async (params: { id: string }) => {
return await get("/api/xsQj/getDetail", params);
};
/**
*
*/
export const xsQjSpApi = async (params: any) => {
return await post("/api/xsQj/sp", params);
};
/**
*
*/
export const xsQjStopApi = async (params: any) => {
return await post("/api/xsQj/stop", params);
};
/**
*
*/
export const xsQjTransferApi = async (params: any) => {
return await post("/api/xsQj/transfer", params);
};
/**
*
* @param date yyyy-MM-dd
*/
export const xsQjStatisticsApi = async (params?: { date?: string }) => {
return await get("/api/xsQj/statistics", params);
};
/**
*
* @param ywId ID
* @param ywType
*/
export const getXsQjApprovalProcessApi = (ywId: string, ywType: string = 'XS_QJ') => {
return get("/api/lcglSp/getByYwIdAndYwType", { ywId, ywType });
};
/**
*
*/
export const confirmReleaseApi = async (params: { id: string; fxjsId: string; fxjsxm: string }) => {
return await post("/api/xsQj/confirmRelease", params);
};

View File

@ -706,6 +706,14 @@
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },
{
"path": "pages/view/routine/jstxl/index",
"style": {
"navigationBarTitleText": "教师通讯录",
"enablePullDownRefresh": false
}
},
{ {
"path": "pages/view/homeSchool/parentAddressBook/index", "path": "pages/view/homeSchool/parentAddressBook/index",
"style": { "style": {
@ -877,6 +885,20 @@
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },
{
"path": "pages/base/xs/qj/detailPush",
"style": {
"navigationBarTitleText": "学生请假推送",
"enablePullDownRefresh": false
}
},
{
"path": "pages/base/xs/qj/statistics",
"style": {
"navigationBarTitleText": "学生请假统计",
"enablePullDownRefresh": false
}
},
{ {
"path": "pages/view/analysis/xs/studentArchive", "path": "pages/view/analysis/xs/studentArchive",
"style": { "style": {

View File

@ -544,6 +544,14 @@ const sections = reactive<Section[]>([
permissionKey: "home-dmtj", // permissionKey: "home-dmtj", //
path: "/pages/view/analysis/xk/dmXkList", path: "/pages/view/analysis/xk/dmXkList",
}, },
{
id: "hs6",
icon: "xsqj",
text: "学生请假",
show: true,
permissionKey: "home-xsqj", //
path: "/pages/base/xs/qj/statistics",
},
], ],
}, },
{ {
@ -583,6 +591,14 @@ const sections = reactive<Section[]>([
permissionKey: "personnel-gzt", // permissionKey: "personnel-gzt", //
path: "/pages/view/hr/salarySlip/index", path: "/pages/view/hr/salarySlip/index",
}, },
{
id: "hr4",
icon: "jstxl",
text: "教师通讯录",
show: true,
permissionKey: "personnel-jstxl", //
path: "/pages/view/routine/jstxl/index",
},
], ],
}, },

View File

@ -54,10 +54,10 @@
<view class="white-bg-color py-5"> <view class="white-bg-color py-5">
<view class="flex-row items-center pb-10 pt-5"> <view class="flex-row items-center pb-10 pt-5">
<u-button <u-button
text="返回页" text="返回上一页"
class="ml-15 mr-7" class="ml-15 mr-7"
:plain="true" type="primary"
@click="goHome" @click="goBack"
/> />
</view> </view>
</view> </view>
@ -66,43 +66,108 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { computed, nextTick } from 'vue';
import { onLoad } from "@dcloudio/uni-app";
import { useDataStore } from "@/store/modules/data"; import { useDataStore } from "@/store/modules/data";
import { getXsQjDetailApi } from "@/api/base/server"; import { useUserStore } from "@/store/modules/user";
import { xsQjFindByIdApi } from "@/api/base/server";
import { xxtsFindByIdApi } from "@/api/base/xxtsApi";
import LcglSp from "@/components/LcglSp/index.vue"; import LcglSp from "@/components/LcglSp/index.vue";
const { getData } = useDataStore(); const { loginByOpenId } = useUserStore();
const dataStore = useDataStore();
const { setQjData } = dataStore;
// URLID // ID - store computed 访
const qjId = getData.id || ''; const qjId = computed(() => {
return (dataStore.qjData as any)?.id || '';
});
// // - 访 store state
const qjData = ref<any>({}); const qjData = computed(() => (dataStore.qjData || {}) as any);
// const goBack = () => {
const loadQjDetail = async () => { uni.navigateBack({
if (!qjId) { delta: 1
console.error('请假ID不能为空'); });
return; };
}
onLoad(async (options: any) => {
console.log('detail.vue onLoad 接收到的参数:', options);
try { try {
const res = await getXsQjDetailApi(qjId); uni.showLoading({ title: "加载中..." });
if (res.resultCode === 1 && res.result) {
qjData.value = res.result; // 1: from=db
if (options && options.from === "db") {
console.log('从待办过来,参数:', options);
//
if (options.openId) {
const isLoggedIn = await loginByOpenId(options.openId);
if (!isLoggedIn) {
console.log("用户未登录,跳过处理");
uni.hideLoading();
return;
}
await nextTick();
}
// urlidXxts
if (options.id) {
const xxtsRes = await xxtsFindByIdApi({ id: options.id });
if (xxtsRes && xxtsRes.result) {
const xxts = xxtsRes.result;
console.log('获取到待办信息:', xxts);
// ID
const qjRes = await xsQjFindByIdApi({ id: xxts.xxzbId });
if (qjRes && qjRes.result) {
setQjData(qjRes.result);
await nextTick();
console.log('获取到请假详情:', qjRes.result);
} else {
throw new Error('获取请假详情失败');
}
} else {
throw new Error('获取待办信息失败');
}
} else {
throw new Error('缺少待办ID参数');
}
} }
} catch (error) { // 2: ID
console.error('获取请假详情失败:', error); else if (options && options.id) {
console.log('直接传请假ID:', options.id);
const qjRes = await xsQjFindByIdApi({ id: options.id });
if (qjRes && qjRes.result) {
setQjData(qjRes.result);
await nextTick();
console.log('获取到请假详情:', qjRes.result);
} else {
throw new Error('获取请假详情失败');
}
}
// 3: store
else if (dataStore.qjData && (dataStore.qjData as any).id) {
console.log('从 store 获取请假ID:', (dataStore.qjData as any).id);
// store
} else {
throw new Error('缺少请假ID参数');
}
uni.hideLoading();
} catch (error: any) {
console.error('加载请假详情失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || "加载失败",
icon: "none"
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
} }
};
const goHome = () => {
uni.reLaunch({ url: '/pages/base/message/index' });
};
//
onMounted(() => {
loadQjDetail();
}); });
</script> </script>

View File

@ -0,0 +1,355 @@
<template>
<view class="qj-push-page">
<!-- 顶部区域 - Logo -->
<view class="header-section">
<image class="logo" src="@/static/system/login/logo.png" mode="aspectFit"></image>
</view>
<!-- 中间白色卡片 -->
<view class="content-section">
<view class="info-card">
<!-- 审批印章 - 右上角如果已审批通过 -->
<view v-if="qjData.spResult === 'B'" class="approval-stamp">
<image class="stamp-image" src="@/static/base/view/sptg.png" mode="aspectFit"></image>
</view>
<!-- 申请人头像 - 中央 -->
<view class="student-photo-center">
<BasicImage :src="qjData.xstx || '/static/base/default-avatar.png'" />
</view>
<!-- 请假信息 -->
<view class="info-list">
<view class="info-item">
<text class="label">请假类型:</text>
<text class="value">{{ qjData.qjlx }}</text>
</view>
<view class="info-item">
<text class="label">开始时间:</text>
<text class="value">{{ qjData.qjkstime }}</text>
</view>
<view class="info-item">
<text class="label">结束时间:</text>
<text class="value">{{ qjData.qjjstime }}</text>
</view>
<view class="info-item">
<text class="label">时长():</text>
<text class="value">{{ qjData.qjsc }}</text>
</view>
<view class="info-item">
<text class="label">是否离校:</text>
<text class="value">{{ qjData.sflx === 1 ? '是' : '否' }}</text>
</view>
<view class="info-item info-item-vertical">
<text class="label">请假事由:</text>
<text class="value">{{ qjData.qjsy }}</text>
</view>
</view>
<!-- 确认放行按钮 -->
<view class="action-button" @click="handleConfirm">
<text class="button-text">确认放行</text>
</view>
</view>
</view>
<!-- 底部区域 -->
<view class="footer-section"></view>
</view>
</template>
<script setup lang="ts">
import { computed, nextTick } from 'vue';
import { onLoad } from "@dcloudio/uni-app";
import { useDataStore } from "@/store/modules/data";
import { useUserStore } from "@/store/modules/user";
import { xsQjFindByIdApi, xsQjConfirmReleaseApi } from "@/api/base/server";
import { xxtsFindByIdApi } from "@/api/base/xxtsApi";
import BasicImage from "@/components/BasicImage/Image.vue";
const { loginByOpenId, getJs } = useUserStore();
const dataStore = useDataStore();
const { setQjData } = dataStore;
// ID - store computed 访
const qjId = computed(() => {
return (dataStore.qjData as any)?.id || '';
});
// - 访 store state
const qjData = computed(() => (dataStore.qjData || {}) as any);
const goHome = () => {
uni.reLaunch({ url: '/pages/base/message/index' });
};
//
const handleConfirm = async () => {
// ID
if (!qjId.value) {
uni.showToast({ title: '请假ID不存在', icon: 'none' });
return;
}
//
const js = getJs;
if (!js || !js.id) {
uni.showToast({ title: '获取教师信息失败', icon: 'none' });
return;
}
uni.showModal({
title: '确认放行',
content: '确认该学生已成功离校?',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '放行中...' });
//
const result = await xsQjConfirmReleaseApi({
id: qjId.value,
fxjsId: js.id,
fxjsxm: js.jsxm
});
uni.hideLoading();
if (result.resultCode === 1) {
uni.showToast({ title: '放行成功', icon: 'success' });
setTimeout(() => {
goHome();
}, 1500);
} else {
uni.showToast({
title: result.message || '放行失败',
icon: 'none'
});
}
} catch (error) {
console.error('放行失败:', error);
uni.hideLoading();
uni.showToast({ title: '放行失败,请重试', icon: 'none' });
}
}
}
});
};
onLoad(async (options: any) => {
console.log('detail.vue onLoad 接收到的参数:', options);
try {
uni.showLoading({ title: "加载中..." });
// 1: from=db
if (options && options.from === "db") {
console.log('从待办过来,参数:', options);
//
if (options.openId) {
const isLoggedIn = await loginByOpenId(options.openId);
if (!isLoggedIn) {
console.log("用户未登录,跳过处理");
uni.hideLoading();
return;
}
await nextTick();
}
// urlidXxts
if (options.id) {
const xxtsRes = await xxtsFindByIdApi({ id: options.id });
if (xxtsRes && xxtsRes.result) {
const xxts = xxtsRes.result;
console.log('获取到待办信息:', xxts);
// ID
const qjRes = await xsQjFindByIdApi({ id: xxts.xxzbId });
if (qjRes && qjRes.result) {
setQjData(qjRes.result);
await nextTick();
console.log('获取到请假详情:', qjRes.result);
} else {
throw new Error('获取请假详情失败');
}
} else {
throw new Error('获取待办信息失败');
}
} else {
throw new Error('缺少待办ID参数');
}
}
// 2: ID
else if (options && options.id) {
console.log('直接传请假ID:', options.id);
const qjRes = await xsQjFindByIdApi({ id: options.id });
if (qjRes && qjRes.result) {
setQjData(qjRes.result);
await nextTick();
console.log('获取到请假详情:', qjRes.result);
} else {
throw new Error('获取请假详情失败');
}
}
// 3: store
else if (dataStore.qjData && (dataStore.qjData as any).id) {
console.log('从 store 获取请假ID:', (dataStore.qjData as any).id);
// store
} else {
throw new Error('缺少请假ID参数');
}
uni.hideLoading();
} catch (error: any) {
console.error('加载请假详情失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || "加载失败",
icon: "none"
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
</script>
<style lang="scss" scoped>
.qj-push-page {
width: 100%;
min-height: 100vh;
background: url("@/static/base/bg.jpg") no-repeat;
background-size: 100% 100%;
display: flex;
flex-direction: column;
position: relative;
}
/* 顶部区域 - Logo */
.header-section {
flex: 0 0 auto;
display: flex;
justify-content: center;
align-items: center;
padding: 80rpx 30rpx 40rpx;
.logo {
width: 500rpx;
height: 120rpx;
}
}
/* 中间白色卡片区域 */
.content-section {
flex: 1 0 auto;
padding: 0 30rpx 40rpx;
.info-card {
background: #FFFFFF;
border-radius: 40rpx 40rpx 20rpx 20rpx;
padding: 40rpx 30rpx;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15);
position: relative;
min-height: 700rpx;
/* 审批印章 - 右上角 */
.approval-stamp {
position: absolute;
top: 30rpx;
right: 30rpx;
width: 160rpx;
height: 160rpx;
z-index: 10;
.stamp-image {
width: 100%;
height: 100%;
}
}
/* 学生头像 - 中央 */
.student-photo-center {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40rpx;
padding-top: 20rpx;
// BasicImage
:deep(.wh-full) {
width: 280rpx !important;
height: 280rpx !important;
border-radius: 32rpx;
overflow: hidden;
background: #F5F5F5;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
display: block;
}
}
/* 信息列表 */
.info-list {
padding-top: 0;
.info-item {
display: flex;
margin-bottom: 32rpx;
line-height: 1.6;
&.info-item-vertical {
flex-direction: column;
.label {
margin-bottom: 12rpx;
}
}
.label {
font-size: 28rpx;
color: #999999;
width: 160rpx;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #333333;
flex: 1;
word-break: break-all;
}
}
}
/* 确认按钮 */
.action-button {
margin-top: 60rpx;
height: 88rpx;
background: linear-gradient(90deg, #FF9500 0%, #FF6B00 100%);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(255, 107, 0, 0.3);
&:active {
opacity: 0.8;
}
.button-text {
font-size: 32rpx;
font-weight: bold;
color: #FFFFFF;
letter-spacing: 4rpx;
}
}
}
}
/* 底部区域 */
.footer-section {
flex: 0 0 auto;
height: 80rpx;
background: transparent;
}
</style>

View File

@ -4,8 +4,7 @@
<!-- 请假信息卡片 --> <!-- 请假信息卡片 -->
<view class="info-card"> <view class="info-card">
<view class="card-header"> <view class="card-header">
<text class="applicant-name" v-if="dbFlag">{{ xxtsData.xxzy }}</text> <text class="applicant-name">学生{{ qjData.xsxm }}的请假申请</text>
<text class="applicant-name" v-else>学生{{ qjData.xsxm }}的请假申请</text>
</view> </view>
<view class="divider"></view> <view class="divider"></view>
<view class="card-body"> <view class="card-body">
@ -43,8 +42,16 @@
<LcglSp :yw-id="qjId" yw-type="XS_QJ" /> <LcglSp :yw-id="qjId" yw-type="XS_QJ" />
</view> </view>
<template #bottom> <template #bottom>
<YwConfirm :spApi="xsQjSpApi" :stopApi="xsQjStopApi" <YwConfirm
:transferApi="xsQjTransferApi" :params="spParams" /> :spApi="xsQjSpApi"
:stopApi="xsQjStopApi"
:transferApi="xsQjTransferApi"
:params="spParams"
:showReject="true"
:showTransfer="false"
:showApprove="true"
:showStop="false"
:showXtDk="false" />
</template> </template>
</BasicLayout> </BasicLayout>
</template> </template>
@ -61,24 +68,25 @@ import YwConfirm from "@/pages/components/YwConfirm/index.vue";
import { XkTfPageUtils } from "@/utils/xkTfPageUtils"; import { XkTfPageUtils } from "@/utils/xkTfPageUtils";
const { getJs, loginByOpenId } = useUserStore(); const { getJs, loginByOpenId } = useUserStore();
const { getQjData, setXxts, setQjData, getXxts } = useDataStore(); const dataStore = useDataStore();
const { setXxts, setQjData } = dataStore;
const dbFlag = ref(false); const dbFlag = ref(false);
// URLID // URLID
const qjId = computed(() => { const qjId = computed(() => {
return getQjData.id || ''; return (dataStore.qjData as any)?.id || '';
}) })
const spParams = computed(() => { const spParams = computed(() => {
return { return {
xxtsId: getXxts.id, xxtsId: (dataStore.xxts as any)?.id,
ywId: qjId.value ywId: qjId.value
}; };
}); });
// // - 访 store state
const qjData = computed(() => getQjData || {}); const qjData = computed(() => (dataStore.qjData || {}) as any);
const xxtsData = ref<any>({}) const xxtsData = ref<any>({})
onLoad(async (data: any) => { onLoad(async (data: any) => {
@ -93,6 +101,9 @@ onLoad(async (data: any) => {
return; return;
} }
// tick
await nextTick();
try { try {
// urlidXxts // urlidXxts
const xxtsRes = await xxtsFindByIdApi({ id: data.id }); const xxtsRes = await xxtsFindByIdApi({ id: data.id });
@ -108,22 +119,27 @@ onLoad(async (data: any) => {
} }
setXxts(xxts); setXxts(xxts);
// tick
await nextTick();
// ID // ID
const res = await xsQjFindByIdApi({ id: xxts.xxzbId }); const res = await xsQjFindByIdApi({ id: xxts.xxzbId });
const xsQj = res.result || {}; const xsQj = res.result || {};
if (xsQj.spResult != "A" && getXxts && getXxts.dbZt === "A") {
if (xsQj.spResult != "A" && dataStore.xxts && (dataStore.xxts as any).dbZt === "A") {
uni.reLaunch({ url: '/pages/base/xs/qj/detail' }); uni.reLaunch({ url: '/pages/base/xs/qj/detail' });
const flag = await XkTfPageUtils.updateXxts(); const flag = await XkTfPageUtils.updateXxts();
} else { } else {
nextTick(() => { setQjData(xsQj);
setQjData(xsQj);
}); // tick
await nextTick();
} }
} }
} catch (error) { } catch (error) {
console.error("获取待办信息失败", error); console.error("获取待办信息失败", error);
// Xxts退 // Xxts退
const xxtsData = getXxts; const xxtsData = dataStore.xxts as any;
if (xxtsData && xxtsData.dbZt === "B") { if (xxtsData && xxtsData.dbZt === "B") {
setQjData({ id: data.id }); setQjData({ id: data.id });
let url = "/pages/base/xs/qj/detail"; let url = "/pages/base/xs/qj/detail";
@ -131,9 +147,10 @@ onLoad(async (data: any) => {
return; return;
} }
const res = await xsQjFindByIdApi({ id: data.id }); const res = await xsQjFindByIdApi({ id: data.id });
nextTick(() => { setQjData(res.result);
setQjData(res.result);
}); // tick
await nextTick();
} }
} else { } else {
dbFlag.value = false; dbFlag.value = false;
@ -159,6 +176,7 @@ onLoad(async (data: any) => {
font-weight: bold; font-weight: bold;
color: #333; color: #333;
margin-bottom: 10px; margin-bottom: 10px;
text-align: center;
.applicant-name { .applicant-name {
font-size: 16px; font-size: 16px;

View File

@ -0,0 +1,547 @@
<template>
<BasicLayout>
<view class="statistics-page">
<!-- 时间范围选择 -->
<view class="section">
<view class="section-title">选择时间范围</view>
<view class="date-range-selector">
<view class="date-item">
<view class="date-label">开始时间</view>
<uni-datetime-picker
v-model="startDate"
type="date"
:clear-icon="false"
@change="onStartDateChange"
>
<view class="date-picker">
<text>{{ startDate }}</text>
<uni-icons type="calendar" size="20" color="#999"></uni-icons>
</view>
</uni-datetime-picker>
</view>
<view class="date-separator"></view>
<view class="date-item">
<view class="date-label">结束时间</view>
<uni-datetime-picker
v-model="endDate"
type="date"
:clear-icon="false"
@change="onEndDateChange"
>
<view class="date-picker">
<text>{{ endDate }}</text>
<uni-icons type="calendar" size="20" color="#999"></uni-icons>
</view>
</uni-datetime-picker>
</view>
</view>
</view>
<!-- 统计卡片 -->
<view class="section">
<view class="section-title">请假统计</view>
<view class="stats-container">
<view
class="stat-item"
:class="{ active: selectedStatType === 'total' }"
@click="onStatItemClick('total')"
>
<view class="stat-number stat-number-total">{{ totalCount }}</view>
<view class="stat-label">总请假人数</view>
</view>
<view
v-for="(count, type, index) in typeCount"
:key="type"
class="stat-item"
:class="{ active: selectedStatType === type }"
@click="onStatItemClick(type)"
>
<view class="stat-number" :class="getStatNumberClass(type, index)">{{ count }}</view>
<view class="stat-label">{{ type }}</view>
</view>
</view>
</view>
<!-- 学生列表 -->
<view class="section" v-if="filteredStudentList.length > 0 || studentList.length > 0">
<view class="section-title">
请假明细 ({{ filteredStudentList.length }})
</view>
<view class="student-list-content">
<view
v-for="item in filteredStudentList"
:key="item.id"
class="student-item"
@click="goDetail(item.id)"
>
<image
class="avatar"
:src="item.xstx || '/static/base/default-avatar.png'"
mode="aspectFill"
></image>
<view class="student-info">
<view class="info-row">
<text class="name">{{ item.xsxm }}</text>
<text class="type-tag">{{ item.qjlx }}</text>
</view>
<view class="info-detail">
<text class="detail-text">所在年级{{ item.bc || '暂无' }}</text>
</view>
<view class="info-detail">
<text class="detail-text">家长{{ item.jzxm || '暂无' }}</text>
</view>
</view>
<uni-icons type="right" size="18" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && filteredStudentList.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 #bottom>
<view class="white-bg-color py-5">
<view class="flex-row items-center pb-10 pt-5">
<u-button
text="返回"
class="mx-15"
type="primary"
@click="goBack"
/>
</view>
</view>
</template>
</BasicLayout>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { xsQjStatisticsApi } from "@/api/base/xsQjApi";
import { showToast } from "@/utils/uniapp";
const startDate = ref('');
const endDate = ref('');
const totalCount = ref(0);
const typeCount = ref<Record<string, number>>({});
const studentList = ref<any[]>([]);
const selectedStatType = ref('total'); //
const loading = ref(false);
const hasSearched = ref(false);
//
onMounted(() => {
const today = new Date();
startDate.value = formatDate(today);
endDate.value = formatDate(today);
loadStatistics();
});
// yyyy-MM-dd
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
//
const formatTime = (timeStr: string) => {
if (!timeStr) return '';
//
return timeStr.replace(/(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}):\d{2}/, '$1 $2');
};
//
const onStartDateChange = (e: any) => {
startDate.value = e;
//
if (startDate.value > endDate.value) {
endDate.value = startDate.value;
}
loadStatistics();
};
//
const onEndDateChange = (e: any) => {
endDate.value = e;
//
if (endDate.value < startDate.value) {
startDate.value = endDate.value;
}
loadStatistics();
};
//
const onStatItemClick = (statType: string) => {
selectedStatType.value = statType;
console.log('选中统计类型:', statType);
};
//
const getStatNumberClass = (type: string, index: number) => {
//
const colorClasses = [
'stat-number-type1', //
'stat-number-type2', //
'stat-number-type3', //
'stat-number-type4', //
'stat-number-type5', //
];
//
if (type.includes('病假')) {
return 'stat-number-sick'; //
} else if (type.includes('事假')) {
return 'stat-number-personal'; //
} else if (type.includes('公假')) {
return 'stat-number-official'; //
} else {
// 使
return colorClasses[index % colorClasses.length];
}
};
//
const filteredStudentList = computed(() => {
if (selectedStatType.value === 'total') {
return studentList.value;
} else {
//
return studentList.value.filter(item => item.qjlx === selectedStatType.value);
}
});
//
const loadStatistics = async () => {
try {
loading.value = true;
hasSearched.value = true;
uni.showLoading({ title: '加载中...' });
// 使date
// 使
const res = await xsQjStatisticsApi({ date: startDate.value });
if (res.resultCode === 1 && res.result) {
totalCount.value = res.result.totalCount || 0;
typeCount.value = res.result.typeCount || {};
studentList.value = res.result.list || [];
} else {
showToast({ title: res.message || '加载失败', icon: 'error' });
}
} catch (error) {
console.error('加载统计数据失败:', error);
showToast({ title: '加载失败', icon: 'error' });
} finally {
loading.value = false;
uni.hideLoading();
}
};
//
const goDetail = (id: string) => {
uni.navigateTo({
url: `/pages/base/xs/qj/detail?id=${id}`
});
};
//
const goBack = () => {
uni.navigateBack({
delta: 1
});
};
</script>
<style lang="scss" scoped>
.statistics-page {
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;
}
.date-range-selector {
display: flex;
align-items: center;
gap: 20rpx;
}
.date-item {
flex: 1;
.date-label {
font-size: 24rpx;
color: #666;
margin-bottom: 12rpx;
}
.date-picker {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
cursor: pointer;
transition: all 0.2s;
&:active {
background-color: #e9ecef;
}
text {
font-size: 28rpx;
color: #333;
}
}
}
.date-separator {
font-size: 28rpx;
color: #999;
padding: 0 10rpx;
margin-top: 36rpx;
}
.stats-container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 60rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
}
.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);
}
&:active {
transform: scale(0.98);
}
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
// - 绿
&.stat-number-total {
color: #52c41a;
}
// -
&.stat-number-sick {
color: #ff4d4f;
}
// - /
&.stat-number-personal {
color: #ff4d4f;
}
// -
&.stat-number-official {
color: #1890ff;
}
//
&.stat-number-type1 {
color: #ff4d4f;
}
&.stat-number-type2 {
color: #fa8c16;
}
&.stat-number-type3 {
color: #1890ff;
}
&.stat-number-type4 {
color: #722ed1;
}
&.stat-number-type5 {
color: #13c2c2;
}
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.student-list-content {
.student-item {
display: flex;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
&:active {
background-color: #f8f9fa;
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 24rpx;
background-color: #f0f0f0;
flex-shrink: 0;
}
.student-info {
flex: 1;
overflow: hidden;
.info-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-right: 16rpx;
}
.type-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
background-color: #e6f7ff;
color: #007aff;
border-radius: 8rpx;
}
}
.info-detail {
margin-bottom: 8rpx;
.detail-text {
font-size: 24rpx;
color: #999;
}
}
}
}
}
.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;
}
}
.white-bg-color {
background-color: #fff;
}
.py-5 {
padding: 10rpx 0;
}
.pb-10 {
padding-bottom: 20rpx;
}
.pt-5 {
padding-top: 10rpx;
}
.flex-row {
display: flex;
flex-direction: row;
}
.items-center {
align-items: center;
}
.mx-15 {
margin: 0 30rpx;
}
</style>

View File

@ -2,7 +2,7 @@
<view class="white-bg-color py-5 yw-confirm"> <view class="white-bg-color py-5 yw-confirm">
<view class="flex-row items-center pt-5 pb-10" v-if="showReject || showTransfer || showApprove || showXtDk"> <view class="flex-row items-center pt-5 pb-10" v-if="showReject || showTransfer || showApprove || showXtDk">
<u-button v-if="showReject" text="驳回" class="flex-1 mx-2 reject-btn" @click="showDlg('reject')" /> <u-button v-if="showReject" text="驳回" class="flex-1 mx-2 reject-btn" @click="showDlg('reject')" />
<u-button v-if="showTransfer" text="转办" class="flex-1 mx-2 transfer-btn" @click="showTransfer" /> <u-button v-if="showTransfer" text="转办" class="flex-1 mx-2 transfer-btn" @click="handleShowTransfer" />
<u-button v-if="showApprove" text="同意" class="flex-1 mx-2" type="primary" @click="submit" /> <u-button v-if="showApprove" text="同意" class="flex-1 mx-2" type="primary" @click="submit" />
<u-button v-if="showXtDk" text="协调代课" class="flex-1 mx-2" type="primary" @click="showXtDlg" /> <u-button v-if="showXtDk" text="协调代课" class="flex-1 mx-2" type="primary" @click="showXtDlg" />
</view> </view>
@ -108,7 +108,7 @@ const closeDlg = () => {
dlgFlag.value = false; dlgFlag.value = false;
}; };
const showTransfer = () => { const handleShowTransfer = () => {
Transferflag.value = true; Transferflag.value = true;
nextTick(() => { nextTick(() => {
transferRef.value.showDlg(); transferRef.value.showDlg();

View File

@ -0,0 +1,526 @@
<template>
<view class="teacher-contact-page">
<!-- 搜索栏 -->
<view class="search-container">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999"></uni-icons>
<input
v-model="searchKeyword"
class="search-input"
placeholder="搜索教师姓名"
@input="handleSearch"
/>
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
<uni-icons type="clear" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 字母索引 -->
<view class="alphabet-index">
<view
v-for="letter in alphabetList"
:key="letter"
class="alphabet-item"
:class="{ active: currentLetter === letter }"
@click="scrollToLetter(letter)"
>
{{ letter }}
</view>
</view>
<!-- 教师列表 -->
<scroll-view
class="teacher-list"
scroll-y
:scroll-with-animation="true"
:enable-back-to-top="true"
@scroll="onScroll"
>
<view v-if="filteredTeachers.length === 0" class="empty-state">
<uni-icons type="person-filled" size="48" color="#ddd"></uni-icons>
<text class="empty-text">暂无教师信息</text>
</view>
<view
v-for="(group, letter) in groupedTeachers"
:key="letter"
:id="`letter-${letter}`"
class="letter-group"
>
<view class="letter-header">
<text class="letter-title">{{ letter }}</text>
<text class="letter-count">{{ group.length }}</text>
</view>
<view
v-for="teacher in group"
:key="teacher.id"
class="teacher-item"
>
<view class="teacher-avatar">
<image
v-if="teacher.headPic"
class="avatar-image"
:src="teacher.headPic"
mode="aspectFill"
/>
<view v-else class="avatar-placeholder">
<text class="avatar-text">{{ getFirstChar(teacher.jsxm) }}</text>
</view>
</view>
<view class="teacher-info">
<view class="teacher-name">{{ teacher.jsxm }}</view>
<view v-if="teacher.bjmc" class="teacher-class">{{ teacher.bjmc }}</view>
<view v-if="shouldShowTeacherType(teacher)" class="teacher-type">{{ teacher.js_type }}</view>
</view>
<view class="teacher-phone" @click.stop="callTeacher(teacher)">
<uni-icons type="phone" size="20" color="#447ade"></uni-icons>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { useCommonStore } from '@/store/modules/common';
import { imagUrl } from '@/utils';
import { pinyin } from 'pinyin-pro';
const commonStore = useCommonStore();
//
const searchKeyword = ref('');
const currentLetter = ref('');
const allTeachers = ref<any[]>([]);
//
const fullAlphabetList = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#'];
//
const filteredTeachers = computed(() => {
if (!searchKeyword.value) {
return allTeachers.value;
}
const keyword = searchKeyword.value;
return allTeachers.value.filter(teacher => {
const name = teacher.jsxm || '';
return name.includes(keyword);
});
});
const groupedTeachers = computed(() => {
const groups: { [key: string]: any[] } = {};
filteredTeachers.value.forEach(teacher => {
const firstChar = getPinyinFirstChar(teacher.jsxm);
const letter = firstChar || '#';
if (!groups[letter]) {
groups[letter] = [];
}
groups[letter].push(teacher);
});
//
const sortedGroups: { [key: string]: any[] } = {};
fullAlphabetList.forEach(letter => {
if (groups[letter]) {
sortedGroups[letter] = groups[letter].sort((a, b) =>
(a.jsxm || '').localeCompare(b.jsxm || '', 'zh-CN')
);
}
});
return sortedGroups;
});
//
const alphabetList = computed(() => {
return Object.keys(groupedTeachers.value);
});
//
const loadTeachers = async () => {
try {
const res = await commonStore.getAllJs();
if (res && res.resultCode === 1 && res.result) {
allTeachers.value = res.result.map((teacher: any) => ({
...teacher,
headPic: teacher.headPic ? imagUrl(teacher.headPic) : null
}));
}
} catch (error) {
console.error('加载教师数据失败:', error);
uni.showToast({
title: '加载教师数据失败',
icon: 'none'
});
}
};
const handleSearch = () => {
//
currentLetter.value = '';
};
const clearSearch = () => {
searchKeyword.value = '';
currentLetter.value = '';
};
const scrollToLetter = (letter: string) => {
currentLetter.value = letter;
// 使 uni-app
setTimeout(() => {
//
uni.createSelectorQuery().selectViewport().scrollOffset((scrollOffset: any) => {
//
uni.createSelectorQuery().select(`#letter-${letter}`).boundingClientRect((rect: any) => {
if (rect && rect.top !== undefined) {
// + -
const targetScrollTop = scrollOffset.scrollTop + rect.top - 100;
uni.pageScrollTo({
scrollTop: Math.max(0, targetScrollTop), //
duration: 300
});
}
}).exec();
}).exec();
// active
setTimeout(() => {
currentLetter.value = '';
}, 2000);
}, 50);
};
const onScroll = (e: any) => {
//
//
};
const callTeacher = (teacher: any) => {
if (!teacher.lxdh) {
uni.showToast({
title: '该教师暂无联系电话',
icon: 'none'
});
return;
}
//
uni.makePhoneCall({
phoneNumber: teacher.lxdh,
fail: (err) => {
console.error('拨打电话失败:', err);
uni.showToast({
title: '拨打电话失败',
icon: 'none'
});
}
});
};
const getFirstChar = (name: string) => {
if (!name) return '';
return name.charAt(0);
};
//
const shouldShowTeacherType = (teacher: any) => {
const jsType = teacher.js_type;
return jsType && jsType !== '教师' && jsType !== '非在编教师';
};
// 使
const getPinyinFirstChar = (name: string) => {
if (!name) return '';
try {
// 使 pinyin-pro
const pinyinStr = pinyin(name.charAt(0), { toneType: 'none' });
return pinyinStr.charAt(0).toUpperCase();
} catch (error) {
//
return name.charAt(0).toUpperCase();
}
};
//
onMounted(() => {
loadTeachers();
});
</script>
<style lang="scss" scoped>
.teacher-contact-page {
min-height: 100vh; /* 改为最小高度,允许内容超出 */
background-color: #f5f7fa;
display: flex;
flex-direction: column;
padding-right: 50px; /* 为整个页面添加右边距 */
}
.search-container {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e4e7ed;
z-index: 200; /* 确保搜索栏在最上层 */
}
.search-bar {
display: flex;
align-items: center;
background-color: #f5f7fa;
border-radius: 20px;
padding: 8px 15px;
.search-input {
flex: 1;
margin-left: 8px;
font-size: 14px;
color: #303133;
border: none;
background: transparent;
&::placeholder {
color: #999;
}
}
.clear-btn {
padding: 4px;
margin-left: 8px;
}
}
.alphabet-index {
position: fixed;
right: 8px; /* 距离屏幕边缘8px */
top: 50%;
transform: translateY(-50%);
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 15px;
padding: 8px 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.alphabet-item {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #447ade;
margin: 2px 0;
border-radius: 50%;
transition: all 0.2s ease;
&.active {
background-color: #e6f0ff; /* 浅蓝色背景 */
color: #447ade; /* 蓝色文字 */
font-weight: bold; /* 加粗 */
transform: scale(1.1); /* 稍微放大突出效果 */
}
&:hover {
background-color: rgba(68, 122, 222, 0.1);
}
}
.teacher-list {
flex: 1;
padding: 0 15px;
background-color: #f5f7fa; /* 确保滚动区域背景色与页面一致 */
height: calc(100vh - 70px); /* 设置固定高度,减去搜索栏高度 */
margin-top: 70px; /* 为固定搜索栏留出空间 */
overflow-y: auto; /* 确保可以滚动 */
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
.empty-text {
margin-top: 15px;
font-size: 14px;
color: #999;
}
}
.letter-group {
margin-bottom: 20px;
background-color: #f5f7fa; /* 确保字母组背景色一致 */
&:last-child {
margin-bottom: 50px; /* 最后一个组增加底部边距 */
}
}
.letter-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0 5px;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 10px;
.letter-title {
font-size: 16px;
font-weight: 600;
color: #447ade;
}
.letter-count {
font-size: 12px;
color: #999;
margin-right: 10px; /* 数字往左移一点 */
}
}
.teacher-item {
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
min-height: 60px;
&:active {
transform: scale(0.98);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
}
.teacher-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
margin-right: 12px;
overflow: hidden;
flex-shrink: 0;
.avatar-image {
width: 100%;
height: 100%;
}
.avatar-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #447ade 0%, #5a8cff 100%);
display: flex;
align-items: center;
justify-content: center;
.avatar-text {
color: #ffffff;
font-size: 18px;
font-weight: 600;
}
}
}
.teacher-info {
flex: 1;
min-width: 0;
margin-right: 10px;
.teacher-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.teacher-class {
font-size: 12px;
color: #909399;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.teacher-type {
font-size: 12px;
color: #447ade;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}
}
.teacher-phone {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #f0f7ff;
margin-left: 12px;
transition: all 0.2s ease;
&:active {
background-color: #e6f0ff;
transform: scale(0.95);
}
}
//
@media (max-width: 375px) {
.search-container {
padding: 12px;
}
.teacher-list {
padding: 0 12px;
}
.teacher-item {
padding: 10px;
}
.teacher-avatar {
width: 42px;
height: 42px;
margin-right: 10px;
}
.teacher-name {
font-size: 15px;
}
.teacher-position {
font-size: 12px;
}
}
</style>

View File

@ -733,11 +733,11 @@ onBeforeUnmount(() => {
font-weight: 400; font-weight: 400;
word-break: break-all; word-break: break-all;
// //
&.study-time { &.study-time {
white-space: nowrap; white-space: normal;
overflow: hidden; word-break: break-word;
text-overflow: ellipsis; line-height: 1.5;
} }
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<!-- 使用 BasicListLayout 包裹记录列表 --> <!-- 使用 BasicListLayout 包裹记录列表 -->
<BasicListLayout @register="register" :fixed="false" class="flex-1"> <BasicListLayout @register="register" :fixed="true" class="flex-1">
<template #top> <template #top>
<!-- 课程信息卡片 --> <!-- 课程信息卡片 -->
<view class="course-card"> <view class="course-card">

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1,64 +0,0 @@
.global-bg-color {
background-color: #F8F8F8
}
/* #ifndef APP-PLUS-NVUE */
page {
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
"Microsoft Yahei", sans-serif;
@extend .global-bg-color
}
.amap-logo, .amap-copyright {
display: none !important;
}
.isSafe {
padding-bottom: constant(safe-area-inset-bottom) !important;
padding-bottom: env(safe-area-inset-bottom) !important;
}
uni-toast{
z-index: 9999999 !important;
}
.basic-item:not(:last-child) {
border-bottom: 1px solid #EFEFEF;
}
::-webkit-scrollbar {
display: none;
}
/* #endif */
//input错误样式
.uni-forms-item__error {
padding-top: 0 !important;
top: 89% !important;
}
.theme-color {
color: var(--themeColor);
}
.theme-color-border {
border: 1px solid var(--themeColor);
}
.theme-bg-color {
background: var(--themeColor);
}
.button {
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
padding: 20rpx 0;
color: white;
background: #2672FF;
margin: 20rpx 30rpx;
}