2025-09-14 17:28:02 +08:00

850 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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"
>
<button
class="submit-btn"
:class="{ 'submit-btn-disabled': isSubmitting }"
:disabled="isSubmitting"
@click="submit"
>
{{ isSubmitting ? '提交中...' : '提交巡查' }}
</button>
</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); // 是否可以巡查
// 提交状态控制
const isSubmitting = ref(false);
// 加载巡查项目
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 () => {
// 防抖动:如果正在提交,直接返回
if (isSubmitting.value) {
uni.showToast({
title: "正在提交中,请勿重复点击",
icon: "none",
duration: 1500,
});
return;
}
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;
}
// 设置提交状态
isSubmitting.value = true;
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不为空
pbJsId: xkkc.value.pbJsId || xkkc.value.id, // 使用传递的pbJsId或课程记录ID
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",
});
} finally {
// 重置提交状态
isSubmitting.value = false;
}
};
// 获取图片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;
transition: all 0.3s ease;
}
.submit-btn-disabled {
background-color: #d9d9d9 !important;
color: #999 !important;
cursor: not-allowed;
}
.cor-primary {
color: #4080ff;
}
.cor-warning {
color: #ff9900;
}
.cor-danger {
color: #ff4d4f;
}
.cor-666 {
color: #666;
}
.cor-999 {
color: #999;
}
</style>