OfficeSystem/pages/task-detail/index.vue
2025-11-06 15:27:11 +08:00

998 lines
24 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>
<text class="task-content-text">{{ task.content }}</text>
<text class="task-content-text">{{ task.content }}</text>
</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">
<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-attachments" v-if="record.attachments && record.attachments.length > 0">
<view
class="attachment-item"
v-for="(attachment, attIndex) in record.attachments"
:key="attIndex"
>
<image
v-if="attachment.type === 'image'"
:src="attachment.path"
mode="aspectFill"
class="attachment-image"
@click="previewAttachmentImage(record.attachments, attIndex)"
/>
<view v-else class="file-attachment">
<text class="file-icon">📄</text>
<text class="file-name">{{ attachment.name }}</text>
</view>
</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>
</view>
</template>
<script setup>
import { ref, onMounted, } from 'vue';
import { onLoad,onShow } from '@dcloudio/uni-app';
import { getStatusFromTagText, getTaskStatusType, getTaskStatusStyle } from '@/utils/taskConfig.js';
import { useTaskStore } from '@/store/task';
import { getTaskDetail } from '@/common/api.js';
// 当前激活的标签
const activeTab = ref('info');
const showMenuIndex = ref(-1);
// 格式化时间为中文格式:年月日星期几时分秒
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('、');
};
// 转换提交记录数据
const transformSubmitRecords = (submitList) => {
if (!Array.isArray(submitList) || submitList.length === 0) {
return [];
}
return submitList.map(item => {
// 处理附件
let attachments = [];
if (item.attaches) {
try {
// 如果 attaches 是字符串,尝试解析
const attachData = typeof item.attaches === 'string' ? JSON.parse(item.attaches) : item.attaches;
if (Array.isArray(attachData)) {
attachments = attachData.map(att => {
// 根据文件扩展名判断类型
const fileName = att.name || att.fileName || '';
const filePath = att.path || att.url || att.filePath || '';
const isImage = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName);
return {
type: isImage ? 'image' : 'file',
name: fileName,
path: filePath
};
});
}
} catch (e) {
console.error('解析附件数据失败:', e);
}
}
return {
id: item.id || '',
userName: item.userName || '',
userAvatar: item.userAvatar || '',
time: formatTimeToChinese(item.createTime) || '',
content: item.remark || item.description || item.taskDescription || '', // 如果没有提交内容,可能显示任务描述
progress: null, // API 返回的数据中没有进度字段
attachments: attachments,
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/submit-task/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 previewAttachmentImage = (attachments, index) => {
const imageUrls = attachments
.filter(att => att.type === 'image')
.map(att => att.path);
const currentIndex = attachments.slice(0, index).filter(att => att.type === 'image').length;
if (imageUrls.length > 0) {
uni.previewImage({
urls: imageUrls,
current: currentIndex
});
}
};
// 获取标签类型用于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/submit-task/index?taskId=${task.value.id || ''}`
});
};
// 申请延期
const applyDelay = () => {
uni.navigateTo({
url: `/pages/apply-delay/index?taskId=${task.value.id || ''}`
});
};
// 加载任务数据
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 || []);
// 更新任务数据
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 || '',
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);
} 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);
}
}
});
// 页面显示时检查是否有新的提交记录或更新的记录
onShow(() => {
// 检查是否有新的提交记录
const newSubmitRecord = uni.getStorageSync('newSubmitRecord');
if (newSubmitRecord) {
// 将新提交记录添加到列表开头
task.value.submitRecords.unshift(newSubmitRecord);
// 切换到提交记录标签页
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) {
// 更新指定索引的记录
task.value.submitRecords[recordIndex] = record;
// 切换到提交记录标签页
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;
}
.record-attachments {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.attachment-item {
.attachment-image {
width: 80px;
height: 80px;
border-radius: 4px;
background-color: #e0e0e0;
}
.file-attachment {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background-color: #f5f5f5;
border-radius: 4px;
}
.file-icon {
font-size: 16px;
}
.file-name {
font-size: 14px;
color: #333;
}
}
.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>