调整选课相关

This commit is contained in:
ywyonui 2025-08-18 20:47:50 +08:00
parent 1d1d2d6458
commit 44e789f595
19 changed files with 1840 additions and 1412 deletions

View File

@ -94,24 +94,11 @@ export const jsdXkListApi = async (params: any) => {
return await get("/mobile/js/xk/list", params); return await get("/mobile/js/xk/list", params);
}; };
/**
*
*/
export const getCurrentSemesterTeacherCoursesApi = async (jsId?: string) => {
const params = jsId ? { jsId } : {};
return await get("/api/xkkc/getCurrentSemesterTeacherCourses", params);
};
// 选课列表 // 选课列表
export const jsdXkkcSaveApi = async (params: any) => { export const jsdXkkcSaveApi = async (params: any) => {
return await post("/api/xkkc/save", params); return await post("/api/xkkc/save", params);
}; };
// 选课学生列表
export const jsdXkXsListApi = async (params: any) => {
return await get("/mobile/js/xkxs/list", params);
};
// 获取班级学生考试成绩(按科目) // 获取班级学生考试成绩(按科目)
export const jsdBjKscjKmApi = async (params: any) => { export const jsdBjKscjKmApi = async (params: any) => {
return await get("/mobile/js/kscj/bjKm", params); return await get("/mobile/js/kscj/bjKm", params);

48
src/api/base/xkApi.ts Normal file
View File

@ -0,0 +1,48 @@
import { get, post } from "@/utils/request";
/**
*
*/
export const xkListByJsIdApi = async (params: any) => {
return await get("/api/xk/findXkListByJsId", params);
};
/**
*
*/
export const xkkcListByJsIdApi = async (params: any) => {
return await get("/api/xkkc/getXkkcListByJsId", params);
};
/**
* - XkkcApiController.findXkkcList
*/
export const findXkkcListApi = async (params: any) => {
return await get("/api/xkkc/findXkkcList", params);
};
/**
*
*/
export const getXkDmPageApi = async (params: any) => {
return await get('/api/xkDm/findPage', params)
}
/**
*
*/
export const getXkDmXsPageApi = async (params: any) => {
return await get('/api/xkDmXs/findPage', params)
}
// 选课学生列表
export const getWaitDmXsListApi = async (params: any) => {
return await get("/api/xkDmXs/getWaitDmXsList", params);
};
/**
*
*/
export const submitXkDmApi = async (data: any) => {
return await post('/api/xkDm/save', data)
}

View File

@ -486,19 +486,6 @@
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },
{
"path": "pages/base/groupTeaching/xkList",
"style": {
"navigationBarTitleText": "选课列表",
"enablePullDownRefresh": false
}
},
{
"path": "pages/base/groupTeaching/xkkcDetail",
"style": {
"navigationBarTitleText": "选课课程详情"
}
},
{ {
"path": "pages/view/routine/RengJiaoRengZhi/index", "path": "pages/view/routine/RengJiaoRengZhi/index",
"style": { "style": {
@ -514,29 +501,43 @@
} }
}, },
{ {
"path": "pages/base/groupTeaching/dmXkList", "path": "pages/view/routine/xk/xkList",
"style": { "style": {
"navigationBarTitleText": "选课列表" "navigationBarTitleText": "选课列表",
"enablePullDownRefresh": false
} }
}, },
{ {
"path": "pages/base/groupTeaching/dmXkkcDetail", "path": "pages/view/routine/xk/xkkcDetail",
"style": {
"navigationBarTitleText": "选课课程详情"
}
},
{
"path": "pages/view/routine/xk/dmIndex",
"style": {
"navigationBarTitleText": "点名选课列表"
}
},
{
"path": "pages/view/routine/xk/dm",
"style": { "style": {
"navigationBarTitleText": "学生点名", "navigationBarTitleText": "学生点名",
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },
{ {
"path": "pages/base/groupTeaching/dmXkkcRecord", "path": "pages/view/routine/xk/dmRecord",
"style": { "style": {
"navigationBarTitleText": "点名记录", "navigationBarTitleText": "点名记录",
"enablePullDownRefresh": false "enablePullDownRefresh": false
} }
}, },
{ {
"path": "pages/base/groupTeaching/photoXkkcDetail", "path": "pages/view/routine/xk/dmXsRecord",
"style": { "style": {
"navigationBarTitleText": "课堂随拍" "navigationBarTitleText": "点名学生记录",
"enablePullDownRefresh": false
} }
}, },
{ {

View File

@ -1,419 +0,0 @@
<template>
<BasicLayout>
<!-- 课程信息卡片 -->
<view class="mb-5">
<view class="flex-row items-center white-bg-color p-15 r-md">
<!-- 左侧课程图片 -->
<view class="course-image-section" v-if="xkkc.lxtp">
<image
:src="getImageUrl(xkkc.lxtp)"
class="course-image"
mode="aspectFill"
@error="handleImageError"
/>
</view>
<view class="flex-col ml-10 flex-1 course-info">
<view class="course-name">{{ xkkc.kcmc }}</view>
<view class="course-info-item">
<view class="info-label">上课周期</view>
<view class="info-data">{{ xkkc.skzqmc }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课时间</view>
<view class="info-data">{{ formatClassTime(xkkc.skkstime, xkkc.skjstime) }}</view>
</view>
<view class="course-info-item">
<view class="info-label">上课地点</view>
<view class="info-data">{{ xkkc.kcdd }}</view>
</view>
</view>
</view>
</view>
<!-- 拍照和视频功能区域 -->
<view class="p-15">
<!-- 现场拍照 -->
<view class="section-card mb-15">
<view class="section-title">现场拍照</view>
<view class="photo-section">
<view class="photo-preview" v-if="photoList.length > 0">
<view
v-for="(photo, index) in photoList"
:key="index"
class="photo-item"
>
<image
:src="photo.url"
class="photo-image"
mode="aspectFill"
/>
<view class="photo-actions">
<view class="action-btn delete-btn" @click="deletePhoto(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<view class="photo-upload" @click="takePhoto">
<u-icon name="camera" size="32" color="#999"></u-icon>
<text class="upload-text">点击拍照</text>
</view>
</view>
</view>
<!-- 现场视频 -->
<view class="section-card mb-15">
<view class="section-title">现场视频</view>
<view class="video-section">
<view class="video-preview" v-if="videoList.length > 0">
<view
v-for="(video, index) in videoList"
:key="index"
class="video-item"
>
<video
:src="video.url"
class="video-player"
controls
show-center-play-btn
></video>
<view class="video-actions">
<view class="action-btn delete-btn" @click="deleteVideo(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<view class="video-upload" @click="recordVideo">
<u-icon name="videocam" size="32" color="#999"></u-icon>
<text class="upload-text">点击录制</text>
</view>
</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="ml-15 mr-7" :plain="true" @click="navigateBack" />
<u-button text="提交" class="mr-15 mr-7" type="primary" @click="submit" />
</view>
</view>
</template>
</BasicLayout>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { navigateBack } from "@/utils/uniapp";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { jsdXkkcPhotoSaveApi } from "@/api/base/server";
import dayjs from "dayjs";
import { BASE_IMAGE_URL } from "@/config";
const { getJs } = useUserStore();
const { getData } = useDataStore();
const js = computed(() => getJs)
const xkkc = ref<any>({})
//
const photoList = ref<Array<{url: string, path: string}>>([])
const videoList = ref<Array<{url: string, path: string}>>([])
//
const formatClassTime = (startTime: string, endTime: string) => {
if (!startTime || !endTime) {
return '';
}
try {
let start, end;
if (startTime.includes(':') && !startTime.includes('-') && !startTime.includes('/')) {
start = startTime;
end = endTime;
} else {
const startDate = dayjs(startTime);
const endDate = dayjs(endTime);
if (startDate.isValid() && endDate.isValid()) {
start = startDate.format('HH:mm:ss');
end = endDate.format('HH:mm:ss');
} else {
return `${startTime}~${endTime}`;
}
}
return `${start}~${end}`;
} catch (error) {
console.error('时间格式化错误:', error);
return `${startTime}~${endTime}`;
}
};
// URL
const getImageUrl = (imagePath: string) => {
if (!imagePath) {
return '';
}
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
return imagePath;
}
return `${BASE_IMAGE_URL}${imagePath}`;
};
//
const handleImageError = () => {
console.log('课程图片加载失败');
};
//
const takePhoto = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
photoList.value.push({
url: tempFilePath,
path: tempFilePath
});
},
fail: (err) => {
console.error('拍照失败:', err);
uni.showToast({
title: '拍照失败',
icon: 'none'
});
}
});
};
//
const recordVideo = () => {
uni.chooseVideo({
sourceType: ['camera'],
maxDuration: 60, // 60
camera: 'back',
success: (res) => {
const tempFilePath = res.tempFilePath;
videoList.value.push({
url: tempFilePath,
path: tempFilePath
});
},
fail: (err) => {
console.error('录制视频失败:', err);
uni.showToast({
title: '录制视频失败',
icon: 'none'
});
}
});
};
//
const deletePhoto = (index: number) => {
photoList.value.splice(index, 1);
};
//
const deleteVideo = (index: number) => {
videoList.value.splice(index, 1);
};
//
const submit = async () => {
if (photoList.value.length === 0 && videoList.value.length === 0) {
uni.showToast({
title: '请至少拍摄一张照片或录制一段视频',
icon: 'none',
duration: 2000
});
return;
}
try {
// API
const submitData = {
xkkcId: xkkc.value.id,
jsId: js.value.id,
photoList: photoList.value,
videoList: videoList.value,
submitTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
};
// API
const res = await jsdXkkcPhotoSaveApi(submitData);
if (res && res.resultCode === 1) {
uni.showToast({
title: '提交成功',
icon: 'success',
duration: 2000
});
//
setTimeout(() => {
uni.navigateBack({
delta: 1
});
}, 1500);
} else {
uni.showToast({
title: res?.resultMessage || '提交失败',
icon: 'none',
duration: 2000
});
}
} catch (error) {
console.error('提交失败:', error);
uni.showToast({
title: '提交失败,请重试',
icon: 'none',
duration: 2000
});
}
};
//
onMounted(() => {
xkkc.value = getData;
});
</script>
<style lang="scss" scoped>
.course-info {
flex: 1;
.course-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
}
.course-info-item {
display: flex;
margin-bottom: 12px;
font-size: 12px;
align-items: center;
.info-label {
color: #949AA4;
flex: 0 0 auto;
margin-right: 4px;
white-space: nowrap;
}
.info-data {
flex: 0 0 auto;
color: #333;
font-weight: 400;
}
}
}
.course-image-section {
flex: 0 0 100px;
margin-right: 15px;
display: flex;
align-items: center;
justify-content: center;
.course-image {
width: 80px;
height: 80px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
}
}
.section-card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
padding-left: 8px;
border-left: 3px solid #007aff;
}
.photo-section, .video-section {
.photo-preview, .video-preview {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.photo-item, .video-item {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.photo-image, .video-player {
width: 100%;
height: 100px;
border-radius: 8px;
}
.video-player {
background-color: #000;
}
.photo-actions, .video-actions {
position: absolute;
top: 5px;
right: 5px;
.action-btn {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.delete-btn {
background-color: rgba(255, 0, 0, 0.8);
}
}
}
}
.photo-upload, .video-upload {
width: 100px;
height: 100px;
border: 2px dashed #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #007aff;
background-color: rgba(0, 122, 255, 0.05);
}
.upload-text {
font-size: 12px;
color: #999;
margin-top: 5px;
}
}
}
</style>

