1121 lines
33 KiB
Vue
1121 lines
33 KiB
Vue
<!-- 作品任务提交页面 -->
|
||
<template>
|
||
<view class="zp-submit-page">
|
||
<!-- 提交遮罩层 -->
|
||
<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" :class="{ 'full-height': hideBottomBtn }">
|
||
<view class="p-15">
|
||
<!-- 第一部分:任务要求 -->
|
||
<view class="zp-info-section">
|
||
<view class="section-title">任务要求</view>
|
||
|
||
<!-- 任务名称 -->
|
||
<view class="info-item">
|
||
<text class="label">任务名称:</text>
|
||
<text class="value title-bold">{{ zp.zpmc || '作品任务' }}</text>
|
||
</view>
|
||
|
||
<!-- 任务描述 -->
|
||
<view v-if="zp.zpms" class="info-item">
|
||
<text class="label">任务描述:</text>
|
||
<text class="value">{{ zp.zpms }}</text>
|
||
</view>
|
||
|
||
<!-- 任务时间 -->
|
||
<view v-if="zp.zpkstime || zp.zpjstime" class="info-item">
|
||
<text class="label">任务时间:</text>
|
||
<text class="value">{{ formatTimeRange(zp.zpkstime, zp.zpjstime) }}</text>
|
||
</view>
|
||
|
||
<!-- 附件预览 -->
|
||
<view v-if="zp.fileUrl" class="file-preview mt-15">
|
||
<BasicFilePreview
|
||
:file-url="zp.fileUrl"
|
||
:file-name="zp.fileName"
|
||
:file-format="zp.fileFormat"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 第二部分:任务执行 -->
|
||
<view class="zp-execute-section">
|
||
<view class="section-title">作品提交</view>
|
||
|
||
<!-- 动态分组渲染 -->
|
||
<view v-for="category in dynamicCategories" :key="category.zpfldm">
|
||
<view v-if="groupedSchema[category.zpfldm] && groupedSchema[category.zpfldm].length > 0" class="part-section">
|
||
<view class="part-title" :data-debug="`分类标题: ${category.label}`">{{ category.label }}</view>
|
||
<view class="execute-form">
|
||
<BasicForm :schema="groupedSchema[category.zpfldm]" v-model="formData">
|
||
</BasicForm>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 未分类的任务项 -->
|
||
<view v-if="groupedSchema[''] && groupedSchema[''].length > 0" class="part-section">
|
||
<view class="execute-form">
|
||
<BasicForm :schema="groupedSchema['']" v-model="formData">
|
||
</BasicForm>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 提交按钮 - 固定在底部 -->
|
||
<view v-if="showSubmitButton" class="submit-button-fixed" :class="{ 'hide-bottom': hideBottomBtn }">
|
||
<button class="action-button" @click="saveZpzx" :disabled="isSubmitting">
|
||
{{ isSubmitting ? '提交中...' : '提交' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, computed } from 'vue';
|
||
import { onLoad } from '@dcloudio/uni-app';
|
||
import { zpFindDetailByIdApi, zpzxSaveApi, zpqdFindByZpzxIdApi } from "@/api/base/server";
|
||
import { useUserStore } from "@/store/modules/user";
|
||
import { ImageVideoUpload, COMPRESS_PRESETS } from "@/components/ImageVideoUpload";
|
||
import { attachmentUpload } from "@/api/system/upload";
|
||
import BasicFilePreview from "@/components/BasicFile/preview.vue";
|
||
import { imagUrl } from "@/utils";
|
||
|
||
// 动态分类列表(从任务类型列表中提取)
|
||
const dynamicCategories = ref<Array<{ zpfldm: string; label: string }>>([]);
|
||
|
||
// 从任务类型列表中提取所有不同的 zpfldm 值
|
||
const extractCategoriesFromZplxList = () => {
|
||
const categoryMap = new Map<string, { zpfldm: string; label: string; minSort: number }>();
|
||
|
||
zplxList.value.forEach((zplx: ZplxItem, index: number) => {
|
||
const zpfldm = (zplx.zpfldm || '').trim();
|
||
if (zpfldm) {
|
||
// 使用索引作为排序依据(保持原始顺序)
|
||
const sort = index;
|
||
if (!categoryMap.has(zpfldm) || categoryMap.get(zpfldm)!.minSort > sort) {
|
||
categoryMap.set(zpfldm, {
|
||
zpfldm: zpfldm,
|
||
label: zpfldm,
|
||
minSort: sort
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// 转换为数组并按 minSort 排序
|
||
const categories = Array.from(categoryMap.values())
|
||
.sort((a, b) => {
|
||
return a.minSort - b.minSort;
|
||
});
|
||
|
||
dynamicCategories.value = categories.map(cat => ({
|
||
zpfldm: cat.zpfldm,
|
||
label: cat.label
|
||
}));
|
||
|
||
console.log('提取的分类列表:', dynamicCategories.value);
|
||
};
|
||
|
||
// 按 zpfldm 动态分组 schema
|
||
const groupedSchema = computed(() => {
|
||
const groups: Record<string, any[]> = {};
|
||
|
||
// 初始化所有分类的分组
|
||
dynamicCategories.value.forEach(category => {
|
||
groups[category.zpfldm] = [];
|
||
});
|
||
// 添加未分类分组
|
||
groups[''] = [];
|
||
|
||
// 将 schema 项分配到对应分组
|
||
schema.value.forEach((item: any) => {
|
||
const zpfldm = (item.zpfldm || '').trim();
|
||
if (groups.hasOwnProperty(zpfldm)) {
|
||
groups[zpfldm].push(item);
|
||
} else {
|
||
// 如果分类不存在,放入未分类
|
||
groups[''].push(item);
|
||
}
|
||
});
|
||
|
||
return groups;
|
||
});
|
||
|
||
// 接口定义
|
||
interface ZpInfo {
|
||
id?: string;
|
||
zpmc?: string; // 作品名称
|
||
zpms?: string; // 作品描述
|
||
zpkstime?: string; // 开始时间
|
||
zpjstime?: string; // 结束时间
|
||
fileUrl?: string; // 附件URL
|
||
fileName?: string; // 附件名称
|
||
[key: string]: any;
|
||
}
|
||
|
||
interface ZplxItem {
|
||
id: string;
|
||
zpbt?: string; // 作品标题
|
||
zpfl?: string; // 作品分类(任务类型)
|
||
isbt?: number; // 是否必填
|
||
remark?: string; // 备注(选项内容等)
|
||
zpfldm?: string; // 部分代码:第一部分/第二部分/第三部分
|
||
[key: string]: any;
|
||
}
|
||
|
||
interface ZpqdItem {
|
||
id?: string;
|
||
zplxId?: string; // 作品类型ID
|
||
zpqdtx?: string; // 作品清单内容
|
||
wjmc?: string; // 文件名
|
||
[key: string]: any;
|
||
}
|
||
|
||
// 响应式数据
|
||
const formData: any = ref({});
|
||
const zpzxId = ref<string>(''); // 作品执行ID
|
||
const xsId = ref<string>(''); // 学生ID
|
||
const isSubmitting = ref<boolean>(false);
|
||
const submitTimer = ref<any>(null);
|
||
const showSubmitButton = ref<boolean>(false);
|
||
const isLoading = ref(false);
|
||
const zplxList = ref<ZplxItem[]>([]);
|
||
const zp = ref<ZpInfo>({});
|
||
const schema = ref<any[]>([]);
|
||
const zpqdList = ref<ZpqdItem[]>([]);
|
||
const userStore = useUserStore();
|
||
const zpzxData = ref<any>({}); // 作品执行数据,用于获取 ispj 字段
|
||
// 从路由参数中获取的状态字段
|
||
const routeZpzt = ref<string>(''); // 从路由参数获取的 zpzt
|
||
const routeIspj = ref<string>(''); // 从路由参数获取的 ispj
|
||
const hideBottomBtn = ref(false); // 控制底部按钮显示/隐藏
|
||
|
||
// 处理选择器弹窗状态变化
|
||
const handlePickerPopupChange = (e: any) => {
|
||
// e.show 为 true 表示弹窗打开,false 表示关闭
|
||
hideBottomBtn.value = e.show;
|
||
};
|
||
|
||
// 格式化时间范围
|
||
const formatTimeRange = (startTime?: string, endTime?: string) => {
|
||
if (!startTime && !endTime) return '未设置';
|
||
const formatTime = (time: string) => {
|
||
if (!time) return '';
|
||
const date = new Date(time);
|
||
return date.toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit'
|
||
});
|
||
};
|
||
const start = formatTime(startTime || '');
|
||
const end = formatTime(endTime || '');
|
||
if (start && end) {
|
||
return `${start} 至 ${end}`;
|
||
} else if (start) {
|
||
return `从 ${start} 开始`;
|
||
} else if (end) {
|
||
return `至 ${end} 结束`;
|
||
}
|
||
return '未设置';
|
||
};
|
||
|
||
|
||
// 页面加载
|
||
onLoad(async (options) => {
|
||
console.log('作品任务提交页面加载参数:', options);
|
||
|
||
const zpId = options?.zpId || '';
|
||
zpzxId.value = options?.zpzxId || '';
|
||
xsId.value = options?.xsId || '';
|
||
const kcId = options?.kcId || ''; // 保存课程ID用于返回
|
||
routeZpzt.value = options?.zpzt || ''; // 从路由参数获取 zpzt
|
||
routeIspj.value = options?.ispj || ''; // 从路由参数获取 ispj
|
||
|
||
if (!zpId) {
|
||
uni.showToast({ title: '缺少任务ID', icon: 'error' });
|
||
setTimeout(() => {
|
||
uni.navigateBack();
|
||
}, 1500);
|
||
return;
|
||
}
|
||
|
||
// 获取学生ID
|
||
if (!xsId.value) {
|
||
const curXs = userStore.curXs;
|
||
xsId.value = curXs?.id || '';
|
||
}
|
||
|
||
if (!xsId.value) {
|
||
uni.showToast({ title: '请先选择学生', icon: 'error' });
|
||
setTimeout(() => {
|
||
uni.navigateBack();
|
||
}, 1500);
|
||
return;
|
||
}
|
||
|
||
await loadTaskDetail(zpId);
|
||
});
|
||
|
||
// 加载任务详情
|
||
const loadTaskDetail = async (zpId: string) => {
|
||
try {
|
||
isLoading.value = true;
|
||
console.log('加载作品任务详情,任务ID:', zpId);
|
||
|
||
const response: any = await zpFindDetailByIdApi({ id: zpId });
|
||
const detailData = response?.result || response?.data || response;
|
||
|
||
if (!detailData) {
|
||
throw new Error('未找到任务数据');
|
||
}
|
||
|
||
// 1. 填充任务基本信息
|
||
const zpData = detailData.zp || {};
|
||
zp.value = zpData;
|
||
|
||
// 2. 保存作品执行数据(用于获取 ispj 字段)
|
||
zpzxData.value = detailData.zpzx || {};
|
||
|
||
// 3. 加载任务类型列表(作品类型列表)
|
||
zplxList.value = detailData.zplxList || [];
|
||
console.log('任务类型列表:', zplxList.value);
|
||
|
||
// 4. 提取分类信息
|
||
extractCategoriesFromZplxList();
|
||
|
||
// 5. 生成表单schema
|
||
generateFormSchema();
|
||
|
||
// 6. 加载已提交的作品清单数据
|
||
await loadZpqdList();
|
||
|
||
} catch (error) {
|
||
console.error('加载任务详情失败:', error);
|
||
uni.showToast({
|
||
title: error instanceof Error ? error.message : '加载失败,请重试',
|
||
icon: 'error'
|
||
});
|
||
} finally {
|
||
isLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 生成表单schema
|
||
const generateFormSchema = () => {
|
||
schema.value = [];
|
||
|
||
for (let i = 0; i < zplxList.value.length; i++) {
|
||
const zplx = zplxList.value[i];
|
||
const fieldId = zplx.id;
|
||
|
||
console.log(`任务项 ${i + 1}:`, {
|
||
zpbt: zplx.zpbt,
|
||
zpfl: zplx.zpfl,
|
||
isbt: zplx.isbt,
|
||
remark: zplx.remark,
|
||
zpfldm: zplx.zpfldm
|
||
});
|
||
|
||
// 根据作品分类(任务类型)生成不同的表单组件
|
||
if (zplx.zpfl === "sctp" || zplx.zpfl === "scsp" || zplx.zpfl === "scwd") {
|
||
// 上传类型:图片、视频、文档
|
||
const fieldName = zplx.zpfl === "sctp" ? `${fieldId}_imageList` :
|
||
zplx.zpfl === "scsp" ? `${fieldId}_videoList` :
|
||
`${fieldId}_fileList`;
|
||
|
||
// 初始化表单数据
|
||
formData.value[fieldName] = [];
|
||
|
||
let componentConfig: any = {};
|
||
|
||
if (zplx.zpfl === "sctp") {
|
||
// 上传图片
|
||
componentConfig = {
|
||
component: "ImageVideoUpload",
|
||
componentProps: {
|
||
enableImage: true,
|
||
enableVideo: false,
|
||
enableFile: false,
|
||
maxImageCount: 30,
|
||
uploadApi: attachmentUpload,
|
||
compressConfig: COMPRESS_PRESETS.medium,
|
||
imageList: formData.value[fieldName] || [],
|
||
showSectionTitle: false
|
||
}
|
||
};
|
||
} else if (zplx.zpfl === "scsp") {
|
||
// 上传视频(视频大小限制 50MB)
|
||
componentConfig = {
|
||
component: "ImageVideoUpload",
|
||
componentProps: {
|
||
enableImage: false,
|
||
enableVideo: true,
|
||
enableFile: false,
|
||
maxVideoCount: 30,
|
||
uploadApi: attachmentUpload,
|
||
compressConfig: {
|
||
...COMPRESS_PRESETS.medium,
|
||
video: {
|
||
...COMPRESS_PRESETS.medium.video,
|
||
maxSize: 50 * 1024 * 1024
|
||
}
|
||
},
|
||
videoList: formData.value[fieldName] || [],
|
||
showSectionTitle: false
|
||
}
|
||
};
|
||
} else if (zplx.zpfl === "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[fieldName] || [],
|
||
showSectionTitle: false
|
||
}
|
||
};
|
||
}
|
||
|
||
const labelText = zplx.zpbt || '作品上传';
|
||
console.log('生成表单字段 - zpbt:', labelText, '长度:', labelText.length);
|
||
schema.value.push({
|
||
field: fieldName,
|
||
label: labelText,
|
||
required: !!(zplx.isbt === 1 || zplx.isbt === true),
|
||
zpfldm: zplx.zpfldm || '',
|
||
itemProps: {
|
||
labelPosition: "top",
|
||
},
|
||
...componentConfig
|
||
});
|
||
|
||
} else if (zplx.zpfl === "fwb") {
|
||
// 富文本类型
|
||
const labelText = zplx.zpbt || '作品描述';
|
||
console.log('生成表单字段 - zpbt:', labelText, '长度:', labelText.length);
|
||
schema.value.push({
|
||
field: fieldId,
|
||
label: labelText,
|
||
component: "BasicEditor",
|
||
required: !!(zplx.isbt === 1 || zplx.isbt === true),
|
||
zpfldm: zplx.zpfldm || '',
|
||
itemProps: {
|
||
labelPosition: "top",
|
||
},
|
||
componentProps: {
|
||
placeholder: "请输入作品描述,支持插入图片"
|
||
},
|
||
});
|
||
|
||
} else if (zplx.zpfl === "text") {
|
||
// 普通文本类型
|
||
const labelText = zplx.zpbt || '作品描述';
|
||
console.log('生成表单字段 - zpbt:', labelText, '长度:', labelText.length);
|
||
schema.value.push({
|
||
field: fieldId,
|
||
label: labelText,
|
||
component: "BasicInput",
|
||
required: !!(zplx.isbt === 1 || zplx.isbt === true),
|
||
zpfldm: zplx.zpfldm || '',
|
||
itemProps: {
|
||
labelPosition: "top",
|
||
},
|
||
componentProps: {
|
||
type: "textarea",
|
||
placeholder: "请输入作品描述"
|
||
},
|
||
});
|
||
|
||
} else if (zplx.zpfl === "dxsx" || zplx.zpfl === "dxxz") {
|
||
// 选择类型:多项选择或单项选择
|
||
// 支持中文分号(;)和英文分号(;)分割选项
|
||
let options = (zplx.remark || '').split(/[;;]/).filter(Boolean);
|
||
let range = options.map(opt => ({ name: opt.trim() }));
|
||
|
||
const labelText = zplx.zpbt || '作品选择';
|
||
console.log('生成表单字段 - zpbt:', labelText, '长度:', labelText.length);
|
||
schema.value.push({
|
||
field: fieldId,
|
||
label: labelText,
|
||
component: "BasicPicker",
|
||
required: !!(zplx.isbt === 1 || zplx.isbt === true),
|
||
zpfldm: zplx.zpfldm || '',
|
||
itemProps: {
|
||
labelPosition: "top",
|
||
},
|
||
componentProps: {
|
||
range: range,
|
||
rangeKey: "name",
|
||
savaKey: "name",
|
||
onPopupChange: handlePickerPopupChange,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log('生成的表单schema:', schema.value);
|
||
};
|
||
|
||
// 加载作品清单数据
|
||
const loadZpqdList = async () => {
|
||
// 如果已有作品执行ID,查询已提交的数据
|
||
if (zpzxId.value) {
|
||
try {
|
||
const response: any = await zpqdFindByZpzxIdApi({ zpzxId: zpzxId.value });
|
||
const pageData = response?.result || response;
|
||
|
||
if (pageData && pageData.rows) {
|
||
zpqdList.value = pageData.rows || [];
|
||
} else if (Array.isArray(response)) {
|
||
zpqdList.value = response;
|
||
} else {
|
||
zpqdList.value = [];
|
||
}
|
||
|
||
console.log('查询到的作品清单数据:', zpqdList.value);
|
||
|
||
// 如果 ispj 为 'A'(已评价),隐藏提交按钮
|
||
// 优先使用路由参数中的 ispj,如果没有则使用从后端加载的数据
|
||
const ispjValue = routeIspj.value || (zpzxData.value && zpzxData.value.ispj) || '';
|
||
const isEvaluated = ispjValue === 'A';
|
||
// 只有 ispj 为 'A'(已评价)时,才隐藏提交按钮,不考虑是否有提交数据
|
||
showSubmitButton.value = !isEvaluated;
|
||
} catch (error) {
|
||
console.error('查询作品清单失败:', error);
|
||
zpqdList.value = [];
|
||
// 检查状态决定是否显示提交按钮
|
||
// 只有 ispj 为 'A'(已评价)时,才隐藏提交按钮
|
||
const ispjValue = routeIspj.value || (zpzxData.value && zpzxData.value.ispj) || '';
|
||
const isEvaluated = ispjValue === 'A';
|
||
showSubmitButton.value = !isEvaluated;
|
||
}
|
||
} else {
|
||
// 新任务,检查状态决定是否显示提交按钮
|
||
// 只有 ispj 为 'A'(已评价)时,才隐藏提交按钮
|
||
const ispjValue = routeIspj.value || (zpzxData.value && zpzxData.value.ispj) || '';
|
||
const isEvaluated = ispjValue === 'A';
|
||
showSubmitButton.value = !isEvaluated;
|
||
}
|
||
|
||
// 处理数据回显
|
||
if (zpqdList.value && zpqdList.value.length > 0) {
|
||
handleDataEcho();
|
||
}
|
||
};
|
||
|
||
// 处理数据回显
|
||
const handleDataEcho = () => {
|
||
console.log('开始处理数据回显,zpqdList.value:', zpqdList.value);
|
||
const showData: Record<string, any> = {};
|
||
|
||
for (let i = 0; i < zpqdList.value.length; i++) {
|
||
const record: ZpqdItem = zpqdList.value[i];
|
||
const zplxId = record.zplxId;
|
||
const zpqdtx = record.zpqdtx;
|
||
|
||
// 查找对应的任务类型
|
||
const taskType = zplxList.value.find((item: ZplxItem) => item.id === zplxId);
|
||
|
||
if (taskType && zplxId) {
|
||
if (taskType.zpfl === "sctp") {
|
||
// 图片上传类型
|
||
if (zpqdtx) {
|
||
const urls = zpqdtx.split(',').filter(Boolean);
|
||
const names = record.wjmc ? record.wjmc.split(',').filter(Boolean) : [];
|
||
const images = urls.map((url: string, index: number) => ({
|
||
url: imagUrl(url.trim()),
|
||
name: names[index] || url.split('/').pop() || 'image.jpg',
|
||
tempPath: undefined
|
||
}));
|
||
showData[`${zplxId}_imageList`] = images;
|
||
}
|
||
} else if (taskType.zpfl === "scsp") {
|
||
// 视频上传类型
|
||
if (zpqdtx) {
|
||
const urls = zpqdtx.split(',').filter(Boolean);
|
||
const names = record.wjmc ? record.wjmc.split(',').filter(Boolean) : [];
|
||
const videos = urls.map((url: string, index: number) => ({
|
||
url: imagUrl(url.trim()),
|
||
name: names[index] || url.split('/').pop() || 'video.mp4',
|
||
tempPath: undefined
|
||
}));
|
||
showData[`${zplxId}_videoList`] = videos;
|
||
}
|
||
} else if (taskType.zpfl === "scwd") {
|
||
// 文档上传类型
|
||
if (zpqdtx) {
|
||
const urls = zpqdtx.split(',').filter(Boolean);
|
||
const names = record.wjmc ? record.wjmc.split(',').filter(Boolean) : [];
|
||
const files = urls.map((url: string, index: number) => ({
|
||
url: imagUrl(url.trim()),
|
||
name: names[index] || url.split('/').pop() || 'document',
|
||
tempPath: undefined,
|
||
type: 'document',
|
||
extension: url.split('.').pop() || ''
|
||
}));
|
||
showData[`${zplxId}_fileList`] = files;
|
||
}
|
||
} else {
|
||
// 文本等其他类型
|
||
if (zplxId) {
|
||
showData[zplxId] = zpqdtx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 合并已保存的数据
|
||
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('数据回显完成 - formData.value:', formData.value);
|
||
};
|
||
|
||
// 提交作品
|
||
const saveZpzx = async () => {
|
||
if (isSubmitting.value) {
|
||
console.log('正在提交中,请勿重复点击');
|
||
return;
|
||
}
|
||
|
||
if (submitTimer.value) {
|
||
clearTimeout(submitTimer.value);
|
||
}
|
||
|
||
submitTimer.value = setTimeout(async () => {
|
||
await performSubmit();
|
||
}, 300);
|
||
};
|
||
|
||
// 执行提交
|
||
const performSubmit = async () => {
|
||
if (isSubmitting.value) {
|
||
return;
|
||
}
|
||
|
||
// 验证必填项
|
||
const result = [];
|
||
for (let i = 0; i < zplxList.value.length; i++) {
|
||
const zplx = zplxList.value[i];
|
||
const fieldId = zplx.id;
|
||
let fieldValue = formData.value[fieldId];
|
||
|
||
// 处理上传类型的任务
|
||
let fileNames = '';
|
||
if (zplx.zpfl === "sctp" || zplx.zpfl === "scsp" || zplx.zpfl === "scwd") {
|
||
const fileUrls: string[] = [];
|
||
const names: string[] = [];
|
||
|
||
if (zplx.zpfl === "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 (zplx.zpfl === "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 (zplx.zpfl === "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 (zplx.isbt === 1 || zplx.isbt === true) {
|
||
let isEmpty = false;
|
||
|
||
if (zplx.zpfl === "sctp" || zplx.zpfl === "scsp" || zplx.zpfl === "scwd") {
|
||
if (zplx.zpfl === "sctp") {
|
||
const images = formData.value[`${fieldId}_imageList`] || [];
|
||
isEmpty = images.length === 0;
|
||
} else if (zplx.zpfl === "scsp") {
|
||
const videos = formData.value[`${fieldId}_videoList`] || [];
|
||
isEmpty = videos.length === 0;
|
||
} else if (zplx.zpfl === "scwd") {
|
||
const files = formData.value[`${fieldId}_fileList`] || [];
|
||
isEmpty = files.length === 0;
|
||
}
|
||
} else {
|
||
isEmpty = !fieldValue || fieldValue === "";
|
||
}
|
||
|
||
if (isEmpty) {
|
||
uni.showToast({ title: `请填写必填项:${zplx.zpbt || '作品内容'}`, icon: 'none' });
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 查找对应的作品清单记录ID
|
||
const existingRecord = zpqdList.value.find((item: ZpqdItem) => item.zplxId === fieldId);
|
||
const recordId = existingRecord ? existingRecord.id : undefined;
|
||
|
||
result.push({
|
||
id: recordId,
|
||
zplxId: fieldId,
|
||
zpqdtx: fieldValue,
|
||
wjmc: fileNames || undefined,
|
||
});
|
||
}
|
||
|
||
try {
|
||
isSubmitting.value = true;
|
||
|
||
if (!xsId.value) {
|
||
uni.showToast({ title: '缺少学生ID,无法提交', icon: 'error' });
|
||
isSubmitting.value = false;
|
||
return;
|
||
}
|
||
|
||
// 获取学生信息
|
||
const curXs = userStore.curXs;
|
||
if (!curXs) {
|
||
uni.showToast({ title: '请先选择学生', icon: 'error' });
|
||
isSubmitting.value = false;
|
||
return;
|
||
}
|
||
|
||
const currentXsId = xsId.value || curXs?.id || '';
|
||
|
||
if (!currentXsId) {
|
||
uni.showToast({ title: '请先选择学生', icon: 'error' });
|
||
isSubmitting.value = false;
|
||
return;
|
||
}
|
||
|
||
// 验证必须有作品执行ID
|
||
if (!zpzxId.value) {
|
||
uni.showToast({ title: '缺少作品执行ID,无法提交', icon: 'error' });
|
||
isSubmitting.value = false;
|
||
return;
|
||
}
|
||
|
||
// 构建提交数据:只传递 id 和作品清单
|
||
const submitData: any = {
|
||
id: zpzxId.value, // 作品执行ID(必须)
|
||
zpqdDtos: result // 作品清单
|
||
};
|
||
|
||
await zpzxSaveApi(submitData);
|
||
|
||
uni.showToast({ title: '提交成功', icon: 'success' });
|
||
|
||
// 延迟返回并刷新列表页面
|
||
setTimeout(() => {
|
||
// 获取当前页面的参数
|
||
const pages = getCurrentPages();
|
||
const currentPage = pages[pages.length - 1];
|
||
const options = currentPage.options || {};
|
||
const kcId = options.kcId || '';
|
||
const xsIdParam = options.xsId || xsId.value || '';
|
||
|
||
// 返回到 index.vue 并刷新
|
||
uni.navigateBack({
|
||
delta: 1,
|
||
success: () => {
|
||
// 通知列表页面刷新
|
||
uni.$emit('refreshTaskList');
|
||
},
|
||
fail: () => {
|
||
// 如果返回失败,直接跳转到列表页面
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 1) {
|
||
uni.navigateBack({ delta: 1 });
|
||
} else {
|
||
uni.redirectTo({
|
||
url: `/pages/base/xszp/index${kcId ? `?kcId=${kcId}` : ''}${xsIdParam ? `&xsId=${xsIdParam}` : ''}`,
|
||
success: () => {
|
||
uni.$emit('refreshTaskList');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}, 1500);
|
||
|
||
} catch (error) {
|
||
console.error('提交失败:', error);
|
||
uni.showToast({
|
||
title: error instanceof Error ? error.message : '提交失败,请重试',
|
||
icon: 'error'
|
||
});
|
||
} finally {
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.zp-submit-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 {
|
||
flex: 1;
|
||
transition: height 0.3s ease;
|
||
|
||
&.full-height {
|
||
height: 100vh;
|
||
}
|
||
|
||
.p-15 {
|
||
padding: 15px;
|
||
}
|
||
|
||
.mt-15 {
|
||
margin-top: 15px;
|
||
}
|
||
}
|
||
|
||
// 第一部分:任务要求
|
||
.zp-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 #4e73df;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 第二部分:任务执行
|
||
.zp-execute-section {
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #fff;
|
||
|
||
.part-section {
|
||
margin-bottom: 30px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.part-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-bottom: 15px;
|
||
color: #4e73df;
|
||
padding: 10px 15px;
|
||
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%);
|
||
border-left: 4px solid #4e73df;
|
||
border-radius: 4px;
|
||
// 允许换行 - 调试样式
|
||
white-space: normal !important;
|
||
word-break: break-word !important;
|
||
word-wrap: break-word !important;
|
||
overflow-wrap: break-word !important;
|
||
line-height: 1.5 !important;
|
||
// 调试边框(可以临时启用查看)
|
||
// border: 1px solid red !important;
|
||
// background: rgba(255, 0, 0, 0.1) !important;
|
||
}
|
||
}
|
||
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 #4e73df;
|
||
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;
|
||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||
|
||
&.hide-bottom {
|
||
transform: translateY(100%);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.action-button {
|
||
width: 100%;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
border-radius: 8px;
|
||
background-color: #4e73df;
|
||
color: #ffffff;
|
||
border: none;
|
||
|
||
&:active:not(:disabled) {
|
||
background-color: #2e59d9;
|
||
transform: translateY(1px);
|
||
}
|
||
|
||
&:disabled {
|
||
background-color: #d9d9d9 !important;
|
||
color: #999 !important;
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提交遮罩层样式
|
||
.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 #4e73df;
|
||
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;
|
||
}
|
||
|
||
// 为输入框添加基本边框
|
||
: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;
|
||
}
|
||
|
||
// 只针对 zpbt 字段(label 文本)的换行问题
|
||
:deep(.zp-execute-section) {
|
||
// 针对 FormsItem 中的 label 容器
|
||
// 只影响 forms-item-row 中直接包含 label 的 flex-row,不影响选择器内部的 flex-row
|
||
.forms-item-row {
|
||
// label 容器(flex-row)- 移除宽度限制,允许换行
|
||
// 使用 :not() 排除选择器组件内部的 flex-row
|
||
> .flex-row:not(.wh-full) {
|
||
// 关键:移除固定宽度限制(FormsItem.vue 中默认是 80px)
|
||
width: 100% !important;
|
||
max-width: 100% !important;
|
||
min-width: 0 !important;
|
||
// 不允许 flex 容器换行,保持 * 号和文本在同一行
|
||
flex-wrap: nowrap !important;
|
||
display: flex !important;
|
||
align-items: flex-start !important;
|
||
|
||
// 必填标记(红色星号)- 与文本保持在同一行
|
||
> view:first-child {
|
||
flex-shrink: 0 !important;
|
||
width: 10px !important;
|
||
// 确保 * 号与文本顶部对齐
|
||
align-self: flex-start !important;
|
||
padding-top: 2px !important;
|
||
}
|
||
|
||
// 图标容器(如果有)
|
||
> view:nth-child(2) {
|
||
flex-shrink: 0 !important;
|
||
align-self: flex-start !important;
|
||
}
|
||
|
||
// 包含 label 文本的 view(第15行的 view,zpbt 字段)
|
||
> view:last-child {
|
||
// 允许文本在容器内换行,但不与 * 号分离
|
||
white-space: normal !important;
|
||
word-break: break-word !important;
|
||
word-wrap: break-word !important;
|
||
overflow-wrap: break-word !important;
|
||
width: auto !important;
|
||
max-width: calc(100% - 20px) !important; // 减去 * 号和图标的空间
|
||
min-width: 0 !important;
|
||
flex: 1 1 auto !important;
|
||
display: block !important;
|
||
line-height: 1.5 !important;
|
||
box-sizing: border-box !important;
|
||
// 确保文本与 * 号顶部对齐
|
||
align-self: flex-start !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 确保选择器内部的提示信息可以换行
|
||
.wh-full.flex-row {
|
||
flex-wrap: wrap !important;
|
||
|
||
// 提示信息容器(第一个 view)
|
||
> view:first-child {
|
||
white-space: normal !important;
|
||
word-break: break-word !important;
|
||
word-wrap: break-word !important;
|
||
overflow-wrap: break-word !important;
|
||
flex: 1 1 auto !important;
|
||
min-width: 0 !important;
|
||
max-width: 100% !important;
|
||
|
||
// 文本内容
|
||
text,
|
||
span {
|
||
white-space: normal !important;
|
||
word-break: break-word !important;
|
||
word-wrap: break-word !important;
|
||
overflow-wrap: break-word !important;
|
||
display: block !important;
|
||
}
|
||
}
|
||
|
||
// 右侧图标保持不换行
|
||
> view:last-child {
|
||
flex-shrink: 0 !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
</style>
|