1598 lines
40 KiB
Vue
Raw Normal View History

2025-10-07 08:58:02 +08:00
<template>
<view class="addkcrw-page">
2025-10-11 21:11:16 +08:00
<!-- 提交遮罩层 -->
<view v-if="isSubmitting" class="submit-overlay">
<view class="submit-loading">
<view class="loading-spinner"></view>
<text class="loading-text">保存中...</text>
</view>
</view>
2025-10-07 08:58:02 +08:00
<!-- 表单内容 -->
<view class="form-container">
<scroll-view scroll-y class="form-scroll">
<!-- 基本信息区域 -->
<view class="section-card">
<view class="section-title">
<text class="title-text">基本信息</text>
</view>
<!-- 任务名称 -->
<view class="form-item">
<text class="item-label required">任务名称</text>
<input
v-model="formData.rwmc"
placeholder="请输入任务名称"
class="item-input"
maxlength="100"
/>
</view>
<!-- 任务描述 -->
<view class="form-item">
<text class="item-label">任务描述</text>
<textarea
v-model="formData.rwms"
placeholder="请输入任务描述"
class="item-textarea"
maxlength="500"
/>
</view>
<!-- 截止时间 -->
<view class="form-item">
<text class="item-label required">截止时间</text>
<picker
mode="date"
:value="formData.rwjstime"
@change="onDateChange"
class="item-picker"
>
<view class="picker-content">
<text :class="['picker-text', { placeholder: !formData.rwjstime }]">
{{ formData.rwjstime || '请选择截止时间' }}
</text>
</view>
</picker>
</view>
<!-- 负责人 -->
<view class="form-item">
<text class="item-label required">负责人</text>
<BasicJsPicker
:multiple="true"
title="选择负责人"
placeholder="请选择负责人"
searchPlaceholder="输入教师名称查询"
@change="onLeaderChange"
:defaultValue="selectedLeaderIds"
/>
</view>
2025-11-23 19:34:40 +08:00
<!-- 附件上传 -->
<view class="form-item attachment-item">
<text class="item-label">附件</text>
<view class="attachment-container">
<ImageVideoUpload
v-model:file-list="fileList"
:enable-image="false"
:enable-video="false"
:enable-file="true"
:max-file-count="5"
:allowed-file-types="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'mp3', 'wav', 'zip', 'rar']"
:compress-config="compressConfig"
:upload-api="attachmentUpload"
@file-upload-success="onFileUploadSuccess"
/>
</view>
2025-10-07 08:58:02 +08:00
</view>
</view>
<!-- 任务下发设置区域 -->
<view class="section-card">
<view class="section-title">
<text class="title-text">任务下发设置</text>
</view>
2025-11-23 19:34:40 +08:00
<!-- 任务类型列表 -->
2025-10-07 08:58:02 +08:00
<view v-for="(taskType, typeIndex) in taskTypes" :key="taskType.id" class="task-type-section">
2025-11-23 19:34:40 +08:00
<!-- 任务类型选择 -->
2025-10-07 08:58:02 +08:00
<view class="task-type-header">
<view class="task-type-selector">
2025-11-23 19:34:40 +08:00
<text class="selector-label">任务类型</text>
2025-10-07 08:58:02 +08:00
<picker
:range="taskTypeOptions"
range-key="label"
:value="getTaskTypeIndex(taskType.rwfl)"
@change="onTaskTypeChange($event, typeIndex)"
class="type-picker"
>
<view class="picker-content">
<text class="picker-text">{{ getTaskTypeLabel(taskType.rwfl) }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
2025-11-23 19:34:40 +08:00
<view class="task-type-actions">
<view class="move-buttons">
<view
v-if="typeIndex > 0"
class="move-btn move-up-btn"
@click="moveTaskType(typeIndex, 'up')"
title="上移"
>
<text class="move-icon"></text>
</view>
<view
v-if="typeIndex < taskTypes.length - 1"
class="move-btn move-down-btn"
@click="moveTaskType(typeIndex, 'down')"
title="下移"
>
<text class="move-icon"></text>
</view>
</view>
<view class="delete-type-btn" @click="removeTaskType(typeIndex)">
<text class="delete-icon">×</text>
</view>
2025-10-07 08:58:02 +08:00
</view>
</view>
2025-11-23 19:34:40 +08:00
<!-- 任务项一个任务类型对应一个任务项 -->
<view class="task-items-container" v-if="taskType.items && taskType.items.length > 0">
<view class="task-item-header">
<text class="item-number">{{ typeIndex + 1 }}</text>
<input
v-model="taskType.items[0].rwbt"
:placeholder="`请输入${getTaskTypeLabel(taskType.rwfl)}任务描述`"
class="item-title-input"
maxlength="100"
/>
<view class="item-actions">
<view class="required-checkbox" @click="toggleRequired(taskType)">
<view :class="['checkbox-box', { checked: taskType.items[0].rwbs }]">
<text v-if="taskType.items[0].rwbs" class="check-icon"></text>
2025-10-07 08:58:02 +08:00
</view>
2025-11-23 19:34:40 +08:00
<view v-if="showTooltip" class="tooltip">默认必填</view>
2025-10-07 08:58:02 +08:00
</view>
</view>
</view>
2025-11-23 19:34:40 +08:00
<!-- 选择题选项 -->
<view v-if="taskType.rwfl === 'dxxz' || taskType.rwfl === 'dxsx'" class="item-options">
<textarea
v-model="taskType.items[0].remark"
:placeholder="taskType.rwfl === 'dxxz' ? '单项选择请输入如选项1选项2选项3支持中文或英文;分号)' : '多项选择请输入如选项1选项2选项3支持中文或英文;分号)'"
class="options-textarea"
maxlength="500"
/>
2025-10-07 08:58:02 +08:00
</view>
</view>
</view>
2025-11-23 19:34:40 +08:00
<!-- 新增任务项按钮 -->
<view class="add-task-item-wrapper">
<view class="add-task-type-btn" @click="addTaskType">
<text class="add-icon">+</text>
<text class="add-text">新增任务项</text>
</view>
</view>
2025-10-07 08:58:02 +08:00
</view>
<view>
<!-- 空内容删除了负责人选择弹窗 -->
</view>
</scroll-view>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<view class="action-buttons">
<button
@click="goBack"
class="cancel-btn"
>
取消
</button>
<button
@click="submitTask"
:disabled="isSubmitting"
class="submit-btn"
>
{{ isSubmitting ? '保存中...' : '保存' }}
</button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { kccyFindByKcjbIdApi } from "@/api/base/kccyApi";
import { rwSaveApi } from "@/api/base/rwApi";
import { attachmentUpload } from "@/api/system/upload";
import BasicJsPicker from "@/components/BasicJsPicker/Picker.vue";
import { ImageVideoUpload, type FileItem, COMPRESS_PRESETS } from "@/components/ImageVideoUpload";
// 接口类型定义
interface CourseInfo {
id: string;
kcmc: string;
kcms: string;
}
interface CourseMember {
id: string;
jsId: string;
jsxm: string;
kcjbId: string;
fzmc?: string;
}
interface TaskType {
id: string;
rwfl: string;
items: TaskItem[];
}
interface TaskItem {
key: string;
id?: string; // 任务项ID编辑时需要
rwbt: string;
rwbs: boolean;
remark: string;
}
// 页面数据
const courseId = ref('');
const taskId = ref('');
const courseInfo = ref<CourseInfo>({
id: '',
kcmc: '',
kcms: ''
});
const courseMembers = ref<CourseMember[]>([]);
const selectedLeaders = ref<any[]>([]);
const selectedLeaderIds = ref<string[]>([]);
const isSubmitting = ref(false);
const isLoadingMembers = ref(false);
2025-10-11 21:11:16 +08:00
const submitTimer = ref<any>(null); // 防抖定时器
2025-10-07 08:58:02 +08:00
// 冒泡提示显示状态
const showTooltip = ref(false);
// 附件相关数据
const compressConfig = ref(COMPRESS_PRESETS.medium);
const fileList = ref<FileItem[]>([]);
// 表单数据
const formData = reactive({
id: '', // 任务ID编辑时需要
rwmc: '',
rwms: '',
rwjstime: '',
rwfzr: '',
rwly: 'B',
rwlyId: '',
rwtsfs: '1', // 默认推送
rwfj: '', // 附件URL
fjmx: '' // 附件明细
});
// 任务类型选项
const taskTypeOptions = [
{ label: '填写文本', value: 'text' },
2025-11-23 19:34:40 +08:00
{ label: '富文本', value: 'fwb' },
2025-10-07 08:58:02 +08:00
{ label: '单项选择', value: 'dxxz' },
{ label: '多项选择', value: 'dxsx' },
{ label: '上传图片', value: 'sctp' },
{ label: '上传视频', value: 'scsp' },
{ label: '上传文档', value: 'scwd' }
];
// 任务方式列表
const taskTypes = ref<TaskType[]>([
{
id: `task_type_${Date.now()}`,
rwfl: 'text',
2025-11-23 19:34:40 +08:00
items: [
{
key: `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
rwbt: '',
rwbs: true, // 默认必填项打勾
remark: ''
}
]
2025-10-07 08:58:02 +08:00
}
]);
// 页面加载
onLoad((options: any) => {
console.log('编辑任务页面接收到的参数:', options);
// 获取任务ID
if (options.taskId) {
taskId.value = options.taskId;
formData.id = options.taskId;
console.log('设置任务ID为:', taskId.value);
}
// 支持多种参数名kcjbId 或 courseId
const receivedCourseId = options.kcjbId || options.courseId;
if (receivedCourseId) {
courseId.value = receivedCourseId;
formData.rwlyId = receivedCourseId;
console.log('设置课程ID为:', courseId.value);
// 如果传递了课程名称,设置课程信息
if (options.kcmc) {
courseInfo.value.kcmc = decodeURIComponent(options.kcmc);
console.log('设置课程名称为:', courseInfo.value.kcmc);
}
loadCourseInfo();
loadCourseMembers();
// 如果是编辑模式,加载任务数据
if (taskId.value) {
// 从全局存储中获取任务数据
loadTaskDataFromStorage();
}
} else {
console.error('未接收到课程ID参数');
uni.showToast({
title: '参数错误,请重新进入',
icon: 'error'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
// 加载课程信息
const loadCourseInfo = () => {
console.log('加载课程信息课程ID:', courseId.value);
// 如果已经有课程名称,保持不变;否则设置默认值
if (!courseInfo.value.kcmc) {
courseInfo.value.kcmc = '课程名称加载中...';
}
courseInfo.value.id = courseId.value;
courseInfo.value.kcms = '课程描述加载中...';
};
// 从全局存储中加载任务数据
const loadTaskDataFromStorage = () => {
try {
console.log('从全局存储中加载任务数据');
const taskInfo = uni.getStorageSync('editTaskData');
if (!taskInfo) {
console.error('未找到任务数据');
uni.showToast({
title: '任务数据不存在',
icon: 'error'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
}
console.log('任务数据:', taskInfo);
// 填充表单数据
formData.rwmc = taskInfo.rwmc || '';
formData.rwms = taskInfo.rwms || '';
formData.rwjstime = taskInfo.rwjstime || '';
formData.rwfzr = taskInfo.rwfzr || '';
formData.rwtsfs = taskInfo.rwtsfs || '1';
formData.rwfj = taskInfo.rwfj || '';
formData.fjmx = taskInfo.fjmx || '';
// 处理负责人数据
if (taskInfo.rwfzr) {
const leaderIds = taskInfo.rwfzr.split(',');
selectedLeaderIds.value = leaderIds;
// 这里需要根据ID找到对应的教师信息
// 暂时使用ID作为显示名称
selectedLeaders.value = leaderIds.map((id: string) => ({
id: id,
jsxm: taskInfo.rwfzrxm || `教师${id}`
}));
}
// 处理任务类型数据
if (taskInfo.rwlxes && taskInfo.rwlxes.length > 0) {
2025-11-23 19:34:40 +08:00
// 先按 sort 排序,如果 sort 为空则按 id 排序
const sortedRwlxes = [...taskInfo.rwlxes].sort((a: any, b: any) => {
const sortA = a.sort || 0;
const sortB = b.sort || 0;
if (sortA !== sortB) {
return sortA - sortB;
2025-10-07 08:58:02 +08:00
}
2025-11-23 19:34:40 +08:00
// sort 相同时按 id 排序
return (a.id || '').localeCompare(b.id || '');
2025-10-07 08:58:02 +08:00
});
2025-11-23 19:34:40 +08:00
// 构建任务类型列表(每个任务类型只保留第一个任务项,保持排序)
// 由于前端显示时一个任务类型对应一个任务项,所以每个 rwfl 只保留第一个出现的
const seenRwfl = new Set<string>();
taskTypes.value = sortedRwlxes
.filter((item: any) => {
if (seenRwfl.has(item.rwfl)) {
return false; // 如果已经见过这个 rwfl跳过
}
seenRwfl.add(item.rwfl);
return true;
})
.map((item: any, index: number) => {
return {
id: `task_type_${index + 1}`,
rwfl: item.rwfl,
items: [
{
key: `item_${index + 1}_1`,
id: item.id, // 保留任务项的ID
rwbt: item.rwbt || '',
rwbs: item.rwbs === 1 || item.rwbs === true,
remark: item.remark || ''
}
]
};
});
2025-10-07 08:58:02 +08:00
}
// 处理附件数据
if (taskInfo.rwfj) {
loadAttachmentData(taskInfo.rwfj, taskInfo.fjmx);
}
console.log('任务数据加载完成');
// 清除存储的任务数据
uni.removeStorageSync('editTaskData');
} catch (error) {
console.error('加载任务数据失败:', error);
uni.showToast({
title: '加载任务数据失败',
icon: 'error'
});
}
};
// 加载附件数据
const loadAttachmentData = (attachmentUrls: string, attachmentDetails?: string) => {
try {
console.log('加载附件数据:', { attachmentUrls, attachmentDetails });
if (!attachmentUrls) {
return;
}
const urls = attachmentUrls.split(',');
const files: FileItem[] = [];
// 解析附件明细,获取文件名
let fileNames: string[] = [];
if (attachmentDetails) {
// 处理 "文件1:file_1;文件2:新建 Microsoft Word 文档.docx" 格式
const details = attachmentDetails.split(';');
fileNames = details.map(detail => {
// 如果有 "文件X:" 前缀,去掉前缀
const colonIndex = detail.indexOf(':');
if (colonIndex > -1) {
return detail.substring(colonIndex + 1).trim();
}
return detail.trim();
});
}
urls.forEach((url, index) => {
if (!url.trim()) return;
const extension = url.toLowerCase().split('.').pop();
// 确定文件类型
let fileType = 'document';
if (['mp4', 'mov', 'avi', 'wmv', 'flv'].includes(extension || '')) {
fileType = 'video';
} else if (['mp3', 'wav', 'aac', 'ogg'].includes(extension || '')) {
fileType = 'audio';
}
// 使用解析出的文件名,如果没有则使用默认名称
const fileName = fileNames[index] || `file_${index + 1}`;
files.push({
url: url.trim(),
name: fileName,
type: fileType,
extension: extension || '',
tempPath: ''
});
});
fileList.value = files;
console.log('附件数据加载完成:', {
files: files.length,
fileNames: fileNames
});
} catch (error) {
console.error('加载附件数据失败:', error);
}
};
// 加载任务数据(编辑模式)
const loadTaskData = async () => {
try {
console.log('加载任务数据任务ID:', taskId.value);
// TODO: 调用API获取任务详情
// const response = await rwFindByIdApi(taskId.value);
// 模拟数据加载
uni.showLoading({ title: '加载中...' });
// 这里应该调用真实的API获取任务数据
// 暂时使用模拟数据
setTimeout(() => {
// 模拟加载任务数据
formData.rwmc = '示例任务名称';
formData.rwms = '示例任务描述';
formData.rwjstime = '2024-12-31';
formData.rwfzr = '1,2';
// 模拟负责人数据
selectedLeaderIds.value = ['1', '2'];
selectedLeaders.value = [
{ id: '1', jsxm: '张三' },
{ id: '2', jsxm: '李四' }
];
// 模拟任务类型数据
taskTypes.value = [
{
id: 'task_type_1',
rwfl: 'text',
items: [
{
key: 'item_1',
rwbt: '示例任务项',
rwbs: true,
remark: ''
}
]
}
];
uni.hideLoading();
}, 1000);
} catch (error) {
console.error('加载任务数据失败:', error);
uni.hideLoading();
uni.showToast({
title: '加载任务数据失败',
icon: 'error'
});
}
};
// 加载课程成员
const loadCourseMembers = async () => {
try {
isLoadingMembers.value = true;
console.log('开始加载课程成员课程ID:', courseId.value);
// 调用真实API获取课程成员
const response = await kccyFindByKcjbIdApi({ kcjbId: courseId.value });
let memberData = [];
if (response && (response.resultCode === 1 || response.resultCode === 0)) {
memberData = response.result || response.rows || response.data || [];
} else if (Array.isArray(response)) {
memberData = response;
} else if (response && response.length !== undefined) {
memberData = response;
}
courseMembers.value = memberData;
console.log('课程成员加载成功:', courseMembers.value.length, '人');
} catch (error) {
console.error('加载课程成员失败:', error);
uni.showToast({
title: '加载成员失败',
icon: 'error'
});
courseMembers.value = [];
} finally {
isLoadingMembers.value = false;
}
};
// 日期选择
const onDateChange = (e: any) => {
formData.rwjstime = e.detail.value;
};
// 负责人选择变化
const onLeaderChange = (selectedList: any[]) => {
console.log('负责人选择变化:', selectedList);
selectedLeaders.value = selectedList || [];
selectedLeaderIds.value = selectedLeaders.value.map(leader => leader.id);
formData.rwfzr = selectedLeaders.value.map(leader => leader.id).join(',');
};
// 附件上传成功回调
const onFileUploadSuccess = (file: FileItem, index: number) => {
console.log('文件上传成功:', file, index);
updateAttachmentFields();
};
// 更新附件字段
const updateAttachmentFields = () => {
// 收集文件URL
const fileUrls = fileList.value
.filter(file => file.url)
.map(file => file.url)
.filter((url): url is string => !!url);
formData.rwfj = fileUrls.join(',');
// 更新附件明细字段 - 只使用文件名,用分号分隔
const fileNames = fileList.value
.filter(file => file.url)
.map(file => file.name || `file_${Date.now()}`);
formData.fjmx = fileNames.join(';');
console.log('附件字段更新:', {
rwfj: formData.rwfj,
fjmx: formData.fjmx
});
};
// 任务类型管理函数
const addTaskType = () => {
const uniqueId = `task_type_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
taskTypes.value.push({
id: uniqueId,
rwfl: 'text',
2025-11-23 19:34:40 +08:00
items: [
{
key: `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
rwbt: '',
rwbs: true, // 默认必填项打勾
remark: ''
}
]
2025-10-07 08:58:02 +08:00
});
2025-11-23 19:34:40 +08:00
// 显示冒泡提示
showTooltip.value = true;
setTimeout(() => {
showTooltip.value = false;
}, 3000);
2025-10-07 08:58:02 +08:00
};
const removeTaskType = (typeIndex: number) => {
taskTypes.value.splice(typeIndex, 1);
};
2025-11-23 19:34:40 +08:00
// 移动任务类型
const moveTaskType = (typeIndex: number, direction: 'up' | 'down') => {
if (direction === 'up' && typeIndex > 0) {
// 上移:与上一个交换位置
const temp = taskTypes.value[typeIndex];
taskTypes.value[typeIndex] = taskTypes.value[typeIndex - 1];
taskTypes.value[typeIndex - 1] = temp;
} else if (direction === 'down' && typeIndex < taskTypes.value.length - 1) {
// 下移:与下一个交换位置
const temp = taskTypes.value[typeIndex];
taskTypes.value[typeIndex] = taskTypes.value[typeIndex + 1];
taskTypes.value[typeIndex + 1] = temp;
}
};
2025-10-07 08:58:02 +08:00
const getTaskTypeIndex = (rwfl: string) => {
return taskTypeOptions.findIndex(option => option.value === rwfl);
};
const getTaskTypeLabel = (rwfl: string) => {
const option = taskTypeOptions.find(option => option.value === rwfl);
return option ? option.label : '填写文本';
};
const onTaskTypeChange = (e: any, typeIndex: number) => {
const selectedIndex = e.detail.value;
const selectedType = taskTypeOptions[selectedIndex];
taskTypes.value[typeIndex].rwfl = selectedType.value;
2025-11-23 19:34:40 +08:00
// 保持任务项,但清空选项内容(如果不是选择题类型)
if (taskTypes.value[typeIndex].items.length === 0) {
// 如果没有任务项,创建一个默认的
taskTypes.value[typeIndex].items = [
{
key: `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
rwbt: '',
rwbs: true,
remark: ''
}
];
} else {
// 如果切换为非选择题类型,清空选项
if (selectedType.value !== 'dxxz' && selectedType.value !== 'dxsx') {
taskTypes.value[typeIndex].items[0].remark = '';
}
}
2025-10-07 08:58:02 +08:00
};
2025-11-23 19:34:40 +08:00
const toggleRequired = (taskType: TaskType) => {
if (taskType.items.length > 0) {
taskType.items[0].rwbs = !taskType.items[0].rwbs;
}
2025-10-07 08:58:02 +08:00
};
// 表单验证
const validateForm = (): boolean => {
if (!formData.rwmc.trim()) {
uni.showToast({ title: '请输入任务名称', icon: 'error' });
return false;
}
if (!formData.rwjstime) {
uni.showToast({ title: '请选择截止时间', icon: 'error' });
return false;
}
if (selectedLeaders.value.length === 0) {
uni.showToast({ title: '请选择负责人', icon: 'error' });
return false;
}
// 验证是否有任务类型
2025-11-23 19:34:40 +08:00
if (taskTypes.value.length === 0) {
uni.showToast({ title: '请至少添加一个任务方式', icon: 'error' });
2025-10-07 08:58:02 +08:00
return false;
}
2025-11-23 19:34:40 +08:00
// 验证任务项(一个任务方式对应一个任务项)
for (let i = 0; i < taskTypes.value.length; i++) {
const taskType = taskTypes.value[i];
2025-10-11 21:11:16 +08:00
const taskTypeName = getTaskTypeLabel(taskType.rwfl);
2025-11-23 19:34:40 +08:00
// 确保每个任务方式都有一个任务项
if (taskType.items.length === 0) {
uni.showToast({
title: `${taskTypeName}缺少任务项`,
icon: 'error',
duration: 2000
});
return false;
}
const item = taskType.items[0];
// 任务项的文字描述必填
if (!item.rwbt || !item.rwbt.trim()) {
uni.showToast({
title: `请填写${taskTypeName}的任务描述`,
icon: 'error',
duration: 2000
});
return false;
}
// 如果任务项打勾(必填)且是单项选择或多项选择
if (item.rwbs && (taskType.rwfl === 'dxxz' || taskType.rwfl === 'dxsx')) {
if (!item.remark || !item.remark.trim()) {
uni.showToast({
title: `${taskTypeName}的选项内容不能为空`,
icon: 'error',
duration: 2000
});
return false;
2025-10-11 21:11:16 +08:00
}
}
}
2025-10-07 08:58:02 +08:00
return true;
};
// 提交任务
const submitTask = async () => {
2025-10-11 21:11:16 +08:00
// 防抖处理
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;
}
2025-10-07 08:58:02 +08:00
if (!validateForm()) {
return;
}
try {
isSubmitting.value = true;
console.log('开始提交任务:', formData);
// 更新附件字段
updateAttachmentFields();
// 构建提交数据
const submitData = {
id: formData.id, // 编辑时需要传递ID
rwmc: formData.rwmc,
rwjstime: formData.rwjstime,
rwms: formData.rwms,
rwfzr: formData.rwfzr,
rwly: formData.rwly,
rwlyId: formData.rwlyId,
rwtsfs: formData.rwtsfs,
rwfj: formData.rwfj, // 附件URL
fjmx: formData.fjmx, // 附件明细
rwlxes: [] as any[]
};
2025-11-23 19:34:40 +08:00
// 收集所有任务类型(一个任务方式对应一个任务项)
taskTypes.value.forEach((taskType, index) => {
if (taskType.items.length > 0) {
const item = taskType.items[0];
2025-10-07 08:58:02 +08:00
if (item.rwbt.trim()) {
const taskItem: any = {
rwfl: taskType.rwfl,
rwbt: item.rwbt,
rwbs: item.rwbs ? 1 : 0,
2025-11-23 19:34:40 +08:00
remark: item.remark,
sort: index + 1 // 排序字段从1开始
2025-10-07 08:58:02 +08:00
};
// 如果任务项有ID说明是编辑现有记录需要传递ID
if (item.id) {
taskItem.id = item.id;
}
submitData.rwlxes.push(taskItem);
}
2025-11-23 19:34:40 +08:00
}
2025-10-07 08:58:02 +08:00
});
await rwSaveApi(submitData);
uni.showToast({
title: taskId.value ? '任务修改成功' : '任务创建成功',
icon: 'success'
});
// 延迟返回上一页,并刷新上一页数据
setTimeout(() => {
uni.navigateBack({
success: () => {
// 通知上一页刷新数据
uni.$emit('refreshTaskList');
}
});
}, 1500);
} catch (error) {
console.error('提交任务失败:', error);
uni.showToast({
title: '提交失败,请重试',
icon: 'error'
});
} finally {
isSubmitting.value = false;
}
};
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
</script>
<style lang="scss" scoped>
/* 第一步:基础页面布局和卡片样式 */
.addkcrw-page {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #e8f4fd 100%);
margin: 0;
padding: 0;
}
.form-container {
flex: 1;
overflow: visible;
margin: 0;
padding: 0;
.form-scroll {
height: 100%;
padding: 0;
padding-bottom: 80px;
box-sizing: border-box;
}
}
.section-card {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 0;
margin: 0 0 20px 0;
padding: 24px;
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.08),
0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: visible;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #409eff 0%, #67c23a 50%, #e6a23c 100%);
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.title-text {
font-size: 18px;
font-weight: 700;
color: #1f2937;
letter-spacing: 0.3px;
}
2025-11-23 19:34:40 +08:00
}
// 新增任务项按钮容器
.add-task-item-wrapper {
margin-top: 16px;
display: flex;
justify-content: center;
2025-10-07 08:58:02 +08:00
.add-task-type-btn {
display: flex;
align-items: center;
2025-11-23 19:34:40 +08:00
justify-content: center;
2025-10-07 08:58:02 +08:00
gap: 6px;
2025-11-23 19:34:40 +08:00
padding: 12px 24px;
2025-10-07 08:58:02 +08:00
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
color: white;
2025-11-23 19:34:40 +08:00
border-radius: 10px;
font-size: 14px;
2025-10-07 08:58:02 +08:00
font-weight: 600;
cursor: pointer;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
transition: all 0.3s ease;
2025-11-23 19:34:40 +08:00
min-width: 140px;
2025-10-07 08:58:02 +08:00
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
}
2025-11-23 19:34:40 +08:00
&:active {
transform: translateY(0);
}
2025-10-07 08:58:02 +08:00
.add-icon {
2025-11-23 19:34:40 +08:00
font-size: 18px;
2025-10-07 08:58:02 +08:00
font-weight: bold;
}
2025-11-23 19:34:40 +08:00
.add-text {
font-size: 14px;
}
2025-10-07 08:58:02 +08:00
}
}
}
.form-item {
display: flex;
align-items: flex-start;
margin-bottom: 20px;
.item-label {
min-width: 90px;
font-size: 15px;
color: #374151;
line-height: 44px;
font-weight: 600;
text-align: left;
&.required::after {
content: '*';
color: #ef4444;
margin-left: 4px;
font-weight: bold;
}
}
.item-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
font-size: 15px;
background-color: #ffffff;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
&:focus {
border-color: #409eff;
outline: none;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
transform: translateY(-1px);
}
}
.item-textarea {
flex: 1;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
font-size: 15px;
min-height: 67px;
background-color: #ffffff;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
resize: vertical;
&:focus {
border-color: #409eff;
outline: none;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
}
}
.item-picker {
flex: 1;
.picker-content {
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
background-color: #ffffff;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
cursor: pointer;
&:hover {
border-color: #409eff;
transform: translateY(-1px);
}
.picker-text {
font-size: 15px;
color: #374151;
&.placeholder {
color: #9ca3af;
}
}
}
}
}
// 附件上传区域
.attachment-item {
flex-direction: column;
align-items: stretch;
.item-label {
margin-bottom: 12px;
line-height: 1.4;
}
.attachment-container {
width: 100%;
}
}
// 任务类型区域
.task-type-section {
margin-bottom: 24px;
padding: 20px;
border: 2px solid #e5e7eb;
border-radius: 12px;
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
transition: all 0.3s ease;
position: relative;
2025-11-23 19:34:40 +08:00
width: 100%;
box-sizing: border-box;
overflow: visible;
2025-10-07 08:58:02 +08:00
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
}
.task-type-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.task-type-selector {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
.selector-label {
font-size: 15px;
color: #374151;
min-width: 80px;
font-weight: 600;
}
.type-picker {
flex: 1;
max-width: 180px;
.picker-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background-color: #ffffff;
font-size: 14px;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
border-color: #409eff;
transform: translateY(-1px);
}
.picker-arrow {
color: #9ca3af;
font-size: 12px;
}
}
}
}
2025-11-23 19:34:40 +08:00
.task-type-actions {
display: flex;
align-items: center;
gap: 8px;
margin-right: 32px; // 为删除按钮留出空间
.move-buttons {
display: flex;
align-items: center;
gap: 4px;
.move-btn {
width: 28px;
height: 28px;
background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%);
color: white;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
font-weight: bold;
box-shadow: 0 2px 6px rgba(103, 194, 58, 0.3);
transition: all 0.3s ease;
border: 2px solid #ffffff;
&:hover {
transform: scale(1.1);
box-shadow: 0 4px 10px rgba(103, 194, 58, 0.4);
}
&:active {
transform: scale(0.95);
}
.move-icon {
font-size: 14px;
font-weight: bold;
line-height: 1;
}
}
}
}
2025-10-07 08:58:02 +08:00
.delete-type-btn {
position: absolute;
top: -8px;
right: -8px;
width: 24px;
height: 24px;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
font-weight: bold;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
transition: all 0.3s ease;
z-index: 10;
border: 2px solid #ffffff;
&:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
}
}
}
// 任务项列表
.task-items-container {
2025-11-23 19:34:40 +08:00
width: 100%;
box-sizing: border-box;
.task-item-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
2025-10-07 08:58:02 +08:00
2025-11-23 19:34:40 +08:00
.item-number {
width: 28px;
height: 28px;
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
2025-10-07 08:58:02 +08:00
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
2025-11-23 19:34:40 +08:00
font-size: 12px;
2025-10-07 08:58:02 +08:00
font-weight: bold;
2025-11-23 19:34:40 +08:00
flex-shrink: 0;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
}
.item-title-input {
flex: 1;
padding: 10px 14px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
2025-10-07 08:58:02 +08:00
transition: all 0.3s ease;
2025-11-23 19:34:40 +08:00
&:focus {
border-color: #409eff;
outline: none;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
2025-10-07 08:58:02 +08:00
}
}
2025-11-23 19:34:40 +08:00
.item-actions {
2025-10-07 08:58:02 +08:00
display: flex;
align-items: center;
gap: 12px;
2025-11-23 19:34:40 +08:00
.required-checkbox {
position: relative;
2025-10-07 08:58:02 +08:00
display: flex;
align-items: center;
2025-11-23 19:34:40 +08:00
cursor: pointer;
padding: 4px;
border-radius: 6px;
2025-10-07 08:58:02 +08:00
transition: all 0.3s ease;
2025-11-23 19:34:40 +08:00
&:hover {
background-color: rgba(64, 158, 255, 0.05);
2025-10-07 08:58:02 +08:00
}
2025-11-23 19:34:40 +08:00
.checkbox-box {
width: 18px;
height: 18px;
border: 2px solid #d1d5db;
border-radius: 4px;
2025-10-07 08:58:02 +08:00
display: flex;
align-items: center;
2025-11-23 19:34:40 +08:00
justify-content: center;
2025-10-07 08:58:02 +08:00
transition: all 0.3s ease;
2025-11-23 19:34:40 +08:00
&.checked {
background-color: #409eff;
border-color: #409eff;
2025-10-07 08:58:02 +08:00
2025-11-23 19:34:40 +08:00
.check-icon {
color: white;
font-size: 12px;
font-weight: bold;
2025-10-07 08:58:02 +08:00
}
}
2025-11-23 19:34:40 +08:00
}
// 冒泡提示样式
.tooltip {
position: absolute;
top: -45px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
z-index: 1000;
animation: tooltipFadeIn 0.3s ease-out;
2025-10-07 08:58:02 +08:00
2025-11-23 19:34:40 +08:00
&::after {
content: '';
2025-10-07 08:58:02 +08:00
position: absolute;
2025-11-23 19:34:40 +08:00
top: 100%;
2025-10-07 08:58:02 +08:00
left: 50%;
transform: translateX(-50%);
2025-11-23 19:34:40 +08:00
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.8);
2025-10-07 08:58:02 +08:00
}
}
}
}
}
2025-11-23 19:34:40 +08:00
.item-options {
margin-top: 12px;
width: 100%;
box-sizing: border-box;
overflow: hidden;
2025-10-07 08:58:02 +08:00
2025-11-23 19:34:40 +08:00
.options-textarea {
width: 100%;
max-width: 100%;
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 13px;
min-height: 80px;
background-color: #ffffff;
transition: all 0.3s ease;
resize: vertical;
box-sizing: border-box;
overflow-wrap: break-word;
word-wrap: break-word;
overflow-x: hidden;
overflow-y: auto;
&:focus {
border-color: #409eff;
outline: none;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
background-color: #ffffff;
}
2025-10-07 08:58:02 +08:00
}
}
}
// 单选按钮组
.radio-group {
flex: 1;
display: flex;
gap: 20px;
.radio-item {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
background-color: rgba(64, 158, 255, 0.05);
}
.radio-dot {
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 50%;
position: relative;
transition: all 0.3s ease;
&.checked {
border-color: #409eff;
background-color: #409eff;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
background-color: #ffffff;
border-radius: 50%;
}
}
}
.radio-text {
font-size: 15px;
color: #374151;
font-weight: 500;
}
}
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
border-top: 1px solid #e5e5e5;
padding: 12px 16px;
z-index: 999;
.action-buttons {
display: flex;
gap: 12px;
.cancel-btn, .submit-btn {
flex: 1;
height: 40px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: translateY(1px);
}
}
.cancel-btn {
background-color: #909399;
color: #fff;
&:hover {
background-color: #82848a;
}
}
.submit-btn {
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
color: #fff;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
&:hover:not(:disabled) {
background: linear-gradient(135deg, #3a8ee6 0%, #337ecc 100%);
}
}
}
}
// 冒泡提示动画
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(-5px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
// 响应式调整
@media screen and (max-width: 375px) {
.form-item {
flex-direction: column;
align-items: stretch;
.item-label {
margin-bottom: 8px;
line-height: 1.4;
}
}
.task-type-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.task-item-header {
flex-wrap: wrap;
}
}
2025-10-11 21:11:16 +08:00
// 提交遮罩层样式
.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;
}
2025-10-07 08:58:02 +08:00
</style>