调整选课相关
This commit is contained in:
parent
1d1d2d6458
commit
44e789f595
@ -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
48
src/api/base/xkApi.ts
Normal 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)
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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 {
|
|
||||||
// 假设字典表中出勤状态的pid为810984651,根据实际情况修改
|
|
||||||
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>
|
|
||||||
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -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",
|
||||||
|
|||||||
225
src/pages/components/dmPs/INTEGRATION.md
Normal file
225
src/pages/components/dmPs/INTEGRATION.md
Normal 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. 数据提交测试
|
||||||
264
src/pages/components/dmPs/README.md
Normal file
264
src/pages/components/dmPs/README.md
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
395
src/pages/components/dmPs/index.vue
Normal file
395
src/pages/components/dmPs/index.vue
Normal 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>
|
||||||
@ -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({
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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,
|
||||||
// 如果后端返回的是njId和bjId,也保留
|
// 如果后端返回的是njId和bjId,也保留
|
||||||
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交数据 - 匹配后端XkDm和XkDmXs实体
|
||||||
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>
|
||||||
|
|
||||||
@ -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);
|
|
||||||
// 从wdNameList读取daysOfWeek对应的周几
|
|
||||||
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.skkstime开始时间向前dmBeforeMinute分钟
|
|
||||||
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(() => {
|
||||||
});
|
});
|
||||||
384
src/pages/view/routine/xk/dmList.vue
Normal file
384
src/pages/view/routine/xk/dmList.vue
Normal 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>
|
||||||
@ -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 {
|
||||||
@ -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`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user