Files
im-uniapp/pages/workbench/task/reportTaskDetail.vue

655 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="detail-container">
<view v-if="loading" class="loading"><u-loading mode="circle" text="加载中..." /></view>
<view v-else-if="taskDetail">
<!-- 任务标题 -->
<view class="title">{{ taskDetail.taskTitle }}</view>
<!-- 基本信息 -->
<view class="section-title">基本信息</view>
<view class="info-grid">
<view class="info-item">
<text class="label">任务状态</text>
<text class="value status-tag" :class="getStatusClass(taskDetail.state)">
{{ getStatusText(taskDetail.state) }}
</text>
</view>
<view class="info-item">
<text class="label">优先级</text>
<text class="value priority-tag" :class="getPriorityClass(taskDetail.taskRank)">
{{ getPriorityText(taskDetail.taskRank) }}
</text>
</view>
<view class="info-item">
<text class="label">负责人</text>
<text class="value">{{ taskDetail.workerNickName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">创建人</text>
<text class="value">{{ taskDetail.createUserNickName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">所属项目</text>
<text class="value">{{ taskDetail.projectName || '-' }}</text>
</view>
</view>
<!-- 时间信息 -->
<view class="section-title">时间信息</view>
<view class="info-grid">
<view class="info-item">
<text class="label">开始时间</text>
<text class="value">{{ formatDate(taskDetail.beginTime) }}</text>
</view>
<view class="info-item">
<text class="label">结束时间</text>
<text class="value">{{ formatDate(taskDetail.finishTime) }}</text>
</view>
<view class="info-item" v-if="taskDetail.overDays > 0">
<text class="label">超期天数</text>
<text class="value over-days">{{ taskDetail.overDays }}</text>
</view>
<view class="info-item">
<text class="label">创建时间</text>
<text class="value">{{ formatDate(taskDetail.createTime) }}</text>
</view>
</view>
<!-- 任务描述 -->
<view class="section-title">任务描述</view>
<mp-html v-if="taskDetail.content" :content="taskDetail.content" class="content-text" />
<view v-else class="empty-content">暂无任务描述</view>
<!-- 备注 -->
<view v-if="taskDetail.remark" class="section-title">备注</view>
<mp-html v-if="taskDetail.remark" :content="taskDetail.remark" class="content-text" />
<!-- 报工进度仅报工任务显示 -->
<view v-if="isReportTask" class="section-title">报工进度</view>
<view v-if="isReportTask && taskDetail.taskItemVoList && taskDetail.taskItemVoList.length">
<view v-for="item in taskDetail.taskItemVoList" :key="item.itemId" class="item-block">
<view class="item-row"><text class="item-label">进度时间</text>{{ item.signTime || '-' }}</view>
<view class="item-row"><text class="item-label">进度区间</text>{{ item.beginTime || '-' }} ~ {{ item.endTime || '-' }}</view>
<view class="item-row"><text class="item-label">进度内容</text></view>
<mp-html v-if="item.content" :content="item.content" />
<view class="item-row"><text class="item-label">完成时间</text>{{ item.completedTime || '-' }}</view>
</view>
</view>
<view v-if="isReportTask && (!taskDetail.taskItemVoList || taskDetail.taskItemVoList.length === 0)" class="empty-progress">
暂无报工进度
</view>
<!-- 附件展示 -->
<view v-if="taskDetail.accessory" class="section-title">附件</view>
<view v-if="taskDetail.accessory" class="attachment-list">
<view
v-for="(file, index) in attachmentFiles"
:key="`${file.ossId}_${index}`"
class="attachment-item"
@click="downloadFile(file)"
>
<view class="file-icon">
<u-icon :name="getFileIcon(file.originalName)" size="24" color="#007aff"></u-icon>
</view>
<view class="file-info">
<text class="file-name">{{ getFileName(file.originalName) }}</text>
<text class="file-size">{{ formatFileSize(file.fileSize) }}</text>
</view>
<view class="file-action">
<u-icon name="download" size="20" color="#007aff"></u-icon>
</view>
</view>
</view>
<view v-if="taskDetail.accessory && attachmentFiles.length === 0" class="no-attachment">
暂无附件
</view>
<!-- 新增报工按钮仅报工任务显示 -->
<u-button v-if="isReportTask" type="primary" class="add-btn" @click="openAddPopup">新增报工</u-button>
<!-- 新增报工弹窗仅报工任务显示 -->
<uni-popup v-if="isReportTask" ref="addPopup" type="bottom" :mask-click="true">
<view class="add-dialog-bottom">
<view class="dialog-title">新增报工</view>
<u-form :model="addForm" ref="addFormRef">
<u-form-item label="进度区间">
<view class="period-row">{{ addForm.beginTime }} ~ {{ addForm.endTime }}</view>
</u-form-item>
<u-form-item label="进度时间">
<view class="period-row">{{ addForm.signTime }}</view>
</u-form-item>
<u-form-item label="完成时间">
<view class="period-row">{{ addForm.completedTime }}</view>
</u-form-item>
<u-form-item label="进度内容">
<u-textarea v-model="addForm.content" placeholder="请输入进度内容" autoHeight />
</u-form-item>
<u-form-item label="备注">
<u-textarea v-model="addForm.remark" placeholder="备注(可选)" autoHeight />
</u-form-item>
</u-form>
<view class="dialog-actions">
<u-button type="primary" @click="submitAdd">提交</u-button>
<u-button @click="$refs.addPopup.close()">取消</u-button>
</view>
</view>
</uni-popup>
</view>
<view v-else class="empty">未查询到任务详情</view>
</view>
</template>
<script>
import { getTask } from '@/api/oa/task.js'
import { addOaTaskItem } from '@/api/oa/taskItem.js'
import { getFilesByIds } from '@/api/common/upload.js'
import mpHtml from 'uni_modules/mp-html/components/mp-html/mp-html.vue'
export default {
components: { mpHtml },
data() {
return {
taskDetail: null,
loading: true,
attachmentFiles: [],
addForm: {
beginTime: '',
endTime: '',
content: '',
remark: ''
}
}
},
computed: {
// 判断是否为报工任务
isReportTask() {
return this.taskDetail && this.taskDetail.status == 1
}
},
onLoad(options) {
const taskId = options.id
if (taskId) {
this.taskId = taskId
this.fetchDetail(taskId)
} else {
this.loading = false
}
},
methods: {
async fetchDetail(taskId) {
this.loading = true
try {
const res = await getTask(taskId)
if (res.code === 200 && res.data) {
this.taskDetail = res.data
// 加载附件信息
if (res.data.accessory) {
await this.loadAttachmentFiles(res.data.accessory)
} else {
this.attachmentFiles = []
}
} else {
uni.showToast({ title: res.msg || '获取详情失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '网络错误', icon: 'none' })
} finally {
this.loading = false
}
},
openAddPopup() {
// 自动获取进度区间
let begin = '', end = '';
const items = this.taskDetail?.taskItemVoList || [];
if (items.length > 0) {
// 取最后一个item的endTime为下一个周期的beginTime
const last = items[items.length - 1];
begin = last.endTime || this.taskDetail.beginTime;
// 结束时间=begin+timeGap天
const gap = this.taskDetail.timeGap || 7;
const bDate = new Date(begin.replace(/-/g, '/'));
bDate.setDate(bDate.getDate() + gap);
end = bDate.getFullYear() + '-' + String(bDate.getMonth() + 1).padStart(2, '0') + '-' + String(bDate.getDate()).padStart(2, '0');
} else {
begin = this.taskDetail.beginTime;
end = this.taskDetail.finishTime;
}
// 进度时间=beginTime完成时间=当天
const today = new Date();
const todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
this.addForm = {
beginTime: begin,
endTime: end,
signTime: begin,
completedTime: todayStr,
content: '',
remark: ''
};
this.$refs.addPopup.open();
},
async submitAdd() {
if (!this.addForm.beginTime || !this.addForm.endTime || !this.addForm.content) {
uni.showToast({ title: '请填写进度内容', icon: 'none' })
return
}
const data = {
taskId: this.taskId,
beginTime: this.addForm.beginTime,
endTime: this.addForm.endTime,
signTime: this.addForm.signTime,
completedTime: this.addForm.completedTime,
content: this.addForm.content,
remark: this.addForm.remark || null,
status: 0
}
try {
const res = await addOaTaskItem(data)
if (res.code === 200) {
uni.showToast({ title: '新增报工成功', icon: 'success' })
this.$refs.addPopup.close()
this.addForm = { beginTime: '', endTime: '', signTime: '', completedTime: '', content: '', remark: '' }
this.fetchDetail(this.taskId)
} else {
uni.showToast({ title: res.msg || '新增失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '网络错误', icon: 'none' })
}
},
// 加载附件文件信息
async loadAttachmentFiles(accessory) {
try {
const ossIds = accessory.split(',').filter(id => id.trim())
if (ossIds.length > 0) {
const files = await getFilesByIds(ossIds)
this.attachmentFiles = files || []
} else {
this.attachmentFiles = []
}
} catch (error) {
console.error('加载附件失败:', error)
this.attachmentFiles = []
}
},
// 下载文件
downloadFile(file) {
// 使用uni.downloadFile下载文件
uni.downloadFile({
url: file.url,
success: (res) => {
if (res.statusCode === 200) {
// 保存文件到本地
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.showToast({
title: '文件已保存到本地',
icon: 'success'
})
},
fail: (err) => {
console.error('保存文件失败:', err)
uni.showToast({
title: '保存文件失败',
icon: 'none'
})
}
})
}
},
fail: (err) => {
console.error('下载文件失败:', err)
uni.showToast({
title: '下载文件失败',
icon: 'none'
})
}
})
},
// 获取文件图标
getFileIcon(fileName) {
const ext = fileName.split('.').pop().toLowerCase()
const iconMap = {
'pdf': 'file-text',
'doc': 'file-text',
'docx': 'file-text',
'xls': 'file-text',
'xlsx': 'file-text',
'ppt': 'file-text',
'pptx': 'file-text',
'txt': 'file-text',
'jpg': 'file-text',
'jpeg': 'file-text',
'png': 'file-text',
'gif': 'file-text',
'zip': 'folder',
'rar': 'folder',
'7z': 'folder',
'dwg': 'file-text'
}
return iconMap[ext] || 'file-text'
},
// 获取文件名
getFileName(fileName) {
if (!fileName) return ''
const name = fileName.includes('/') ? fileName.split('/').pop() : fileName
// 如果文件名超过20个字符截取前17个字符并添加省略号
if (name.length > 20) {
return name.substring(0, 17) + '...'
}
return name
},
// 格式化文件大小
formatFileSize(size) {
if (!size) return ''
if (size < 1024) {
return size + 'B'
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + 'KB'
} else {
return (size / (1024 * 1024)).toFixed(2) + 'MB'
}
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return '未设置'
const date = new Date(dateStr)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
},
// 获取状态文本
getStatusText(state) {
const statusMap = {
15: '申请延期',
0: '进行中',
1: '完成等待评分',
2: '完成'
}
return statusMap[state] || '未知状态'
},
// 获取状态样式类
getStatusClass(state) {
const classMap = {
15: 'status-pending',
0: 'status-processing',
1: 'status-waiting',
2: 'status-completed'
}
return classMap[state] || 'status-unknown'
},
// 获取优先级文本
getPriorityText(priority) {
const priorityMap = {
0: '普通',
1: '低',
2: '中',
3: '高',
4: '紧急'
}
return priorityMap[priority] || '未设置'
},
// 获取优先级样式类
getPriorityClass(priority) {
const classMap = {
0: 'priority-normal',
1: 'priority-low',
2: 'priority-medium',
3: 'priority-high',
4: 'priority-urgent'
}
return classMap[priority] || 'priority-unknown'
}
}
}
</script>
<style scoped>
.detail-container {
padding: 32rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 24rpx;
}
.row {
font-size: 28rpx;
margin-bottom: 12rpx;
}
.label {
color: #888;
}
.section-title {
margin-top: 32rpx;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.item-block {
background: #f8f9fa;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.item-row {
font-size: 26rpx;
margin-bottom: 8rpx;
}
.item-label {
color: #666;
}
.empty-progress, .empty {
text-align: center;
color: #bbb;
margin: 40rpx 0;
}
.loading {
text-align: center;
margin: 80rpx 0;
}
.add-btn {
margin-top: 40rpx;
width: 100%;
}
.add-dialog-bottom {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 40rpx 32rpx 32rpx 32rpx;
width: 100vw;
max-width: 750rpx;
box-sizing: border-box;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 32rpx;
text-align: center;
}
.dialog-actions {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
justify-content: center;
}
.period-row {
font-size: 28rpx;
color: #333;
padding: 12rpx 0 12rpx 8rpx;
}
.attachment-list {
margin-top: 16rpx;
}
.attachment-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
margin-bottom: 16rpx;
cursor: pointer;
transition: background-color 0.2s;
}
.attachment-item:hover {
background-color: #e9ecef;
}
.file-icon {
margin-right: 16rpx;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
}
.file-name {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 400rpx;
}
.file-size {
font-size: 24rpx;
color: #999;
}
.file-action {
margin-left: 16rpx;
}
.no-attachment {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 40rpx 0;
}
.info-grid {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 24rpx;
}
.info-item {
display: flex;
align-items: center;
font-size: 28rpx;
line-height: 1.5;
}
.info-item .label {
color: #666;
width: 160rpx;
flex-shrink: 0;
}
.info-item .value {
color: #333;
flex: 1;
}
.content-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
background-color: #f8f9fa;
padding: 20rpx;
border-radius: 12rpx;
border-left: 4rpx solid #1890ff;
margin-bottom: 24rpx;
}
.empty-content {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 40rpx 0;
background-color: #f8f9fa;
border-radius: 12rpx;
}
.status-tag {
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 24rpx;
font-weight: 500;
}
.status-pending {
background-color: #fff2e8;
color: #fa8c16;
}
.status-processing {
background-color: #e6f7ff;
color: #1890ff;
}
.status-waiting {
background-color: #fff7e6;
color: #fa8c16;
}
.status-completed {
background-color: #f6ffed;
color: #52c41a;
}
.priority-tag {
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 24rpx;
font-weight: 500;
}
.priority-normal {
background-color: #f5f5f5;
color: #666;
}
.priority-low {
background-color: #f6ffed;
color: #52c41a;
}
.priority-medium {
background-color: #fff7e6;
color: #fa8c16;
}
.priority-high {
background-color: #fff2f0;
color: #ff4d4f;
}
.priority-urgent {
background-color: #f9f0ff;
color: #722ed1;
}
.over-days {
color: #ff4d4f;
font-weight: 600;
}
</style>