OfficeSystem/pages/task/detail/index.vue
2025-11-22 17:34:38 +08:00

1519 lines
38 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>
<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">
<uv-tags
v-for="(status, index) in task.statusTags"
:key="index"
:text="status"
:type="getTagType(status)"
size="mini"
:plain="false"
:custom-style="getTagStyle(status)"
></uv-tags>
</view>
</view>
<!-- 基本信息区域 -->
<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>
<view class="info-value-with-avatar">
<image
v-if="task.creatorAvatar"
:src="task.creatorAvatar"
class="creator-avatar"
mode="aspectFill"
/>
<text>{{ task.creator }}</text>
</view>
</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>
</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>
</view>
<!-- 任务文件展示 -->
<view class="task-files-wrapper" v-if="task.files && task.files.length > 0">
<view
class="file-attachment-item"
v-for="(file, fileIndex) in task.files"
:key="fileIndex"
@click="previewTaskFile(file)"
>
<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>
</view>
</view>
<view class="delay-btn-wrapper" v-if="showDelayBtn">
<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">
<image
v-if="record.userAvatar"
:src="record.userAvatar"
class="avatar-img"
mode="aspectFill"
/>
<view v-else class="avatar-placeholder"></view>
<text class="user-name">{{ record.userName }}</text>
</view>
<view class="record-header-right">
<text class="record-time">{{ record.time }}</text>
<!-- <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>-->
</view>
</view>
<view class="record-content-wrapper" v-if="record.content">
<text class="record-content-text">{{ record.content }}</text>
</view>
<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>
<!-- 图片附件展示(一行三个) -->
<view class="record-images-wrapper" v-if="record.imageAttachments && record.imageAttachments.length > 0">
<view
class="record-image-item"
v-for="(imageUrl, imgIndex) in record.imageAttachments"
:key="imgIndex"
@click="previewRecordImages(record.imageAttachments, imgIndex)"
>
<image
:src="imageUrl"
mode="aspectFill"
class="record-image"
/>
</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"
@click="previewRecordFile(file)"
>
<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>
</view>
</view>
<view class="delay-btn-wrapper" v-if="record.showDelayBtn && showDelayBtn">
<uv-button type="error" size="small" @click="applyDelay">申请延期</uv-button>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作按钮 -->
<view class="action-buttons" v-if="showSubmitBtn || showCompleteBtn || showCancelBtn">
<view class="btn-wrapper" v-if="showCompleteBtn">
<uv-button type="primary" size="normal" @click="completeTask">完成任务</uv-button>
</view>
<view class="btn-wrapper" v-if="showSubmitBtn">
<uv-button type="primary" size="normal" @click="submitTask">提交任务</uv-button>
</view>
<view class="btn-wrapper" v-if="showCancelBtn">
<uv-button type="error" size="normal" @click="cancelTask">取消任务</uv-button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { onLoad,onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import { getStatusFromTagText, getTaskStatusType, getTaskStatusStyle } from '@/utils/taskConfig.js';
import { useTaskStore } from '@/store/task';
import { useUserStore } from '@/store/user';
import { getTaskDetail } from '@/api';
// 当前激活的标签
const activeTab = ref('info');
const showMenuIndex = ref(-1);
// 用户权限相关
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
// 判断是否是管理员(总管理员)
const isAdmin = computed(() => {
if (!userInfo.value || !Array.isArray(userInfo.value.roles)) return false;
let is = userInfo.value.roles.some((role) => ['admin', 'sys_admin'].includes(role));
console.log('isAdmin',is)
return is
});
// 判断是否有指定权限
const hasPermission = (permission) => {
// 如果用户是admin给予所有权限
if (isAdmin.value) return true;
if (!userInfo.value || !userInfo.value.permissions) return false;
// 处理对象格式的权限数据(键为数字索引,值为权限字符串)
const permissions = userInfo.value.permissions;
// 如果是数组直接使用includes
if (Array.isArray(permissions)) {
return permissions.includes(permission);
}
// 如果是对象使用Object.values()获取所有权限值
if (typeof permissions === 'object') {
return Object.values(permissions).includes(permission);
}
return false;
};
// 判断任务是否为待完成状态
const isTaskPending = computed(() => {
// 优先从原始数据判断任务状态
if (task.value.rawData) {
const status = task.value.rawData.status;
// 如果任务已完成状态为4则不是待完成
if (status === 4 || status === '4' || String(status) === '4') {
return false;
}
}
if (task.value.statusTags?.some(tag =>
['待完成', '即将逾期', '已逾期', '逾期'].includes(tag)
)) {
return true;
}
// 如果状态标签中包含"已完成",则不是待完成
if (task.value.statusTags && task.value.statusTags.includes('已完成')) {
return false;
}
// 默认返回 false安全起见
return false;
});
// 判断是否显示提交任务按钮:有权限 && 任务状态为待完成,或者管理员
const showSubmitBtn = computed(() => {
console.log('showSubmitBtn',(hasPermission('bst:task:submit') && isTaskPending.value) || isAdmin.value)
console.log('hasPermission(\'bst:task:submit\') ',hasPermission('bst:task:submit') )
console.log('isTaskPending.value',isTaskPending.value)
return (hasPermission('bst:task:submit') && isTaskPending.value) ;
});
// 判断是否显示完成任务按钮:有权限 && 任务状态为待完成,或者管理员
const showCompleteBtn = computed(() => {
return (hasPermission('bst:task:pass') && isTaskPending.value) ;
});
// 判断是否显示取消任务按钮:有权限 && 任务状态为待完成 && 是管理员
const showCancelBtn = computed(() => {
return hasPermission('bst:task:cancel') && isTaskPending.value && isAdmin.value;
});
// 判断是否显示申请延期按钮:任务状态为待完成
const showDelayBtn = computed(() => {
return isTaskPending.value;
});
// 格式化时间为中文格式:年月日星期几时分秒
const formatTimeToChinese = (date) => {
if (!date) return '';
if (typeof date === 'string') {
// 如果是字符串,尝试解析
date = new Date(date);
}
if (isNaN(date.getTime())) return '';
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}`;
};
// 格式化日期:将 "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('、');
};
// 解析逗号分隔的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);
};
// 解析任务附件(区分图片和文件)
const parseTaskAttachments = (attachStr) => {
if (!attachStr) return { pictures: [], files: [] };
if (typeof attachStr !== 'string') return { pictures: [], files: [] };
const urls = parseAttachUrls(attachStr);
const pictures = [];
const files = [];
urls.forEach(url => {
if (isImageUrl(url)) {
pictures.push(url);
} else {
// 从URL中提取文件名
const fileName = url.split('/').pop().split('?')[0] || '文件';
files.push({
name: fileName,
path: url,
size: 0 // 如果API没有返回文件大小默认为0
});
}
});
return { pictures, files };
};
// 转换提交记录数据
const transformSubmitRecords = (submitList) => {
if (!Array.isArray(submitList) || submitList.length === 0) {
return [];
}
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;
});
return sortedList.map(item => {
// 处理附件可能是逗号分隔的URL字符串
let imageAttachments = [];
let fileAttachments = [];
if (item.attaches) {
try {
// 先尝试作为JSON解析
let attachData = null;
try {
attachData = typeof item.attaches === 'string' ? JSON.parse(item.attaches) : item.attaches;
} catch (e) {
// 如果不是JSON则作为逗号分隔的URL字符串处理
attachData = null;
}
if (Array.isArray(attachData)) {
// 如果是数组格式
attachData.forEach(att => {
const filePath = att.path || att.url || att.filePath || '';
const fileName = att.name || att.fileName || '';
const isImage = fileName ? /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName) : isImageUrl(filePath);
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
});
}
});
}
} catch (e) {
console.error('解析附件数据失败:', e);
// 如果解析失败尝试作为逗号分隔的URL字符串处理
const urls = parseAttachUrls(item.attaches);
urls.forEach(url => {
if (isImageUrl(url)) {
imageAttachments.push(url);
}
});
}
}
return {
id: item.id || '',
userName: item.userName || '',
userAvatar: item.userAvatar || '',
time: formatTimeToChinese(item.createTime) || '',
content: item.remark || '', // 如果没有提交内容,可能显示任务描述
progress: null, // API 返回的数据中没有进度字段
imageAttachments: imageAttachments,
fileAttachments: fileAttachments,
showDelayBtn: false, // 根据业务需求决定是否显示
canEdit: true // 根据业务需求决定是否可以编辑
};
});
};
// 任务数据
const task = ref({
});
// 切换标签
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) => {
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({
url: `/pages/task/submit/index?taskId=${task.value.id}&mode=edit&recordIndex=${index}`
});
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;
}
});
};
// 预览任务图片
const previewTaskImages = (imageUrls, index) => {
if (imageUrls && imageUrls.length > 0) {
uni.previewImage({
urls: imageUrls,
current: index
});
}
};
// 预览/下载任务文件
const previewTaskFile = (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
}
};
// 预览提交记录图片
const previewRecordImages = (imageUrls, index) => {
if (imageUrls && imageUrls.length > 0) {
uni.previewImage({
urls: imageUrls,
current: index
});
}
};
// 获取文件图标
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
}
};
// 获取标签类型用于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);
return {
backgroundColor: styleConfig.backgroundColor,
color: styleConfig.color,
borderColor: styleConfig.borderColor
};
};
// 返回上一页
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 = () => {
uni.navigateTo({
url: `/pages/task/submit/index?taskId=${task.value.id || ''}`
});
};
// 申请延期
const applyDelay = () => {
uni.navigateTo({
url: `/pages/task/apply-delay/index?taskId=${task.value.id || ''}`
});
};
// 取消任务
const cancelTask = () => {
uni.showModal({
title: '提示',
content: '确定要取消任务吗?',
success: (res) => {
if (res.confirm) {
console.log("取消任务", task.value.id);
uni.showToast({
title: '任务已取消',
icon: 'success'
});
// 可以在这里添加取消任务的API调用
}
}
});
};
// 加载任务数据
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 || []);
// 解析任务附件(图片和文件)
// 优先使用 file 字段,如果没有则使用 picture 字段(可能包含图片和文件)
let taskAttachments = { pictures: [], files: [] };
if (res.file) {
// 如果 API 返回了 file 字段,解析它
taskAttachments = parseTaskAttachments(res.file);
} else if (res.picture) {
// 如果只有 picture 字段,也解析它(可能包含图片和文件)
taskAttachments = parseTaskAttachments(res.picture);
}
// 更新任务数据
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 || '',
pictures: taskAttachments.pictures, // 任务图片数组
files: taskAttachments.files, // 任务文件数组
submitRecords: submitRecords,
// 保存原始数据,供其他功能使用
rawData: res
};
uni.hideLoading();
} catch (err) {
console.error('加载任务详情失败:', err);
uni.hideLoading();
uni.showToast({
title: '加载任务详情失败',
icon: 'none'
});
}
};
// 页面加载时接收参数
onLoad((options) => {
const taskId = options.id || options.taskId;
if (taskId) {
task.value.id = taskId;
// 优先从 API 加载数据
loadTaskData(taskId);
console.log('<UNK>:',userInfo.value.permissions );
}
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);
// }
}
});
// 转换旧格式的提交记录为新格式(兼容本地存储的数据)
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: []
};
};
// 页面显示时检查是否有新的提交记录或更新的记录
onShow(() => {
// 检查是否有新的提交记录
const newSubmitRecord = uni.getStorageSync('newSubmitRecord');
if (newSubmitRecord) {
// 转换格式并添加到列表开头
const convertedRecord = convertOldFormatRecord(newSubmitRecord);
task.value.submitRecords.unshift(convertedRecord);
// 切换到提交记录标签页
activeTab.value = 'records';
// 清除存储的记录
uni.removeStorageSync('newSubmitRecord');
}
// 检查是否有更新的提交记录(编辑后的记录)
const updatedSubmitRecord = uni.getStorageSync('updatedSubmitRecord');
if (updatedSubmitRecord) {
const { recordIndex, record } = updatedSubmitRecord;
if (recordIndex !== undefined && recordIndex >= 0 && recordIndex < task.value.submitRecords.length) {
// 转换格式并更新指定索引的记录
const convertedRecord = convertOldFormatRecord(record);
task.value.submitRecords[recordIndex] = convertedRecord;
// 切换到提交记录标签页
activeTab.value = 'records';
}
// 清除存储的记录
uni.removeStorageSync('updatedSubmitRecord');
}
});
</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;
height: calc(100vh - 60px);
}
/* 任务状态栏 */
.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 {
font-size: 16px;
color: #666;
font-weight: 500;
}
&.active {
text {
color: #1976d2;
font-weight: 600;
}
&::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;
}
.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;
}
.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;
}
.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;
}
/* 任务图片展示(一行最多三张) */
.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;
}
/* 任务文件展示 */
.task-files-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
/* 提交记录图片展示(一行三个) */
.record-images-wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.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;
gap: 12px;
padding: 12px;
background-color: #f5f5f5;
border-radius: 8px;
cursor: pointer;
&:active {
background-color: #e0e0e0;
}
}
.file-icon {
font-size: 24px;
flex-shrink: 0;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.file-name {
font-size: 14px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.file-size {
font-size: 12px;
color: #999;
}
.no-record {
color: #999;
font-size: 14px;
padding: 40px 0;
text-align: center;
}
/* 底部操作按钮 */
.action-buttons {
display: flex;
padding: 15px;
gap: 10px;
background-color: #ffffff;
border-top: 1px solid #eee;
position: fixed;
right: 0;
left: 0;
bottom: 0;
}
.btn-wrapper {
flex: 1;
}
</style>