View File

@ -1,410 +0,0 @@
<template>
<BasicLayout>
<!-- 课程信息卡片 -->
<view class="course-card mx-15 my-15 bg-white white-bg-color r-md p-15">
<view class="flex-row items-center mb-15">
<view class="course-icon flex-center mr-10">
<u-icon name="calendar" color="#4080ff" size="20"></u-icon>
</view>
<text class="font-16 font-bold">机器人创客</text>
<text class="font-14 cor-999 ml-auto">2024-12-25 (周三)</text>
</view>
<!-- 考勤统计 -->
<view class="attendance-stats flex-row">
<view class="stat-item flex-col items-center">
<text class="font-18 font-bold">18</text>
<text class="font-12 cor-666 mt-3">应到</text>
</view>
<view class="stat-item flex-col items-center">
<text class="font-18 font-bold cor-primary">18</text>
<text class="font-12 cor-666 mt-3">实到</text>
</view>
<view class="stat-item flex-col items-center">
<text class="font-18 font-bold cor-warning">0</text>
<text class="font-12 cor-666 mt-3">请假</text>
</view>
<view class="stat-item flex-col items-center">
<text class="font-18 font-bold cor-danger">0</text>
<text class="font-12 cor-666 mt-3">缺勤</text>
</view>
<view class="stat-circle flex-col flex-center ml-auto">
<text class="font-20 font-bold">18</text>
<text class="font-10 cor-666">总人数</text>
</view>
</view>
</view>
<!-- 学生列表 -->
<view class="student-list mb-30 white-bg-color">
<view class="student-grid">
<view
v-for="(student, index) in studentList"
:key="index"
class="student-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="student-avatar"
:src="student.avatar || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="flex-row items-center mb-3">
<text class="font-14 font-bold mr-5 text-ellipsis">{{
student.name
}}
</text>
<view
class="status-tag"
:class="getStatusClass(student.status)"
@click="openStatusPicker(student)"
>
{{ student.status }}
<u-icon name="arrow-down" size="10"></u-icon>
</view>
</view>
<text class="font-12 cor-666">{{ student.className }}</text>
<view class="contact-parent mt-8 flex-center">
<text class="font-12 cor-primary">联系家长</text>
<u-icon
name="phone"
color="#4080ff"
size="14"
class="ml-2"
></u-icon>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 状态选择弹窗 -->
<u-picker
:show="statusPickerVisible"
:columns="[statusOptions]"
@confirm="confirmStatus"
@cancel="statusPickerVisible = false"
></u-picker>
<template #bottom>
<view class="submit-btn-wrap py-10 px-20 bg-white">
<button class="submit-btn" @click="submit">提交</button>
</view>
</template>
</BasicLayout>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { useDicStore } from "@/store/modules/dic";
const { findByPid } = useDicStore();
//
const studentList = ref([
{
id: 1,
name: "伍添昊",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar1.png",
},
{
id: 2,
name: "时振宇",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar2.png",
},
{
id: 3,
name: "程子璇",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar3.png",
},
{
id: 4,
name: "柘延兴",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar4.png",
},
{
id: 5,
name: "张茜溪",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar5.png",
},
{
id: 6,
name: "孟嘉乐",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar6.png",
},
{
id: 7,
name: "韩汝鑫",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar7.png",
},
{
id: 8,
name: "曹佳毅",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar8.png",
},
{
id: 9,
name: "郎甜",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar9.png",
},
{
id: 10,
name: "萧文懿",
status: "正常",
className: "三年八班",
avatar: "/static/images/avatar10.png",
},
]);
//
const statusPickerVisible = ref(false);
const statusOptions = ref<Array<{ text: string, value: string }>>([]);
const currentStudent = ref<any>(null);
//
const getStatusClass = (status: string) => {
switch (status) {
case "正常":
return "status-normal";
case "请假":
return "status-leave";
case "缺勤":
return "status-absent";
default:
return "status-normal";
}
};
//
const fetchStatusOptions = async () => {
try {
// pid810984651
const res = await findByPid({pid: 810984651});
if (res && res.result) {
statusOptions.value = res.result.map((item: any) => {
return {
text: item.dictionaryValue,
value: item.dictionaryCode
};
});
}
} catch (error) {
console.error("获取状态选项失败", error);
// 使
statusOptions.value = [
{text: "正常", value: "1"},
{text: "请假", value: "2"},
{text: "缺勤", value: "3"}
];
}
};
//
const openStatusPicker = (student: any) => {
currentStudent.value = student;
statusPickerVisible.value = true;
};
//
const confirmStatus = (e: any) => {
if (currentStudent.value && e.value && e.value[0]) {
const selectedStatus = statusOptions.value.find(
(option: any) => option.value === e.value[0]
);
if (selectedStatus) {
//
currentStudent.value.status = selectedStatus.text;
}
}
statusPickerVisible.value = false;
};
//
const navigateBack = () => {
uni.navigateBack();
};
const toRollCallRecord = () => {
uni.navigateTo({
url: "/pages/base/groupTeaching/rollCallRecord",
});
};
//
const contactParent = (student: any) => {
console.log("联系家长", student.name);
};
//
const submit = () => {
uni.showToast({
title: "提交成功",
icon: "success",
});
};
//
onMounted(() => {
fetchStatusOptions();
});
</script>
<style scoped lang="scss">
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
height: 44px;
background-color: #fff;
}
.course-card {
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.course-icon {
width: 30px;
height: 30px;
border-radius: 4px;
background-color: rgba(64, 128, 255, 0.1);
}
.attendance-stats {
padding: 10px 0;
}
.stat-item {
flex: 1;
}
.stat-circle {
width: 60px;
height: 60px;
border-radius: 50%;
}
.student-list {
padding: 0 15px;
}
.student-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.student-item {
position: relative;
}
.avatar-container {
width: 46px;
height: 46px;
border-radius: 50%;
padding: 3px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.student-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #f5f5f5;
}
.status-tag {
font-size: 10px;
padding: 1px 5px;
border-radius: 4px;
display: flex;
align-items: center;
cursor: pointer;
}
.status-normal {
color: #4080ff;
}
.status-leave {
color: #ff9900;
}
.status-absent {
color: #ff4d4f;
}
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60px;
}
.contact-parent {
padding: 3px 8px;
border-radius: 4px;
border: 1px solid #4080ff;
display: inline-flex;
}
.submit-btn {
background-color: #4080ff;
color: #fff;
height: 44px;
border-radius: 22px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.cor-primary {
color: #4080ff;
}
.cor-warning {
color: #ff9900;
}
.cor-danger {
color: #ff4d4f;
}
.cor-666 {
color: #666;
}
.cor-999 {
color: #999;
}
</style>

View File

@ -1,57 +0,0 @@
<template>
<view class="wh-full">
<BasicListLayout @register="register">
<template v-slot="{ item }">
<view
class="white-bg-color r-md p-15 mb-15"
@click="toCourseDetail(item)"
>
<view class="flex-row items-center">
<image
src="/static/test/1.png"
class="course-image r-md wi-180 he-180"
mode="aspectFill"
/>
<view class="flex-col ml-10 flex-1">
<text class="font-bold font-16">{{ "机器人创客" }}</text>
<view class="text-gray-600 text-md mt-10">
<text class="font-12 cor-949AA4">开课老师</text>
<text class="font-12">{{ "叶老师" }}</text>
</view>
<view class="text-gray-600 text-md mt-5">
<text class="font-12 cor-949AA4">上课地点</text>
<text class="font-12">{{ "第一教学楼302" }}</text>
</view>
<view class="mt-5">
<text class="font-12 cor-949AA4">金额</text>
<text class="font-12 cor-FF8D02">¥{{ "142" }}</text>
</view>
</view>
</view>
</view>
</template>
</BasicListLayout>
</view>
</template>
<script setup lang="ts">
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import { testList } from "@/api/test/test";
const [register, { reload }] = useLayout({
api: testList,
componentProps: {},
});
const toCourseDetail = (item: any) => {
uni.navigateTo({
url: `/pages/base/groupTeaching/zhujiaoDetails`,
});
};
</script>
<style lang="scss" scoped>
.course-image {
width: 120rpx;
height: 120rpx;
}
</style>

View File

@ -1,165 +0,0 @@
<template>
<BasicLayout>
<template #top>
<view class="flex-row items-center white-bg-color p-15 r-md">
<image
src="/static/test/1.png"
class="course-image r-md wi-180 he-180"
mode="aspectFill"
/>
<view class="flex-col ml-10 flex-1">
<text class="font-bold font-16">{{ "机器人创客" }}</text>
<view class="text-gray-600 text-md mt-10">
<text class="font-12 cor-949AA4">开课老师</text>
<text class="font-12">{{ "叶老师" }}</text>
</view>
<view class="text-gray-600 text-md mt-5">
<text class="font-12 cor-949AA4">上课地点</text>
<text class="font-12">{{ "第一教学楼302" }}</text>
</view>
<view class="mt-5">
<text class="font-12 cor-949AA4">金额</text>
<text class="font-12 cor-FF8D02">¥{{ "142" }}</text>
</view>
</view>
</view>
</template>
<view class="p-15">
<BasicForm @register="register">
<template #jxjh>
<view class="back-f8f8f8">
<view class="flex-row items-center justify-between py-15 global-bg-color">
<view>
<BasicTitle line title="教学计划" :isBorder="false"/>
</view>
<view @click="addEducation">
<BasicIcon type="icon-tianjia" size="25"/>
</view>
</view>
<view v-if="education.xl.length>0">
<template v-for="(item,index) in education.xl" :key="index">
<view class="po-re mb-15">
<BasicForm v-model="item.value" :schema="schema" :formsProps="{labelWidth: 100}"/>
<view @click="deleteMemberFamily(index as number,item.value)" class="delete-icon">
<BasicIcon type="clear" size="30"/>
</view>
</view>
</template>
</view>
<view v-else class="p-15 flex-row-center color-9 font-13 white-bg-color">教学计划暂无数据</view>
</view>
</template>
</BasicForm>
</view>
<template #bottom>
<view class="white-bg-color py-5">
<view class="flex-row items-center pb-10 pt-5">
<u-button
text="返回"
class="ml-15 mr-7"
:plain="true"
@click="navigateBack"
/>
<u-button
text="提交"
class="mr-15 mr-7"
type="primary"
@click="submit"
/>
</view>
</view>
</template>
</BasicLayout>
</template>
<script lang="ts" setup>
import {navigateBack} from "@/utils/uniapp";
import {useForm} from "@/components/BasicForm/hooks/useForm";
import {cloneDeep} from "lodash";
const [register, {getValue}] = useForm({
schema: [
{
field: "ttlx",
label: "老师信息",
component: "BasicInput",
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
},
},
{
field: "ttlx",
label: "教学概念",
component: "BasicInput",
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
},
},
{colSlot: 'jxjh'},
],
});
const schema = reactive<FormsSchema[]>([
{
field: "ttlx",
label: "阶段",
component: "BasicInput",
componentProps: {},
},
{
field: "ttlx",
label: "计划时间",
component: "BasicDateTimes",
componentProps: {},
},
{
field: "ttlx",
label: "地址",
component: "BasicInput",
componentProps: {},
},
{
field: "ttlx",
label: "计划内容",
component: "BasicInput",
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
},
},
])
const education = reactive<any>(
{
xl: []
}
)
function addEducation() {
education.xl.push({value: {}})
}
function deleteMemberFamily(index: number, item: any) {
const list = cloneDeep(education.xl)
list.splice(index, 1)
education.xl = list
}
</script>
<style>
.delete-icon {
position: absolute;
right: 0;
top: 0;
z-index: 1;
}
</style>

