任务管理0.5

This commit is contained in:
WindowBird 2025-11-22 16:07:16 +08:00
parent 4dc9c9e989
commit 1bd14c1e01
3 changed files with 617 additions and 19 deletions

View File

@ -82,6 +82,12 @@
"navigationBarTitleText": "任务管理"
}
},
{
"path": "pages/task/search/index",
"style": {
"navigationBarTitleText": "任务搜索"
}
},
{
"path": "pages/task/add/index",
"style": {

View File

@ -1,7 +1,19 @@
<template>
<view class="task-manage-page">
<!-- 筛选区域 -->
<view class="filter-section">
<!-- 顶部标题栏 -->
<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 style="flex: 1;"></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>
@ -97,19 +109,13 @@
</view>
<view class="filter-actions">
<uv-button type="primary" size="normal" @click="handleSearch">
<text class="btn-icon">🔍</text>
<text>搜索</text>
</uv-button>
<uv-button size="normal" @click="handleReset">
<text class="btn-icon">🔄</text>
<text>重置</text>
</uv-button>
<uv-button size="small" @click="handleReset">重置</uv-button>
<uv-button type="primary" size="small" @click="handleSearch">确定</uv-button>
</view>
</view>
<!-- 状态标签和排序 -->
<view class="status-sort-section">
<view class="status-sort-section" :class="{ 'with-filter': showFilter }">
<view class="status-tabs">
<view
class="status-tab"
@ -169,6 +175,7 @@
<!-- 任务列表 -->
<scroll-view
class="task-scroll"
:class="{ 'with-filter': showFilter }"
scroll-y
@scrolltolower="handleScrollToLower"
>
@ -345,6 +352,9 @@ const filterForm = ref({
expireTimeEnd: ''
});
//
const showFilter = ref(false);
//
const activeStatusTab = ref('all');
@ -717,6 +727,13 @@ const goToTaskDetail = (task) => {
});
};
//
const goToTaskSearch = () => {
uni.navigateTo({
url: '/pages/task/search/index'
});
};
//
const goToCreateTask = () => {
uni.navigateTo({
@ -788,10 +805,62 @@ onMounted(() => {
flex-direction: column;
}
.filter-section {
background: #fff;
.header {
display: flex;
align-items: center;
gap: 16px;
padding: 5px 24px;
background-color: #fff;
border-bottom: 1px solid #e4e7ed;
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 100;
}
.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;
margin-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
position: fixed;
top: 41px;
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;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.filter-row {
@ -817,7 +886,7 @@ onMounted(() => {
.filter-label {
font-size: 14px;
color: #333;
color: #606266;
font-weight: 500;
}
@ -871,6 +940,7 @@ onMounted(() => {
display: flex;
gap: 12px;
margin-top: 16px;
justify-content: flex-end;
}
.btn-icon {
@ -881,6 +951,29 @@ onMounted(() => {
background: #fff;
padding: 12px 16px;
margin-bottom: 8px;
position: fixed;
top: 41px;
right: 0;
left: 0;
z-index: 98;
transition: top 0.3s ease;
&.with-filter {
top: auto;
position: relative;
margin-top: 0;
}
}
.task-scroll {
flex: 1;
width: 100%;
padding-top: 100px; /* header(41px) + status-sort-section(约60px) */
transition: padding-top 0.3s ease;
&.with-filter {
padding-top: 0;
}
}
.status-tabs {
@ -938,10 +1031,6 @@ onMounted(() => {
}
}
.task-scroll {
flex: 1;
width: 100%;
}
.task-container {
padding: 16px;

503
pages/task/search/index.vue Normal file
View File

@ -0,0 +1,503 @@
<template>
<view class="task-search-page">
<view class="search-panel">
<view class="form-row">
<text class="form-label">项目名称</text>
<uv-input
v-model="form.projectName"
placeholder="请输入项目名称"
clearable
/>
</view>
<view class="form-row">
<text class="form-label">任务描述</text>
<uv-input
v-model="form.description"
placeholder="请输入任务描述"
clearable
/>
</view>
<view class="form-row">
<text class="form-label">创建人</text>
<uv-input
v-model="form.createUserName"
placeholder="请输入创建人姓名"
clearable
/>
</view>
<view class="form-row">
<text class="form-label">负责人</text>
<uv-input
v-model="form.ownerUserName"
placeholder="请输入负责人姓名"
clearable
/>
</view>
<text class="form-tip">项目名称 / 任务描述 / 创建人 / 负责人至少填写一项</text>
<view class="form-actions">
<uv-button size="small" plain @click="handleReset">重置</uv-button>
<uv-button
size="small"
type="primary"
:disabled="!canSearch"
@click="handleSearch"
>
搜索
</uv-button>
</view>
</view>
<view class="result-section">
<view v-if="!hasSearched" class="result-placeholder">
输入任务信息后点击搜索查看结果
</view>
<template v-else>
<view
class="task-card"
v-for="task in list"
:key="task.id"
@click="handleTaskClick(task)"
>
<view class="task-header">
<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>
<uv-tags
v-if="task.overdue && task.status !== '4' && task.status !== 4"
text="逾期"
type="error"
size="mini"
:plain="false"
style="margin-left: 8px;"
></uv-tags>
</view>
<view class="task-meta">
<text class="task-project">{{ task.projectName || '未分配项目' }}</text>
<text class="task-date">{{ formatDate(task.expireTime) }}</text>
</view>
</view>
<view class="task-content">
<text class="task-description">{{ truncateText(task.description, 80) }}</text>
</view>
<view class="task-footer">
<view class="task-users">
<text class="task-user-label">创建人:</text>
<text class="task-user-name">{{ task.createName || '未知' }}</text>
<text class="task-user-label" style="margin-left: 12px;">负责人:</text>
<text class="task-user-name">{{ getOwnerNames(task.memberList) || '未分配' }}</text>
</view>
<view class="task-times">
<text class="task-time">发布时间: {{ formatDate(task.createTime) }}</text>
<text v-if="task.passTime" class="task-time">通过时间: {{ formatDate(task.passTime) }}</text>
</view>
</view>
</view>
<view class="empty-state" v-if="isEmpty && !loading">
<text class="empty-text">暂无匹配任务</text>
</view>
<view class="loading-state" v-if="loading && list.length === 0">
<text class="loading-text">加载中...</text>
</view>
<view class="load-more" v-if="hasSearched && list.length > 0">
<text class="load-more-text">
{{ loading ? '加载中...' : (noMore ? '没有更多数据了' : '上拉加载更多') }}
</text>
</view>
</template>
</view>
</view>
</template>
<script setup>
import { reactive, ref, computed } from 'vue';
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
import { getTaskList } from '@/api/task';
import { usePagination } from '@/composables';
import { useDictStore } from '@/store/dict';
import { getDictLabel } from '@/utils/dict';
const PAGE_SIZE = 20;
const BASE_QUERY = {
orderByColumn: 'createTime',
isAsc: 'descending',
};
const dictStore = useDictStore();
const form = reactive({
projectName: '',
description: '',
createUserName: '',
ownerUserName: ''
});
const hasSearched = ref(false);
const {
list,
loading,
noMore,
isEmpty,
getList,
loadMore,
updateParams,
reset,
queryParams
} = usePagination({
fetchData: getTaskList,
pageSize: PAGE_SIZE,
defaultParams: {
...BASE_QUERY
}
});
const toast = (message) => {
if (uni?.$uv?.toast) {
uni.$uv.toast(message);
} else {
uni.showToast({
title: message,
icon: 'none'
});
}
};
const sanitize = (value) => {
if (!value) return undefined;
const trimmed = value.trim();
return trimmed.length ? trimmed : undefined;
};
const canSearch = computed(() =>
Boolean(
sanitize(form.projectName) ||
sanitize(form.description) ||
sanitize(form.createUserName) ||
sanitize(form.ownerUserName)
)
);
const handleSearch = () => {
if (!canSearch.value) {
toast('请输入项目名称/任务描述/创建人/负责人任意一项');
return;
}
hasSearched.value = true;
updateParams({
...BASE_QUERY,
projectName: sanitize(form.projectName),
description: sanitize(form.description),
createUserName: sanitize(form.createUserName),
ownerUserName: sanitize(form.ownerUserName)
});
};
const handleReset = () => {
form.projectName = '';
form.description = '';
form.createUserName = '';
form.ownerUserName = '';
hasSearched.value = false;
reset();
queryParams.value = {
pageNum: 1,
pageSize: PAGE_SIZE,
...BASE_QUERY
};
};
//
const formatDate = (dateStr) => {
if (!dateStr) return '未知';
if (typeof dateStr === 'number') {
return formatDateValue(dateStr);
}
return dateStr.split(' ')[0];
};
const formatDateValue = (value) => {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
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 getStatusText = (status) => {
if (status === undefined || status === null) return '未知';
const dictLabel = getDictLabel('task_status', status);
if (dictLabel && dictLabel !== String(status)) {
return dictLabel;
}
return `状态${status}`;
};
//
const getTaskStatusType = (status) => {
if (status === '4' || status === 4) {
return 'success';
} else if (status === '6' || status === 6) {
return 'info';
} else {
return 'warning';
}
};
//
const getTagCustomStyle = (status) => {
if (status === '4' || status === 4) {
return { backgroundColor: '#67C23A', color: '#fff' };
} else if (status === '6' || status === 6) {
return { backgroundColor: '#909399', color: '#fff' };
} else {
return { backgroundColor: '#E6A23C', color: '#fff' };
}
};
//
const truncateText = (text, maxLength) => {
if (!text) return '';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
//
const getOwnerNames = (memberList) => {
if (!memberList || !Array.isArray(memberList) || memberList.length === 0) {
return '';
}
return memberList.map(member => member.userName || member.name || '').filter(name => name).join('、') || '';
};
//
const handleTaskClick = (task) => {
uni.navigateTo({
url: `/pages/task/detail/index?id=${task.id}`
});
};
onPullDownRefresh(async () => {
if (!hasSearched.value) {
uni.stopPullDownRefresh();
return;
}
try {
await getList(true);
} finally {
uni.stopPullDownRefresh();
}
});
onReachBottom(() => {
if (hasSearched.value && !loading.value && !noMore.value) {
loadMore();
}
});
</script>
<style lang="scss" scoped>
.task-search-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f7fa;
padding-bottom: 40px;
}
.search-panel {
background-color: #fff;
padding: 16px;
margin: 12px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.form-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.form-label {
width: 80px;
font-size: 14px;
color: #606266;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-bottom: 12px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.result-section {
flex: 1;
}
.result-placeholder {
margin: 80px auto;
text-align: center;
color: #909399;
font-size: 14px;
}
.task-card {
background: #fff;
border-radius: 8px;
padding: 16px;
margin: 0 12px 12px 12px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
position: relative;
transition: all 0.3s ease;
cursor: pointer;
}
.task-card:active {
transform: scale(0.98);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.task-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 4px;
}
.task-badge-wrapper {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.task-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.task-project {
font-size: 12px;
color: #666;
}
.task-date {
font-size: 12px;
color: #999;
}
.task-content {
margin-top: 8px;
}
.task-description {
font-size: 14px;
color: #333;
line-height: 1.5;
}
.task-footer {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
}
.task-users {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
.task-user-label {
font-size: 12px;
color: #999;
}
.task-user-name {
font-size: 12px;
color: #666;
}
.task-times {
display: flex;
flex-direction: column;
gap: 4px;
}
.task-time {
font-size: 12px;
color: #999;
}
.empty-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 80px 20px;
}
.empty-text {
font-size: 14px;
color: #909399;
margin-top: 12px;
}
.loading-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 60px 20px;
}
.loading-text {
font-size: 14px;
color: #909399;
}
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
margin-bottom: 20px;
}
.load-more-text {
font-size: 13px;
color: #909399;
}
</style>