OfficeSystem/pages/task/manage/index.vue

1153 lines
28 KiB
Vue
Raw Normal View History

2025-11-22 15:50:58 +08:00
<template>
<view class="task-manage-page">
2025-11-22 16:07:16 +08:00
<!-- 顶部标题栏 -->
<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">
2025-11-22 15:50:58 +08:00
<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-actions">
2025-11-22 16:07:16 +08:00
<uv-button size="small" @click="handleReset">重置</uv-button>
<uv-button type="primary" size="small" @click="handleSearch">确定</uv-button>
2025-11-22 15:50:58 +08:00
</view>
</view>
<!-- 状态标签和排序 -->
2025-11-22 16:07:16 +08:00
<view class="status-sort-section" :class="{ 'with-filter': showFilter }">
2025-11-22 15:50:58 +08:00
<view class="status-tabs">
<view
class="status-tab"
:class="{ active: activeStatusTab === 'pending' }"
@click="selectStatusTab('pending')"
>
待完成
</view>
<view
class="status-tab"
:class="{ active: activeStatusTab === 'completed' }"
@click="selectStatusTab('completed')"
>
已完成
</view>
<view
class="status-tab"
:class="{ active: activeStatusTab === 'cancelled' }"
@click="selectStatusTab('cancelled')"
>
已取消
</view>
<view
class="status-tab"
:class="{ active: activeStatusTab === 'all' }"
@click="selectStatusTab('all')"
>
全部任务
</view>
</view>
<view class="sort-options">
<view
class="sort-option"
:class="{ active: sortBy === 'createTime' }"
@click="selectSort('createTime')"
>
发布时间<text v-if="sortBy === 'createTime'" class="sort-arrow">{{ sortAsc ? '' : '' }}</text>
</view>
<view
class="sort-option"
:class="{ active: sortBy === 'expireTime' }"
@click="selectSort('expireTime')"
>
到期时间<text v-if="sortBy === 'expireTime'" class="sort-arrow">{{ sortAsc ? '' : '' }}</text>
</view>
<view
class="sort-option"
:class="{ active: sortBy === 'passTime' }"
@click="selectSort('passTime')"
>
通过时间<text v-if="sortBy === 'passTime'" class="sort-arrow">{{ sortAsc ? '' : '' }}</text>
</view>
</view>
</view>
<!-- 任务列表 -->
<scroll-view
class="task-scroll"
2025-11-22 16:07:16 +08:00
:class="{ 'with-filter': showFilter }"
2025-11-22 15:50:58 +08:00
scroll-y
@scrolltolower="handleScrollToLower"
>
<view class="task-container">
<view
class="task-card"
v-for="task in tasks"
:key="task.id"
@click="goToTaskDetail(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="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 { usePagination } from '@/composables';
import { getDictLabel } from '@/utils/dict';
import { truncateText } from '@/utils/textSolve/truncateText';
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: ''
});
2025-11-22 16:07:16 +08:00
// 显示筛选面板
const showFilter = ref(false);
2025-11-22 15:50:58 +08:00
// 状态标签
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: {}
});
// 任务列表
const tasks = computed(() => list.value);
// 获取状态文本(根据数字状态)
const getStatusText = (status) => {
if (!status && status !== 0) return '未知';
// 从字典获取状态文本
const dictLabel = getDictLabel('task_status', String(status));
if (dictLabel && dictLabel !== String(status)) {
return dictLabel;
}
// 默认映射
const statusMap = {
'1': '待接收',
'2': '进行中',
'3': '已提交',
'4': '已完成',
'5': '已驳回',
'6': '已取消',
'7': '逾期完成',
'10': '待完成'
};
return statusMap[String(status)] || `状态${status}`;
};
// 获取状态类型(用于标签颜色)
const getTaskStatusType = (status) => {
if (!status && status !== 0) return 'primary';
// 从字典获取listClass映射到uv-tags的type
const statusDict = dictStore.getDictByType('task_status');
const statusItem = statusDict.find(item => item.dictValue === String(status));
if (statusItem) {
const listClassMap = {
'primary': 'primary',
'success': 'success',
'warning': 'warning',
'danger': 'error',
'info': 'info'
};
return listClassMap[statusItem.listClass] || 'primary';
}
// 默认映射
const typeMap = {
'1': 'info', // 待接收
'2': 'warning', // 进行中
'3': 'primary', // 已提交
'4': 'success', // 已完成
'5': 'error', // 已驳回
'6': 'error', // 已取消
'7': 'warning', // 逾期完成
'10': 'primary' // 待完成
};
return typeMap[String(status)] || 'primary';
};
// 获取标签自定义样式
const getTagCustomStyle = (status) => {
const statusDict = dictStore.getDictByType('task_status');
const statusItem = statusDict.find(item => item.dictValue === String(status));
// 默认样式
const defaultStyle = {
backgroundColor: '#909399',
color: '#fff',
borderColor: '#909399'
};
if (!statusItem) {
return defaultStyle;
}
// 根据listClass设置颜色
const colorMap = {
'primary': { backgroundColor: '#2885ff', color: '#fff', borderColor: '#2885ff' },
'success': { backgroundColor: '#67c23a', color: '#fff', borderColor: '#67c23a' },
'warning': { backgroundColor: '#ff9800', color: '#fff', borderColor: '#ff9800' },
'danger': { backgroundColor: '#f56c6c', color: '#fff', borderColor: '#f56c6c' },
'info': { backgroundColor: '#909399', color: '#fff', borderColor: '#909399' }
};
return colorMap[statusItem.listClass] || defaultStyle;
};
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '';
return dateStr.split(' ')[0];
};
// 获取负责人名称
const getOwnerNames = (memberList) => {
if (!Array.isArray(memberList) || memberList.length === 0) return '';
return memberList.map(member => member.userName || member.name || '').filter(name => name).join('、');
};
// 打开选择器
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) => {
uni.navigateTo({
url: `/pages/task/detail/index?id=${task.id}`
});
};
2025-11-22 16:07:16 +08:00
// 跳转到任务搜索页面
const goToTaskSearch = () => {
uni.navigateTo({
url: '/pages/task/search/index'
});
};
2025-11-22 15:50:58 +08:00
// 跳转到创建任务
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;
}
2025-11-22 16:07:16 +08:00
.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;
2025-11-22 15:50:58 +08:00
padding: 16px;
2025-11-22 16:07:16 +08:00
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);
}
2025-11-22 15:50:58 +08:00
}
.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;
2025-11-22 16:07:16 +08:00
color: #606266;
2025-11-22 15:50:58 +08:00
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;
2025-11-22 16:07:16 +08:00
justify-content: flex-end;
2025-11-22 15:50:58 +08:00
}
.btn-icon {
margin-right: 4px;
}
.status-sort-section {
background: #fff;
padding: 12px 16px;
margin-bottom: 8px;
2025-11-22 16:07:16 +08:00
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;
}
2025-11-22 15:50:58 +08:00
}
.status-tabs {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.status-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: #f5f5f5;
border-radius: 16px;
font-size: 14px;
color: #666;
transition: all 0.2s;
&.active {
background: #2885ff;
color: #fff;
}
.count {
font-size: 12px;
opacity: 0.8;
}
}
.sort-options {
display: flex;
gap: 16px;
align-items: center;
}
.sort-option {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: #666;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
&.active {
color: #2885ff;
font-weight: 500;
}
.sort-arrow {
font-size: 12px;
}
}
.task-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.task-card {
background: #fff;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.task-card:active {
transform: scale(0.98);
opacity: 0.9;
}
.task-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.task-badge-wrapper {
flex-shrink: 0;
}
.task-meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-end;
}
.task-project {
font-size: 14px;
color: #666;
}
.task-date {
font-size: 12px;
color: #999;
}
.task-content {
margin-top: 4px;
}
.task-description {
font-size: 14px;
color: #333;
line-height: 1.6;
}
.task-footer {
display: flex;
flex-direction: column;
gap: 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 {
padding: 60px 20px;
text-align: center;
}
.empty-text {
font-size: 14px;
color: #999;
}
.load-more-tip {
padding: 20px;
text-align: center;
}
.load-more-text {
font-size: 12px;
color: #999;
}
</style>