View File

@ -237,7 +237,7 @@ const sections = reactive<Section[]>([
text: "课程填报", text: "课程填报",
show: true, show: true,
permissionKey: "routine-kcjs", // permissionKey: "routine-kcjs", //
path: "/pages/base/groupTeaching/xkList", path: "/pages/view/routine/xk/xkList",
}, },
{ {
id: "r6", id: "r6",
@ -295,7 +295,7 @@ const sections = reactive<Section[]>([
text: "选课点名", text: "选课点名",
show: true, show: true,
permissionKey: "routine-kcdm", // permissionKey: "routine-kcdm", //
path: "/pages/base/groupTeaching/dmXkList", path: "/pages/view/routine/xk/dmIndex",
}, },
{ {
id: "r11", id: "r11",

View File

@ -0,0 +1,225 @@
# DmPs 组件集成文档
## 概述
`DmPs` 组件已成功集成到选课点名和就餐点名两个页面中,提供统一的拍照和视频录制功能。
## 集成页面
### 1. 选课点名页面
**文件路径**: `zhxy-jsd/src/pages/view/routine/xk/dm.vue`
**功能特点**:
- 课程现场拍照和视频录制
- 支持最多9张照片和3个视频
- 视频最大时长60秒
- 提交时自动上传媒体文件到服务器
**关键代码**:
```vue
<DmPsComponent
v-model="mediaData"
:photo-title="'现场拍照'"
:video-title="'现场视频'"
:max-photo-count="9"
:max-video-count="3"
:max-video-duration="60"
@photo-change="handlePhotoChange"
@video-change="handleVideoChange"
@photo-add="handlePhotoAdd"
@video-add="handleVideoAdd"
@upload-complete="handleUploadComplete"
ref="dmPsRef"
/>
```
### 2. 就餐点名页面
**文件路径**: `zhxy-jsd/src/pages/view/routine/jc/components/dm.vue`
**功能特点**:
- 就餐现场拍照和视频录制
- 支持最多9张照片和3个视频
- 视频最大时长60秒
- 提交时自动上传媒体文件到服务器
**关键代码**:
```vue
<DmPsComponent
v-model="mediaData"
:photo-title="'就餐现场拍照'"
:video-title="'就餐现场视频'"
:max-photo-count="9"
:max-video-count="3"
:max-video-duration="60"
@photo-change="handlePhotoChange"
@video-change="handleVideoChange"
@photo-add="handlePhotoAdd"
@video-add="handleVideoAdd"
@upload-complete="handleUploadComplete"
ref="dmPsRef"
/>
```
## 数据结构
### 媒体数据对象
```typescript
interface MediaData {
photoList: Array<{
url: string; // 照片URL本地临时路径
path: string; // 照片路径(本地临时路径)
}>;
videoList: Array<{
url: string; // 视频URL本地临时路径
path: string; // 视频路径(本地临时路径)
}>;
}
```
### 上传结果对象
```typescript
interface UploadResult {
photoUrls: string; // 照片服务器地址,逗号分隔
videoUrls: string; // 视频服务器地址,逗号分隔
}
```
## 事件处理
### 事件处理函数
两个页面都实现了以下事件处理函数:
```typescript
// 处理照片变化
const handlePhotoChange = (photoList: Array<{url: string, path: string}>) => {
console.log('照片列表变化:', photoList);
};
// 处理视频变化
const handleVideoChange = (videoList: Array<{url: string, path: string}>) => {
console.log('视频列表变化:', videoList);
};
// 处理照片添加
const handlePhotoAdd = (photo: {url: string, path: string}) => {
console.log('添加照片:', photo);
};
// 处理视频添加
const handleVideoAdd = (video: {url: string, path: string}) => {
console.log('添加视频:', video);
};
// 处理上传完成事件
const handleUploadComplete = (result: {photoUrls: string, videoUrls: string}) => {
console.log('媒体文件上传完成:', result);
console.log('照片地址:', result.photoUrls);
console.log('视频地址:', result.videoUrls);
};
```
## 提交流程
### 选课点名提交流程
1. 用户点击提交按钮
2. 验证数据完整性
3. 如果有媒体文件,调用 `dmPsRef.value.uploadMedia()`
4. 等待上传完成,获取服务器地址
5. 将地址转换为逗号分隔字符串
6. 设置到 `dmData.zp`(照片)和 `dmData.sp`(视频)
7. 提交到后端 API
### 就餐点名提交流程
1. 用户点击提交按钮
2. 验证班级和学生数据
3. 如果有媒体文件,调用 `dmPsRef.value.uploadMedia()`
4. 等待上传完成,获取服务器地址
5. 将地址转换为逗号分隔字符串
6. 设置到 `dmData.zp`(照片)和 `dmData.sp`(视频)
7. 提交到后端 API
## 错误处理
### 上传失败处理
```typescript
try {
if (dmPsRef.value) {
const uploadResult = await dmPsRef.value.uploadMedia();
photoUrls = uploadResult.photoUrls;
videoUrls = uploadResult.videoUrls;
}
} catch (uploadError) {
console.error('媒体文件上传失败:', uploadError);
uni.showToast({
title: '媒体文件上传失败,请重试',
icon: 'none'
});
// 停止提交流程
return;
}
```
### TypeScript 类型安全
- 使用 `ref<any>(null)` 定义组件引用
- 添加空值检查 `if (dmPsRef.value)`
- 完整的类型定义和错误处理
## 样式适配
### 选课点名样式
- 使用 `BasicLayout` 布局
- 卡片式设计
- 响应式网格布局
### 就餐点名样式
- 使用 `section` 分区块设计
- 卡片式学生列表
- 固定底部提交按钮
## 数据存储
### 后端字段映射
- `zp`: 照片服务器地址(逗号分隔字符串)
- `sp`: 视频服务器地址(逗号分隔字符串)
### 数据重置
提交成功后,两个页面都会重置媒体数据:
```typescript
mediaData.value = {
photoList: [],
videoList: []
};
```
## 使用建议
1. **权限管理**: 确保应用有相机和麦克风权限
2. **网络状态**: 上传前检查网络连接状态
3. **文件大小**: 注意控制照片和视频的文件大小
4. **用户体验**: 上传过程中显示加载状态
5. **错误恢复**: 上传失败时提供重试机制
## 扩展功能
### 可能的扩展
1. **批量上传**: 支持同时上传多个文件
2. **进度显示**: 显示上传进度百分比
3. **压缩功能**: 自动压缩大文件
4. **预览功能**: 上传前预览媒体文件
5. **编辑功能**: 简单的图片编辑功能
## 维护说明
### 组件更新
`DmPs` 组件更新时,需要同步更新两个页面的:
1. 组件引用
2. 事件处理函数
3. 数据结构定义
4. 错误处理逻辑
### 测试要点
1. 拍照功能测试
2. 视频录制测试
3. 文件上传测试
4. 错误处理测试
5. 数据提交测试

View File

@ -0,0 +1,264 @@
# DmPsComponent 拍照视频组件
这是一个通用的拍照和视频录制组件,可以在就餐点名和选课点名等场景中复用。
## 功能特性
- 📸 **拍照功能**:支持相机拍照,可设置最大照片数量
- 🎥 **视频录制**:支持视频录制,可设置最大视频数量和时长
- 🖼️ **照片预览**:点击照片可全屏预览
- 🗑️ **删除功能**:支持删除已拍摄的照片和视频
- 📤 **文件上传**:支持将本地文件上传到服务器
- 📱 **响应式设计**:适配不同屏幕尺寸
- 🎨 **可定制样式**:支持自定义标题、按钮文字等
## 使用方法
### 基础用法
```vue
<template>
<DmPsComponent
v-model="mediaData"
ref="dmPsRef"
/>
</template>
<script setup>
import DmPsComponent from "@/pages/components/dmPs/index.vue";
import { ref } from "vue";
const mediaData = ref({
photoList: [],
videoList: []
});
const dmPsRef = ref(null);
</script>
```
### 完整用法(包含上传功能)
```vue
<template>
<DmPsComponent
v-model="mediaData"
:photo-title="'现场拍照'"
:video-title="'现场视频'"
:max-photo-count="9"
:max-video-count="3"
:max-video-duration="60"
@photo-change="handlePhotoChange"
@video-change="handleVideoChange"
@photo-add="handlePhotoAdd"
@video-add="handleVideoAdd"
@upload-complete="handleUploadComplete"
ref="dmPsRef"
/>
</template>
<script setup>
import DmPsComponent from "@/pages/components/dmPs/index.vue";
import { ref } from "vue";
const mediaData = ref({
photoList: [],
videoList: []
});
const dmPsRef = ref(null);
// 处理照片变化
const handlePhotoChange = (photoList) => {
console.log('照片列表变化:', photoList);
};
// 处理视频变化
const handleVideoChange = (videoList) => {
console.log('视频列表变化:', videoList);
};
// 处理照片添加
const handlePhotoAdd = (photo) => {
console.log('添加照片:', photo);
};
// 处理视频添加
const handleVideoAdd = (video) => {
console.log('添加视频:', video);
};
// 处理上传完成
const handleUploadComplete = (result) => {
console.log('上传完成:', result);
console.log('照片地址:', result.photoUrls);
console.log('视频地址:', result.videoUrls);
};
// 提交时上传文件
const submit = async () => {
try {
// 上传媒体文件
const uploadResult = await dmPsRef.value.uploadMedia();
// 构建提交数据
const submitData = {
zp: uploadResult.photoUrls, // 照片地址,逗号分隔
sp: uploadResult.videoUrls, // 视频地址,逗号分隔
// 其他数据...
};
// 提交到后端
// await submitApi(submitData);
} catch (error) {
console.error('提交失败:', error);
}
};
</script>
```
## Props 属性
| 属性名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| modelValue | Object | - | 双向绑定的媒体数据对象 |
| photoTitle | String | '现场拍照' | 拍照区域标题 |
| photoUploadText | String | '点击拍照' | 拍照按钮文字 |
| maxPhotoCount | Number | 9 | 最大照片数量 |
| videoTitle | String | '现场视频' | 视频区域标题 |
| videoUploadText | String | '点击录制' | 录制按钮文字 |
| maxVideoCount | Number | 3 | 最大视频数量 |
| maxVideoDuration | Number | 60 | 最大视频时长(秒) |
## Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:modelValue | value | 媒体数据变化时触发 |
| photoChange | photoList | 照片列表变化时触发 |
| videoChange | videoList | 视频列表变化时触发 |
| photoAdd | photo | 添加照片时触发 |
| photoDelete | index | 删除照片时触发 |
| videoAdd | video | 添加视频时触发 |
| videoDelete | index | 删除视频时触发 |
| uploadComplete | result | 文件上传完成时触发 |
## Methods 方法
通过 ref 可以调用以下方法:
| 方法名 | 参数 | 返回值 | 说明 |
|--------|------|--------|------|
| takePhoto | - | - | 触发拍照 |
| recordVideo | - | - | 触发录制视频 |
| deletePhoto | index | - | 删除指定索引的照片 |
| deleteVideo | index | - | 删除指定索引的视频 |
| uploadMedia | - | Promise<{photoUrls: string, videoUrls: string}> | 上传所有媒体文件到服务器 |
| clearAll | - | - | 清空所有媒体数据 |
## 数据结构
### modelValue 对象结构
```typescript
interface MediaData {
photoList: Array<{
url: string; // 照片URL本地临时路径
path: string; // 照片路径(本地临时路径)
}>;
videoList: Array<{
url: string; // 视频URL本地临时路径
path: string; // 视频路径(本地临时路径)
}>;
}
```
### uploadMedia 返回值结构
```typescript
interface UploadResult {
photoUrls: string; // 照片服务器地址,逗号分隔
videoUrls: string; // 视频服务器地址,逗号分隔
}
```
## 文件上传流程
1. **拍照/录制**:用户拍照或录制视频,文件存储在本地临时路径
2. **预览/删除**:用户可以预览或删除已拍摄的媒体文件
3. **提交时上传**:调用 `uploadMedia()` 方法,将所有文件上传到服务器
4. **获取服务器地址**:上传成功后返回服务器文件路径
5. **存储到数据库**:将服务器路径(逗号分隔)存储到数据库对应字段
## 使用场景
### 1. 选课点名拍照
```vue
<DmPsComponent
v-model="mediaData"
photo-title="课程现场拍照"
video-title="课程现场视频"
:max-photo-count="6"
:max-video-count="2"
/>
```
### 2. 就餐点名拍照
```vue
<DmPsComponent
v-model="mediaData"
photo-title="就餐现场拍照"
video-title="就餐现场视频"
:max-photo-count="4"
:max-video-count="1"
:max-video-duration="30"
/>
```
### 3. 活动记录拍照
```vue
<DmPsComponent
v-model="mediaData"
photo-title="活动记录拍照"
video-title="活动记录视频"
:max-photo-count="12"
:max-video-count="5"
/>
```
## 注意事项
1. **权限要求**:使用相机和麦克风需要相应的系统权限
2. **文件大小**:注意控制照片和视频的文件大小,避免影响性能
3. **存储空间**:大量媒体文件可能占用较多存储空间
4. **网络上传**:提交时需要将本地文件上传到服务器
5. **上传失败处理**:上传失败时会抛出异常,需要妥善处理
6. **服务器地址格式**:上传成功后返回的是服务器相对路径,需要配合 `imagUrl()` 函数使用
## 样式定制
组件使用 SCSS 编写,可以通过以下方式定制样式:
```scss
// 自定义组件样式
.dm-ps-component {
.section-card {
background: #f8f9fa;
border-radius: 12px;
}
.section-title {
color: #007aff;
font-weight: bold;
}
.photo-upload, .video-upload {
border-color: #007aff;
background: rgba(0, 122, 255, 0.1);
}
}
```

View File

@ -0,0 +1,395 @@
<template>
<view class="dm-ps-component">
<!-- 现场拍照 -->
<view class="section-card mb-15">
<view class="section-title">{{ photoTitle || '现场拍照' }}</view>
<view class="photo-section">
<view class="photo-preview" v-if="photoList.length > 0">
<view
v-for="(photo, index) in photoList"
:key="index"
class="photo-item"
>
<image
:src="photo.url"
class="photo-image"
mode="aspectFill"
@click="previewPhoto(photo.url)"
/>
<view class="photo-actions">
<view class="action-btn delete-btn" @click="deletePhoto(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<view class="photo-upload" @click="takePhoto">
<u-icon name="camera" size="32" color="#999"></u-icon>
<text class="upload-text">{{ photoUploadText || '点击拍照' }}</text>
</view>
</view>
</view>
<!-- 现场视频 -->
<view class="section-card mb-15">
<view class="section-title">{{ videoTitle || '现场视频' }}</view>
<view class="video-section">
<view class="video-preview" v-if="videoList.length > 0">
<view
v-for="(video, index) in videoList"
:key="index"
class="video-item"
>
<video
:src="video.url"
class="video-player"
controls
show-center-play-btn
></video>
<view class="video-actions">
<view class="action-btn delete-btn" @click="deleteVideo(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<view class="video-upload" @click="recordVideo">
<u-icon name="videocam" size="32" color="#999"></u-icon>
<text class="upload-text">{{ videoUploadText || '点击录制' }}</text>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { attachmentUpload } from "@/api/system/upload";
import { showLoading, hideLoading, showToast } from "@/utils/uniapp";
//
interface Props {
//
photoTitle?: string;
photoUploadText?: string;
maxPhotoCount?: number;
//
videoTitle?: string;
videoUploadText?: string;
maxVideoCount?: number;
maxVideoDuration?: number;
//
modelValue?: {
photoList: Array<{url: string, path: string}>;
videoList: Array<{url: string, path: string}>;
};
}
//
interface Emits {
(e: 'update:modelValue', value: {photoList: Array<{url: string, path: string}>, videoList: Array<{url: string, path: string}>}): void;
(e: 'photoChange', photoList: Array<{url: string, path: string}>): void;
(e: 'videoChange', videoList: Array<{url: string, path: string}>): void;
(e: 'photoAdd', photo: {url: string, path: string}): void;
(e: 'photoDelete', index: number): void;
(e: 'videoAdd', video: {url: string, path: string}): void;
(e: 'videoDelete', index: number): void;
(e: 'uploadComplete', result: {photoUrls: string, videoUrls: string}): void;
}
const props = withDefaults(defineProps<Props>(), {
photoTitle: '现场拍照',
photoUploadText: '点击拍照',
maxPhotoCount: 9,
videoTitle: '现场视频',
videoUploadText: '点击录制',
maxVideoCount: 3,
maxVideoDuration: 60
});
const emit = defineEmits<Emits>();
//
const photoList = ref<Array<{url: string, path: string}>>([]);
const videoList = ref<Array<{url: string, path: string}>>([]);
//
watch(() => props.modelValue, (newValue) => {
if (newValue) {
photoList.value = newValue.photoList || [];
videoList.value = newValue.videoList || [];
}
}, { immediate: true, deep: true });
//
watch([photoList, videoList], ([newPhotoList, newVideoList]) => {
const value = {
photoList: newPhotoList,
videoList: newVideoList
};
emit('update:modelValue', value);
emit('photoChange', newPhotoList);
emit('videoChange', newVideoList);
}, { deep: true });
//
const takePhoto = () => {
if (photoList.value.length >= props.maxPhotoCount) {
uni.showToast({
title: `最多只能拍摄${props.maxPhotoCount}张照片`,
icon: 'none'
});
return;
}
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
const photo = {
url: tempFilePath,
path: tempFilePath
};
photoList.value.push(photo);
emit('photoAdd', photo);
},
fail: (err) => {
console.error('拍照失败:', err);
uni.showToast({
title: '拍照失败',
icon: 'none'
});
}
});
};
//
const recordVideo = () => {
if (videoList.value.length >= props.maxVideoCount) {
uni.showToast({
title: `最多只能录制${props.maxVideoCount}个视频`,
icon: 'none'
});
return;
}
uni.chooseVideo({
sourceType: ['camera'],
maxDuration: props.maxVideoDuration,
camera: 'back',
success: (res) => {
const tempFilePath = res.tempFilePath;
const video = {
url: tempFilePath,
path: tempFilePath
};
videoList.value.push(video);
emit('videoAdd', video);
},
fail: (err) => {
console.error('录制视频失败:', err);
uni.showToast({
title: '录制视频失败',
icon: 'none'
});
}
});
};
//
const deletePhoto = (index: number) => {
const deletedPhoto = photoList.value[index];
photoList.value.splice(index, 1);
emit('photoDelete', index);
};
//
const deleteVideo = (index: number) => {
const deletedVideo = videoList.value[index];
videoList.value.splice(index, 1);
emit('videoDelete', index);
};
//
const previewPhoto = (url: string) => {
const urls = photoList.value.map(photo => photo.url);
uni.previewImage({
current: url,
urls: urls
});
};
//
const uploadMedia = async (): Promise<{photoUrls: string, videoUrls: string}> => {
const photoUrls: string[] = [];
const videoUrls: string[] = [];
try {
//
if (photoList.value.length > 0) {
showLoading({ title: '正在上传照片...' });
for (let i = 0; i < photoList.value.length; i++) {
const photo = photoList.value[i];
try {
const res: any = await attachmentUpload(photo.path);
if (res && res.result && res.result[0]) {
photoUrls.push(res.result[0].filePath);
}
} catch (error) {
console.error('照片上传失败:', error);
throw new Error(`${i + 1}张照片上传失败`);
}
}
hideLoading();
}
//
if (videoList.value.length > 0) {
showLoading({ title: '正在上传视频...' });
for (let i = 0; i < videoList.value.length; i++) {
const video = videoList.value[i];
try {
const res: any = await attachmentUpload(video.path);
if (res && res.result && res.result[0]) {
videoUrls.push(res.result[0].filePath);
}
} catch (error) {
console.error('视频上传失败:', error);
throw new Error(`${i + 1}个视频上传失败`);
}
}
hideLoading();
}
const result = {
photoUrls: photoUrls.join(','),
videoUrls: videoUrls.join(',')
};
//
emit('uploadComplete', result);
return result;
} catch (error) {
hideLoading();
showToast({
title: error instanceof Error ? error.message : '上传失败',
icon: 'none'
});
throw error;
}
};
//
defineExpose({
photoList,
videoList,
takePhoto,
recordVideo,
deletePhoto,
deleteVideo,
uploadMedia,
clearAll: () => {
photoList.value = [];
videoList.value = [];
}
});
</script>
<style lang="scss" scoped>
.dm-ps-component {
.section-card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
padding-left: 8px;
border-left: 3px solid #007aff;
}
.photo-section, .video-section {
.photo-preview, .video-preview {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.photo-item, .video-item {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.photo-image, .video-player {
width: 100%;
height: 100px;
border-radius: 8px;
}
.video-player {
background-color: #000;
}
.photo-actions, .video-actions {
position: absolute;
top: 5px;
right: 5px;
.action-btn {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.delete-btn {
background-color: rgba(255, 0, 0, 0.8);
}
}
}
}
.photo-upload, .video-upload {
width: 100px;
height: 100px;
border: 2px dashed #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #007aff;
background-color: rgba(0, 122, 255, 0.05);
}
.upload-text {
font-size: 12px;
color: #999;
margin-top: 5px;
}
}
}
}
//
.mb-15 { margin-bottom: 15px; }
</style>

