zhxy-jsd/src/pages/view/routine/jiaoyan/cg/achievementForm.vue
2025-10-20 11:51:15 +08:00

883 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>
<view class="achievement-form-page">
<!-- 表单内容 -->
<scroll-view scroll-y class="form-scroll-view">
<view class="form-container">
<!-- 成果名称 -->
<view class="form-section">
<view class="section-label">
<text class="label-text">成果名称</text>
<text class="required-star">*</text>
</view>
<view class="simple-text-wrapper">
<textarea
v-model="formData.jycgmc"
placeholder="请输入成果名称..."
class="simple-textarea"
:maxlength="200"
auto-height
/>
</view>
</view>
<!-- 解决的主要问题 -->
<view class="form-section">
<view class="section-label">
<text class="label-text">解决的主要问题</text>
<text class="required-star">*</text>
</view>
<view class="simple-text-wrapper">
<textarea
v-model="formData.jjdywt"
placeholder="请输入解决的主要问题..."
class="simple-textarea"
:maxlength="2000"
auto-height
/>
</view>
</view>
<!-- 最终成果 -->
<view class="form-section">
<view class="section-label">
<text class="label-text">最终成果</text>
<text class="required-star">*</text>
</view>
<view class="simple-text-wrapper">
<textarea
v-model="formData.zzcg"
placeholder="请输入最终成果内容..."
class="simple-textarea"
:maxlength="5000"
auto-height
/>
</view>
</view>
<!-- 添加附件 -->
<view class="form-section">
<view class="section-label">
<text class="label-text">添加附件</text>
</view>
<view class="file-upload-wrapper">
<ImageVideoUpload
v-model:file-list="fileList"
:max-file-count="30"
:enable-image="false"
:enable-video="false"
:enable-file="true"
:allowed-file-types="allowedFileTypes"
:upload-api="customUploadApi"
@file-upload-success="onFileUploadSuccess"
@upload-progress="onUploadProgress"
@file-upload-error="onFileUploadError"
/>
</view>
</view>
<!-- 提交人 -->
<view class="form-section">
<view class="section-label">
<text class="label-text">提交人</text>
</view>
<view class="readonly-input-wrapper">
<text class="readonly-text">{{ formData.jsxm }}</text>
</view>
</view>
<!-- 提交时间 -->
<view class="form-section">
<view class="section-label">
<text class="label-text">提交时间</text>
</view>
<view class="readonly-input-wrapper">
<text class="readonly-text">{{ formData.createdTime }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部固定操作按钮 -->
<view class="bottom-actions">
<view class="action-btn cancel-btn" @click="cancelForm">
<text class="btn-text">取消</text>
</view>
<view
class="action-btn submit-btn"
:class="{ 'disabled': isUploading }"
@click="submitAchievement"
>
<text class="btn-text">{{ isEdit ? '保存' : '提交' }}</text>
</view>
</view>
<!-- 文件上传遮罩层 -->
<view v-if="isUploading" class="upload-mask">
<view class="upload-content">
<view class="upload-icon">
<text class="icon-text">📤</text>
</view>
<view class="upload-title">文件上传中...</view>
<view class="upload-status">{{ uploadStatus }}</view>
<view class="upload-progress">
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: uploadProgress + '%' }"
></view>
</view>
<text class="progress-text">{{ uploadProgress }}%</text>
</view>
<view class="upload-tip">请等待文件上传完成后再提交</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { jycgSaveApi, jycgFindByIdApi, jycgUploadFileApi } from "@/api/base/jycgApi";
import { useUserStore } from "@/store/modules/user";
import { ImageVideoUpload, type FileItem } from "@/components/ImageVideoUpload";
import { attachmentUpload } from "@/api/system/upload";
const userStore = useUserStore();
const { getJs, getUser } = userStore;
// 是否为编辑模式
const isEdit = ref(false);
const isLoading = ref(false);
// 文件上传状态管理
const isUploading = ref(false);
const uploadProgress = ref(0);
const uploadStatus = ref('');
// 表单数据
const formData = reactive({
id: '', // 成果ID编辑时使用
jycgmc: '', // 成果名称
jjdywt: '', // 解决的主要问题
jjgc: '', // 解决过程
zzcg: '', // 最终成果
jsId: '', // 教师ID
jsxm: '', // 教师姓名(提交人)
jyjbId: '', // 教研组ID
createdTime: '', // 创建时间(提交时间)
fileName: '', // 文件名
fileUrl: '', // 文件路径
fileFormat: '', // 文件格式
remark: '' // 备注
});
// 文件上传数据
const fileList = ref<FileItem[]>([]);
// 允许的文件类型(支持所有格式)
const allowedFileTypes = [
// 图片格式
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'tiff', 'ico',
// 视频格式
'mp4', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'webm', '3gp', 'm4v',
// 音频格式
'mp3', 'wav', 'aac', 'ogg', 'flac', 'm4a', 'wma',
// 文档格式
'pdf', 'doc', 'docx', 'txt', 'rtf', 'wps',
// 表格格式
'xls', 'xlsx', 'csv', 'et',
// 演示格式
'ppt', 'pptx', 'dps',
// 压缩格式
'zip', 'rar', '7z', 'tar', 'gz',
// 代码格式
'html', 'htm', 'css', 'js', 'json', 'xml'
];
// 自定义上传API保持原始文件名
const customUploadApi = async (filePath: string) => {
try {
const result: any = await attachmentUpload(filePath as any);
console.log('上传接口返回结果:', result);
// 判断上传是否成功resultCode === 1 且 success === true
if (result && result.resultCode === 1 && result.success === true && result.result && result.result.length > 0) {
const uploadedFile = result.result[0];
// 从文件路径中提取原始文件名去掉UUID前缀
const originalFileName = filePath.split('/').pop() || 'unknown';
console.log('文件上传成功,准备返回结果');
// 返回包含原始文件名的结果
return {
...result,
result: [{
...uploadedFile,
originalFileName: originalFileName,
fileName: originalFileName
}]
};
}
// 上传失败
console.error('上传失败,接口返回:', result);
throw new Error(result?.message || '文件上传失败');
} catch (error) {
console.error('上传失败:', error);
throw error;
}
};
// 文件上传成功回调
const onFileUploadSuccess = (file: FileItem, index: number) => {
console.log('=== 文件上传成功回调 ===');
console.log('接收到的文件对象:', file);
console.log('文件索引:', index);
console.log('当前fileList长度:', fileList.value.length);
console.log('当前fileList内容:', fileList.value);
// 从服务器返回的URL中提取真实文件名
if (file.url) {
const urlParts = file.url.split('/');
const serverFileName = urlParts[urlParts.length - 1];
console.log('解析的文件信息:', {
url: file.url,
serverFileName,
originalName: file.originalName
});
// 更新文件信息
if (fileList.value[index]) {
console.log('更新前的文件项:', fileList.value[index]);
fileList.value[index].url = file.url; // 确保URL被正确设置
fileList.value[index].name = serverFileName;
// 保持原始文件名用于显示和保存
fileList.value[index].originalName = file.originalName || serverFileName;
console.log('更新后的文件项:', fileList.value[index]);
} else {
console.error('文件索引超出范围或文件不存在:', index, fileList.value);
}
// 立即测试文件信息收集
console.log('=== 文件信息收集测试 ===');
console.log('当前文件列表:', fileList.value);
console.log('收集的文件URLs:', getFileUrls());
console.log('收集的文件名:', getFileNames());
console.log('收集的文件格式:', getFileFormats());
// 检查是否所有文件都已上传完成(真正的上传成功)
const allFilesUploaded = fileList.value.every(f => f.url);
console.log('所有文件是否已上传:', allFilesUploaded);
if (allFilesUploaded) {
// 延迟关闭遮罩层,让用户看到上传完成的提示
setTimeout(() => {
isUploading.value = false;
uploadProgress.value = 0;
uploadStatus.value = '';
console.log('所有文件上传成功,关闭遮罩层');
}, 500);
}
} else {
console.error('文件URL为空:', file);
}
};
// 文件上传进度回调
const onUploadProgress = (type: string, current: number, total: number) => {
console.log('上传进度:', { type, current, total });
isUploading.value = true;
uploadProgress.value = Math.round((current / total) * 100);
uploadStatus.value = `正在上传文件 ${current}/${total}`;
// 注意不在这里关闭遮罩层因为进度100%不代表接口返回成功
// 遮罩层的关闭在 onFileUploadSuccess 回调中处理
};
// 文件上传错误回调
const onFileUploadError = (error: any, index: number) => {
console.error('文件上传失败:', error, index);
uploadStatus.value = '文件上传失败,请重试';
// 延迟隐藏遮罩层
setTimeout(() => {
isUploading.value = false;
uploadProgress.value = 0;
uploadStatus.value = '';
}, 2000);
};
// 获取文件URL列表
const getFileUrls = () => {
const urls = fileList.value
.filter(file => file.url)
.map(file => file.url)
.filter((url): url is string => !!url);
return urls.length > 0 ? urls.join(',') : '';
};
// 获取文件名列表(不包含扩展名)
const getFileNames = () => {
const names = fileList.value
.filter(file => file.originalName || file.name)
.map(file => {
const fullName = file.originalName || file.name || '';
// 去除扩展名,只保留文件名
return fullName.replace(/\.[^/.]+$/, '');
})
.filter((name): name is string => !!name);
return names.length > 0 ? names.join(',') : '';
};
// 获取文件格式列表
const getFileFormats = () => {
const formats = fileList.value
.filter(file => file.originalName || file.name)
.map(file => (file.originalName || file.name)?.split('.').pop()?.toLowerCase() || '')
.filter(format => !!format);
return formats.length > 0 ? formats.join(',') : '';
};
// 取消表单
const cancelForm = () => {
uni.showModal({
title: '确认取消',
content: '确定要取消并返回吗?未保存的内容将丢失',
success: (res) => {
if (res.confirm) {
uni.navigateBack();
}
}
});
};
// 提交成果
const submitAchievement = async () => {
// 检查文件上传状态
if (isUploading.value) {
uni.showToast({
title: '文件正在上传中,请稍候...',
icon: 'none'
});
return;
}
// 验证必填项
if (!formData.jycgmc) {
uni.showToast({
title: '请输入成果名称',
icon: 'none'
});
return;
}
if (!formData.jjdywt) {
uni.showToast({
title: '请输入解决的主要问题',
icon: 'none'
});
return;
}
if (!formData.zzcg) {
uni.showToast({
title: '请输入最终成果',
icon: 'none'
});
return;
}
try {
uni.showLoading({ title: isEdit.value ? '保存中...' : '提交中...' });
// 收集所有文件URL和文件名
const fileUrls = getFileUrls();
const fileNames = getFileNames();
const fileFormats = getFileFormats();
console.log('=== 提交前文件信息收集 ===');
console.log('当前文件列表:', fileList.value);
console.log('文件列表长度:', fileList.value.length);
console.log('文件列表详情:', fileList.value.map((file, index) => ({
index,
url: file.url,
name: file.name,
originalName: file.originalName,
hasUrl: !!file.url,
hasOriginalName: !!file.originalName
})));
console.log('收集的文件信息:', {
fileUrls,
fileNames,
fileFormats
});
console.log('fileName字段不含扩展名:', fileNames);
console.log('fileFormat字段仅扩展名:', fileFormats);
console.log('fileUrl字段文件路径:', fileUrls);
// 检查每个文件的信息收集过程
fileList.value.forEach((file, index) => {
console.log(`文件${index + 1}信息收集:`, {
url: file.url,
name: file.name,
originalName: file.originalName,
getFileUrls: file.url ? file.url : '无URL',
getFileNames: (file.originalName || file.name) ? (file.originalName || file.name || '').replace(/\.[^/.]+$/, '') : '无名称',
getFileFormats: (file.originalName || file.name) ? (file.originalName || file.name || '').split('.').pop()?.toLowerCase() : '无格式'
});
});
const params = {
...formData,
fileUrl: fileUrls,
fileName: fileNames,
fileFormat: fileFormats, // 添加文件格式
status: 'A' // 已发布状态
};
console.log(`${isEdit.value ? '更新' : '新增'}成果参数:`, params);
await jycgSaveApi(params);
uni.hideLoading();
uni.showToast({
title: isEdit.value ? '保存成功' : '提交成功',
icon: 'success'
});
// 返回列表页
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (error) {
uni.hideLoading();
console.error(`${isEdit.value ? '保存' : '提交'}失败:`, error);
uni.showToast({
title: isEdit.value ? '保存失败' : '提交失败',
icon: 'none'
});
}
};
// 页面加载时接收路由参数
onLoad(async (options: any) => {
console.log('页面加载参数:', options);
// 接收教研组ID
if (options && options.jyjbId) {
formData.jyjbId = options.jyjbId;
console.log('接收到的教研组ID:', formData.jyjbId);
}
// 接收成果ID编辑模式
if (options && options.id) {
isEdit.value = true;
formData.id = options.id;
console.log('编辑模式成果ID:', formData.id);
// 加载成果详情
await loadAchievementDetail(options.id);
}
});
// 加载成果详情
const loadAchievementDetail = async (id: string) => {
isLoading.value = true;
try {
uni.showLoading({ title: '加载中...' });
const response: any = await jycgFindByIdApi({ id });
if (response && response.result) {
const detail = response.result;
console.log('从后端获取的详情数据:', detail);
console.log('文件相关字段:', {
fileUrl: detail.fileUrl,
fileName: detail.fileName,
fileFormat: detail.fileFormat
});
// 回显基本信息
formData.id = detail.id || '';
formData.jycgmc = detail.jycgmc || '';
formData.jjdywt = detail.jjdywt || '';
formData.jjgc = detail.jjgc || '';
formData.zzcg = detail.zzcg || '';
formData.jsId = detail.jsId || '';
formData.jsxm = detail.jsxm || '';
formData.jyjbId = detail.jyjbId || '';
// 处理时间格式(保持完整时间戳)
if (detail.createdTime) {
formData.createdTime = detail.createdTime; // 保持完整时间戳
} else {
formData.createdTime = '';
}
formData.remark = detail.remark || '';
// 回显附件
console.log('开始回显附件fileUrl:', detail.fileUrl);
if (detail.fileUrl) {
const urls = detail.fileUrl.split(',').filter(Boolean);
const names = detail.fileName ? detail.fileName.split(',').filter(Boolean) : [];
const formats = detail.fileFormat ? detail.fileFormat.split(',').filter(Boolean) : [];
console.log('解析的文件数据:', {
urls,
names,
formats
});
// 清空现有文件列表
fileList.value = [];
// 回显所有文件
urls.forEach((url: string, index: number) => {
const fileName = names[index] || url.split('/').pop() || '';
const fileFormat = formats[index] || fileName.split('.').pop()?.toLowerCase() || '';
// 组合完整的文件名用于显示
const fullFileName = fileFormat ? `${fileName}.${fileFormat}` : fileName;
console.log(`文件${index + 1}:`, {
url: url.trim(),
fileName,
fileFormat,
fullFileName
});
fileList.value.push({
url: url.trim(),
name: fullFileName,
originalName: fullFileName, // 回显时使用完整文件名
type: 'document', // 默认为文档类型
extension: fileFormat
});
});
console.log('回显后的文件列表:', fileList.value);
} else {
console.log('没有文件需要回显');
fileList.value = [];
}
console.log('成果详情加载成功:', formData);
console.log('附件回显:', { fileList: fileList.value });
}
uni.hideLoading();
} catch (error) {
uni.hideLoading();
console.error('加载成果详情失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
// 加载失败返回列表页
setTimeout(() => {
uni.navigateBack();
}, 1500);
} finally {
isLoading.value = false;
}
};
// 页面加载时初始化
onMounted(() => {
// 编辑模式下不需要设置默认值(会从详情中获取)
if (isEdit.value) {
return;
}
// 新增模式:设置默认日期为今天
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
formData.createdTime = `${year}-${month}-${day}`;
// 新增模式:设置提交人信息
console.log('教师信息 getJs:', getJs);
formData.jsId = getJs.id || '';
formData.jsxm = getJs.jsxm || '';
});
</script>
<style lang="scss" scoped>
.achievement-form-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
}
// 表单滚动区域
.form-scroll-view {
flex: 1;
height: 0;
}
.form-container {
padding: 20px 16px 100px 16px; // 底部留出空间给固定按钮
}
// 表单区域
.form-section {
margin-bottom: 24px;
.section-label {
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 4px;
.label-text {
font-size: 16px;
font-weight: 600;
color: #2d3748;
}
.required-star {
font-size: 16px;
color: #ef4444;
font-weight: bold;
line-height: 1;
}
}
}
// 日期输入
.date-input-wrapper {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
padding: 12px 16px;
.date-picker {
width: 100%;
}
}
// 只读输入框
.readonly-input-wrapper {
background-color: #f5f5f5;
border-radius: 8px;
border: 1px solid #e2e8f0;
padding: 12px 16px;
.readonly-text {
font-size: 14px;
color: #666;
}
}
// 简单文本区域
.simple-text-wrapper {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
overflow: hidden;
.simple-textarea {
width: 100%;
min-height: 120px;
padding: 16px;
border: none;
outline: none;
font-size: 14px;
line-height: 1.6;
color: #2d3748;
background-color: #ffffff;
resize: none;
}
}
// 文件上传
.file-upload-wrapper {
background-color: #ffffff;
border-radius: 8px;
padding: 12px;
}
// 底部固定操作按钮
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
border-top: 1px solid #e8ecf1;
padding: 12px 16px;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
z-index: 1000;
display: flex;
gap: 8px;
.action-btn {
flex: 1;
height: 44px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
.btn-text {
font-size: 14px;
font-weight: 500;
color: #ffffff;
}
&:active {
transform: translateY(1px);
}
}
.cancel-btn {
background-color: #6b7280;
&:active {
background-color: #4b5563;
}
}
.submit-btn {
background-color: #10b981;
&:active {
background-color: #059669;
}
}
}
// 深度选择器样式
:deep(.uni-datetime-picker) {
width: 100%;
.uni-date {
background-color: transparent;
border: none;
padding: 0;
}
.uni-date__x-input {
font-size: 14px;
color: #2d3748;
}
}
// 文件上传遮罩层样式
.upload-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.upload-content {
background-color: #ffffff;
border-radius: 16px;
padding: 40px 32px;
display: flex;
flex-direction: column;
align-items: center;
min-width: 280px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
.upload-icon {
margin-bottom: 16px;
.icon-text {
font-size: 48px;
animation: bounce 1.5s ease-in-out infinite;
}
}
.upload-title {
font-size: 18px;
font-weight: 600;
color: #2d3748;
margin-bottom: 8px;
}
.upload-status {
font-size: 14px;
color: #718096;
margin-bottom: 20px;
text-align: center;
}
.upload-progress {
width: 100%;
margin-bottom: 16px;
.progress-bar {
width: 100%;
height: 8px;
background-color: #e2e8f0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #0ea5e9 0%, #0284c7 100%);
border-radius: 4px;
transition: width 0.3s ease;
}
}
.progress-text {
font-size: 12px;
color: #0ea5e9;
font-weight: 600;
text-align: center;
display: block;
}
}
.upload-tip {
font-size: 12px;
color: #a0aec0;
text-align: center;
line-height: 1.4;
}
}
}
// 提交按钮禁用状态
.action-btn.disabled {
opacity: 0.5;
pointer-events: none;
}
// 弹跳动画
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
</style>