850 lines
23 KiB
Vue
Raw Normal View History

2025-09-12 11:20:21 +08:00
<template>
<BasicLayout>
<!-- 待巡查内容 -->
<view class="pending-inspection">
<!-- 课程信息卡片 -->
<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.kcmc }}</text>
<text class="font-14 cor-999 ml-10"
>{{ todayInfo.date }} ({{ todayInfo.weekName }})</text
>
</view>
<!-- 课业辅导信息 -->
<view class="course-time-info">
<view class="time-item">
<view class="time-label">年级</view>
<view class="time-value">{{ xkkc.gradeName || '暂无' }}</view>
</view>
<view class="time-item" v-if="xkkc.bjmc">
<view class="time-label">班级</view>
<view class="time-value">{{ xkkc.bjmc || '暂无' }}</view>
</view>
<view class="time-item">
<view class="time-label">巡查类型</view>
<view class="time-value">{{ xkkc.xclx === 'B' ? '课业辅导巡查' : '课程巡查' }}</view>
</view>
<view class="time-item">
<view class="time-label">排班标题</view>
<view class="time-value">{{ xkkc.xcbt || '暂无' }}</view>
</view>
<view class="time-item">
<view class="time-label">学期</view>
<view class="time-value">{{ xkkc.xqmc || '暂无' }}</view>
</view>
</view>
<!-- 巡查时间状态 -->
<view class="inspection-status" v-if="!canInspect">
<u-icon name="clock" color="#ff9900" size="16"></u-icon>
<text class="status-text">{{ inspectionStatusText }}</text>
</view>
</view>
<view v-if="canInspect">
<!-- 巡查项目 -->
<view class="section mx-15 mb-15">
<view class="section-title-bar">
<view class="decorator"></view>
<text class="title-text">巡查项目</text>
</view>
<view class="check-card bg-white r-md p-15">
<template v-if="checkItems && checkItems.length > 0">
<view class="check-list">
<view
v-for="(item, index) in checkItems"
:key="item.id"
class="check-item"
>
<view class="item-info flex-1">
<!-- 项目名称单独一行 -->
<text class="item-text"
>{{ index + 1 }}{{ item.xcMc }}</text
>
<!-- 分值和结果同一行 -->
<view class="item-score-result">
<text class="item-deduction mr-20">
分值{{ item.xmFz }}
</text>
<view class="item-result">
<radio-group
:name="'result_' + item.id"
class="item-radio-group"
@change="onCheckItemChange($event, item)"
>
<label class="item-radio-label mr-10">
<radio
:value="'A'"
:checked="item.checked === true"
color="#4080ff"
class="item-radio"
/>
<text class="ml-2"></text>
</label>
<label class="item-radio-label">
<radio
:value="'B'"
:checked="item.checked === false"
color="#4080ff"
class="item-radio"
/>
<text class="ml-2"></text>
</label>
</radio-group>
</view>
</view>
</view>
</view>
</view>
</template>
<template v-else>
<view
class="no-check-items"
style="text-align: center; color: #999; padding: 20px 0"
>
暂无巡查项目
</view>
</template>
</view>
</view>
<!-- 拍照上传 -->
<view class="section mx-15 mb-15">
<view class="section-title-bar">
<view class="decorator"></view>
<text class="title-text">拍照上传</text>
</view>
<view class="upload-card bg-white r-md p-15">
<view class="upload-section">
<view class="upload-list">
<view
v-for="(image, index) in imageList"
:key="index"
class="upload-item"
>
<image
:src="image.url ? imagUrl(image.url) : image.tempPath"
mode="aspectFill"
class="upload-image"
@click="previewImage(index)"
/>
<view class="upload-delete" @click="deleteImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view
v-if="imageList.length < 5"
class="upload-add"
@click="chooseImage"
>
<text class="add-icon">+</text>
<text class="add-text">添加图片</text>
</view>
</view>
</view>
</view>
</view>
<!-- 拍视频上传 -->
<view class="section mx-15 mb-30">
<view class="section-title-bar">
<view class="decorator"></view>
<text class="title-text">拍视频上传</text>
</view>
<view class="upload-card bg-white r-md p-15">
<view class="upload-section">
<view class="upload-list">
<view
v-for="(video, index) in videoList"
:key="index"
class="upload-item"
>
<video
:src="video.url ? video.url : video.tempPath"
class="upload-video"
:controls="false"
:show-center-play-btn="false"
:show-play-btn="false"
:show-fullscreen-btn="false"
:show-progress="false"
:show-mute-btn="false"
:enable-progress-gesture="false"
:enable-play-gesture="false"
:loop="false"
:muted="true"
:poster="''"
></video>
<view class="video-play-icon">
<u-icon name="play-right-fill" color="#fff" size="20"></u-icon>
</view>
<view class="upload-delete" @click="deleteVideo(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view
v-if="videoList.length < 3"
class="upload-add"
@click="chooseVideo"
>
<text class="add-icon">+</text>
<text class="add-text">添加视频</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<template #bottom>
<view
v-if="canInspect"
class="submit-btn-wrap py-10 px-20 bg-white"
>
2025-09-13 18:25:33 +08:00
<button
class="submit-btn"
:class="{ 'submit-btn-disabled': isSubmitting }"
:disabled="isSubmitting"
@click="submit"
>
{{ isSubmitting ? '提交中...' : '提交巡查' }}
</button>
2025-09-12 11:20:21 +08:00
</view>
</template>
</BasicLayout>
</template>
<script setup lang="ts">
import { xcXmFindByXcLxApi } from "@/api/base/xcXmApi";
import { kyXcSaveApi } from "@/api/base/kyXcApi";
import { attachmentUpload } from "@/api/system/upload";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { useDataStore } from "@/store/modules/data";
import { useUserStore } from "@/store/modules/user";
import { computed, onMounted, ref } from "vue";
import dayjs from "dayjs";
import { imagUrl } from "@/utils";
import { showLoading, hideLoading, showToast } from "@/utils/uniapp";
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 checkItems = ref<any[]>([]);
// 图片列表 - 修改为包含临时路径和服务器路径的对象
interface ImageItem {
tempPath?: string; // 临时路径(用于预览)
url?: string; // 服务器路径(上传成功后)
name?: string; // 文件名
}
// 视频列表 - 修改为包含临时路径和服务器路径的对象
interface VideoItem {
tempPath?: string; // 临时路径(用于预览)
url?: string; // 服务器路径(上传成功后)
name?: string; // 文件名
}
const imageList = ref<ImageItem[]>([]);
const videoList = ref<VideoItem[]>([]);
// 巡查状态相关
const inspectionStatusText = ref("");
const canInspect = ref(true); // 是否可以巡查
2025-09-13 18:25:33 +08:00
// 提交状态控制
const isSubmitting = ref(false);
2025-09-12 11:20:21 +08:00
// 加载巡查项目
const loadCheckItems = async () => {
try {
const res = await xcXmFindByXcLxApi("课业辅导巡查");
if (res && res.resultCode === 1 && res.result && res.result.length > 0) {
checkItems.value = res.result.map((item: any) => {
return {
id: item.id,
xcMc: item.xcMc,
xmFz: item.xmFz,
xcLx: item.xcLx,
checked: false,
};
});
} else {
checkItems.value = [];
}
} catch (error) {
console.error("加载巡查项目失败:", error);
checkItems.value = [];
}
};
const onCheckItemChange = (e: any, item: any) => {
item.checked = e.detail.value === "A";
};
// 选择图片
const chooseImage = () => {
uni.chooseImage({
count: 5 - imageList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
// 添加临时图片到列表
const tempFilePaths = res.tempFilePaths as string[];
const newImages = tempFilePaths.map((path: string) => ({
tempPath: path,
name: path.split('/').pop() || 'image.jpg'
}));
imageList.value = [...imageList.value, ...newImages];
// 自动上传图片
await uploadImages(newImages);
}
});
};
// 选择视频
const chooseVideo = () => {
uni.chooseVideo({
sourceType: ['album', 'camera'],
maxDuration: 60,
camera: 'back',
success: async (res) => {
// 添加临时视频到列表
const tempFilePath = res.tempFilePath;
const newVideo = {
tempPath: tempFilePath,
name: tempFilePath.split('/').pop() || 'video.mp4'
};
videoList.value = [...videoList.value, newVideo];
// 自动上传视频
await uploadVideos([newVideo]);
}
});
};
// 上传图片到服务器
const uploadImages = async (images: ImageItem[]) => {
try {
showLoading('上传图片中...');
for (let i = 0; i < images.length; i++) {
const image = images[i];
if (image.tempPath) {
try {
// 调用上传接口
const uploadResult: any = await attachmentUpload(image.tempPath as any);
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
// 保存服务器返回的文件路径
const serverPath = uploadResult.result[0].filePath;
// 更新图片对象,保存服务器路径
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
if (index !== -1) {
imageList.value[index].url = serverPath;
// 可以删除临时路径以节省内存
delete imageList.value[index].tempPath;
}
} else {
throw new Error('上传响应格式异常');
}
} catch (error) {
console.error('图片上传失败:', error);
showToast({ title: `${image.name || '图片'}上传失败`, icon: 'none' });
// 从列表中移除上传失败的图片
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
if (index !== -1) {
imageList.value.splice(index, 1);
}
}
}
}
hideLoading();
showToast({ title: '图片上传完成', icon: 'success' });
} catch (error) {
hideLoading();
console.error('批量上传图片失败:', error);
showToast({ title: '图片上传失败,请重试', icon: 'none' });
}
};
// 上传视频到服务器
const uploadVideos = async (videos: VideoItem[]) => {
try {
showLoading('上传视频中...');
for (let i = 0; i < videos.length; i++) {
const video = videos[i];
if (video.tempPath) {
try {
// 调用上传接口
const uploadResult: any = await attachmentUpload(video.tempPath as any);
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
// 保存服务器返回的文件路径
const serverPath = uploadResult.result[0].filePath;
// 更新视频对象,保存服务器路径
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath);
if (index !== -1) {
videoList.value[index].url = serverPath;
// 可以删除临时路径以节省内存
delete videoList.value[index].tempPath;
}
} else {
throw new Error('上传响应格式异常');
}
} catch (error) {
console.error('视频上传失败:', error);
showToast({ title: `${video.name || '视频'}上传失败`, icon: 'none' });
// 从列表中移除上传失败的视频
const index = videoList.value.findIndex(v => v.tempPath === video.tempPath);
if (index !== -1) {
videoList.value.splice(index, 1);
}
}
}
}
hideLoading();
showToast({ title: '视频上传完成', icon: 'success' });
} catch (error) {
hideLoading();
console.error('批量上传视频失败:', error);
showToast({ title: '视频上传失败,请重试', icon: 'none' });
}
};
// 预览图片
const previewImage = (index: number) => {
const urls = imageList.value.map(img =>
img.url ? imagUrl(img.url) : img.tempPath
).filter((url): url is string => !!url);
uni.previewImage({
urls: urls,
current: index
});
};
// 删除图片
const deleteImage = (index: number) => {
imageList.value.splice(index, 1);
};
// 删除视频
const deleteVideo = (index: number) => {
videoList.value.splice(index, 1);
};
// 检查巡查时间 - 课业辅导巡查不需要时间验证
const checkInspectionTime = () => {
// 课业辅导巡查直接允许巡查,不需要时间验证
canInspect.value = true;
inspectionStatusText.value = "可以巡查";
};
// 提交数据
const submit = async () => {
2025-09-13 18:25:33 +08:00
// 防抖动:如果正在提交,直接返回
if (isSubmitting.value) {
uni.showToast({
title: "正在提交中,请勿重复点击",
icon: "none",
duration: 1500,
});
return;
}
2025-09-12 11:20:21 +08:00
if (!canInspect.value) {
uni.showToast({
title: inspectionStatusText.value,
icon: "none",
duration: 2000,
});
return;
}
// 验证巡查项目是否已选择
const hasCheckedItems = checkItems.value.some(item => item.checked !== undefined);
if (!hasCheckedItems) {
uni.showToast({
title: "请至少选择一个巡查项目",
icon: "none",
duration: 2000,
});
return;
}
2025-09-13 18:25:33 +08:00
// 设置提交状态
isSubmitting.value = true;
2025-09-12 11:20:21 +08:00
try {
// 构建巡查项目列表
const xkXcXmList = checkItems.value.map((item: any) => {
const newItem = {
...item,
xcXmId: item.id,
xcJg: item.checked ? "A" : "B",
xkXcId: "", // 这个会在后端设置
};
newItem.id = "";
return newItem;
});
const submitData = {
jsId: js.value.id,
jsxm: js.value.xm || js.value.jsxm, // 添加教师姓名
njId: xkkc.value.njId || '', // 确保年级ID不为空
njmcId: xkkc.value.njmcId || '', // 确保年级名称ID不为空
bjId: xkkc.value.bjId || '', // 确保班级ID不为空
2025-09-14 17:28:02 +08:00
pbJsId: xkkc.value.pbJsId || xkkc.value.id, // 使用传递的pbJsId或课程记录ID
2025-09-12 11:20:21 +08:00
xctime: now.format("YYYY-MM-DD HH:mm:ss"),
zp: getImageUrls(),
sp: getVideoUrls(),
kyXcXmList: xkXcXmList, // 添加巡查项目列表
};
const res = await kyXcSaveApi(submitData);
if (res && res.resultCode === 1) {
uni.showToast({
title: "提交成功",
icon: "success",
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
uni.showToast({
title: "提交失败",
icon: "none",
});
}
} catch (error) {
console.error('提交巡查失败:', error);
uni.showToast({
title: "提交失败",
icon: "none",
});
2025-09-13 18:25:33 +08:00
} finally {
// 重置提交状态
isSubmitting.value = false;
2025-09-12 11:20:21 +08:00
}
};
// 获取图片URL列表 - 转换为逗号分隔的字符串
const getImageUrls = () => {
const urls = imageList.value
.filter(img => img.url) // 只返回有url的图片
.map(img => img.url)
.filter((url): url is string => !!url); // 过滤掉undefined和null
// 返回逗号分隔的字符串,如果没有图片则返回空字符串
return urls.length > 0 ? urls.join(',') : '';
};
// 获取视频URL列表 - 转换为逗号分隔的字符串
const getVideoUrls = () => {
const urls = videoList.value
.filter(video => video.url) // 只返回有url的视频
.map(video => video.url)
.filter((url): url is string => !!url); // 过滤掉undefined和null
// 返回逗号分隔的字符串,如果没有视频则返回空字符串
return urls.length > 0 ? urls.join(',') : '';
};
// 页面加载时获取状态选项
onMounted(async () => {
await loadCheckItems(); // 加载巡查项目
checkInspectionTime();
});
</script>
<style scoped lang="scss">
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
.bg-white {
background-color: white;
}
.pending-inspection {
padding-top: 0;
}
.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);
}
.course-time-info {
margin-top: 15px;
padding: 10px 15px;
background-color: #f9f9f9;
border-radius: 4px;
border: 1px solid #eee;
.time-item {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
.time-label {
font-size: 14px;
color: #666;
}
.time-value {
font-size: 14px;
font-weight: bold;
color: #333;
}
}
}
.inspection-status {
display: flex;
align-items: center;
margin-top: 15px;
padding: 10px 15px;
background-color: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 4px;
color: #faad14;
font-size: 14px;
.status-text {
margin-left: 5px;
}
}
.section {
.section-title-bar {
display: flex;
align-items: center;
margin-bottom: 10px;
.decorator {
width: 4px;
height: 16px;
background-color: #4080ff;
margin-right: 8px;
border-radius: 2px;
}
.title-text {
font-size: 16px;
font-weight: bold;
color: #333;
}
}
}
.check-card {
background-color: white;
.check-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
.item-score-result {
display: flex;
align-items: center;
justify-content: space-between;
}
&:first-child {
padding-top: 0;
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
.item-info {
display: flex;
flex-direction: column;
}
.item-text {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.item-deduction {
font-size: 12px;
color: #999;
}
}
.upload-card {
background-color: white;
}
.upload-section {
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.upload-item {
position: relative;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e9ecef;
.upload-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 32px;
height: 32px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.upload-delete {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.delete-icon {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
}
}
.upload-add {
width: 80px;
height: 80px;
border: 2px dashed #e9ecef;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
cursor: pointer;
.add-icon {
font-size: 24px;
color: #6c757d;
margin-bottom: 4px;
}
.add-text {
font-size: 12px;
color: #6c757d;
}
}
}
.submit-btn {
background-color: #4080ff;
color: #fff;
height: 44px;
border-radius: 22px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
2025-09-13 18:25:33 +08:00
transition: all 0.3s ease;
}
.submit-btn-disabled {
background-color: #d9d9d9 !important;
color: #999 !important;
cursor: not-allowed;
2025-09-12 11:20:21 +08:00
}
.cor-primary {
color: #4080ff;
}
.cor-warning {
color: #ff9900;
}
.cor-danger {
color: #ff4d4f;
}
.cor-666 {
color: #666;
}
.cor-999 {
color: #999;
}
</style>