OfficeSystem/pages/task/manage/index.vue
2025-11-22 15:50:58 +08:00

1064 lines
26 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-manage-page">
<!-- 筛选区域 -->
<view class="filter-section">
<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">
<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>
</view>
</view>
<!-- 状态标签和排序 -->
<view class="status-sort-section">
<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"
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: ''
});
// 状态标签
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}`
});
};
// 跳转到创建任务
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;
}
.filter-section {
background: #fff;
padding: 16px;
margin-bottom: 8px;
}
.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: #333;
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;
}
.btn-icon {
margin-right: 4px;
}
.status-sort-section {
background: #fff;
padding: 12px 16px;
margin-bottom: 8px;
}
.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-scroll {
flex: 1;
width: 100%;
}
.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>