1154 lines
32 KiB
Vue
1154 lines
32 KiB
Vue
<template>
|
||
<view class="task-manage-page">
|
||
<!-- 顶部标题栏 -->
|
||
<view class="header">
|
||
<view @click="goToTaskSearch">
|
||
<view style="height: 5px;"></view>
|
||
<img src="https://api.ccttiot.com/image-1763782244238.png" alt="" style="width: 20px !important; height: 20px !important;">
|
||
</view>
|
||
<view class="header-center">
|
||
<view class="status-tabs">
|
||
<view
|
||
class="status-tab"
|
||
:class="{ active: activeStatusTab === 'pending' }"
|
||
@click="selectStatusTab('pending')"
|
||
>
|
||
待完成
|
||
</view>
|
||
<view
|
||
class="status-tab"
|
||
:class="{ active: activeStatusTab === 'all' }"
|
||
@click="selectStatusTab('all')"
|
||
>
|
||
全部
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="filter-btn" @click="showFilter = !showFilter">
|
||
<text class="filter-text">筛选</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 筛选面板 -->
|
||
<view class="filter-panel" v-if="showFilter">
|
||
<view class="filter-row">
|
||
<view class="filter-item" @click="openProjectPicker">
|
||
<text class="filter-label">项目</text>
|
||
<view class="filter-value">
|
||
<text v-if="filterForm.projectName" class="value-text">{{ filterForm.projectName }}</text>
|
||
<text v-else class="placeholder">请选择项目</text>
|
||
</view>
|
||
</view>
|
||
<view class="filter-item" @click="openTypePicker">
|
||
<text class="filter-label">类型</text>
|
||
<view class="filter-value">
|
||
<text v-if="filterForm.typeName" class="value-text">{{ filterForm.typeName }}</text>
|
||
<text v-else class="placeholder">请选择类型</text>
|
||
</view>
|
||
</view>
|
||
<view class="filter-item" @click="openLevelPicker">
|
||
<text class="filter-label">优先级</text>
|
||
<view class="filter-value">
|
||
<text v-if="filterForm.levelName" class="value-text">{{ filterForm.levelName }}</text>
|
||
<text v-else class="placeholder">请选择优先级</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="filter-row">
|
||
<view class="filter-item" @click="openCreateUserPicker">
|
||
<text class="filter-label">创建人</text>
|
||
<view class="filter-value">
|
||
<text v-if="filterForm.createUserName" class="value-text">{{ filterForm.createUserName }}</text>
|
||
<text v-else class="placeholder">请选择用户</text>
|
||
</view>
|
||
</view>
|
||
<view class="filter-item" @click="openOwnerPicker">
|
||
<text class="filter-label">负责人</text>
|
||
<view class="filter-value">
|
||
<text v-if="filterForm.ownerUserName" class="value-text">{{ filterForm.ownerUserName }}</text>
|
||
<text v-else class="placeholder">请选择用户</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="filter-row">
|
||
<view class="filter-item full-width">
|
||
<text class="filter-label">是否逾期</text>
|
||
<view class="overdue-options">
|
||
<view
|
||
class="overdue-option"
|
||
:class="{ active: filterForm.overdue === '' }"
|
||
@click="selectOverdue('')"
|
||
>
|
||
全部
|
||
</view>
|
||
<view
|
||
class="overdue-option"
|
||
:class="{ active: filterForm.overdue === true }"
|
||
@click="selectOverdue(true)"
|
||
>
|
||
逾期
|
||
</view>
|
||
<view
|
||
class="overdue-option"
|
||
:class="{ active: filterForm.overdue === false }"
|
||
@click="selectOverdue(false)"
|
||
>
|
||
正常
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="filter-row">
|
||
<view class="filter-item" @click="openPassDatePicker">
|
||
<text class="filter-label">完成日期</text>
|
||
<view class="filter-value">
|
||
<text v-if="filterForm.passDateRangeText" class="value-text">{{ filterForm.passDateRangeText }}</text>
|
||
<text v-else class="placeholder">请选择日期</text>
|
||
</view>
|
||
</view>
|
||
<view class="filter-item" @click="openExpireDatePicker">
|
||
<text class="filter-label">开始日期</text>
|
||
<view class="filter-value">
|
||
<text v-if="filterForm.expireTimeStart" class="value-text">{{ filterForm.expireTimeStart }}</text>
|
||
<text v-else class="placeholder">请选择日期</text>
|
||
</view>
|
||
</view>
|
||
<view class="filter-item" @click="openExpireEndDatePicker">
|
||
<text class="filter-label">结束日期</text>
|
||
<view class="filter-value">
|
||
<text v-if="filterForm.expireTimeEnd" class="value-text">{{ filterForm.expireTimeEnd }}</text>
|
||
<text v-else class="placeholder">请选择日期</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="filter-row">
|
||
<view class="filter-item full-width">
|
||
<text class="filter-label">排序方式</text>
|
||
<view class="sort-options-filter">
|
||
<view
|
||
class="sort-option-filter"
|
||
:class="{ active: sortBy === 'createTime' }"
|
||
@click="selectSort('createTime')"
|
||
>
|
||
发布时间<text v-if="sortBy === 'createTime'" class="sort-arrow">{{ sortAsc ? '↑' : '↓' }}</text>
|
||
</view>
|
||
<view
|
||
class="sort-option-filter"
|
||
:class="{ active: sortBy === 'expireTime' }"
|
||
@click="selectSort('expireTime')"
|
||
>
|
||
到期时间<text v-if="sortBy === 'expireTime'" class="sort-arrow">{{ sortAsc ? '↑' : '↓' }}</text>
|
||
</view>
|
||
<view
|
||
class="sort-option-filter"
|
||
:class="{ active: sortBy === 'passTime' }"
|
||
@click="selectSort('passTime')"
|
||
>
|
||
通过时间<text v-if="sortBy === 'passTime'" class="sort-arrow">{{ sortAsc ? '↑' : '↓' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="filter-actions">
|
||
<uv-button size="small" @click="handleReset">重置</uv-button>
|
||
<uv-button type="primary" size="small" @click="handleSearch">确定</uv-button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 任务列表 -->
|
||
<scroll-view
|
||
class="task-scroll"
|
||
:class="{ 'with-filter': showFilter }"
|
||
scroll-y
|
||
:lower-threshold="50"
|
||
@scrolltolower="handleScrollToLower"
|
||
>
|
||
<view class="task-container">
|
||
<!-- 任务卡片列表 -->
|
||
<view
|
||
class="task-card"
|
||
v-for="task in tasks"
|
||
:key="task.id"
|
||
:class="getTaskCardClass(task.status)"
|
||
@click="goToTaskDetail(task)"
|
||
>
|
||
<!-- 状态标签和日期 -->
|
||
<view class="task-header">
|
||
<view style="display: flex;align-items: center;gap: 12px">
|
||
<view class="task-badge-wrapper">
|
||
<uv-tags
|
||
:text="getStatusText(task.status)"
|
||
:type="getTaskStatusType(task.status)"
|
||
size="mini"
|
||
:plain="false"
|
||
:custom-style="getTagCustomStyle(task.status)"
|
||
></uv-tags>
|
||
</view>
|
||
<view class="task-date-wrapper">
|
||
<text class="task-date">{{ task.date }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 立即处理按钮 -->
|
||
<view class="task-action" v-if="task.status !== 'completed'&&task.status !=='cancelled'">
|
||
<uv-button
|
||
:type="getButtonType(task.status)"
|
||
size="small"
|
||
@click.stop="handleTask(task)"
|
||
>
|
||
立即处理
|
||
</uv-button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 任务内容 -->
|
||
<view class="task-content">
|
||
<text class="task-project">所属项目: {{ task.project }}</text>
|
||
<text class="task-description">{{truncateText(task.description)}}</text>
|
||
<view class="task-meta">
|
||
<text class="task-owner">负责人: {{ task.owner }}</text>
|
||
<text class="task-owner">创建人: {{ task.createName }}</text>
|
||
<view class="task-time-row">
|
||
<text class="task-time">发布时间: {{ task.releaseTime }}</text>
|
||
<view class="task-countdown" v-if="task.status !== 'completed' && task.remainingDays !== null">
|
||
<text class="countdown-icon">🕐</text>
|
||
<text class="countdown-text" :class="getCountdownClass(task.status)">
|
||
{{ task.remainingDays < 0 ? `已逾期${Math.abs(task.remainingDays)}天` : `剩余${task.remainingDays}天` }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载状态 -->
|
||
<view class="empty-state" v-if="loading">
|
||
<text class="empty-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view class="empty-state" v-else-if="isEmpty">
|
||
<text class="empty-text">暂无任务</text>
|
||
</view>
|
||
|
||
<!-- 加载更多提示 -->
|
||
<view class="load-more-tip" v-if="!isEmpty && !loading && !noMore">
|
||
<text class="load-more-text">上拉加载更多</text>
|
||
</view>
|
||
<view class="load-more-tip" v-if="!isEmpty && noMore">
|
||
<text class="load-more-text">没有更多数据了</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 悬浮球按钮 -->
|
||
<FabPlus @click="goToCreateTask" />
|
||
|
||
<!-- 项目选择器 -->
|
||
<uv-picker
|
||
ref="projectPickerRef"
|
||
:columns="projectColumns"
|
||
keyName="label"
|
||
@confirm="handleProjectConfirm"
|
||
></uv-picker>
|
||
|
||
<!-- 类型选择器 -->
|
||
<uv-picker
|
||
ref="typePickerRef"
|
||
:columns="typeColumns"
|
||
keyName="label"
|
||
@confirm="handleTypeConfirm"
|
||
></uv-picker>
|
||
|
||
<!-- 优先级选择器 -->
|
||
<uv-picker
|
||
ref="levelPickerRef"
|
||
:columns="levelColumns"
|
||
keyName="label"
|
||
@confirm="handleLevelConfirm"
|
||
></uv-picker>
|
||
|
||
<!-- 创建人选择器 -->
|
||
<uv-picker
|
||
ref="createUserPickerRef"
|
||
:columns="userColumns"
|
||
keyName="label"
|
||
@confirm="handleCreateUserConfirm"
|
||
></uv-picker>
|
||
|
||
<!-- 负责人选择器 -->
|
||
<uv-picker
|
||
ref="ownerPickerRef"
|
||
:columns="userColumns"
|
||
keyName="label"
|
||
@confirm="handleOwnerConfirm"
|
||
></uv-picker>
|
||
|
||
<!-- 日期选择器 -->
|
||
<uv-datetime-picker
|
||
ref="passDateStartPickerRef"
|
||
v-model="passDateStartValue"
|
||
mode="date"
|
||
@confirm="onPassDateStartConfirm"
|
||
></uv-datetime-picker>
|
||
|
||
<uv-datetime-picker
|
||
ref="passDateEndPickerRef"
|
||
v-model="passDateEndValue"
|
||
mode="date"
|
||
@confirm="onPassDateEndConfirm"
|
||
></uv-datetime-picker>
|
||
|
||
<uv-datetime-picker
|
||
ref="expireDateStartPickerRef"
|
||
v-model="expireDateStartValue"
|
||
mode="date"
|
||
@confirm="onExpireDateStartConfirm"
|
||
></uv-datetime-picker>
|
||
|
||
<uv-datetime-picker
|
||
ref="expireDateEndPickerRef"
|
||
v-model="expireDateEndValue"
|
||
mode="date"
|
||
@confirm="onExpireDateEndConfirm"
|
||
></uv-datetime-picker>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue';
|
||
import { getTaskList, getProjectListAll, getUserList } from '@/api';
|
||
import { useDictStore } from '@/store/dict';
|
||
import { useTaskStore } from '@/store/task';
|
||
import { usePagination } from '@/composables';
|
||
import { getDictLabel } from '@/utils/dict';
|
||
import { truncateText } from '@/utils/textSolve/truncateText';
|
||
import { getStatusText, getTaskStatusType, getTaskStatusStyle } from '@/utils/taskConfig.js';
|
||
import FabPlus from '@/components/FabPlus.vue';
|
||
|
||
const dictStore = useDictStore();
|
||
|
||
// 筛选表单
|
||
const filterForm = ref({
|
||
projectId: '',
|
||
projectName: '',
|
||
type: '',
|
||
typeName: '',
|
||
level: '',
|
||
levelName: '',
|
||
createId: '',
|
||
createUserName: '',
|
||
ownerId: '',
|
||
ownerUserName: '',
|
||
overdue: '',
|
||
passDateRange: [],
|
||
passDateRangeText: '',
|
||
expireTimeStart: '',
|
||
expireTimeEnd: ''
|
||
});
|
||
|
||
// 显示筛选面板
|
||
const showFilter = ref(false);
|
||
|
||
// 状态标签
|
||
const activeStatusTab = ref('all');
|
||
|
||
// 排序
|
||
const sortBy = ref('expireTime');
|
||
const sortAsc = ref(true);
|
||
|
||
// 选择器引用
|
||
const projectPickerRef = ref(null);
|
||
const typePickerRef = ref(null);
|
||
const levelPickerRef = ref(null);
|
||
const createUserPickerRef = ref(null);
|
||
const ownerPickerRef = ref(null);
|
||
const passDateStartPickerRef = ref(null);
|
||
const passDateEndPickerRef = ref(null);
|
||
const expireDateStartPickerRef = ref(null);
|
||
const expireDateEndPickerRef = ref(null);
|
||
|
||
// 日期选择器值
|
||
const passDateStartValue = ref(Date.now());
|
||
const passDateEndValue = ref(Date.now());
|
||
const expireDateStartValue = ref(Date.now());
|
||
const expireDateEndValue = ref(Date.now());
|
||
|
||
// 选项数据
|
||
const projectOptions = ref([]);
|
||
const projectColumns = ref([[]]);
|
||
const typeOptions = ref([]);
|
||
const typeColumns = ref([[]]);
|
||
const levelOptions = ref([]);
|
||
const levelColumns = ref([[]]);
|
||
const userOptions = ref([]);
|
||
const userColumns = ref([[]]);
|
||
|
||
// 使用分页组合式函数
|
||
const {
|
||
list,
|
||
loading,
|
||
noMore,
|
||
isEmpty,
|
||
getList,
|
||
loadMore,
|
||
updateParams,
|
||
reset
|
||
} = usePagination({
|
||
fetchData: async (params) => {
|
||
// 构建请求参数
|
||
const requestParams = {
|
||
...params,
|
||
orderByColumn: sortBy.value,
|
||
isAsc: sortAsc.value ? 'ascending' : 'descending'
|
||
};
|
||
|
||
// 添加筛选参数
|
||
if (filterForm.value.projectId) {
|
||
requestParams.projectId = filterForm.value.projectId;
|
||
}
|
||
if (filterForm.value.type) {
|
||
requestParams.type = filterForm.value.type;
|
||
}
|
||
if (filterForm.value.level) {
|
||
requestParams.level = filterForm.value.level;
|
||
}
|
||
if (filterForm.value.createId) {
|
||
requestParams.createId = filterForm.value.createId;
|
||
}
|
||
if (filterForm.value.ownerId) {
|
||
requestParams.ownerId = filterForm.value.ownerId;
|
||
}
|
||
if (filterForm.value.overdue !== '') {
|
||
requestParams.overdue = filterForm.value.overdue;
|
||
}
|
||
if (filterForm.value.passDateRange.length === 2) {
|
||
requestParams.passDateRange = filterForm.value.passDateRange;
|
||
}
|
||
if (filterForm.value.expireTimeStart) {
|
||
requestParams.expireTimeStart = filterForm.value.expireTimeStart + ' 00:00:00';
|
||
}
|
||
if (filterForm.value.expireTimeEnd) {
|
||
requestParams.expireTimeEnd = filterForm.value.expireTimeEnd + ' 23:59:59';
|
||
}
|
||
|
||
// 根据状态标签添加状态筛选
|
||
if (activeStatusTab.value === 'pending') {
|
||
requestParams.statusList = [2]; // 进行中
|
||
} else if (activeStatusTab.value === 'completed') {
|
||
requestParams.statusList = [4]; // 已完成
|
||
} else if (activeStatusTab.value === 'cancelled') {
|
||
requestParams.statusList = [6]; // 已取消
|
||
}
|
||
// all 不添加状态筛选,显示所有
|
||
|
||
const res = await getTaskList(requestParams);
|
||
return res;
|
||
},
|
||
mode: 'loadMore',
|
||
pageSize: 20,
|
||
defaultParams: {}
|
||
});
|
||
|
||
// 格式化日期:将 "2024-10-31 23:59:59" 转换为 "2024-10-31"
|
||
const formatDate = (dateStr) => {
|
||
if (!dateStr) return '';
|
||
// 如果包含空格,取日期部分
|
||
return dateStr.split(' ')[0];
|
||
};
|
||
|
||
// 计算剩余天数
|
||
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;
|
||
};
|
||
|
||
// 提取负责人:从 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 determineTaskStatus = (item, expireTime) => {
|
||
// 如果任务已完成(状态为4),直接返回 completed
|
||
const taskStatusFromBackend = item.status;
|
||
if (taskStatusFromBackend === 4 || taskStatusFromBackend === 'completed') {
|
||
return 'completed';
|
||
}
|
||
|
||
// 如果没有过期时间,使用默认 pending
|
||
if (!expireTime) {
|
||
return 'pending';
|
||
}
|
||
|
||
const expireDate = new Date(expireTime);
|
||
const now = new Date();
|
||
|
||
// 如果已过期,标记为逾期
|
||
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 transformTaskData = (item) => {
|
||
const expireTime = item.expireTime || item.expire_time || '';
|
||
|
||
// 最高优先级:判断任务状态 - 先检查已取消,再检查已完成
|
||
// 支持多种字段名和数据类型:status、taskStatus、statusId 等
|
||
const taskStatus = item.status !== undefined ? item.status :
|
||
item.taskStatus !== undefined ? item.taskStatus :
|
||
item.statusId !== undefined ? item.statusId : null;
|
||
|
||
// 检查是否已取消(支持数字6、字符串'6'等多种格式)- 优先级最高
|
||
const isCancelled = taskStatus === 6 ||
|
||
taskStatus === '6' ||
|
||
String(taskStatus) === '6';
|
||
|
||
// 如果已取消,直接返回取消状态,不做过期校验,不计算剩余天数
|
||
if (isCancelled) {
|
||
return {
|
||
id: item.id || '',
|
||
status: 'cancelled', // 固定为cancelled,确保显示已取消样式
|
||
createName: item.createName || '',
|
||
date: formatDate(expireTime) || '',
|
||
project: item.projectName || item.project_name || '',
|
||
description: item.description || item.task_name || '',
|
||
owner: getOwnerNames(item.memberList || item.member_list || []),
|
||
releaseTime: formatDate(item.createTime || item.create_time) || '',
|
||
remainingDays: null // 已取消任务不计算剩余天数
|
||
};
|
||
}
|
||
|
||
// 检查是否已完成(支持数字4、字符串'4'、字符串'completed'等多种格式)
|
||
const isCompleted = taskStatus === 4 ||
|
||
taskStatus === '4' ||
|
||
taskStatus === 'completed' ||
|
||
String(taskStatus) === '4';
|
||
|
||
// 如果已完成,直接返回完成状态,不做过期校验,不计算剩余天数
|
||
if (isCompleted) {
|
||
return {
|
||
id: item.id || '',
|
||
status: 'completed', // 固定为completed,确保显示灰色样式
|
||
createName: item.createName || '',
|
||
date: formatDate(expireTime) || '',
|
||
project: item.projectName || item.project_name || '',
|
||
description: item.description || item.task_name || '',
|
||
owner: getOwnerNames(item.memberList || item.member_list || []),
|
||
releaseTime: formatDate(item.createTime || item.create_time) || '',
|
||
remainingDays: null // 已完成任务不计算剩余天数
|
||
};
|
||
}
|
||
|
||
// 未完成的任务才计算剩余天数并进行过期校验
|
||
const remainingDays = calculateRemainingDays(expireTime);
|
||
const finalStatus = determineTaskStatus(item, expireTime);
|
||
|
||
return {
|
||
id: item.id || '',
|
||
status: finalStatus,
|
||
createName: item.createName || '',
|
||
date: formatDate(expireTime) || '',
|
||
project: item.projectName || item.project_name || '',
|
||
description: item.description || item.task_name || '',
|
||
owner: getOwnerNames(item.memberList || item.member_list || []),
|
||
releaseTime: formatDate(item.createTime || item.create_time) || '',
|
||
remainingDays: remainingDays
|
||
};
|
||
};
|
||
|
||
// 将分页器的 list 转换为任务列表格式
|
||
const tasks = computed(() => {
|
||
return list.value.map(item => transformTaskData(item));
|
||
});
|
||
|
||
// 使用全局配置获取标签自定义样式
|
||
const getTagCustomStyle = (status) => {
|
||
const styleConfig = getTaskStatusStyle(status);
|
||
return {
|
||
backgroundColor: styleConfig.backgroundColor,
|
||
color: styleConfig.color,
|
||
borderColor: styleConfig.borderColor
|
||
};
|
||
};
|
||
|
||
// 获取卡片样式类
|
||
const getTaskCardClass = (status) => {
|
||
return {
|
||
'task-card-imminent': status === 'imminent',
|
||
'task-card-pending': status === 'pending',
|
||
'task-card-completed': status === 'completed',
|
||
'task-card-overdue': status === 'overdue',
|
||
'task-card-cancelled': status === 'cancelled'
|
||
};
|
||
};
|
||
|
||
// 获取按钮类型
|
||
const getButtonType = (status) => {
|
||
const typeMap = {
|
||
'imminent': 'warning',
|
||
'pending': 'primary',
|
||
'overdue': 'error'
|
||
};
|
||
return typeMap[status] || 'primary';
|
||
};
|
||
|
||
// 获取倒计时样式类
|
||
const getCountdownClass = (status) => {
|
||
return {
|
||
'countdown-warning': status === 'imminent',
|
||
'countdown-primary': status === 'pending',
|
||
'countdown-error': status === 'overdue'
|
||
};
|
||
};
|
||
|
||
// 处理任务
|
||
const handleTask = (task) => {
|
||
goToTaskDetail(task);
|
||
};
|
||
|
||
// 打开选择器
|
||
const openProjectPicker = () => {
|
||
projectPickerRef.value?.open();
|
||
};
|
||
|
||
const openTypePicker = () => {
|
||
typePickerRef.value?.open();
|
||
};
|
||
|
||
const openLevelPicker = () => {
|
||
levelPickerRef.value?.open();
|
||
};
|
||
|
||
const openCreateUserPicker = () => {
|
||
createUserPickerRef.value?.open();
|
||
};
|
||
|
||
const openOwnerPicker = () => {
|
||
ownerPickerRef.value?.open();
|
||
};
|
||
|
||
const openPassDatePicker = () => {
|
||
passDateStartPickerRef.value?.open();
|
||
};
|
||
|
||
const openExpireDatePicker = () => {
|
||
expireDateStartPickerRef.value?.open();
|
||
};
|
||
|
||
const openExpireEndDatePicker = () => {
|
||
expireDateEndPickerRef.value?.open();
|
||
};
|
||
|
||
// 选择器确认
|
||
const handleProjectConfirm = (e) => {
|
||
const selected = e.value[0];
|
||
filterForm.value.projectId = selected.value;
|
||
filterForm.value.projectName = selected.label;
|
||
};
|
||
|
||
const handleTypeConfirm = (e) => {
|
||
const selected = e.value[0];
|
||
filterForm.value.type = selected.value;
|
||
filterForm.value.typeName = selected.label;
|
||
};
|
||
|
||
const handleLevelConfirm = (e) => {
|
||
const selected = e.value[0];
|
||
filterForm.value.level = selected.value;
|
||
filterForm.value.levelName = selected.label;
|
||
};
|
||
|
||
const handleCreateUserConfirm = (e) => {
|
||
const selected = e.value[0];
|
||
filterForm.value.createId = selected.value;
|
||
filterForm.value.createUserName = selected.label;
|
||
};
|
||
|
||
const handleOwnerConfirm = (e) => {
|
||
const selected = e.value[0];
|
||
filterForm.value.ownerId = selected.value;
|
||
filterForm.value.ownerUserName = selected.label;
|
||
};
|
||
|
||
// 日期选择确认
|
||
const onPassDateStartConfirm = (e) => {
|
||
const date = formatDatePickerValue(e.value);
|
||
filterForm.value.passDateRange[0] = date;
|
||
// 如果结束日期已选择,更新文本
|
||
if (filterForm.value.passDateRange[1]) {
|
||
filterForm.value.passDateRangeText = `${date} 至 ${filterForm.value.passDateRange[1]}`;
|
||
} else {
|
||
filterForm.value.passDateRangeText = date;
|
||
// 自动打开结束日期选择器
|
||
setTimeout(() => {
|
||
passDateEndPickerRef.value?.open();
|
||
}, 300);
|
||
}
|
||
};
|
||
|
||
const onPassDateEndConfirm = (e) => {
|
||
const date = formatDatePickerValue(e.value);
|
||
filterForm.value.passDateRange[1] = date;
|
||
if (filterForm.value.passDateRange[0]) {
|
||
filterForm.value.passDateRangeText = `${filterForm.value.passDateRange[0]} 至 ${date}`;
|
||
} else {
|
||
filterForm.value.passDateRangeText = date;
|
||
}
|
||
};
|
||
|
||
const onExpireDateStartConfirm = (e) => {
|
||
filterForm.value.expireTimeStart = formatDatePickerValue(e.value);
|
||
};
|
||
|
||
const onExpireDateEndConfirm = (e) => {
|
||
filterForm.value.expireTimeEnd = formatDatePickerValue(e.value);
|
||
};
|
||
|
||
// 格式化日期选择器值
|
||
const formatDatePickerValue = (timestamp) => {
|
||
const date = new Date(timestamp);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
};
|
||
|
||
// 选择是否逾期
|
||
const selectOverdue = (value) => {
|
||
filterForm.value.overdue = value;
|
||
};
|
||
|
||
// 选择状态标签
|
||
const selectStatusTab = (tab) => {
|
||
activeStatusTab.value = tab;
|
||
reset();
|
||
getList();
|
||
};
|
||
|
||
// 选择排序
|
||
const selectSort = (field) => {
|
||
if (sortBy.value === field) {
|
||
sortAsc.value = !sortAsc.value;
|
||
} else {
|
||
sortBy.value = field;
|
||
sortAsc.value = true;
|
||
}
|
||
reset();
|
||
getList();
|
||
};
|
||
|
||
// 搜索
|
||
const handleSearch = () => {
|
||
reset();
|
||
getList();
|
||
};
|
||
|
||
// 重置
|
||
const handleReset = () => {
|
||
filterForm.value = {
|
||
projectId: '',
|
||
projectName: '',
|
||
type: '',
|
||
typeName: '',
|
||
level: '',
|
||
levelName: '',
|
||
createId: '',
|
||
createUserName: '',
|
||
ownerId: '',
|
||
ownerUserName: '',
|
||
overdue: '',
|
||
passDateRange: [],
|
||
passDateRangeText: '',
|
||
expireTimeStart: '',
|
||
expireTimeEnd: ''
|
||
};
|
||
activeStatusTab.value = 'all';
|
||
sortBy.value = 'expireTime';
|
||
sortAsc.value = true;
|
||
reset();
|
||
getList();
|
||
};
|
||
|
||
// 滚动到底部
|
||
const handleScrollToLower = () => {
|
||
if (!noMore.value && !loading.value) {
|
||
loadMore();
|
||
}
|
||
};
|
||
|
||
// 跳转到任务详情页
|
||
const goToTaskDetail = (task) => {
|
||
// 使用 Pinia store 存储任务详情数据
|
||
const taskStore = useTaskStore();
|
||
taskStore.setTaskDetail({
|
||
id: task.id,
|
||
name: task.description || '待办任务名称',
|
||
project: task.project || '所属项目',
|
||
statusTags: task.status === 'overdue' ? ['已逾期', '紧急'] :
|
||
task.status === 'imminent' ? ['即将逾期'] :
|
||
task.status === 'pending' ? ['待完成'] :
|
||
['已完成'],
|
||
deadline: task.date || '2025-10-14 18:00',
|
||
creator: task.createName,
|
||
responsible: task.owner || '张珊珊、李志',
|
||
publishTime: task.releaseTime || '2025-10-17',
|
||
content: task.description || '任务内容任务。这里是详细的任务描述,可以包含多行文本。根据实际需求,这里可以展示任务的详细要求、步骤说明、注意事项等。任务内容应该清晰明了,便于负责人理解和执行。',
|
||
submitRecords: []
|
||
});
|
||
|
||
uni.navigateTo({
|
||
url: `/pages/task/detail/index?id=${task.id}`
|
||
});
|
||
};
|
||
|
||
// 跳转到任务搜索页面
|
||
const goToTaskSearch = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/task/search/index'
|
||
});
|
||
};
|
||
|
||
// 跳转到创建任务
|
||
const goToCreateTask = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/task/add/index'
|
||
});
|
||
};
|
||
|
||
// 加载选项数据
|
||
const loadOptions = async () => {
|
||
try {
|
||
// 加载项目列表
|
||
const projectRes = await getProjectListAll();
|
||
if (projectRes && Array.isArray(projectRes)) {
|
||
projectOptions.value = projectRes.map(item => ({
|
||
label: item.name || item.projectName || '',
|
||
value: item.id || ''
|
||
})).filter(item => item.label && item.value);
|
||
projectColumns.value = [projectOptions.value];
|
||
}
|
||
|
||
// 加载任务类型
|
||
const typeDict = dictStore.getDictByType('task_type');
|
||
typeOptions.value = typeDict.map(item => ({
|
||
label: item.dictLabel,
|
||
value: item.dictValue
|
||
}));
|
||
typeColumns.value = [typeOptions.value];
|
||
|
||
// 加载优先级
|
||
const levelDict = dictStore.getDictByType('task_level');
|
||
levelOptions.value = levelDict.map(item => ({
|
||
label: item.dictLabel,
|
||
value: item.dictValue
|
||
}));
|
||
levelColumns.value = [levelOptions.value];
|
||
|
||
// 加载用户列表
|
||
const userRes = await getUserList({ pageSize: 1000 });
|
||
if (userRes && userRes.rows && Array.isArray(userRes.rows)) {
|
||
userOptions.value = userRes.rows.map(item => ({
|
||
label: item.userName || item.nickName || '',
|
||
value: item.userId || ''
|
||
})).filter(item => item.label && item.value);
|
||
userColumns.value = [userOptions.value];
|
||
}
|
||
} catch (error) {
|
||
console.error('加载选项数据失败:', error);
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
loadOptions();
|
||
getList();
|
||
|
||
// 监听任务列表刷新事件
|
||
uni.$on('taskListRefresh', () => {
|
||
reset();
|
||
getList();
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.task-manage-page {
|
||
width: 100%;
|
||
height: 100vh;
|
||
background: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 0px 24px;
|
||
background-color: #fff;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
left: 0;
|
||
z-index: 100;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
min-height: 48px;
|
||
}
|
||
|
||
.header-center {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 6px 0;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
|
||
&:active {
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
.filter-text {
|
||
font-size: 14px;
|
||
color: #2885ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.filter-panel {
|
||
background-color: #fff;
|
||
padding: 16px;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
position: fixed;
|
||
top: 48px;
|
||
right: 0;
|
||
left: 0;
|
||
z-index: 99;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
animation: slideDown 0.3s ease;
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
margin-top: 0;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.filter-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.filter-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
&.full-width {
|
||
flex: 1 1 100%;
|
||
}
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.filter-value {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #f5f5f5;
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
min-height: 36px;
|
||
}
|
||
|
||
.value-text {
|
||
font-size: 14px;
|
||
color: #333;
|
||
flex: 1;
|
||
}
|
||
|
||
.placeholder {
|
||
font-size: 14px;
|
||
color: #999;
|
||
flex: 1;
|
||
}
|
||
|
||
.overdue-options {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.overdue-option {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #f5f5f5;
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
border: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
|
||
&.active {
|
||
background: #e3f2fd;
|
||
border-color: #2885ff;
|
||
color: #2885ff;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.filter-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.btn-icon {
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.task-scroll {
|
||
flex: 1;
|
||
width: 100%;
|
||
height: 0; /* 关键:flex布局中需要设置为0才能正确计算高度 */
|
||
padding-top: 56px; /* header高度 */
|
||
transition: padding-top 0.3s ease;
|
||
box-sizing: border-box;
|
||
/* 确保scroll-view有明确的高度,这样才能触发scrolltolower事件 */
|
||
overflow: hidden;
|
||
}
|
||
|
||
.status-tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.status-tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 4px 12px;
|
||
background: transparent;
|
||
border-radius: 0;
|
||
font-size: 14px;
|
||
color: #666;
|
||
transition: all 0.2s;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
position: relative;
|
||
|
||
&:active {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
&.active {
|
||
background: transparent;
|
||
color: #2885ff;
|
||
font-weight: 500;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 12px;
|
||
right: 12px;
|
||
height: 2px;
|
||
background-color: #2885ff;
|
||
border-radius: 1px;
|
||
}
|
||
}
|
||
|
||
.count {
|
||
font-size: 12px;
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.sort-options-filter {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.sort-option-filter {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
background: #f5f5f5;
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
border: 2px solid transparent;
|
||
font-size: 14px;
|
||
color: #666;
|
||
transition: all 0.2s;
|
||
|
||
&.active {
|
||
background: #e3f2fd;
|
||
border-color: #2885ff;
|
||
color: #2885ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.sort-arrow {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
|
||
@import '@/styles/task-card.scss';
|
||
</style>
|
||
|