2025-11-05 14:18:51 +08:00
|
|
|
|
<template>
|
2025-11-05 15:52:46 +08:00
|
|
|
|
|
2025-11-05 14:18:51 +08:00
|
|
|
|
|
|
|
|
|
|
<scroll-view class="content-scroll" scroll-y>
|
|
|
|
|
|
<!-- 任务状态栏 -->
|
|
|
|
|
|
<view class="status-section">
|
|
|
|
|
|
<view class="task-info">
|
|
|
|
|
|
<text class="task-name">{{ task.name }}</text>
|
|
|
|
|
|
<text class="project-name">{{ task.project }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="status-tags">
|
2025-11-05 16:20:12 +08:00
|
|
|
|
<uv-tags
|
2025-11-05 14:18:51 +08:00
|
|
|
|
v-for="(status, index) in task.statusTags"
|
|
|
|
|
|
:key="index"
|
2025-11-05 16:20:12 +08:00
|
|
|
|
:text="status"
|
|
|
|
|
|
:type="getTagType(status)"
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
:plain="false"
|
|
|
|
|
|
:custom-style="getTagStyle(status)"
|
|
|
|
|
|
></uv-tags>
|
2025-11-05 14:18:51 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-05 16:20:12 +08:00
|
|
|
|
<!-- 基本信息区域 -->
|
2025-11-05 14:18:51 +08:00
|
|
|
|
<view class="basic-info">
|
|
|
|
|
|
<view class="info-item">
|
|
|
|
|
|
<text class="info-label">截止时间:</text>
|
|
|
|
|
|
<text class="info-value">{{ task.deadline }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="info-item">
|
|
|
|
|
|
<text class="info-label">创建人:</text>
|
2025-11-06 15:27:11 +08:00
|
|
|
|
<view class="info-value-with-avatar">
|
|
|
|
|
|
<image
|
|
|
|
|
|
v-if="task.creatorAvatar"
|
|
|
|
|
|
:src="task.creatorAvatar"
|
|
|
|
|
|
class="creator-avatar"
|
|
|
|
|
|
mode="aspectFill"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<text>{{ task.creator }}</text>
|
|
|
|
|
|
</view>
|
2025-11-05 14:18:51 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="info-item">
|
|
|
|
|
|
<text class="info-label">负责人:</text>
|
|
|
|
|
|
<text class="info-value">{{ task.responsible }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 标签切换 -->
|
|
|
|
|
|
<view class="tab-container">
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ active: activeTab === 'info' }"
|
|
|
|
|
|
@click="switchTab('info')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text>任务信息</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ active: activeTab === 'records' }"
|
|
|
|
|
|
@click="switchTab('records')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text>提交记录</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 任务信息标签页 -->
|
|
|
|
|
|
<view class="tab-content" v-if="activeTab === 'info'">
|
|
|
|
|
|
<view class="task-info-card">
|
|
|
|
|
|
<view class="publish-time-row">
|
|
|
|
|
|
<text class="clock-icon">🕐</text>
|
|
|
|
|
|
<text class="publish-time-text">发布时间:{{ task.publishTime }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="task-content-wrapper">
|
|
|
|
|
|
<text class="task-content-text">{{ task.content }}</text>
|
2025-11-06 16:49:06 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<!-- 任务图片展示 -->
|
|
|
|
|
|
<view class="task-images-wrapper" v-if="task.pictures && task.pictures.length > 0">
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="task-image-item"
|
|
|
|
|
|
v-for="(imageUrl, imgIndex) in task.pictures"
|
|
|
|
|
|
:key="imgIndex"
|
|
|
|
|
|
@click="previewTaskImages(task.pictures, imgIndex)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<image
|
|
|
|
|
|
:src="imageUrl"
|
|
|
|
|
|
mode="aspectFill"
|
|
|
|
|
|
class="task-image"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
2025-11-05 14:18:51 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="delay-btn-wrapper">
|
|
|
|
|
|
<uv-button type="error" size="small" @click="applyDelay">申请延期</uv-button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 提交记录标签页 -->
|
|
|
|
|
|
<view class="tab-content" v-if="activeTab === 'records'" @click="closeMenu">
|
|
|
|
|
|
<view class="no-record" v-if="task.submitRecords.length === 0">
|
|
|
|
|
|
<text>暂无提交记录</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="submit-record-card" v-for="(record, index) in task.submitRecords" :key="index">
|
|
|
|
|
|
<view class="record-header">
|
|
|
|
|
|
<view class="user-info">
|
2025-11-06 15:27:11 +08:00
|
|
|
|
<image
|
|
|
|
|
|
v-if="record.userAvatar"
|
|
|
|
|
|
:src="record.userAvatar"
|
|
|
|
|
|
class="avatar-img"
|
|
|
|
|
|
mode="aspectFill"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<view v-else class="avatar-placeholder"></view>
|
2025-11-05 14:18:51 +08:00
|
|
|
|
<text class="user-name">{{ record.userName }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="record-header-right">
|
|
|
|
|
|
<text class="record-time">{{ record.time }}</text>
|
2025-11-13 16:32:34 +08:00
|
|
|
|
<!-- <view class="more-menu" v-if="record.canEdit" @click.stop="toggleMenu(index)">-->
|
|
|
|
|
|
<!-- <text class="more-icon">⋮</text>-->
|
|
|
|
|
|
<!-- <view class="menu-dropdown" v-if="showMenuIndex === index" @click.stop>-->
|
|
|
|
|
|
<!-- <view class="menu-item" @click="editRecord(index)">-->
|
|
|
|
|
|
<!-- <text>编辑</text>-->
|
|
|
|
|
|
<!-- </view>-->
|
|
|
|
|
|
<!-- <view class="menu-item" @click="deleteRecord(index)">-->
|
|
|
|
|
|
<!-- <text>删除</text>-->
|
|
|
|
|
|
<!-- </view>-->
|
|
|
|
|
|
<!-- </view>-->
|
|
|
|
|
|
<!-- </view>-->
|
2025-11-05 14:18:51 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-11-05 15:23:52 +08:00
|
|
|
|
<view class="record-content-wrapper" v-if="record.content">
|
2025-11-05 14:18:51 +08:00
|
|
|
|
<text class="record-content-text">{{ record.content }}</text>
|
|
|
|
|
|
</view>
|
2025-11-05 15:23:52 +08:00
|
|
|
|
<view class="record-progress" v-if="record.progress !== null && record.progress !== undefined">
|
|
|
|
|
|
<text class="progress-label">任务进度:</text>
|
|
|
|
|
|
<text class="progress-value">{{ record.progress }}%</text>
|
|
|
|
|
|
</view>
|
2025-11-06 16:49:06 +08:00
|
|
|
|
<!-- 图片附件展示(一行三个) -->
|
|
|
|
|
|
<view class="record-images-wrapper" v-if="record.imageAttachments && record.imageAttachments.length > 0">
|
2025-11-05 14:18:51 +08:00
|
|
|
|
<view
|
2025-11-06 16:49:06 +08:00
|
|
|
|
class="record-image-item"
|
|
|
|
|
|
v-for="(imageUrl, imgIndex) in record.imageAttachments"
|
|
|
|
|
|
:key="imgIndex"
|
|
|
|
|
|
@click="previewRecordImages(record.imageAttachments, imgIndex)"
|
2025-11-05 14:18:51 +08:00
|
|
|
|
>
|
2025-11-05 15:23:52 +08:00
|
|
|
|
<image
|
2025-11-06 16:49:06 +08:00
|
|
|
|
:src="imageUrl"
|
2025-11-05 15:23:52 +08:00
|
|
|
|
mode="aspectFill"
|
2025-11-06 16:49:06 +08:00
|
|
|
|
class="record-image"
|
2025-11-05 15:23:52 +08:00
|
|
|
|
/>
|
2025-11-06 16:49:06 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- 文件附件展示 -->
|
|
|
|
|
|
<view class="record-files-wrapper" v-if="record.fileAttachments && record.fileAttachments.length > 0">
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="file-attachment-item"
|
|
|
|
|
|
v-for="(file, fileIndex) in record.fileAttachments"
|
|
|
|
|
|
:key="fileIndex"
|
2025-11-06 18:01:41 +08:00
|
|
|
|
@click="previewRecordFile(file)"
|
2025-11-06 16:49:06 +08:00
|
|
|
|
>
|
2025-11-06 18:01:41 +08:00
|
|
|
|
<text class="file-icon">{{ getFileIcon(file.name) }}</text>
|
|
|
|
|
|
<view class="file-info">
|
|
|
|
|
|
<text class="file-name">{{ file.name }}</text>
|
|
|
|
|
|
<text class="file-size" v-if="file.size > 0">{{ formatFileSize(file.size) }}</text>
|
|
|
|
|
|
</view>
|
2025-11-05 14:18:51 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="delay-btn-wrapper" v-if="record.showDelayBtn">
|
|
|
|
|
|
<uv-button type="error" size="small" @click="applyDelay">申请延期</uv-button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 底部操作按钮 -->
|
|
|
|
|
|
<view class="action-buttons">
|
|
|
|
|
|
<view class="btn-wrapper">
|
|
|
|
|
|
<uv-button type="primary" size="normal" @click="completeTask">完成任务</uv-button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="btn-wrapper">
|
|
|
|
|
|
<uv-button type="primary" size="normal" @click="submitTask">提交任务</uv-button>
|
|
|
|
|
|
</view>
|
2025-11-05 15:23:52 +08:00
|
|
|
|
|
2025-11-05 14:18:51 +08:00
|
|
|
|
</view>
|
2025-11-05 15:52:46 +08:00
|
|
|
|
|
2025-11-05 14:18:51 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-11-05 15:23:52 +08:00
|
|
|
|
import { ref, onMounted, } from 'vue';
|
|
|
|
|
|
import { onLoad,onShow } from '@dcloudio/uni-app';
|
2025-11-05 16:20:12 +08:00
|
|
|
|
import { getStatusFromTagText, getTaskStatusType, getTaskStatusStyle } from '@/utils/taskConfig.js';
|
2025-11-06 14:33:07 +08:00
|
|
|
|
import { useTaskStore } from '@/store/task';
|
2025-11-12 15:33:53 +08:00
|
|
|
|
import { getTaskDetail } from '@/api';
|
2025-11-05 14:18:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 当前激活的标签
|
|
|
|
|
|
const activeTab = ref('info');
|
|
|
|
|
|
const showMenuIndex = ref(-1);
|
|
|
|
|
|
|
2025-11-05 15:52:46 +08:00
|
|
|
|
// 格式化时间为中文格式:年月日星期几时分秒
|
|
|
|
|
|
const formatTimeToChinese = (date) => {
|
2025-11-06 15:27:11 +08:00
|
|
|
|
if (!date) return '';
|
2025-11-05 15:52:46 +08:00
|
|
|
|
if (typeof date === 'string') {
|
|
|
|
|
|
// 如果是字符串,尝试解析
|
|
|
|
|
|
date = new Date(date);
|
|
|
|
|
|
}
|
2025-11-06 15:27:11 +08:00
|
|
|
|
if (isNaN(date.getTime())) return '';
|
2025-11-05 15:52:46 +08:00
|
|
|
|
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
|
const weekday = weekdays[date.getDay()];
|
|
|
|
|
|
const hour = String(date.getHours()).padStart(2, '0');
|
|
|
|
|
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
|
|
|
|
const second = String(date.getSeconds()).padStart(2, '0');
|
|
|
|
|
|
|
|
|
|
|
|
return `${year}年${month}月${day}日 ${weekday} ${hour}:${minute}:${second}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 15:27:11 +08:00
|
|
|
|
// 格式化日期:将 "2024-10-31 23:59:59" 转换为 "2024-10-31"
|
|
|
|
|
|
const formatDate = (dateStr) => {
|
|
|
|
|
|
if (!dateStr) return '';
|
|
|
|
|
|
// 如果包含空格,取日期部分
|
|
|
|
|
|
return dateStr.split(' ')[0];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化日期时间:格式化为 yyyy-MM-dd HH:mm:ss
|
|
|
|
|
|
const formatDateTime = (date) => {
|
|
|
|
|
|
if (!date) return '';
|
|
|
|
|
|
const d = new Date(date);
|
|
|
|
|
|
if (isNaN(d.getTime())) return '';
|
|
|
|
|
|
const year = d.getFullYear();
|
|
|
|
|
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
|
|
|
|
const hours = String(d.getHours()).padStart(2, '0');
|
|
|
|
|
|
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
|
|
|
|
const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
|
|
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 计算剩余天数
|
|
|
|
|
|
const calculateRemainingDays = (expireTime) => {
|
|
|
|
|
|
if (!expireTime) return null;
|
|
|
|
|
|
const expireDate = new Date(expireTime);
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
now.setHours(0, 0, 0, 0);
|
|
|
|
|
|
expireDate.setHours(0, 0, 0, 0);
|
|
|
|
|
|
const diffTime = expireDate.getTime() - now.getTime();
|
|
|
|
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
|
|
return diffDays;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 根据过期时间和状态判断任务状态
|
|
|
|
|
|
const determineTaskStatus = (status, expireTime) => {
|
|
|
|
|
|
// 如果任务已完成(状态为4),直接返回 completed
|
|
|
|
|
|
const taskStatus = status !== undefined ? status : null;
|
|
|
|
|
|
const isCompleted = taskStatus === 4 ||
|
|
|
|
|
|
taskStatus === '4' ||
|
|
|
|
|
|
taskStatus === 'completed' ||
|
|
|
|
|
|
String(taskStatus) === '4';
|
|
|
|
|
|
|
|
|
|
|
|
if (isCompleted) {
|
|
|
|
|
|
return 'completed';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有过期时间,返回待完成
|
|
|
|
|
|
if (!expireTime) {
|
|
|
|
|
|
return 'pending';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const expireDate = new Date(expireTime);
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
|
|
|
|
// 设置时间到当天0点,便于日期比较
|
|
|
|
|
|
now.setHours(0, 0, 0, 0);
|
|
|
|
|
|
expireDate.setHours(23, 59, 59, 999);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已过期,标记为逾期
|
|
|
|
|
|
if (expireDate.getTime() < now.getTime()) {
|
|
|
|
|
|
return 'overdue';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算距离过期的天数
|
|
|
|
|
|
const diffTime = expireDate.getTime() - now.getTime();
|
|
|
|
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
|
|
|
|
|
|
|
|
// 如果3天内到期,标记为即将逾期
|
|
|
|
|
|
if (diffDays <= 3 && diffDays > 0) {
|
|
|
|
|
|
return 'imminent';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 否则返回待完成状态
|
|
|
|
|
|
return 'pending';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取状态标签数组
|
|
|
|
|
|
const getStatusTags = (status, expireTime) => {
|
|
|
|
|
|
const taskStatus = determineTaskStatus(status, expireTime);
|
|
|
|
|
|
const tags = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (taskStatus === 'completed') {
|
|
|
|
|
|
tags.push('已完成');
|
|
|
|
|
|
} else if (taskStatus === 'overdue') {
|
|
|
|
|
|
tags.push('已逾期', '紧急');
|
|
|
|
|
|
} else if (taskStatus === 'imminent') {
|
|
|
|
|
|
tags.push('即将逾期');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tags.push('待完成');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return tags;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 提取负责人:从 memberList 中提取所有成员的名称
|
|
|
|
|
|
const getOwnerNames = (memberList) => {
|
|
|
|
|
|
if (!Array.isArray(memberList) || memberList.length === 0) return '';
|
|
|
|
|
|
return memberList.map(member => member.userName || member.name || '').filter(name => name).join('、');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 解析逗号分隔的URL字符串
|
|
|
|
|
|
const parseAttachUrls = (attachStr) => {
|
|
|
|
|
|
if (!attachStr) return [];
|
|
|
|
|
|
if (typeof attachStr !== 'string') return [];
|
|
|
|
|
|
|
|
|
|
|
|
// 按逗号分割,过滤空值
|
|
|
|
|
|
return attachStr.split(',').map(url => url.trim()).filter(url => url);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 判断URL是否为图片
|
|
|
|
|
|
const isImageUrl = (url) => {
|
|
|
|
|
|
if (!url || typeof url !== 'string') return false;
|
|
|
|
|
|
return /\.(jpg|jpeg|png|gif|bmp|webp)(\?|$)/i.test(url);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 15:27:11 +08:00
|
|
|
|
// 转换提交记录数据
|
|
|
|
|
|
const transformSubmitRecords = (submitList) => {
|
2025-11-13 16:52:06 +08:00
|
|
|
|
if (!Array.isArray(submitList) || submitList.length === 0) {
|
2025-11-06 15:27:11 +08:00
|
|
|
|
return [];
|
|
|
|
|
|
}
|
2025-11-13 16:52:06 +08:00
|
|
|
|
|
|
|
|
|
|
const sortedList = [...submitList].sort((a, b) => {
|
|
|
|
|
|
const timeA = a?.createTime ? new Date(a.createTime).getTime() : 0;
|
|
|
|
|
|
const timeB = b?.createTime ? new Date(b.createTime).getTime() : 0;
|
|
|
|
|
|
return timeB - timeA;
|
|
|
|
|
|
});
|
2025-11-06 15:27:11 +08:00
|
|
|
|
|
2025-11-13 16:52:06 +08:00
|
|
|
|
return sortedList.map(item => {
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 处理附件:可能是逗号分隔的URL字符串
|
|
|
|
|
|
let imageAttachments = [];
|
|
|
|
|
|
let fileAttachments = [];
|
|
|
|
|
|
|
2025-11-06 15:27:11 +08:00
|
|
|
|
if (item.attaches) {
|
|
|
|
|
|
try {
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 先尝试作为JSON解析
|
|
|
|
|
|
let attachData = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
attachData = typeof item.attaches === 'string' ? JSON.parse(item.attaches) : item.attaches;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 如果不是JSON,则作为逗号分隔的URL字符串处理
|
|
|
|
|
|
attachData = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 15:27:11 +08:00
|
|
|
|
if (Array.isArray(attachData)) {
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 如果是数组格式
|
|
|
|
|
|
attachData.forEach(att => {
|
2025-11-06 15:27:11 +08:00
|
|
|
|
const filePath = att.path || att.url || att.filePath || '';
|
2025-11-06 16:49:06 +08:00
|
|
|
|
const fileName = att.name || att.fileName || '';
|
|
|
|
|
|
const isImage = fileName ? /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName) : isImageUrl(filePath);
|
2025-11-06 15:27:11 +08:00
|
|
|
|
|
2025-11-06 16:49:06 +08:00
|
|
|
|
if (isImage && filePath) {
|
|
|
|
|
|
imageAttachments.push(filePath);
|
|
|
|
|
|
} else if (filePath) {
|
|
|
|
|
|
fileAttachments.push({
|
|
|
|
|
|
name: fileName || '文件',
|
|
|
|
|
|
path: filePath
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 作为逗号分隔的URL字符串处理
|
|
|
|
|
|
const urls = parseAttachUrls(item.attaches);
|
|
|
|
|
|
urls.forEach(url => {
|
|
|
|
|
|
if (isImageUrl(url)) {
|
|
|
|
|
|
imageAttachments.push(url);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 从URL中提取文件名
|
|
|
|
|
|
const fileName = url.split('/').pop().split('?')[0] || '文件';
|
|
|
|
|
|
fileAttachments.push({
|
|
|
|
|
|
name: fileName,
|
|
|
|
|
|
path: url
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-06 15:27:11 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('解析附件数据失败:', e);
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 如果解析失败,尝试作为逗号分隔的URL字符串处理
|
|
|
|
|
|
const urls = parseAttachUrls(item.attaches);
|
|
|
|
|
|
urls.forEach(url => {
|
|
|
|
|
|
if (isImageUrl(url)) {
|
|
|
|
|
|
imageAttachments.push(url);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-06 15:27:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: item.id || '',
|
|
|
|
|
|
userName: item.userName || '',
|
|
|
|
|
|
userAvatar: item.userAvatar || '',
|
|
|
|
|
|
time: formatTimeToChinese(item.createTime) || '',
|
2025-11-06 18:01:41 +08:00
|
|
|
|
content: item.remark || '', // 如果没有提交内容,可能显示任务描述
|
2025-11-06 15:27:11 +08:00
|
|
|
|
progress: null, // API 返回的数据中没有进度字段
|
2025-11-06 16:49:06 +08:00
|
|
|
|
imageAttachments: imageAttachments,
|
|
|
|
|
|
fileAttachments: fileAttachments,
|
2025-11-06 15:27:11 +08:00
|
|
|
|
showDelayBtn: false, // 根据业务需求决定是否显示
|
|
|
|
|
|
canEdit: true // 根据业务需求决定是否可以编辑
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-05 14:18:51 +08:00
|
|
|
|
// 任务数据
|
|
|
|
|
|
const task = ref({
|
2025-11-06 14:33:07 +08:00
|
|
|
|
|
2025-11-05 14:18:51 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 切换标签
|
|
|
|
|
|
const switchTab = (tab) => {
|
|
|
|
|
|
activeTab.value = tab;
|
|
|
|
|
|
showMenuIndex.value = -1; // 关闭菜单
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 切换菜单显示
|
|
|
|
|
|
const toggleMenu = (index) => {
|
|
|
|
|
|
showMenuIndex.value = showMenuIndex.value === index ? -1 : index;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭菜单
|
|
|
|
|
|
const closeMenu = () => {
|
|
|
|
|
|
showMenuIndex.value = -1;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 编辑记录
|
|
|
|
|
|
const editRecord = (index) => {
|
2025-11-05 15:56:46 +08:00
|
|
|
|
const record = task.value.submitRecords[index];
|
|
|
|
|
|
if (!record) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '记录不存在',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
showMenuIndex.value = -1;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将编辑数据存储到本地,供提交任务页面使用
|
|
|
|
|
|
uni.setStorageSync('editSubmitRecord', {
|
|
|
|
|
|
recordIndex: index,
|
|
|
|
|
|
record: record,
|
|
|
|
|
|
taskId: task.value.id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 跳转到提交任务页面
|
|
|
|
|
|
uni.navigateTo({
|
2025-11-07 11:40:13 +08:00
|
|
|
|
url: `/pages/task/submit/index?taskId=${task.value.id}&mode=edit&recordIndex=${index}`
|
2025-11-05 14:18:51 +08:00
|
|
|
|
});
|
2025-11-05 15:56:46 +08:00
|
|
|
|
|
2025-11-05 14:18:51 +08:00
|
|
|
|
showMenuIndex.value = -1;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 删除记录
|
|
|
|
|
|
const deleteRecord = (index) => {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '确定要删除这条记录吗?',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
task.value.submitRecords.splice(index, 1);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '已删除',
|
|
|
|
|
|
icon: 'success'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
showMenuIndex.value = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 预览任务图片
|
|
|
|
|
|
const previewTaskImages = (imageUrls, index) => {
|
|
|
|
|
|
if (imageUrls && imageUrls.length > 0) {
|
|
|
|
|
|
uni.previewImage({
|
|
|
|
|
|
urls: imageUrls,
|
|
|
|
|
|
current: index
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 预览提交记录图片
|
|
|
|
|
|
const previewRecordImages = (imageUrls, index) => {
|
|
|
|
|
|
if (imageUrls && imageUrls.length > 0) {
|
2025-11-05 15:23:52 +08:00
|
|
|
|
uni.previewImage({
|
|
|
|
|
|
urls: imageUrls,
|
2025-11-06 16:49:06 +08:00
|
|
|
|
current: index
|
2025-11-05 15:23:52 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 18:01:41 +08:00
|
|
|
|
// 获取文件图标
|
|
|
|
|
|
const getFileIcon = (fileName) => {
|
|
|
|
|
|
if (!fileName) return '📄';
|
|
|
|
|
|
const ext = fileName.split('.').pop().toLowerCase();
|
|
|
|
|
|
const iconMap = {
|
|
|
|
|
|
'pdf': '📕',
|
|
|
|
|
|
'doc': '📘',
|
|
|
|
|
|
'docx': '📘',
|
|
|
|
|
|
'xls': '📗',
|
|
|
|
|
|
'xlsx': '📗',
|
|
|
|
|
|
'ppt': '📙',
|
|
|
|
|
|
'pptx': '📙',
|
|
|
|
|
|
'txt': '📄',
|
|
|
|
|
|
'zip': '📦',
|
|
|
|
|
|
'rar': '📦',
|
|
|
|
|
|
'jpg': '🖼️',
|
|
|
|
|
|
'jpeg': '🖼️',
|
|
|
|
|
|
'png': '🖼️',
|
|
|
|
|
|
'gif': '🖼️'
|
|
|
|
|
|
};
|
|
|
|
|
|
return iconMap[ext] || '📄';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化文件大小
|
|
|
|
|
|
const formatFileSize = (bytes) => {
|
|
|
|
|
|
if (!bytes || bytes === 0) return '';
|
|
|
|
|
|
const k = 1024;
|
|
|
|
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 预览/下载提交记录中的文件
|
|
|
|
|
|
const previewRecordFile = (file) => {
|
|
|
|
|
|
if (!file.path) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '文件路径不存在',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是图片,使用预览图片功能
|
|
|
|
|
|
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
|
|
|
|
const ext = file.name ? file.name.split('.').pop().toLowerCase() : '';
|
|
|
|
|
|
|
|
|
|
|
|
if (imageExts.includes(ext)) {
|
|
|
|
|
|
uni.previewImage({
|
|
|
|
|
|
urls: [file.path],
|
|
|
|
|
|
current: file.path
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他文件类型,尝试打开或下载
|
|
|
|
|
|
// #ifdef H5
|
|
|
|
|
|
window.open(file.path, '_blank');
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifdef APP-PLUS
|
|
|
|
|
|
plus.runtime.openURL(file.path);
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifndef H5 || APP-PLUS
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '正在下载文件...',
|
|
|
|
|
|
icon: 'loading',
|
|
|
|
|
|
duration: 2000
|
|
|
|
|
|
});
|
|
|
|
|
|
// 下载并打开文档
|
|
|
|
|
|
uni.downloadFile({
|
|
|
|
|
|
url: file.path,
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.statusCode === 200) {
|
|
|
|
|
|
uni.openDocument({
|
|
|
|
|
|
filePath: res.tempFilePath,
|
|
|
|
|
|
success: () => {
|
|
|
|
|
|
console.log('打开文档成功');
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('打开文档失败:', err);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '无法打开此文件',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('下载文件失败:', err);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '下载文件失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-05 16:20:12 +08:00
|
|
|
|
// 获取标签类型(用于uv-tags组件)
|
|
|
|
|
|
const getTagType = (tagText) => {
|
|
|
|
|
|
const status = getStatusFromTagText(tagText);
|
|
|
|
|
|
return getTaskStatusType(status);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取标签样式(用于uv-tags组件)
|
|
|
|
|
|
const getTagStyle = (tagText) => {
|
|
|
|
|
|
const status = getStatusFromTagText(tagText);
|
|
|
|
|
|
const styleConfig = getTaskStatusStyle(status);
|
2025-11-06 16:49:06 +08:00
|
|
|
|
return {
|
2025-11-05 16:20:12 +08:00
|
|
|
|
backgroundColor: styleConfig.backgroundColor,
|
|
|
|
|
|
color: styleConfig.color,
|
|
|
|
|
|
borderColor: styleConfig.borderColor
|
2025-11-05 14:18:51 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 返回上一页
|
|
|
|
|
|
const goBack = () => {
|
|
|
|
|
|
uni.navigateBack();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 完成任务
|
|
|
|
|
|
const completeTask = () => {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
|
|
|
|
|
content: '确定要完成任务吗?',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
console.log("完成任务", task.value.id);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '任务已完成',
|
|
|
|
|
|
icon: 'success'
|
|
|
|
|
|
});
|
|
|
|
|
|
// 可以在这里添加完成任务的API调用
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 提交任务
|
|
|
|
|
|
const submitTask = () => {
|
2025-11-05 15:23:52 +08:00
|
|
|
|
uni.navigateTo({
|
2025-11-07 11:40:13 +08:00
|
|
|
|
url: `/pages/task/submit/index?taskId=${task.value.id || ''}`
|
2025-11-05 14:18:51 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 申请延期
|
|
|
|
|
|
const applyDelay = () => {
|
2025-11-05 16:05:34 +08:00
|
|
|
|
uni.navigateTo({
|
2025-11-07 11:40:13 +08:00
|
|
|
|
url: `/pages/task/apply-delay/index?taskId=${task.value.id || ''}`
|
2025-11-05 14:18:51 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 15:27:11 +08:00
|
|
|
|
// 加载任务数据
|
|
|
|
|
|
const loadTaskData = async (taskId) => {
|
|
|
|
|
|
if (!taskId) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '任务ID不能为空',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 显示加载提示
|
|
|
|
|
|
uni.showLoading({
|
|
|
|
|
|
title: '加载中...'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 调用 API 获取任务详情
|
|
|
|
|
|
const res = await getTaskDetail(taskId);
|
|
|
|
|
|
console.log('任务详情数据:', res);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换数据格式
|
|
|
|
|
|
const taskStatus = res.status !== undefined ? res.status : null;
|
|
|
|
|
|
const expireTime = res.expireTime || null;
|
|
|
|
|
|
const statusTags = getStatusTags(taskStatus, expireTime);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换提交记录
|
|
|
|
|
|
const submitRecords = transformSubmitRecords(res.submitList || []);
|
|
|
|
|
|
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 解析任务图片(逗号分隔的URL字符串)
|
|
|
|
|
|
const taskPictures = res.picture ? parseAttachUrls(res.picture) : [];
|
|
|
|
|
|
|
2025-11-06 15:27:11 +08:00
|
|
|
|
// 更新任务数据
|
|
|
|
|
|
task.value = {
|
|
|
|
|
|
id: res.id || taskId,
|
|
|
|
|
|
name: res.description || '任务名称',
|
|
|
|
|
|
project: res.projectName || '',
|
|
|
|
|
|
statusTags: statusTags,
|
|
|
|
|
|
deadline: expireTime ? expireTime : '无',
|
|
|
|
|
|
creator: res.createName || '',
|
|
|
|
|
|
creatorAvatar: res.createAvatar || '',
|
|
|
|
|
|
responsible: getOwnerNames(res.memberList || []),
|
|
|
|
|
|
publishTime: res.createTime ? formatTimeToChinese(res.createTime) : '',
|
|
|
|
|
|
content: res.description || '',
|
2025-11-06 16:49:06 +08:00
|
|
|
|
pictures: taskPictures, // 任务图片数组
|
2025-11-06 15:27:11 +08:00
|
|
|
|
submitRecords: submitRecords,
|
|
|
|
|
|
// 保存原始数据,供其他功能使用
|
|
|
|
|
|
rawData: res
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('加载任务详情失败:', err);
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '加载任务详情失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
2025-11-05 14:18:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 页面加载时接收参数
|
|
|
|
|
|
onLoad((options) => {
|
|
|
|
|
|
const taskId = options.id || options.taskId;
|
|
|
|
|
|
if (taskId) {
|
|
|
|
|
|
task.value.id = taskId;
|
2025-11-06 15:27:11 +08:00
|
|
|
|
// 优先从 API 加载数据
|
2025-11-05 14:18:51 +08:00
|
|
|
|
loadTaskData(taskId);
|
2025-11-06 15:27:11 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 如果没有 taskId,尝试从 Pinia store 获取任务详情数据(兼容旧逻辑)
|
|
|
|
|
|
const taskStore = useTaskStore();
|
|
|
|
|
|
const storedTask = taskStore.getTaskDetail;
|
|
|
|
|
|
if (storedTask) {
|
|
|
|
|
|
task.value = {
|
|
|
|
|
|
...task.value,
|
|
|
|
|
|
...storedTask
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '缺少任务ID',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
uni.navigateBack();
|
|
|
|
|
|
}, 1500);
|
|
|
|
|
|
}
|
2025-11-05 14:18:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-05 15:23:52 +08:00
|
|
|
|
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 转换旧格式的提交记录为新格式(兼容本地存储的数据)
|
|
|
|
|
|
const convertOldFormatRecord = (record) => {
|
|
|
|
|
|
// 如果已经是新格式(有 imageAttachments 和 fileAttachments),直接返回
|
|
|
|
|
|
if (record.imageAttachments !== undefined || record.fileAttachments !== undefined) {
|
|
|
|
|
|
return record;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是旧格式(有 attachments 数组),转换为新格式
|
|
|
|
|
|
if (record.attachments && Array.isArray(record.attachments)) {
|
|
|
|
|
|
const imageAttachments = [];
|
|
|
|
|
|
const fileAttachments = [];
|
|
|
|
|
|
|
|
|
|
|
|
record.attachments.forEach(att => {
|
|
|
|
|
|
if (att.type === 'image' && att.path) {
|
|
|
|
|
|
imageAttachments.push(att.path);
|
|
|
|
|
|
} else if (att.type === 'file') {
|
|
|
|
|
|
fileAttachments.push({
|
|
|
|
|
|
name: att.name || '文件',
|
|
|
|
|
|
path: att.path || ''
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...record,
|
|
|
|
|
|
imageAttachments: imageAttachments,
|
|
|
|
|
|
fileAttachments: fileAttachments
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果都没有,返回空数组
|
|
|
|
|
|
return {
|
|
|
|
|
|
...record,
|
|
|
|
|
|
imageAttachments: [],
|
|
|
|
|
|
fileAttachments: []
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-05 15:56:46 +08:00
|
|
|
|
// 页面显示时检查是否有新的提交记录或更新的记录
|
2025-11-05 15:23:52 +08:00
|
|
|
|
onShow(() => {
|
2025-11-05 15:56:46 +08:00
|
|
|
|
// 检查是否有新的提交记录
|
2025-11-05 15:23:52 +08:00
|
|
|
|
const newSubmitRecord = uni.getStorageSync('newSubmitRecord');
|
|
|
|
|
|
if (newSubmitRecord) {
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 转换格式并添加到列表开头
|
|
|
|
|
|
const convertedRecord = convertOldFormatRecord(newSubmitRecord);
|
|
|
|
|
|
task.value.submitRecords.unshift(convertedRecord);
|
2025-11-05 15:23:52 +08:00
|
|
|
|
// 切换到提交记录标签页
|
|
|
|
|
|
activeTab.value = 'records';
|
|
|
|
|
|
// 清除存储的记录
|
|
|
|
|
|
uni.removeStorageSync('newSubmitRecord');
|
|
|
|
|
|
}
|
2025-11-05 15:56:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否有更新的提交记录(编辑后的记录)
|
|
|
|
|
|
const updatedSubmitRecord = uni.getStorageSync('updatedSubmitRecord');
|
|
|
|
|
|
if (updatedSubmitRecord) {
|
|
|
|
|
|
const { recordIndex, record } = updatedSubmitRecord;
|
|
|
|
|
|
if (recordIndex !== undefined && recordIndex >= 0 && recordIndex < task.value.submitRecords.length) {
|
2025-11-06 16:49:06 +08:00
|
|
|
|
// 转换格式并更新指定索引的记录
|
|
|
|
|
|
const convertedRecord = convertOldFormatRecord(record);
|
|
|
|
|
|
task.value.submitRecords[recordIndex] = convertedRecord;
|
2025-11-05 15:56:46 +08:00
|
|
|
|
// 切换到提交记录标签页
|
|
|
|
|
|
activeTab.value = 'records';
|
|
|
|
|
|
}
|
|
|
|
|
|
// 清除存储的记录
|
|
|
|
|
|
uni.removeStorageSync('updatedSubmitRecord');
|
|
|
|
|
|
}
|
2025-11-05 15:23:52 +08:00
|
|
|
|
});
|
2025-11-05 14:18:51 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.task-detail-container {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 顶部导航栏 */
|
|
|
|
|
|
.header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
position: sticky;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
z-index: 100;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.back-btn {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-title {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.placeholder {
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 内容滚动区域 */
|
|
|
|
|
|
.content-scroll {
|
|
|
|
|
|
flex: 1;
|
2025-11-05 14:47:08 +08:00
|
|
|
|
height: calc(100vh - 60px);
|
2025-11-05 14:18:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 任务状态栏 */
|
|
|
|
|
|
.status-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-name {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.project-name {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-tags {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 基本信息区域 */
|
|
|
|
|
|
.basic-info {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-label {
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-value {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 标签切换 */
|
|
|
|
|
|
.tab-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
padding: 0 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 16px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
text {
|
2025-11-05 16:20:12 +08:00
|
|
|
|
font-size: 16px;
|
2025-11-05 14:18:51 +08:00
|
|
|
|
color: #666;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
|
|
text {
|
|
|
|
|
|
color: #1976d2;
|
2025-11-05 16:20:12 +08:00
|
|
|
|
font-weight: 600;
|
2025-11-05 14:18:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
width: 60px;
|
|
|
|
|
|
height: 3px;
|
|
|
|
|
|
background-color: #1976d2;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 标签页内容 */
|
|
|
|
|
|
.tab-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
min-height: calc(100vh - 400px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 任务信息卡片 */
|
|
|
|
|
|
.task-info-card {
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-time-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clock-icon {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-time-text {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-content-wrapper {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-content-text {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.delay-btn-wrapper {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 提交记录卡片 */
|
|
|
|
|
|
.submit-record-card {
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-placeholder {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 15:27:11 +08:00
|
|
|
|
.avatar-img {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-value-with-avatar {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.creator-avatar {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 14:18:51 +08:00
|
|
|
|
.user-name {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-header-right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-time {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.more-menu {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.more-icon {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transform: rotate(90deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.menu-dropdown {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 100%;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border: 1px solid #eee;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
z-index: 100;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.menu-item {
|
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
border-bottom: 1px solid #f5f5f5;
|
|
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
text {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-content-wrapper {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-content-text {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 15:23:52 +08:00
|
|
|
|
.record-progress {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-value {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #1976d2;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 16:49:06 +08:00
|
|
|
|
/* 任务图片展示(一行最多三张) */
|
|
|
|
|
|
.task-images-wrapper {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-image-item {
|
|
|
|
|
|
/* 一行三个:每个图片宽度 = (100% - 2个gap) / 3 */
|
|
|
|
|
|
width: calc((100% - 16px) / 3);
|
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-image {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 提交记录图片展示(一行三个) */
|
|
|
|
|
|
.record-images-wrapper {
|
2025-11-05 14:18:51 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 16:49:06 +08:00
|
|
|
|
.record-image-item {
|
|
|
|
|
|
/* 一行三个:每个图片宽度 = (100% - 2个gap) / 3 */
|
|
|
|
|
|
width: calc((100% - 16px) / 3);
|
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-image {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 文件附件展示 */
|
|
|
|
|
|
.record-files-wrapper {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-attachment-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-11-06 18:01:41 +08:00
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 12px;
|
2025-11-06 16:49:06 +08:00
|
|
|
|
background-color: #f5f5f5;
|
2025-11-06 18:01:41 +08:00
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
|
}
|
2025-11-06 16:49:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-icon {
|
2025-11-06 18:01:41 +08:00
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
min-width: 0;
|
2025-11-06 16:49:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
2025-11-06 18:01:41 +08:00
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-size {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #999;
|
2025-11-05 14:18:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.no-record {
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
padding: 40px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 底部操作按钮 */
|
|
|
|
|
|
.action-buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
gap: 10px;
|
2025-11-05 14:30:29 +08:00
|
|
|
|
background-color: #ffffff;
|
2025-11-05 14:18:51 +08:00
|
|
|
|
border-top: 1px solid #eee;
|
2025-11-05 14:30:29 +08:00
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
left: 0;
|
2025-11-05 14:18:51 +08:00
|
|
|
|
bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-wrapper {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
2025-11-05 14:47:08 +08:00
|
|
|
|
|
2025-11-05 14:18:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|