2025-11-23 19:34:40 +08:00

814 lines
24 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>
<view class="rw-detail-page" :class="{ 'readonly-mode': isReadOnly }">
<!-- 提交遮罩层 -->
<view v-if="isSubmitting" class="submit-overlay">
<view class="submit-loading">
<view class="loading-spinner"></view>
<text class="loading-text">提交中...</text>
</view>
</view>
<view v-if="isLoading" class="loading-indicator">加载中...</view>
<view v-else class="content-wrapper">
<view class="p-15">
<!-- 第一部分:任务要求 -->
<view class="rw-info-section">
<view class="section-title">任务要求</view>
<!-- 只读模式提示 -->
<view v-if="isReadOnly" class="readonly-tip">
<text class="tip-text">📖 只读模式 - 查看已提交的内容</text>
</view>
<!-- 任务名称 -->
<view class="info-item">
<text class="label">任务名称:</text>
<text class="value title-bold">{{ rw.rwmc }}</text>
</view>
<!-- 任务描述 -->
<view v-if="rw.rwms" class="info-item">
<text class="label">任务描述:</text>
<text class="value">{{ rw.rwms }}</text>
</view>
<!-- 任务时间 -->
<view v-if="rw.rwkstime" class="info-item">
<text class="label">任务时间:</text>
<text class="value">{{ rw.rwkstime }}</text>
</view>
<!-- 附件预览 -->
<BasicFilePreview
v-if="rw.rwfj && rw.fjmx"
:file-url="rw.rwfj"
:file-name="rw.fjmx"
:file-format="getFileFormat(rw.fjmx)"
class="mt-15"
/>
</view>
<!-- 第二部分:任务执行 -->
<view class="rw-execute-section">
<view class="section-title">任务执行</view>
<view class="execute-form">
<BasicForm :schema="schema" v-model="formData" :disabled="isReadOnly">
</BasicForm>
</view>
</view>
</view>
</view>
<!-- 提交按钮 - 固定在底部 -->
<view v-if="!isReadOnly" class="submit-button-fixed">
<button class="action-button" @click="saveRwZx" :disabled="isSubmitting">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</view>
</view>
</template>
<script lang="ts" setup>
import {ref, reactive} from 'vue';
import {onLoad} from '@dcloudio/uni-app';
import {rwFindInfoByRwId, rwflFindRwlxsByRwId, rwzxExecutedInfoByRwIdAndJsApi, rwzxSaveApi} from "@/api/base/server";
import {useForm} from "@/components/BasicForm/hooks/useForm";
import {navigateBack, showToast} from "@/utils/uniapp";
import {useUserStore} from "@/store/modules/user";
import { ImageVideoUpload, type FileItem, COMPRESS_PRESETS } from "@/components/ImageVideoUpload";
import { attachmentUpload } from "@/api/system/upload";
import BasicFilePreview from "@/components/BasicFile/preview.vue";
interface MessageDetail {
id: string; // Assuming an ID is passed or can be derived
title: string;
desc: string;
date: string;
timeAgo: string;
tagText: string;
tagType: string;
likes?: number;
comments?: number;
// Add other fields as necessary
}
interface RwInfo {
id?: string;
rwmc?: string; // 任务名称
rwms?: string; // 任务描述
rwkstime?: string; // 任务开始时间
rwfj?: string; // 任务附件URL
fjmx?: string; // 附件名称
rwlxes?: any[]; // 任务类型列表
rwzxqds?: any[]; // 任务执行清单
[key: string]: any; // 允许其他属性
}
const formData: any = ref({})
const messageId = ref<string>('');
const jsId = ref<string>(''); // 教师ID
const executionId = ref<string>(''); // 执行ID
const isReadOnly = ref<boolean>(false); // 只读模式
const isSubmitting = ref<boolean>(false); // 提交状态
const submitTimer = ref<any>(null); // 防抖定时器
const messageDetail = ref<MessageDetail | null>({
id: 'todo1',
title: '教务通知 (待办)',
desc: '学校召开期初教学准备会议暨首次教学工作例会. 会议强调了新学期的教学重点和要求,请各位老师认真准备。',
date: '2025-02-17',
timeAgo: '8 mins 前',
tagText: '通知',
tagType: 'notice',
likes: 6,
comments: 12
});
const isLoading = ref(false);
const rwflx: any = ref([])
const rw = ref<RwInfo>({})
const schema = ref<FormsSchema[]>([])
const {getUser} = useUserStore()
async function saveRwZx() {
// 防抖处理
if (isSubmitting.value) {
console.log('正在提交中,请勿重复点击');
return;
}
// 清除之前的定时器
if (submitTimer.value) {
clearTimeout(submitTimer.value);
}
// 设置防抖延迟
submitTimer.value = setTimeout(async () => {
await performSubmit();
}, 300);
}
async function performSubmit() {
if (isSubmitting.value) {
return;
}
isSubmitting.value = true;
try {
const result = [];
for (let i = 0; i < rwflx.value.length; i++) {
const fieldId = rwflx.value[i].id;
let fieldValue = formData.value[fieldId];
// 处理上传类型的任务
let fileNames = '';
if (rwflx.value[i].rwfl == "sctp" || rwflx.value[i].rwfl == "scsp" || rwflx.value[i].rwfl == "scwd") {
// 收集上传文件的URL和文件名
const fileUrls = [];
const names = [];
if (rwflx.value[i].rwfl == "sctp") {
// 图片上传
const images = formData.value[`${fieldId}_imageList`] || [];
images.forEach((img: any) => {
if (img.url) {
fileUrls.push(img.url);
names.push(img.name || img.url.split('/').pop() || 'image.jpg');
}
});
} else if (rwflx.value[i].rwfl == "scsp") {
// 视频上传
const videos = formData.value[`${fieldId}_videoList`] || [];
videos.forEach((video: any) => {
if (video.url) {
fileUrls.push(video.url);
names.push(video.name || video.url.split('/').pop() || 'video.mp4');
}
});
} else if (rwflx.value[i].rwfl == "scwd") {
// 文档上传
const files = formData.value[`${fieldId}_fileList`] || [];
files.forEach((file: any) => {
if (file.url) {
fileUrls.push(file.url);
names.push(file.name || file.url.split('/').pop() || 'document');
}
});
}
fieldValue = fileUrls.join(',');
fileNames = names.join(',');
}
// 验证必填项 - 针对上传类型使用不同的判断方式
if (rwflx.value[i].rwbs) {
let isEmpty = false;
if (rwflx.value[i].rwfl == "sctp" || rwflx.value[i].rwfl == "scsp" || rwflx.value[i].rwfl == "scwd") {
// 上传类型的验证:检查是否有上传的文件
if (rwflx.value[i].rwfl == "sctp") {
const images = formData.value[`${fieldId}_imageList`] || [];
isEmpty = images.length === 0;
} else if (rwflx.value[i].rwfl == "scsp") {
const videos = formData.value[`${fieldId}_videoList`] || [];
isEmpty = videos.length === 0;
} else if (rwflx.value[i].rwfl == "scwd") {
const files = formData.value[`${fieldId}_fileList`] || [];
isEmpty = files.length === 0;
}
} else {
// 非上传类型的验证:检查字段值是否为空
isEmpty = !fieldValue || fieldValue == "";
}
if (isEmpty) {
showToast("请填写必填项!")
isSubmitting.value = false;
return;
}
}
// 查找对应的执行记录ID
const existingRecord = rwzxqds.value.find(item => item.rwlxId === fieldId);
const recordId = existingRecord ? existingRecord.id : undefined;
result.push({
id: recordId, // 如果有现有记录则保留ID否则为undefined新增
rwlxId: fieldId,
rwzxqdtx: fieldValue,
wjmc: fileNames || undefined, // 文件名,只有上传类型才有
})
}
await rwzxSaveApi({
id: executionId.value, // 执行记录ID如果有的话就是更新没有就是新增
rwId: rw.value.id, // 任务ID
rwzxfzr: jsId.value || getUser.id, // 执行人教师ID
mobile: getUser.mobile, // 手机号
rwzxqdDtos: result // 执行清单
})
showToast("操作成功!");
// 发送刷新事件
uni.$emit('refreshTaskExecution');
// 延迟一下再跳转,让用户看到成功提示
setTimeout(() => {
// 返回到任务执行页面
uni.navigateBack();
}, 1500);
} catch (error) {
console.error('提交失败:', error);
showToast("提交失败,请重试");
} finally {
isSubmitting.value = false;
}
}
// 获取文件格式
const getFileFormat = (fileName: string) => {
if (!fileName) return '';
const parts = fileName.split('.');
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
};
const rwzxqds = ref<any[]>([])
onLoad(async (options: any) => {
console.log('页面加载参数:', options);
let taskId = '';
// 从全局存储中获取参数数据
const storedParams = uni.getStorageSync('taskSubmitData');
if (storedParams) {
try {
taskId = storedParams.id;
jsId.value = storedParams.jsId || '';
executionId.value = storedParams.executionId || '';
isReadOnly.value = storedParams.isReadOnly || false;
console.log('从全局存储解析参数成功:', { taskId, jsId: jsId.value, executionId: executionId.value, isReadOnly: isReadOnly.value });
// 清除存储的参数数据
uni.removeStorageSync('taskSubmitData');
} catch (error) {
console.error('解析全局存储参数失败:', error);
taskId = options.id || '';
}
} else {
// 兼容原有的URL参数方式向后兼容
if (options && options.params) {
try {
const params = JSON.parse(decodeURIComponent(options.params));
taskId = params.id;
jsId.value = params.jsId || '';
executionId.value = params.executionId || '';
isReadOnly.value = params.isReadOnly || false;
console.log('解析URL参数成功:', { taskId, jsId: jsId.value, executionId: executionId.value, isReadOnly: isReadOnly.value });
} catch (error) {
console.error('解析URL参数失败:', error);
taskId = options.id || '';
}
} else if (options && options.id) {
taskId = options.id;
}
}
if (taskId) {
const {result} = await rwFindInfoByRwId({
rwId: taskId
});
rwflx.value = result.rwlxes;
rw.value = result;
for (let i = 0; i < rwflx.value.length; i++) {
// 处理上传类型的任务
if (rwflx.value[i].rwfl == "sctp" || rwflx.value[i].rwfl == "scsp" || rwflx.value[i].rwfl == "scwd") {
// 初始化表单数据
const fieldId = rwflx.value[i].id;
if (rwflx.value[i].rwfl == "sctp") {
formData.value[`${fieldId}_imageList`] = [];
} else if (rwflx.value[i].rwfl == "scsp") {
formData.value[`${fieldId}_videoList`] = [];
} else if (rwflx.value[i].rwfl == "scwd") {
formData.value[`${fieldId}_fileList`] = [];
}
// 根据任务类型配置上传组件
let componentConfig = {};
if (rwflx.value[i].rwfl == "sctp") {
// 上传图片
componentConfig = {
component: "ImageVideoUpload",
componentProps: {
enableImage: true,
enableVideo: false,
enableFile: false,
maxImageCount: 30,
uploadApi: attachmentUpload,
compressConfig: COMPRESS_PRESETS.medium,
imageList: formData.value[`${fieldId}_imageList`] || []
}
};
} else if (rwflx.value[i].rwfl == "scsp") {
// 上传视频
componentConfig = {
component: "ImageVideoUpload",
componentProps: {
enableImage: false,
enableVideo: true,
enableFile: false,
maxVideoCount: 30,
uploadApi: attachmentUpload,
compressConfig: COMPRESS_PRESETS.medium,
videoList: formData.value[`${fieldId}_videoList`] || []
}
};
} else if (rwflx.value[i].rwfl == "scwd") {
// 上传文档
componentConfig = {
component: "ImageVideoUpload",
componentProps: {
enableImage: false,
enableVideo: false,
enableFile: true,
maxFileCount: 30,
allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'mp3', 'wav', 'zip', 'rar'],
uploadApi: attachmentUpload,
compressConfig: COMPRESS_PRESETS.medium,
fileList: formData.value[`${fieldId}_fileList`] || []
}
};
}
// 对于上传类型,使用特殊的字段名
const schemaField = rwflx.value[i].rwfl === "sctp" ? `${rwflx.value[i].id}_imageList` :
rwflx.value[i].rwfl === "scsp" ? `${rwflx.value[i].id}_videoList` :
rwflx.value[i].rwfl === "scwd" ? `${rwflx.value[i].id}_fileList` :
`${rwflx.value[i].id}`;
schema.value.push({
field: schemaField,
label: `${rwflx.value[i].rwbt}`,
required: rwflx.value[i].rwbs && !isReadOnly.value, // 只读模式下不要求必填
itemProps: {
labelPosition: "top",
},
...componentConfig,
componentProps: {
...componentConfig.componentProps,
disabled: isReadOnly.value // 禁用上传组件
}
})
} else if (rwflx.value[i].rwfl == "fwb") {
// 富文本类型 - 使用富文本编辑器
schema.value.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicEditor", // 使用富文本编辑器组件
required: rwflx.value[i].rwbs && !isReadOnly.value, // 只读模式下不要求必填
itemProps: {
labelPosition: "top",
},
componentProps: {
placeholder: isReadOnly.value ? "" : "请输入富文本内容,支持插入图片",
disabled: isReadOnly.value // 禁用编辑器
},
})
} else if (rwflx.value[i].rwfl == "text") {
// 普通文本类型 - 使用 BasicInput
schema.value.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicInput",
required: rwflx.value[i].rwbs && !isReadOnly.value, // 只读模式下不要求必填
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
placeholder: isReadOnly.value ? "" : "请输入内容",
disabled: isReadOnly.value // 禁用文本输入
},
})
} else if (rwflx.value[i].rwfl == "dxsx" || rwflx.value[i].rwfl == "dxxz") {
// 同时支持中文分号(;)和英文分号(;)分割选项
let options = rwflx.value[i].remark.split(/[;]/);
let range = [];
for (let j = 0; j < options.length; j++) {
range.push({
name: options[j]
});
}
schema.value.push({
field: `${rwflx.value[i].id}`,
label: `${rwflx.value[i].rwbt}`,
component: "BasicPicker",
required: rwflx.value[i].rwbs && !isReadOnly.value, // 只读模式下不要求必填
itemProps: {
labelPosition: "top",
},
componentProps: {
range: range,
rangeKey: "name",
savaKey: "name",
disabled: isReadOnly.value // 禁用选择器
},
})
}
}
const res = await rwzxExecutedInfoByRwIdAndJsApi({
rwId: taskId,
jsId: jsId.value || getUser.id // 使用传入的jsId如果没有则使用当前用户ID
});
if (res && res.result && res.result.length) {
rwzxqds.value = res.result;
const showData = {};
for (let i = 0; i < rwzxqds.value.length; i++) {
const record = rwzxqds.value[i];
const rwlxId = record.rwlxId;
const rwzxqdtx = record.rwzxqdtx;
// 查找对应的任务类型
const taskType = rwflx.value.find(item => item.id === rwlxId);
if (taskType) {
if (taskType.rwfl === "sctp") {
// 图片上传类型 - 解析URL和文件名并创建图片对象
if (rwzxqdtx) {
const urls = rwzxqdtx.split(',').filter(Boolean);
const names = record.wjmc ? record.wjmc.split(',').filter(Boolean) : [];
const images = urls.map((url, index) => ({
url: url.trim(),
name: names[index] || url.split('/').pop() || 'image.jpg',
tempPath: undefined
}));
showData[`${rwlxId}_imageList`] = images;
}
} else if (taskType.rwfl === "scsp") {
// 视频上传类型
if (rwzxqdtx) {
const urls = rwzxqdtx.split(',').filter(Boolean);
const names = record.wjmc ? record.wjmc.split(',').filter(Boolean) : [];
const videos = urls.map((url, index) => ({
url: url.trim(),
name: names[index] || url.split('/').pop() || 'video.mp4',
tempPath: undefined
}));
showData[`${rwlxId}_videoList`] = videos;
}
} else if (taskType.rwfl === "scwd") {
// 文档上传类型
if (rwzxqdtx) {
const urls = rwzxqdtx.split(',').filter(Boolean);
const names = record.wjmc ? record.wjmc.split(',').filter(Boolean) : [];
const files = urls.map((url, index) => ({
url: url.trim(),
name: names[index] || url.split('/').pop() || 'document',
tempPath: undefined,
type: 'document',
extension: url.split('.').pop() || ''
}));
showData[`${rwlxId}_fileList`] = files;
}
} else {
// 文本等其他类型
showData[rwlxId] = rwzxqdtx;
}
}
}
// 合并已保存的数据,而不是完全覆盖
formData.value = { ...formData.value, ...showData };
// 更新 schema 中上传组件的初始值
for (let i = 0; i < schema.value.length; i++) {
const schemaItem = schema.value[i];
if (schemaItem.component === "ImageVideoUpload" && schemaItem.field) {
const fieldName = schemaItem.field;
if (fieldName.includes('_imageList') && formData.value[fieldName]) {
schemaItem.componentProps.imageList = formData.value[fieldName];
} else if (fieldName.includes('_videoList') && formData.value[fieldName]) {
schemaItem.componentProps.videoList = formData.value[fieldName];
} else if (fieldName.includes('_fileList') && formData.value[fieldName]) {
schemaItem.componentProps.fileList = formData.value[fieldName];
}
}
}
// 调试:输出回显数据
console.log('数据回显 - showData:', showData);
console.log('数据回显 - formData.value:', formData.value);
console.log('更新后的 schema:', schema.value);
// 调试:输出文件名处理情况
console.log('文件名处理 - rwzxqds.value:', rwzxqds.value);
}
} else {
console.error('Message ID/Data is missing!');
uni.showToast({title: '加载失败,缺少信息', icon: 'none'});
}
});
</script>
<style scoped lang="scss">
.rw-detail-page {
background-color: #f4f5f7;
min-height: 100vh;
padding-bottom: 80px; // 为底部按钮留出空间
box-sizing: border-box;
}
.loading-indicator {
text-align: center;
color: #999;
padding: 40px 15px;
font-size: 14px;
}
.content-wrapper {
.p-15 {
padding: 15px;
}
.mt-15 {
margin-top: 15px;
}
}
// 第一部分:任务要求
.rw-info-section {
margin-bottom: 20px;
padding: 15px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
border-bottom: 2px solid #007aff;
padding-bottom: 5px;
}
.readonly-tip {
margin-bottom: 15px;
padding: 8px 12px;
background-color: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 4px;
.tip-text {
font-size: 12px;
color: #1890ff;
}
}
.info-item {
display: flex;
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
.label {
width: 80px;
color: #666;
font-size: 14px;
flex-shrink: 0;
}
.value {
flex: 1;
color: #333;
font-size: 14px;
word-break: break-word;
&.title-bold {
font-weight: bold;
font-size: 16px;
}
}
}
}
// 第二部分:任务执行
.rw-execute-section {
margin-bottom: 20px;
padding: 15px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
border-bottom: 2px solid #007aff;
padding-bottom: 5px;
}
.execute-form {
padding-top: 5px;
}
}
// 提交按钮 - 固定在底部
.submit-button-fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 15px;
background-color: #fff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 10;
border-top: 1px solid #eee;
.action-button {
width: 100%;
height: 44px;
line-height: 44px;
font-size: 16px;
font-weight: 500;
border-radius: 8px;
background-color: #409eff;
color: #ffffff;
border: none;
&:active:not(:disabled) {
background-color: #3a8ee6;
transform: translateY(1px);
}
&:disabled {
background-color: #d9d9d9 !important;
color: #999 !important;
cursor: not-allowed;
opacity: 0.6;
}
}
}
// 为输入框添加基本边框
:deep(.uni-input),
:deep(.uni-textarea) {
width: 100% !important;
min-height: 35px !important;
font-size: 13px !important;
border: 1px #CCCCCC solid !important;
border-radius: 3px !important;
padding: 8px 12px !important;
box-sizing: border-box !important;
}
:deep(input),
:deep(textarea) {
width: 100% !important;
min-height: 35px !important;
font-size: 13px !important;
border: 1px #CCCCCC solid !important;
border-radius: 3px !important;
padding: 8px 12px !important;
box-sizing: border-box !important;
}
// 为 BasicForm 中的 label 添加样式
:deep(.basic-form) {
.form-label,
.label,
.uni-form-item__label {
width: 100% !important;
white-space: normal !important;
word-break: break-word !important;
font-size: 15px !important;
font-weight: bold !important;
color: #333 !important;
text-align: left !important;
display: block !important;
line-height: 1.4 !important;
max-width: 100% !important;
overflow-wrap: break-word !important;
}
}
// 直接针对包含标签文本的 view 元素
:deep(view) {
white-space: normal !important;
word-break: normal !important;
overflow-wrap: break-word !important;
}
// 只读模式样式
.readonly-mode {
:deep(input),
:deep(textarea),
:deep(.uni-input),
:deep(.uni-textarea) {
background-color: #f5f5f5 !important;
color: #666 !important;
cursor: not-allowed !important;
}
:deep(.upload-container),
:deep(.picker-container) {
opacity: 0.6;
pointer-events: none;
}
}
// 提交遮罩层样式
.submit-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.submit-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 16px;
color: #333;
font-weight: 500;
}
</style>