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

1598 lines
40 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="addkcrw-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 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>
<!-- 附件上传 -->
<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>
</view>
</view>
<!-- 任务下发设置区域 -->
<view class="section-card">
<view class="section-title">
<text class="title-text">任务下发设置</text>
</view>
<!-- 任务类型列表 -->
<view v-for="(taskType, typeIndex) in taskTypes" :key="taskType.id" class="task-type-section">
<!-- 任务类型选择 -->
<view class="task-type-header">
<view class="task-type-selector">
<text class="selector-label">任务类型</text>
<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>
<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>
</view>
</view>
<!-- 任务项一个任务类型对应一个任务项 -->
<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>
</view>
<view v-if="showTooltip" class="tooltip">默认必填</view>
</view>
</view>
</view>
<!-- 选择题选项 -->
<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"
/>
</view>
</view>
</view>
<!-- 新增任务项按钮 -->
<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>
</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);
const submitTimer = ref<any>(null); // 防抖定时器
// 冒泡提示显示状态
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' },
{ label: '富文本', value: 'fwb' },
{ 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',
items: [
{
key: `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
rwbt: '',
rwbs: true, // 默认必填项打勾
remark: ''
}
]
}
]);
// 页面加载
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) {
// 先按 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;
}
// sort 相同时按 id 排序
return (a.id || '').localeCompare(b.id || '');
});
// 构建任务类型列表(每个任务类型只保留第一个任务项,保持排序)
// 由于前端显示时一个任务类型对应一个任务项,所以每个 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 || ''
}
]
};
});
}
// 处理附件数据
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',
items: [
{
key: `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
rwbt: '',
rwbs: true, // 默认必填项打勾
remark: ''
}
]
});
// 显示冒泡提示
showTooltip.value = true;
setTimeout(() => {
showTooltip.value = false;
}, 3000);
};
const removeTaskType = (typeIndex: number) => {
taskTypes.value.splice(typeIndex, 1);
};
// 移动任务类型
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;
}
};
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;
// 保持任务项,但清空选项内容(如果不是选择题类型)
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 = '';
}
}
};
const toggleRequired = (taskType: TaskType) => {
if (taskType.items.length > 0) {
taskType.items[0].rwbs = !taskType.items[0].rwbs;
}
};
// 表单验证
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;
}
// 验证是否有任务类型
if (taskTypes.value.length === 0) {
uni.showToast({ title: '请至少添加一个任务方式', icon: 'error' });
return false;
}
// 验证任务项(一个任务方式对应一个任务项)
for (let i = 0; i < taskTypes.value.length; i++) {
const taskType = taskTypes.value[i];
const taskTypeName = getTaskTypeLabel(taskType.rwfl);
// 确保每个任务方式都有一个任务项
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;
}
}
}
return true;
};
// 提交任务
const submitTask = async () => {
// 防抖处理
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;
}
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[]
};
// 收集所有任务类型(一个任务方式对应一个任务项)
taskTypes.value.forEach((taskType, index) => {
if (taskType.items.length > 0) {
const item = taskType.items[0];
if (item.rwbt.trim()) {
const taskItem: any = {
rwfl: taskType.rwfl,
rwbt: item.rwbt,
rwbs: item.rwbs ? 1 : 0,
remark: item.remark,
sort: index + 1 // 排序字段从1开始
};
// 如果任务项有ID说明是编辑现有记录需要传递ID
if (item.id) {
taskItem.id = item.id;
}
submitData.rwlxes.push(taskItem);
}
}
});
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;
}
}
// 新增任务项按钮容器
.add-task-item-wrapper {
margin-top: 16px;
display: flex;
justify-content: center;
.add-task-type-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 24px;
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
color: white;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
transition: all 0.3s ease;
min-width: 140px;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
}
&:active {
transform: translateY(0);
}
.add-icon {
font-size: 18px;
font-weight: bold;
}
.add-text {
font-size: 14px;
}
}
}
}
.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;
width: 100%;
box-sizing: border-box;
overflow: visible;
&: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;
}
}
}
}
.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;
}
}
}
}
.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 {
width: 100%;
box-sizing: border-box;
.task-item-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.item-number {
width: 28px;
height: 28px;
background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
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;
transition: all 0.3s ease;
&:focus {
border-color: #409eff;
outline: none;
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
}
}
.item-actions {
display: flex;
align-items: center;
gap: 12px;
.required-checkbox {
position: relative;
display: flex;
align-items: center;
cursor: pointer;
padding: 4px;
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
background-color: rgba(64, 158, 255, 0.05);
}
.checkbox-box {
width: 18px;
height: 18px;
border: 2px solid #d1d5db;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.checked {
background-color: #409eff;
border-color: #409eff;
.check-icon {
color: white;
font-size: 12px;
font-weight: bold;
}
}
}
// 冒泡提示样式
.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;
&::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.8);
}
}
}
}
}
.item-options {
margin-top: 12px;
width: 100%;
box-sizing: border-box;
overflow: hidden;
.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;
}
}
}
}
// 单选按钮组
.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;
}
}
// 提交遮罩层样式
.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>