Files
im-uniapp/pages/workbench/task/task.vue
2025-07-11 18:05:05 +08:00

801 lines
17 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="task-container">
<!-- 搜索栏 -->
<view class="search-bar">
<u-search
v-model="searchKeyword"
placeholder="搜索任务名称"
:show-action="false"
@search="handleSearch"
@clear="handleClear"
></u-search>
</view>
<!-- 任务列表 -->
<view class="task-list">
<uni-swipe-action>
<uni-swipe-action-item
v-for="(task, index) in taskList"
:key="task.taskId"
:right-options="getSwipeOptions(task)"
@click="handleSwipeClick($event, index)"
>
<view
class="task-item"
@click="handleTaskClick(task)"
>
<view v-if="task.status === 0" class="task-checkbox" @click.stop="handleTaskComplete(task)">
<view v-if="task.state === 0" class="checkbox-container">
<u-checkbox
:value="false"
:active-color="'#ff9500'"
size="20"
></u-checkbox>
</view>
<view v-else-if="task.state === 1" class="checkbox-container">
<u-icon name="checkmark" size="20" color="#999"></u-icon>
</view>
<view v-else-if="task.state === 2" class="checkbox-container">
<u-icon name="checkmark" size="20" color="#52c41a"></u-icon>
</view>
<view v-else class="checkbox-container">
<u-checkbox
:value="false"
:disabled="true"
:active-color="'#999'"
size="20"
></u-checkbox>
</view>
</view>
<view class="task-content">
<view class="task-title">{{ task.taskTitle || '未命名任务' }}</view>
<view class="task-status" :class="getStatusClass(task.state)">
{{ getStatusText(task.state) }}
</view>
</view>
<view v-if="task.ownRank === 1" class="top-badge">
<u-icon name="arrow-up" size="12" color="#ff9500"></u-icon>
</view>
</view>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
<!-- 空状态 -->
<view v-if="taskList.length === 0 && !loading" class="empty-state">
<u-empty text="暂无发布的任务" mode="list"></u-empty>
</view>
<!-- 加载更多 -->
<u-load-more
:status="loadMoreStatus"
@loadmore="loadMore"
></u-load-more>
<!-- 悬浮按钮 -->
<view class="fab-button" @click="createTask">
<u-icon name="plus" color="#fff" size="24"></u-icon>
</view>
<!-- 任务详情弹窗 -->
<uni-popup ref="taskDetailPopup" type="bottom" :mask-click="true">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">任务详情</text>
<u-icon name="close" size="20" @click="closeTaskDetail"></u-icon>
</view>
<scroll-view class="popup-body" scroll-y>
<view v-if="currentTask" class="task-detail">
<view class="detail-section">
<view class="section-title">基本信息</view>
<view class="detail-item">
<text class="label">任务标题</text>
<text class="value">{{ currentTask.taskTitle }}</text>
</view>
<view class="detail-item">
<text class="label">任务状态</text>
<text class="value status-tag" :class="getStatusClass(currentTask.state)">
{{ getStatusText(currentTask.state) }}
</text>
</view>
<view class="detail-item">
<text class="label">优先级</text>
<text class="value priority-tag" :class="getPriorityClass(currentTask.taskRank)">
{{ getPriorityText(currentTask.taskRank) }}
</text>
</view>
<view class="detail-item">
<text class="label">所属项目</text>
<text class="value">{{ currentTask.projectName }}</text>
</view>
</view>
<view class="detail-section">
<view class="section-title">时间信息</view>
<view class="detail-item">
<text class="label">开始时间</text>
<text class="value">{{ formatDate(currentTask.beginTime) }}</text>
</view>
<view class="detail-item">
<text class="label">结束时间</text>
<text class="value">{{ formatDate(currentTask.finishTime) }}</text>
</view>
<view class="detail-item" v-if="currentTask.overDays > 0">
<text class="label">超期天数</text>
<text class="value over-days">{{ currentTask.overDays }}</text>
</view>
</view>
<view class="detail-section">
<view class="section-title">人员信息</view>
<view class="detail-item">
<text class="label">负责人</text>
<text class="value">{{ currentTask.workerNickName }}</text>
</view>
<view class="detail-item">
<text class="label">创建人</text>
<text class="value">{{ currentTask.createUserNickName }}</text>
</view>
<view class="detail-item">
<text class="label">创建时间</text>
<text class="value">{{ formatDate(currentTask.createTime) }}</text>
</view>
</view>
<view class="detail-section" v-if="currentTask.content">
<view class="section-title">任务描述</view>
<view class="task-content-text">{{ currentTask.content }}</view>
</view>
<view class="detail-section" v-if="currentTask.remark">
<view class="section-title">备注</view>
<view class="task-content-text">{{ currentTask.remark }}</view>
</view>
</view>
</scroll-view>
<!-- <view class="popup-footer">
<u-button type="primary" @click="editCurrentTask">编辑任务</u-button>
<u-button type="error" @click="deleteCurrentTask">删除任务</u-button>
</view> -->
</view>
</uni-popup>
</view>
</template>
<script>
import { listTaskWork, delTask, updateTask } from '@/api/oa/task.js'
export default {
data() {
return {
taskList: [],
loading: false,
searchKeyword: '',
queryParams: {
pageNum: 1,
pageSize: 10,
taskTitle: ''
},
loadMoreStatus: 'more', // more, loading, noMore
total: 0,
currentTask: null
}
},
onLoad() {
this.loadTaskList()
},
onPullDownRefresh() {
this.refreshList()
},
onReachBottom() {
this.loadMore()
},
methods: {
// 加载任务列表
async loadTaskList() {
if (this.loading) return
this.loading = true
try {
const response = await listTaskWork(this.queryParams)
if (response.code === 200) {
const { rows, total } = response
if (this.queryParams.pageNum === 1) {
this.taskList = rows || []
} else {
this.taskList = [...this.taskList, ...(rows || [])]
}
this.total = total
this.updateLoadMoreStatus(rows, total)
} else {
uni.showToast({
title: response.msg || '获取任务列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('加载任务列表失败:', error)
uni.showToast({
title: '网络错误,请重试',
icon: 'none'
})
} finally {
this.loading = false
uni.stopPullDownRefresh()
}
},
// 刷新列表
refreshList() {
this.queryParams.pageNum = 1
this.loadTaskList()
},
// 加载更多
loadMore() {
if (this.loadMoreStatus === 'noMore' || this.loading) return
this.queryParams.pageNum++
this.loadTaskList()
},
// 更新加载更多状态
updateLoadMoreStatus(rows, total) {
const currentTotal = this.taskList.length
if (currentTotal >= total) {
this.loadMoreStatus = 'noMore'
} else if (rows && rows.length < this.queryParams.pageSize) {
this.loadMoreStatus = 'noMore'
} else {
this.loadMoreStatus = 'more'
}
},
// 搜索
handleSearch() {
this.queryParams.taskTitle = this.searchKeyword
this.queryParams.pageNum = 1
this.loadTaskList()
},
// 清除搜索
handleClear() {
this.searchKeyword = ''
this.queryParams.taskTitle = ''
this.queryParams.pageNum = 1
this.loadTaskList()
},
// 显示任务详情弹窗
showTaskDetail(task) {
this.currentTask = task
this.$refs.taskDetailPopup.open()
},
// 关闭任务详情弹窗
closeTaskDetail() {
this.$refs.taskDetailPopup.close()
this.currentTask = null
},
// 获取左划操作选项
getSwipeOptions(task) {
const options = []
if (task.ownRank === 1) {
// 已置顶,显示取消置顶
options.push({
text: '取消置顶',
style: {
backgroundColor: '#999',
color: '#fff'
}
})
} else {
// 未置顶,显示置顶
options.push({
text: '置顶',
style: {
backgroundColor: '#ff9500',
color: '#fff'
}
})
}
return options
},
// 处理左划操作点击
async handleSwipeClick(e, index) {
console.log('swipe click event:', e, 'index:', index)
const { content, position } = e
if (!content) {
console.error('Invalid swipe event:', e)
return
}
const task = this.taskList[index]
if (!task) {
console.error('Task not found at index:', index)
return
}
if (content.text === '置顶') {
await this.setTaskTop(task, 1)
} else if (content.text === '取消置顶') {
await this.setTaskTop(task, 0)
}
},
// 设置任务置顶状态
async setTaskTop(task, ownRank) {
try {
const response = await updateTask({
taskId: task.taskId,
ownRank: ownRank
})
if (response.code === 200) {
uni.showToast({
title: ownRank === 1 ? '置顶成功' : '取消置顶成功',
icon: 'success'
})
// 重新请求数据以获取最新排序
this.refreshList()
} else {
uni.showToast({
title: response.msg || '操作失败',
icon: 'none'
})
}
} catch (error) {
console.error('设置置顶失败:', error)
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
}
},
// 处理任务完成
async handleTaskComplete(task) {
// 只有单任务status为0且状态为0进行中的任务才能完成
if (task.status !== 0 || task.state !== 0) {
return
}
uni.showModal({
title: '确认完成',
content: `确定要将任务"${task.taskTitle}"标记为完成吗?`,
success: async (res) => {
if (res.confirm) {
await this.completeTask(task)
}
}
})
},
// 完成任务
async completeTask(task) {
try {
const response = await updateTask({
taskId: task.taskId,
state: 1 // 设置为完成等待评分状态
})
if (response.code === 200) {
uni.showToast({
title: '任务完成',
icon: 'success'
})
// 重新请求数据
this.refreshList()
} else {
uni.showToast({
title: response.msg || '操作失败',
icon: 'none'
})
}
} catch (error) {
console.error('完成任务失败:', error)
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
}
},
// 删除任务
async deleteTask(task) {
uni.showModal({
title: '确认删除',
content: `确定要删除任务"${task.taskTitle}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
const response = await delTask(task.taskId)
if (response.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
this.refreshList()
} else {
uni.showToast({
title: response.msg || '删除失败',
icon: 'none'
})
}
} catch (error) {
console.error('删除任务失败:', error)
uni.showToast({
title: '删除失败,请重试',
icon: 'none'
})
}
}
}
})
},
// 创建任务
createTask() {
uni.navigateTo({
url: '/pages/workbench/task/create'
})
},
// 格式化日期
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'
},
// 新增:区分任务类型的点击事件
handleTaskClick(task) {
if (task.status === 1) {
// 报工任务,跳转到报工任务详情页
uni.navigateTo({
url: `/pages/workbench/task/reportTaskDetail?id=${task.taskId}`
});
} else {
// 单任务,弹出详情弹窗
this.showTaskDetail(task);
}
}
}
}
</script>
<style lang="scss" scoped>
.task-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.search-bar {
padding: 20rpx;
position: sticky;
top: 0;
z-index: 100;
}
.task-list {
padding: 20rpx;
}
.task-item {
box-sizing: border-box;
padding: 24rpx;
display: flex;
align-items: center;
position: relative;
transition: all 0.3s ease;
}
.task-checkbox {
margin-right: 20rpx;
flex-shrink: 0;
}
.checkbox-container {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
}
.task-content {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.task-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
flex: 1;
margin-right: 20rpx;
line-height: 1.4;
height: 88rpx; /* 两行文字的高度32rpx * 1.4 * 2 */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.task-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
&.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;
}
}
.empty-state {
padding: 100rpx 20rpx;
text-align: center;
}
.fab-button {
position: fixed;
right: 40rpx;
bottom: 40rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
z-index: 999;
}
// 弹窗样式
.popup-content {
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
z-index: 1001;
}
.top-badge {
position: absolute;
top: 8rpx;
right: 8rpx;
background-color: #fff8e6;
border: 1rpx solid #ff9500;
border-radius: 50%;
width: 28rpx;
height: 28rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(255, 149, 0, 0.2);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 24rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.popup-body {
flex: 1;
padding: 24rpx;
max-height: 60vh;
}
.task-detail {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.detail-section {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
padding-bottom: 12rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-item {
display: flex;
align-items: flex-start;
font-size: 28rpx;
line-height: 1.5;
}
.detail-item .label {
color: #666;
width: 160rpx;
flex-shrink: 0;
}
.detail-item .value {
color: #333;
flex: 1;
&.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;
}
}
.task-content-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
background-color: #f8f9fa;
padding: 20rpx;
border-radius: 12rpx;
border-left: 4rpx solid #1890ff;
}
.popup-footer {
display: flex;
gap: 20rpx;
padding: 24rpx;
border-top: 1rpx solid #f0f0f0;
}
</style>