View File

@ -156,7 +156,7 @@ const handleGetCode = async () => {
function toHome(data: any) { function toHome(data: any) {
if (data.type == 1) { if (data.type == 1) {
uni.reLaunch({ uni.reLaunch({
url: "/pages/base/groupTeaching/zhujiao", url: "/pages/view/routine/xk/zhujiao",
}); });
} else { } else {
uni.switchTab({ uni.switchTab({

View File

@ -193,6 +193,24 @@
</view> </view>
</view> </view>
<!-- 拍照视频组件 -->
<view class="section" v-if="curBj">
<DmPsComponent
v-model="mediaData"
:photo-title="'就餐现场拍照'"
:video-title="'就餐现场视频'"
:max-photo-count="9"
:max-video-count="3"
:max-video-duration="60"
@photo-change="handlePhotoChange"
@video-change="handleVideoChange"
@photo-add="handlePhotoAdd"
@video-add="handleVideoAdd"
@upload-complete="handleUploadComplete"
ref="dmPsRef"
/>
</view>
<!-- 状态选择弹窗 --> <!-- 状态选择弹窗 -->
<u-picker <u-picker
:defaultIndex="mqXz" :defaultIndex="mqXz"
@ -235,6 +253,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import NjBjPicker from '@/pages/components/NjBjPicker/index.vue' import NjBjPicker from '@/pages/components/NjBjPicker/index.vue'
import JsPicker from '@/pages/components/JsPicker/index.vue' import JsPicker from '@/pages/components/JsPicker/index.vue'
import DmPsComponent from '@/pages/components/dmPs/index.vue'
import { getClassStudentDmDataApi, submitJcDmDataApi } from '@/api/base/jcApi' import { getClassStudentDmDataApi, submitJcDmDataApi } from '@/api/base/jcApi'
import { imagUrl } from "@/utils"; import { imagUrl } from "@/utils";
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
@ -289,6 +308,13 @@ const dqJs = ref<any>(null) // 当前教师
const jsZtXzK = ref(false) // const jsZtXzK = ref(false) //
const jsMqXz = ref<any>([0]) // const jsMqXz = ref<any>([0]) //
//
const mediaData = ref({
photoList: [],
videoList: []
});
const dmPsRef = ref<any>(null);
// //
const kTj = computed(() => { const kTj = computed(() => {
return curBj.value && yjfXs.value.length > 0 // return curBj.value && yjfXs.value.length > 0 //
@ -312,6 +338,33 @@ const changeNjBj = async (nj: any, bj: any) => {
await jzXsLb() await jzXsLb()
}; };
//
const handlePhotoChange = (photoList: Array<{url: string, path: string}>) => {
console.log('照片列表变化:', photoList);
};
//
const handleVideoChange = (videoList: Array<{url: string, path: string}>) => {
console.log('视频列表变化:', videoList);
};
//
const handlePhotoAdd = (photo: {url: string, path: string}) => {
console.log('添加照片:', photo);
};
//
const handleVideoAdd = (video: {url: string, path: string}) => {
console.log('添加视频:', video);
};
//
const handleUploadComplete = (result: {photoUrls: string, videoUrls: string}) => {
console.log('媒体文件上传完成:', result);
console.log('照片地址:', result.photoUrls);
console.log('视频地址:', result.videoUrls);
};
const jsXz = (teachers: any[]) => { const jsXz = (teachers: any[]) => {
xzJs.value = teachers.map(teacher => ({ xzJs.value = teachers.map(teacher => ({
...teacher, ...teacher,
@ -553,6 +606,28 @@ const tjDm = async () => {
jzZt.value = true jzZt.value = true
try { try {
//
let photoUrls = '';
let videoUrls = '';
if (mediaData.value.photoList.length > 0 || mediaData.value.videoList.length > 0) {
try {
if (dmPsRef.value) {
const uploadResult = await dmPsRef.value.uploadMedia();
photoUrls = uploadResult.photoUrls;
videoUrls = uploadResult.videoUrls;
}
} catch (uploadError) {
console.error('媒体文件上传失败:', uploadError);
uni.showToast({
title: '媒体文件上传失败,请重试',
icon: 'none'
});
jzZt.value = false;
return;
}
}
// //
const dmData: any = { const dmData: any = {
bjId: curBj.value.key, bjId: curBj.value.key,
@ -561,6 +636,9 @@ const tjDm = async () => {
njmc: curNj.value.title, njmc: curNj.value.title,
dmJsId: getJs.id || '', // ID dmJsId: getJs.id || '', // ID
dmTime: new Date(), dmTime: new Date(),
//
zp: photoUrls, //
sp: videoUrls, //
xsList: yjfXs.value.map(student => ({ xsList: yjfXs.value.map(student => ({
xsId: student.id, xsId: student.id,
xsXm: student.xm, // xsXm: student.xm, //
@ -592,6 +670,10 @@ const tjDm = async () => {
curBj.value = null curBj.value = null
xsLb.value = [] xsLb.value = []
xzJs.value = [] xzJs.value = []
mediaData.value = {
photoList: [],
videoList: []
};
// //
uni.navigateBack() uni.navigateBack()

View File

@ -13,19 +13,19 @@
<!-- 考勤统计 --> <!-- 考勤统计 -->
<view class="attendance-stats flex-row"> <view class="attendance-stats flex-row">
<view class="stat-item flex-col items-center"> <view class="stat-item flex-col items-center">
<text class="font-18 font-bold">{{ numInfo.zg }}</text> <text class="font-18 font-bold">{{ dmInfo.zrs }}</text>
<text class="font-12 cor-666 mt-3">总人数</text> <text class="font-12 cor-666 mt-3">总人数</text>
</view> </view>
<view class="stat-item flex-col items-center"> <view class="stat-item flex-col items-center">
<text class="font-18 font-bold cor-primary">{{ numInfo.sd }}</text> <text class="font-18 font-bold cor-primary">{{ dmInfo.sdRs }}</text>
<text class="font-12 cor-666 mt-3">实到</text> <text class="font-12 cor-666 mt-3">实到</text>
</view> </view>
<view class="stat-item flex-col items-center"> <view class="stat-item flex-col items-center">
<text class="font-18 font-bold cor-warning">{{ numInfo.qj }}</text> <text class="font-18 font-bold cor-warning">{{ dmInfo.qjRs }}</text>
<text class="font-12 cor-666 mt-3">请假</text> <text class="font-12 cor-666 mt-3">请假</text>
</view> </view>
<view class="stat-item flex-col items-center"> <view class="stat-item flex-col items-center">
<text class="font-18 font-bold cor-danger">{{ numInfo.qq }}</text> <text class="font-18 font-bold cor-danger">{{ dmInfo.qqRs }}</text>
<text class="font-12 cor-666 mt-3">缺勤</text> <text class="font-12 cor-666 mt-3">缺勤</text>
</view> </view>
</view> </view>
@ -43,19 +43,19 @@
<view class="avatar-container mr-8"> <view class="avatar-container mr-8">
<image <image
class="student-avatar" class="student-avatar"
:src="getImageUrl(xs.xstx)" :src="getImageUrl(xs.tx || xs.xstx)"
mode="aspectFill" mode="aspectFill"
></image> ></image>
</view> </view>
<view class="flex-1 overflow-hidden"> <view class="flex-1 overflow-hidden">
<view class="flex-row items-center mb-3"> <view class="flex-row items-center mb-3">
<text class="font-14 font-bold mr-5 text-ellipsis">{{ xs.xsxm }}</text> <text class="font-14 font-bold mr-5 text-ellipsis">{{ xs.xsXm || xs.xsxm }}</text>
<view <view
class="status-tag" class="status-tag"
:class="getStatusClass(xs.xszt)" :class="getStatusClass(xs.xsZt || xs.xszt)"
@click="openStatusPicker(xs)" @click="openStatusPicker(xs)"
> >
{{ xs.xszt }} {{ getStatusText(xs.xsZt || xs.xszt) }}
<u-icon name="arrow-down" size="10"></u-icon> <u-icon name="arrow-down" size="10"></u-icon>
</view> </view>
</view> </view>
@ -75,6 +75,16 @@
</view> </view>
</view> </view>
<DmPsComponent
v-model="mediaData"
:photo-title="'现场拍照'"
:video-title="'现场视频'"
:max-photo-count="9"
:max-video-count="3"
:max-video-duration="60"
ref="dmPsRef"
/>
<!-- 状态选择弹窗 --> <!-- 状态选择弹窗 -->
<u-picker <u-picker
:defaultIndex="defSel" :defaultIndex="defSel"
@ -110,9 +120,10 @@
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, ref, computed} from "vue"; import {onMounted, ref, computed} from "vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue"; import BasicLayout from "@/components/BasicLayout/Layout.vue";
import DmPsComponent from "@/pages/components/dmPs/index.vue";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data"; import { useDataStore } from "@/store/modules/data";
import { jsdXkXsListApi, jsdXkdmListApi } from "@/api/base/server"; import { getWaitDmXsListApi, submitXkDmApi, findXkkcListApi } from "@/api/base/xkApi";
import { useDicStore } from "@/store/modules/dic"; import { useDicStore } from "@/store/modules/dic";
import { BASE_IMAGE_URL } from "@/config"; import { BASE_IMAGE_URL } from "@/config";
const { findByPid } = useDicStore(); const { findByPid } = useDicStore();
@ -125,6 +136,12 @@ const { getData, setData } = useDataStore();
const js = computed(() => getJs) const js = computed(() => getJs)
const xkkc = computed(() => getData) const xkkc = computed(() => getData)
const mediaData = ref({
photoList: [],
videoList: []
});
const dmPsRef = ref(null);
const now = dayjs(); const now = dayjs();
let wDay = now.day(); let wDay = now.day();
if (wDay === 0) { if (wDay === 0) {
@ -135,18 +152,15 @@ const todayInfo = ref({
date: now.format("YYYY-MM-DD"), date: now.format("YYYY-MM-DD"),
weekName: wdNameList[wDay - 1] weekName: wdNameList[wDay - 1]
}) })
//
const numInfo = ref({
yd: 18,
sd: 18,
qj: 0,
qq: 0,
zg: 18
});
// const dmInfo = ref<any>({
const xsList = ref<any>([ zrs: 0,
]); sdRs: 0,
qjRs: 0,
qqRs: 0
});
// - XkDmXs
const xsList = ref<any[]>([]);
// //
const statusPickerVisible = ref(false); const statusPickerVisible = ref(false);
@ -167,10 +181,13 @@ const getImageUrl = (imagePath: string) => {
// //
const getStatusClass = (status: string) => { const getStatusClass = (status: string) => {
switch (status) { switch (status) {
case "A":
case "正常": case "正常":
return "status-normal"; return "status-normal";
case "B":
case "请假": case "请假":
return "status-leave"; return "status-leave";
case "C":
case "缺勤": case "缺勤":
return "status-absent"; return "status-absent";
default: default:
@ -178,6 +195,20 @@ const getStatusClass = (status: string) => {
} }
}; };
//
const getStatusText = (status: string) => {
switch (status) {
case "A":
return "正常";
case "B":
return "请假";
case "C":
return "缺勤";
default:
return status || "正常";
}
};
// //
const loadStatusOptions = async () => { const loadStatusOptions = async () => {
try { try {
@ -193,60 +224,93 @@ const loadStatusOptions = async () => {
} }
} catch (error) { } catch (error) {
console.error("获取状态选项失败", error); console.error("获取状态选项失败", error);
// 使 // 使 -
statusOptions.value = [ statusOptions.value = [
{text: "正常", value: "正常"}, {text: "正常", value: "A"},
{text: "请假", value: "请假"}, {text: "请假", value: "B"},
{text: "缺勤", value: "缺勤"} {text: "缺勤", value: "C"}
]; ];
} }
}; };
// -
const loadXsList = async () => { const loadXsList = async () => {
const res = await jsdXkXsListApi({ // 使
xkkcId: xkkc.value.id try {
}); const res = await getWaitDmXsListApi({
if (res && res.resultCode === 1) { xkkcId: xkkc.value.id
xsList.value = res.result || []; });
rebuildNumInfo(); if (res && res.resultCode === 1) {
dmInfo.value = res.result || {};
xsList.value = (dmInfo.value.xsList || []).map((dmXs: any) => ({
id: dmXs.id,
xsId: dmXs.xsId || dmXs.id,
xsXm: dmXs.xsXm || dmXs.xsxm || dmXs.xm,
xsZt: dmXs.xsZt || dmXs.xszt || "A",
qdId: dmXs.qdId,
tx: dmXs.tx || dmXs.xstx || dmXs.avatar,
bjmc: dmXs.bjmc,
njmc: dmXs.njmc,
jzxm: dmXs.jzxm,
jzdh: dmXs.jzdh,
xsxm: dmXs.xsxm || dmXs.xm,
xstx: dmXs.xstx || dmXs.avatar,
xszt: dmXs.xszt || "正常"
}));
rebuildNumInfo();
}
} catch (fallbackError) {
console.error("备用接口也失败:", fallbackError);
uni.showToast({
title: "加载学生列表失败",
icon: "none"
});
} }
}; };
// -
const rebuildNumInfo = () => { const rebuildNumInfo = () => {
let sd = 0; let sd = 0;
let qj = 0; let qj = 0;
let qq = 0; let qq = 0;
//
for (let i = 0; i < xsList.value.length; i++) { //
const xs = xsList.value[i]; for (let i = 0; i < xsList.value.length; i++) {
switch (xs.xszt) { const xs = xsList.value[i];
case "正常": const status = xs.xsZt || xs.xszt;
sd++;
break; switch (status) {
case "请假": case "A":
qj++; case "正常":
break; sd++;
case "缺勤": break;
qq++; case "B":
break; case "请假":
default: qj++;
break; break;
} case "C":
} case "缺勤":
numInfo.value = { qq++;
zg: xsList.value.length, break;
yd: xsList.value.length - qj, default:
sd: sd, sd++; //
qj: qj, break;
qq: qq }
}; }
} dmInfo.value.sdRs = sd; //
dmInfo.value.qjRs = qj; //
dmInfo.value.qqRs = qq; //
};
// //
const openStatusPicker = (xs: any) => { const openStatusPicker = (xs: any) => {
curXs.value = xs; curXs.value = xs;
const currentStatus = xs.xsZt || xs.xszt;
//
for (let i = 0; i < statusOptions.value.length; i++) { for (let i = 0; i < statusOptions.value.length; i++) {
if (statusOptions.value[i].text === xs.xszt) { if (statusOptions.value[i].value === currentStatus ||
statusOptions.value[i].text === currentStatus) {
defSel.value = [i]; defSel.value = [i];
break; break;
} }
@ -262,42 +326,32 @@ const confirmStatus = (e: any) => {
); );
if (selectedStatus) { if (selectedStatus) {
// // - 使
curXs.value.xszt = selectedStatus.text; curXs.value.xsZt = selectedStatus.value; // 使
curXs.value.xszt = selectedStatus.text; //
} }
rebuildNumInfo(); rebuildNumInfo();
} }
statusPickerVisible.value = false; statusPickerVisible.value = false;
}; };
//
const navigateBack = () => {
uni.navigateBack();
};
const toRollCallRecord = () => {
uni.navigateTo({
url: "/pages/base/groupTeaching/rollCallRecord",
});
};
// //
const contactParent = (student: any) => { const contactParent = (dmXs: any) => {
// //
const completeStudent = { const completeStudent = {
...student, ...dmXs,
// //
id: student.xsId || student.id, id: dmXs.xsId || dmXs.id,
xsxm: student.xsxm || student.xm, xsxm: dmXs.xsXm || dmXs.xsxm || dmXs.xm,
xstx: student.xstx || student.avatar, xstx: dmXs.tx || dmXs.xstx || dmXs.avatar,
xb: student.xb || student.gender, xb: dmXs.xb || dmXs.gender,
sfzh: student.sfzh, sfzh: dmXs.sfzh,
cstime: student.cstime, cstime: dmXs.cstime,
njmc: student.njmcName || student.njmc, njmc: dmXs.njmcName || dmXs.njmc,
bjmc: student.bjmc, bjmc: dmXs.bjmc,
// njIdbjId // njIdbjId
njId: student.njId, njId: dmXs.njId,
bjId: student.bjId bjId: dmXs.bjId
}; };
// store使 // store使
@ -309,19 +363,97 @@ const contactParent = (student: any) => {
}); });
}; };
// //
const validateData = () => {
if (!xkkc.value || !xkkc.value.id) {
uni.showToast({
title: "课程信息不完整",
icon: "none"
});
return false;
}
if (!js.value || !js.value.id) {
uni.showToast({
title: "教师信息不完整",
icon: "none"
});
return false;
}
if (xsList.value.length === 0) {
uni.showToast({
title: "没有学生数据",
icon: "none"
});
return false;
}
return true;
};
// - XkDmXkDmXs
const submit = async () => { const submit = async () => {
if (isSubmitting.value) { if (isSubmitting.value) {
return; return;
} }
//
if (!validateData()) {
return;
}
isSubmitting.value = true; isSubmitting.value = true;
try { try {
const res = await jsdXkdmListApi({ //
jsId: js.value.id, let photoUrls = '';
xkkcId: xkkc.value.id, let videoUrls = '';
dmtime: now,
xkdmList: xsList.value if (mediaData.value.photoList.length > 0 || mediaData.value.videoList.length > 0) {
}); try {
if (dmPsRef.value) {
const uploadResult = await dmPsRef.value.uploadMedia();
photoUrls = uploadResult.photoUrls;
videoUrls = uploadResult.videoUrls;
}
} catch (uploadError) {
console.error('媒体文件上传失败:', uploadError);
uni.showToast({
title: '媒体文件上传失败,请重试',
icon: 'none'
});
isSubmitting.value = false;
return;
}
}
// - XkDm
const dmData = {
...dmInfo.value,
//
zp: photoUrls, //
sp: videoUrls, //
// - XkDmXs
xkDmXsList: xsList.value.map((xs: any) => ({
xsId: xs.xsId || xs.id, // ID
xsZt: xs.xsZt || xs.xszt, //
qdId: xs.qdId, // ID
xsXm: xs.xsXm || xs.xsxm, //
tx: xs.tx || xs.xstx, //
status: "A" //
}))
};
dmData.jsId = js.value.id; // ID
dmData.xkId = xkkc.value.xkId; // ID
dmData.xkkcId = xkkc.value.id; // ID
dmData.xkMc = xkkc.value.xkMc || xkkc.value.xkmc; //
dmData.xkkcMc = xkkc.value.kcmc; //
dmData.status = "A"; //
console.log("提交的点名数据:", dmData);
const res:any = await submitXkDmApi(dmData);
if (res && res.resultCode === 1) { if (res && res.resultCode === 1) {
uni.showToast({ uni.showToast({
@ -338,7 +470,7 @@ const submit = async () => {
}, 1500); }, 1500);
} else { } else {
uni.showToast({ uni.showToast({
title: res?.resultMessage || "提交失败", title: res.resultMessage || "提交失败",
icon: "none", icon: "none",
}); });
} }
@ -355,8 +487,10 @@ const submit = async () => {
// //
onMounted(async () => { onMounted(async () => {
console.log("页面加载,课程信息:", xkkc.value);
await loadXsList(); await loadXsList();
await loadStatusOptions(); await loadStatusOptions();
console.log("学生列表加载完成,共", xsList.value.length, "人");
}); });
</script> </script>

View File

@ -40,7 +40,6 @@
<view class="course-btn-group"> <view class="course-btn-group">
<view class="dm-btn" @click.stop="goDm(xkkc)">点名</view> <view class="dm-btn" @click.stop="goDm(xkkc)">点名</view>
<view class="record-btn" @click.stop="goRecord(xkkc)">点名记录</view> <view class="record-btn" @click.stop="goRecord(xkkc)">点名记录</view>
<!-- <view class="photo-btn" @click.stop="goPhoto(xkkc)">课堂随拍</view>-->
</view> </view>
</view> </view>
</view> </view>
@ -65,7 +64,7 @@ import {
} from "vue"; } from "vue";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data"; import { useDataStore } from "@/store/modules/data";
import { getCurrentSemesterTeacherCoursesApi } from "@/api/base/server"; import { xkkcListByJsIdApi } from "@/api/base/xkApi";
import { dmBeforeMinuteApi } from "@/api/system/config/index"; import { dmBeforeMinuteApi } from "@/api/system/config/index";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -100,15 +99,15 @@ onMounted(async () => {
// //
const loadCourseList = async () => { const loadCourseList = async () => {
try { try {
const res = await getCurrentSemesterTeacherCoursesApi(getJs.id); const res:any = await xkkcListByJsIdApi({ jsId: getJs.id });
if (res.resultCode == 1) { if (res.resultCode == 1) {
if (res.result && res.result.length) { if (res.result && res.result.length) {
xkkcList.value = res.result; xkkcList.value = res.result;
// //
processCoursePeriods(); processCoursePeriods();
} else { } else {
xkkcList.value = []; xkkcList.value = [];
} }
} else { } else {
xkkcList.value = []; xkkcList.value = [];
uni.showToast({ uni.showToast({
@ -244,7 +243,7 @@ const goDm = (xkkc: any) => {
if (dmFlag) { if (dmFlag) {
setData(xkkc); setData(xkkc);
uni.navigateTo({ uni.navigateTo({
url: `/pages/base/groupTeaching/dmXkkcDetail`, url: `/pages/view/routine/xk/dm`,
}); });
} else { } else {
if (msg === "") { if (msg === "") {
@ -299,7 +298,7 @@ const goRecord = (xkkc: any) => {
if (recordFlag) { if (recordFlag) {
setData(xkkc); setData(xkkc);
uni.navigateTo({ uni.navigateTo({
url: `/pages/base/groupTeaching/dmXkkcRecord`, url: `/pages/view/routine/xk/dmXkkcRecord`,
}); });
} else { } else {
if (msg === "") { if (msg === "") {
@ -313,61 +312,6 @@ const goRecord = (xkkc: any) => {
} }
}; };
//
const goPhoto = (xkkc: any) => {
const now = dayjs();
let wDay = now.day();
if (wDay === 0) {
wDay = 7;
}
let mDay = now.date();
const strDate = now.format('YYYY-MM-DD') + ' ';
let photoFlag = false;
let msg = "";
//
switch (xkkc.skzqlx) {
case '每天':
photoFlag = true;
break;
case '每周':
const daysOfWeek = xkkc.skzq.split(',').map(Number);
photoFlag = daysOfWeek.includes(wDay);
// wdNameListdaysOfWeek
xkkc.skzqmc = daysOfWeek.map((day: number) => wdNameList[day - 1]).join(',');
break;
case '每月':
const daysOfMonth = xkkc.skzq.split(',').map(Number);
photoFlag = daysOfMonth.includes(mDay);
//
xkkc.skzqmc = daysOfMonth.map((day: number) => day + "号").join(',');
break;
}
//
if (photoFlag) {
// xkkc.skkstimedmBeforeMinute
const startTime = dayjs(strDate + xkkc.skkstime).subtract(dmBeforeMinute.value, 'minute').format('YYYY-MM-DD HH:mm:ss');
const endTime = dayjs(strDate + xkkc.skjstime, 'YYYY-MM-DD HH:mm:ss');
photoFlag = now.isBefore(endTime) && now.isAfter(startTime)
} else {
msg = "上课时间未到,无法随拍";
}
if (photoFlag) {
setData(xkkc);
uni.navigateTo({
url: `/pages/base/groupTeaching/photoXkkcDetail`,
});
} else {
if (msg === "") {
msg = "上课时间未到,无法随拍";
}
uni.showToast({
title: msg,
icon: 'none',
duration: 2000
});
}
};
// //
onBeforeUnmount(() => { onBeforeUnmount(() => {
}); });

View File

@ -0,0 +1,384 @@
<template>
<BasicLayout>
<!-- 课程信息卡片 -->
<view class="course-card mx-15 my-15 bg-white white-bg-color r-md p-15">
<view class="flex-row items-center mb-15">
<view class="course-icon flex-center mr-10">
<u-icon name="calendar" color="#4080ff" size="20"></u-icon>
</view>
<text class="font-16 font-bold">{{ xkkc.xkkcMc || xkkc.kcmc }}</text>
<text class="font-14 cor-999 ml-10">{{ todayInfo.date }} ({{ todayInfo.weekName }})</text>
</view>
</view>
<!-- 点名记录统计 -->
<view class="record-stats mx-15 mb-15">
<view class="section-title">点名记录</view>
<view class="records-grid">
<view
v-for="(record, index) in dmRecords"
:key="record.id"
class="record-card"
@click="viewRecordDetail(record)"
>
<view class="record-header">
<view class="record-time">
<u-icon name="clock" color="#666" size="14"></u-icon>
<text class="time-text">{{ formatDateTime(record.dmTime) }}</text>
</view>
<view class="record-status" :class="getRecordStatusClass(record.status)">
{{ getRecordStatusText(record.status) }}
</view>
</view>
<view class="record-stats">
<view class="stat-item">
<text class="stat-number">{{ record.zrs || 0 }}</text>
<text class="stat-label">总人数</text>
</view>
<view class="stat-item present">
<text class="stat-number">{{ record.sdRs || 0 }}</text>
<text class="stat-label">实到</text>
</view>
<view class="stat-item leave">
<text class="stat-number">{{ record.qjRs || 0 }}</text>
<text class="stat-label">请假</text>
</view>
<view class="stat-item absent">
<text class="stat-number">{{ record.qqRs || 0 }}</text>
<text class="stat-label">缺勤</text>
</view>
</view>
<view class="record-footer">
<text class="teacher-name">教师{{ record.jsMc || '未知' }}</text>
<view class="view-detail">
<text class="detail-text">查看详情</text>
<u-icon name="arrow-right" color="#4080ff" size="12"></u-icon>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="dmRecords.length === 0" class="empty-state">
<u-icon name="info-circle" color="#ccc" size="48"></u-icon>
<text class="empty-text">暂无点名记录</text>
</view>
</view>
<!-- 返回按钮 -->
<view class="bottom-actions mx-15 mb-30">
<button class="back-btn" @click="goBack">返回</button>
</view>
</BasicLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { getXkDmPageApi } from "@/api/base/xkApi";
import dayjs from "dayjs";
const { getJs } = useUserStore();
const { getData } = useDataStore();
const js = computed(() => getJs);
const xkkc = computed(() => getData);
//
const now = dayjs();
let wDay = now.day();
if (wDay === 0) {
wDay = 7;
}
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const todayInfo = ref({
date: now.format("YYYY-MM-DD"),
weekName: wdNameList[wDay - 1],
});
//
const dmRecords = ref<any[]>([]);
//
const formatDateTime = (dateTime: string | Date) => {
if (!dateTime) return '';
return dayjs(dateTime).format('MM-DD HH:mm');
};
//
const getRecordStatusClass = (status: string) => {
switch (status) {
case 'A': return 'status-active';
case 'B': return 'status-pending';
case 'C': return 'status-cancelled';
default: return 'status-active';
}
};
//
const getRecordStatusText = (status: string) => {
switch (status) {
case 'A': return '正常';
case 'B': return '待处理';
case 'C': return '已取消';
default: return '正常';
}
};
//
const viewRecordDetail = (record: any) => {
// store
const { setData } = useDataStore();
setData({
...xkkc.value,
dmRecord: record
});
//
uni.navigateTo({
url: '/pages/view/routine/xk/dmXsList'
});
};
//
const goBack = () => {
uni.navigateBack();
};
//
const loadStudentList = async () => {
try {
uni.showLoading({ title: '加载中...' });
const res = await getXkDmPageApi({
xkkcId: xkkc.value.id,
jsId: js.value.id,
pageNum: 1,
pageSize: 50,
sidx: 'dmTime',
sord: 'desc'
});
if (res && res.resultCode === 1) {
dmRecords.value = res.result?.rows || [];
} else {
dmRecords.value = [];
uni.showToast({
title: (res as any)?.resultMessage || '获取点名记录失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载点名记录失败:', error);
dmRecords.value = [];
uni.showToast({
title: '加载点名记录失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
onMounted(() => {
loadStudentList();
});
</script>
<style scoped lang="scss">
.course-card {
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.course-icon {
width: 30px;
height: 30px;
border-radius: 4px;
background-color: rgba(64, 128, 255, 0.1);
}
.record-stats {
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-left: 10px;
border-left: 4px solid #4080ff;
}
.records-grid {
display: flex;
flex-direction: column;
gap: 15px;
}
.record-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.record-time {
display: flex;
align-items: center;
gap: 5px;
.time-text {
font-size: 14px;
color: #666;
}
}
.record-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.status-active {
background: rgba(40, 121, 255, 0.1);
color: #2879ff;
}
&.status-pending {
background: rgba(255, 153, 0, 0.1);
color: #ff9900;
}
&.status-cancelled {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
}
}
.record-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 15px;
.stat-item {
text-align: center;
padding: 10px 5px;
border-radius: 8px;
background: #f8f9fa;
&.present {
background: rgba(40, 121, 255, 0.1);
.stat-number { color: #2879ff; }
}
&.leave {
background: rgba(255, 153, 0, 0.1);
.stat-number { color: #ff9900; }
}
&.absent {
background: rgba(255, 77, 79, 0.1);
.stat-number { color: #ff4d4f; }
}
.stat-number {
font-size: 18px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
}
}
.record-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
.teacher-name {
font-size: 14px;
color: #666;
}
.view-detail {
display: flex;
align-items: center;
gap: 5px;
.detail-text {
font-size: 14px;
color: #4080ff;
}
}
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #ccc;
.empty-text {
display: block;
margin-top: 15px;
font-size: 16px;
}
}
}
.bottom-actions {
.back-btn {
width: 100%;
height: 44px;
background: #4080ff;
color: white;
border: none;
border-radius: 22px;
font-size: 16px;
font-weight: 500;
}
}
//
.mx-15 { margin-left: 15px; margin-right: 15px; }
.my-15 { margin-top: 15px; margin-bottom: 15px; }
.mb-15 { margin-bottom: 15px; }
.mb-30 { margin-bottom: 30px; }
.mr-10 { margin-right: 10px; }
.ml-10 { margin-left: 10px; }
.bg-white { background-color: white; }
.white-bg-color { background-color: white; }
.r-md { border-radius: 8px; }
.p-15 { padding: 15px; }
.flex-row { display: flex; flex-direction: row; }
.items-center { align-items: center; }
.font-16 { font-size: 16px; }
.font-14 { font-size: 14px; }
.font-bold { font-weight: bold; }
.cor-999 { color: #999; }
</style>

View File

@ -1,73 +1,93 @@
<template> <template>
<BasicLayout> <BasicLayout>
<!-- 课程信息卡片 --> <!-- 点名详情卡片 -->
<view class="course-card mx-15 my-15 bg-white white-bg-color r-md p-15"> <view class="dm-detail-card mx-15 my-15 bg-white white-bg-color r-md p-15">
<view class="flex-row items-center mb-15"> <view class="detail-header">
<view class="course-icon flex-center mr-10"> <view class="flex-row items-center mb-15">
<u-icon name="calendar" color="#4080ff" size="20"></u-icon> <view class="course-icon flex-center mr-10">
<u-icon name="calendar" color="#4080ff" size="20"></u-icon>
</view>
<text class="font-16 font-bold">{{ xkkc.xkkcMc || xkkc.kcmc }}</text>
</view> </view>
<text class="font-16 font-bold">{{ xkkc.kcmc }}</text> <view class="dm-time">
<text class="font-14 cor-999 ml-10">{{ todayInfo.date }} ({{ todayInfo.weekName }})</text> <u-icon name="clock" color="#666" size="14"></u-icon>
</view> <text class="time-text">{{ formatDateTime(dmRecord.dmTime) }}</text>
</view>
<!-- 点名记录统计 -->
<view class="record-stats mx-15 mb-15">
<view class="stats-grid">
<view class="stat-item total" @click="showStudentList('total')">
<view class="stat-number">{{ totalStudents }}</view>
<view class="stat-label">总人数</view>
</view>
<view class="stat-item present" @click="showStudentList('present')">
<view class="stat-number">{{ presentStudents }}</view>
<view class="stat-label">实到</view>
</view>
<view class="stat-item leave" @click="showStudentList('leave')">
<view class="stat-number">{{ leaveStudents }}</view>
<view class="stat-label">请假</view>
</view>
<view class="stat-item absent" @click="showStudentList('absent')">
<view class="stat-number">{{ absentStudents }}</view>
<view class="stat-label">缺勤</view>
</view> </view>
</view> </view>
</view>
<!-- 学生列表弹窗 --> <!-- 考勤统计 -->
<uni-popup ref="studentPopup" type="bottom" :mask-click="false"> <view class="attendance-stats">
<view class="student-popup"> <view class="stats-grid">
<view class="popup-header"> <view class="stat-item total">
<text class="popup-title">{{ currentStatTitle }}</text> <view class="stat-number">{{ dmRecord.zrs || 0 }}</view>
<view class="close-btn" @click="closeStudentList"> <view class="stat-label">总人数</view>
<u-icon name="close" color="#666" size="20"></u-icon> </view>
<view class="stat-item present">
<view class="stat-number">{{ dmRecord.sdRs || 0 }}</view>
<view class="stat-label">实到</view>
</view>
<view class="stat-item leave">
<view class="stat-number">{{ dmRecord.qjRs || 0 }}</view>
<view class="stat-label">请假</view>
</view>
<view class="stat-item absent">
<view class="stat-number">{{ dmRecord.qqRs || 0 }}</view>
<view class="stat-label">缺勤</view>
</view> </view>
</view> </view>
<view class="student-list"> </view>
</view>
<!-- 学生列表 -->
<view class="student-section mx-15 mb-15">
<view class="section-header">
<text class="section-title">学生列表</text>
<view class="filter-tabs">
<view <view
v-for="(student, index) in currentStudentList" v-for="tab in filterTabs"
:key="student.xsId" :key="tab.value"
class="student-item" class="filter-tab"
:class="{ active: currentFilter === tab.value }"
@click="setFilter(tab.value)"
> >
<view class="student-avatar"> {{ tab.label }}
<image
v-if="student.xstx"
:src="getImageUrl(student.xstx)"
mode="aspectFill"
class="avatar-img"
/>
<view v-else class="avatar-text">{{ student.xsxm?.charAt(0) || '学' }}</view>
</view>
<view class="student-info">
<view class="student-name">{{ student.xsxm }}</view>
<view class="student-class">{{ student.njmcName }} {{ student.bjmc }}</view>
</view>
<view class="student-status">
<text :class="getStatusClass(student.xszt)">{{ getStatusText(student.xszt) }}</text>
</view>
</view> </view>
</view> </view>
</view> </view>
</uni-popup>
<view class="student-list">
<view
v-for="(student, index) in filteredStudents"
:key="student.id"
class="student-item"
>
<view class="student-avatar">
<image
v-if="student.tx || student.xstx"
:src="getImageUrl(student.tx || student.xstx)"
mode="aspectFill"
class="avatar-img"
/>
<view v-else class="avatar-text">{{ (student.xsXm || student.xsxm)?.charAt(0) || '学' }}</view>
</view>
<view class="student-info">
<view class="student-name">{{ student.xsXm || student.xsxm }}</view>
<view class="student-class">{{ student.njmc }} {{ student.bjmc }}</view>
</view>
<view class="student-status">
<text :class="getStatusClass(student.xsZt || student.xszt)">
{{ getStatusText(student.xsZt || student.xszt) }}
</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filteredStudents.length === 0" class="empty-state">
<u-icon name="info-circle" color="#ccc" size="48"></u-icon>
<text class="empty-text">暂无学生数据</text>
</view>
</view>
<!-- 返回按钮 --> <!-- 返回按钮 -->
<view class="bottom-actions mx-15 mb-30"> <view class="bottom-actions mx-15 mb-30">
@ -81,7 +101,7 @@ import { ref, computed, onMounted } from "vue";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data"; import { useDataStore } from "@/store/modules/data";
import BasicLayout from "@/components/BasicLayout/Layout.vue"; import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { jsdXkXsListApi } from "@/api/base/server"; import { getXkDmXsPageApi } from "@/api/base/xkApi";
import { BASE_IMAGE_URL } from "@/config"; import { BASE_IMAGE_URL } from "@/config";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -90,33 +110,35 @@ const { getData } = useDataStore();
const js = computed(() => getJs); const js = computed(() => getJs);
const xkkc = computed(() => getData); const xkkc = computed(() => getData);
const dmRecord = computed(() => getData?.dmRecord || {});
//
const now = dayjs();
let wDay = now.day();
if (wDay === 0) {
wDay = 7;
}
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const todayInfo = ref({
date: now.format("YYYY-MM-DD"),
weekName: wdNameList[wDay - 1],
});
// //
const studentList = ref<any[]>([]); const studentList = ref<any[]>([]);
const currentStatType = ref<string>(''); const currentFilter = ref<string>('all');
const currentStatTitle = ref<string>('');
const currentStudentList = ref<any[]>([]);
// //
const totalStudents = computed(() => studentList.value.length); const filterTabs = ref([
const presentStudents = computed(() => studentList.value.filter(s => s.xszt === '正常').length); { label: '全部', value: 'all' },
const leaveStudents = computed(() => studentList.value.filter(s => s.xszt === '请假').length); { label: '正常', value: 'A' },
const absentStudents = computed(() => studentList.value.filter(s => s.xszt === '缺勤').length); { label: '请假', value: 'B' },
{ label: '缺勤', value: 'C' }
]);
// //
const studentPopup = ref<any>(null); const filteredStudents = computed(() => {
if (currentFilter.value === 'all') {
return studentList.value;
}
return studentList.value.filter(student =>
(student.xsZt || student.xszt) === currentFilter.value
);
});
//
const formatDateTime = (dateTime: string | Date) => {
if (!dateTime) return '';
return dayjs(dateTime).format('MM-DD HH:mm');
};
// URL // URL
const getImageUrl = (path: string) => { const getImageUrl = (path: string) => {
@ -128,8 +150,11 @@ const getImageUrl = (path: string) => {
// //
const getStatusClass = (status: string) => { const getStatusClass = (status: string) => {
switch (status) { switch (status) {
case 'A':
case '正常': return 'status-normal'; case '正常': return 'status-normal';
case 'B':
case '请假': return 'status-leave'; case '请假': return 'status-leave';
case 'C':
case '缺勤': return 'status-absent'; case '缺勤': return 'status-absent';
default: return 'status-normal'; default: return 'status-normal';
} }
@ -138,42 +163,16 @@ const getStatusClass = (status: string) => {
// //
const getStatusText = (status: string) => { const getStatusText = (status: string) => {
switch (status) { switch (status) {
case '正常': return '正常'; case 'A': return '正常';
case '请假': return '请假'; case 'B': return '请假';
case '缺勤': return '缺勤'; case 'C': return '缺勤';
default: return '正常'; default: return status || '正常';
} }
}; };
// //
const showStudentList = (type: string) => { const setFilter = (filter: string) => {
currentStatType.value = type; currentFilter.value = filter;
switch (type) {
case 'total':
currentStatTitle.value = '总人数学生列表';
currentStudentList.value = studentList.value;
break;
case 'present':
currentStatTitle.value = '实到学生列表';
currentStudentList.value = studentList.value.filter(s => s.xszt === '正常');
break;
case 'leave':
currentStatTitle.value = '请假学生列表';
currentStudentList.value = studentList.value.filter(s => s.xszt === '请假');
break;
case 'absent':
currentStatTitle.value = '缺勤学生列表';
currentStudentList.value = studentList.value.filter(s => s.xszt === '缺勤');
break;
}
studentPopup.value.open();
};
//
const closeStudentList = () => {
studentPopup.value.close();
}; };
// //
@ -186,17 +185,20 @@ const loadStudentList = async () => {
try { try {
uni.showLoading({ title: '加载中...' }); uni.showLoading({ title: '加载中...' });
const res = await jsdXkXsListApi({ const res = await getXkDmXsPageApi({
xkkcId: xkkc.value.id, dmId: dmRecord.value.id,
date: todayInfo.value.date pageNum: 1,
pageSize: 100,
sidx: 'xsXm',
sord: 'asc'
}); });
if (res && res.resultCode === 1) { if (res && res.resultCode === 1) {
studentList.value = res.result || []; studentList.value = res.result?.rows || [];
} else { } else {
studentList.value = []; studentList.value = [];
uni.showToast({ uni.showToast({
title: res?.message || '获取学生列表失败', title: (res as any)?.resultMessage || '获取学生列表失败',
icon: 'none' icon: 'none'
}); });
} }
@ -218,7 +220,7 @@ onMounted(() => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.course-card { .dm-detail-card {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
} }
@ -230,16 +232,21 @@ onMounted(() => {
background-color: rgba(64, 128, 255, 0.1); background-color: rgba(64, 128, 255, 0.1);
} }
.record-stats { .detail-header {
.stats-title { .dm-time {
font-size: 16px; display: flex;
font-weight: bold; align-items: center;
color: #333; gap: 5px;
margin-bottom: 15px; margin-bottom: 15px;
padding-left: 10px;
border-left: 4px solid #4080ff;
}
.time-text {
font-size: 14px;
color: #666;
}
}
}
.attendance-stats {
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
@ -252,13 +259,6 @@ onMounted(() => {
padding: 20px 15px; padding: 20px 15px;
text-align: center; text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
&.total { &.total {
border: 2px solid #666; border: 2px solid #666;
@ -293,51 +293,54 @@ onMounted(() => {
} }
} }
.student-popup { .student-section {
background: white; .section-header {
border-radius: 20px 20px 0 0;
max-height: 70vh;
overflow: hidden;
.popup-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px; margin-bottom: 15px;
border-bottom: 1px solid #eee;
.popup-title { .section-title {
font-size: 18px; font-size: 16px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
padding-left: 10px;
border-left: 4px solid #4080ff;
} }
.close-btn { .filter-tabs {
width: 32px;
height: 32px;
display: flex; display: flex;
align-items: center; gap: 10px;
justify-content: center;
border-radius: 50%; .filter-tab {
background: #f5f5f5; padding: 6px 12px;
cursor: pointer; border-radius: 16px;
font-size: 12px;
background: #f5f5f5;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
&.active {
background: #4080ff;
color: white;
}
}
} }
} }
.student-list { .student-list {
padding: 0 20px 20px; display: flex;
max-height: 60vh; flex-direction: column;
overflow-y: auto; gap: 10px;
.student-item { .student-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 15px 0; padding: 15px;
border-bottom: 1px solid #f0f0f0; background: white;
border-radius: 12px;
&:last-child { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-bottom: none;
}
.student-avatar { .student-avatar {
width: 40px; width: 40px;
@ -408,6 +411,18 @@ onMounted(() => {
} }
} }
} }
.empty-state {
text-align: center;
padding: 60px 20px;
color: #ccc;
.empty-text {
display: block;
margin-top: 15px;
font-size: 16px;
}
}
} }
.bottom-actions { .bottom-actions {

View File

@ -66,7 +66,7 @@ import {
} from "vue"; } from "vue";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data"; import { useDataStore } from "@/store/modules/data";
import { getCurrentSemesterTeacherCoursesApi } from "@/api/base/server"; import { xkkcListByJsIdApi } from "@/api/base/xkApi";
import dayjs from "dayjs"; import dayjs from "dayjs";
const { getJs } = useUserStore(); const { getJs } = useUserStore();
@ -87,7 +87,7 @@ const loadCourseList = async () => {
title: "加载中...", title: "加载中...",
}); });
try { try {
const res = await getCurrentSemesterTeacherCoursesApi(getJs.id); const res:any = await xkkcListByJsIdApi({ jsId: getJs.id });
if (res.resultCode == 1) { if (res.resultCode == 1) {
if (res.result && res.result.length) { if (res.result && res.result.length) {
xkkcList.value = res.result; xkkcList.value = res.result;
@ -203,7 +203,7 @@ const getStatusClass = (xkkc: any) => {
const goDetail = (xkkc: any) => { const goDetail = (xkkc: any) => {
setData(xkkc); setData(xkkc);
uni.navigateTo({ uni.navigateTo({
url: `/pages/base/groupTeaching/xkkcDetail`, url: `/pages/view/routine/xk/xkkcDetail`,
}); });
}; };