OfficeSystem/pages/task-detail/index.vue
2025-11-05 16:20:12 +08:00

788 lines
18 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>
<text class="info-value">{{ task.creator }}</text>
</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">
<view 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';
// 当前激活的标签
const activeTab = ref('info');
const showMenuIndex = ref(-1);
// 格式化时间为中文格式:年月日星期几时分秒
const formatTimeToChinese = (date) => {
if (typeof date === 'string') {
// 如果是字符串,尝试解析
date = new Date(date);
}
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}`;
};
// 任务数据
const task = ref({
id: null,
name: "待办任务名称",
project: "所属项目",
statusTags: ["已逾期", "紧急"],
deadline: "2025-10-14 18:00",
creator: "张珊珊",
responsible: "张珊珊、李志",
publishTime: "2025-10-17",
content: "任务内容任务。这里是详细的任务描述,可以包含多行文本。根据实际需求,这里可以展示任务的详细要求、步骤说明、注意事项等。任务内容应该清晰明了,便于负责人理解和执行。",
submitRecords: [
{
userName: "张珊珊",
time: formatTimeToChinese(new Date('2025-10-20 15:20:56')),
content: "任务内容任务。这里是详细的任务描述,可以包含多行文本。根据实际需求,这里可以展示任务的详细要求、步骤说明、注意事项等。任务内容应该清晰明了,便于负责人理解和执行。",
attachments: [
{ type: "image" },
{ type: "image" },
{ type: "image" }
],
canEdit: true,
showDelayBtn: false
},
{
userName: "李志",
time: formatTimeToChinese(new Date('2025-10-20 15:20:56')),
content: "任务内容任务。这里是详细的任务描述,可以包含多行文本。根据实际需求,这里可以展示任务的详细要求、步骤说明、注意事项等。任务内容应该清晰明了,便于负责人理解和执行。",
attachments: [
{ type: "file", name: "AA_573_1280.JPG" }
],
canEdit: false,
showDelayBtn: true
}
]
});
// 切换标签
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 = (taskId) => {
// 这里可以根据 taskId 从 API 加载任务数据
// 暂时使用模拟数据
if (taskId) {
// 可以根据 taskId 加载不同的任务数据
console.log('加载任务数据:', taskId);
}
};
// 页面加载时接收参数
onLoad((options) => {
const taskId = options.id || options.taskId;
if (taskId) {
task.value.id = taskId;
loadTaskData(taskId);
}
// 如果通过存储传递数据(类似于 event-detail 的方式)
const storedTask = uni.getStorageSync('taskDetailData');
if (storedTask) {
task.value = {
...task.value,
...storedTask
};
uni.removeStorageSync('taskDetailData');
}
});
// 页面显示时检查是否有新的提交记录或更新的记录
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;
}
.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>