refactor(workbench): 重构待办面板功能提升用户体验
- 替换原有时间桶过滤为分类标签页和截止时间筛选器 - 添加优先级排序功能,支持任务类别内按优先级排序 - 重构待办数据结构,新增创建时间和优先级字段 - 移除高优先级标记,统一使用优先级枚举值 - 添加个人事项创建对话框和相关操作功能 - 更新模拟数据以匹配新的数据结构和功能需求 - 优化列表排序逻辑,按创建时间升序排列,无截止时间排最后 - 为各类别待办项添加逾期状态标识和计数统计 - 实现分页加载,每页显示5条待办记录 - 更新样式类名以匹配新的逾期判断逻辑 refactor(project): 优化项目执行模块提升性能和可维护性 - 移除执行项点击切换功能相关的事件和方法 - 删除不再使用的select-execution事件发射器 - 移除执行标签的悬停效果和鼠标指针样式 - 重构任务表格视图,将日期格式化函数名称标准化 - 在跨执行模式下也显示进度列,统一界面布局 - 更新最近更新列宽度并调整日期格式显示 - 将默认页面大小从10增加到20以提高加载效率 feat(list): 统一日期格式化功能简化代码维护 - 将日期时间格式化函数重命名为更准确的date格式化 - 在产品列表和项目列表中统一使用新的日期格式化函数 - 移除秒数显示,仅保留年月日格式提高可读性 refactor(todo): 重构待办事项数据模型和过滤逻辑 - 重新定义待办事项分类类型,移除mention添加personal - 新增主标签、截止时间筛选器和优先级类型定义 - 添加创建时间字段用于排序和显示 - 实现基于分类、截止时间和优先级的过滤函数 - 创建优先级权重映射用于排序算法 - 更新待办项构建函数以支持新的排序逻辑 - 修改逾期判断逻辑以适应新的数据结构 - 移除原有的高优先级字段,统一使用优先级枚举 - 添加优先级排序功能支持升序降序切换 - 重构排序算法,优先按创建时间,其次按截止时间排序 refactor(task): 清理任务模块中已废弃的功能 - 移除通过ID选择执行项的相关函数和事件处理器 - 删除任务卡片和表格中的执行项点击切换功能 - 更新任务工作区组件以移除废弃的事件监听 - 调整任务表格视图中进度条的样式和状态显示 refactor(components): 项目列表中添加进度条可视化组件 - 引入Element Plus进度条组件用于项目进度展示 - 在项目列表中添加进度列并实现进度条渲染 - 配置进度条样式包括内嵌文字、成功状态和边框圆角 - 调整进度列宽度以适应进度条显示需求 refactor(widgets): 整理工作台模块配置和清理冗余组件 - 从工作台模块注册中移除已废弃的myTicket组件 - 更新模块注释说明,明确myTicket已废弃的原因 - 删除不再使用的workbench-my-ticket.vue组件文件 - 更新模块总数注释从16个调整为15个
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { RouteKey } from '@elegant-router/types';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
||||
import {
|
||||
type WorkbenchTodoDeadlineFilter,
|
||||
type WorkbenchTodoItem,
|
||||
type WorkbenchTodoTimeBucket,
|
||||
type WorkbenchTodoMainTab,
|
||||
buildWorkbenchTodoItems,
|
||||
filterWorkbenchTodoItems
|
||||
filterWorkbenchTodoItemsByCategory,
|
||||
filterWorkbenchTodoItemsByDeadline,
|
||||
isWorkbenchTodoOverdue,
|
||||
sortWorkbenchTodoItemsByPriority
|
||||
} from '../homepage';
|
||||
import { workbenchTodoMock } from '../mock';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
type SortKey = 'created' | 'priority' | 'deadline';
|
||||
|
||||
defineOptions({ name: 'WorkbenchTodoPanel' });
|
||||
|
||||
interface Props {
|
||||
@@ -27,25 +34,129 @@ defineEmits<{
|
||||
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const activeBucket = ref<WorkbenchTodoTimeBucket>('all');
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
const buckets: Array<{ key: WorkbenchTodoTimeBucket; label: string }> = [
|
||||
const activeTab = ref<WorkbenchTodoMainTab>('all');
|
||||
const activeDeadlineFilter = ref<WorkbenchTodoDeadlineFilter>(null);
|
||||
const activeSort = ref<SortKey>('deadline');
|
||||
const currentPage = ref(1);
|
||||
|
||||
const sortOptions = computed<Array<{ key: SortKey; label: string }>>(() => {
|
||||
const base: Array<{ key: SortKey; label: string }> = [
|
||||
{ key: 'deadline', label: '截止时间' },
|
||||
{ key: 'created', label: '创建时间' }
|
||||
];
|
||||
if (activeTab.value === 'task') {
|
||||
base.push({ key: 'priority', label: '优先级' });
|
||||
}
|
||||
return base;
|
||||
});
|
||||
|
||||
const mainTabs: Array<{ key: WorkbenchTodoMainTab; label: string }> = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'today', label: '今日' },
|
||||
{ key: 'week', label: '本周' },
|
||||
{ key: 'overdue', label: '逾期' }
|
||||
{ key: 'task', label: '任务' },
|
||||
{ key: 'ticket', label: '工单' },
|
||||
{ key: 'personal', label: '个人事项' },
|
||||
{ key: 'review', label: '待评审' }
|
||||
];
|
||||
|
||||
const items = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
|
||||
const deadlineFilters: Array<{ key: Exclude<WorkbenchTodoDeadlineFilter, null>; label: string }> = [
|
||||
{ key: 'overdue', label: '已逾期' },
|
||||
{ key: 'today', label: '今日到期' },
|
||||
{ key: 'week', label: '本周到期' }
|
||||
];
|
||||
|
||||
const bucketCounts = computed(() => ({
|
||||
all: items.value.length,
|
||||
today: filterWorkbenchTodoItems(items.value, 'today').length,
|
||||
week: filterWorkbenchTodoItems(items.value, 'week').length,
|
||||
overdue: filterWorkbenchTodoItems(items.value, 'overdue').length
|
||||
}));
|
||||
const allItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
|
||||
|
||||
const filteredItems = computed(() => filterWorkbenchTodoItems(items.value, activeBucket.value));
|
||||
const addDialogVisible = ref(false);
|
||||
|
||||
function handleOpenAdd() {
|
||||
addDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleAddSubmitted() {
|
||||
activeTab.value = 'personal';
|
||||
activeDeadlineFilter.value = null;
|
||||
}
|
||||
|
||||
const tabCounts = computed(() => {
|
||||
const counts: Record<WorkbenchTodoMainTab, number> = {
|
||||
all: allItems.value.length,
|
||||
task: 0,
|
||||
ticket: 0,
|
||||
personal: 0,
|
||||
review: 0
|
||||
};
|
||||
allItems.value.forEach(item => {
|
||||
counts[item.category] += 1;
|
||||
});
|
||||
return counts;
|
||||
});
|
||||
|
||||
const tabOverdueCount = computed(() => {
|
||||
const map: Record<WorkbenchTodoMainTab, number> = {
|
||||
all: 0,
|
||||
task: 0,
|
||||
ticket: 0,
|
||||
personal: 0,
|
||||
review: 0
|
||||
};
|
||||
allItems.value.forEach(item => {
|
||||
if (!isWorkbenchTodoOverdue(item)) return;
|
||||
map.all += 1;
|
||||
map[item.category] += 1;
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const itemsInTab = computed(() => filterWorkbenchTodoItemsByCategory(allItems.value, activeTab.value));
|
||||
|
||||
const filteredItems = computed(() => filterWorkbenchTodoItemsByDeadline(itemsInTab.value, activeDeadlineFilter.value));
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
const base = filteredItems.value;
|
||||
if (activeSort.value === 'priority') {
|
||||
return sortWorkbenchTodoItemsByPriority(base, 'desc');
|
||||
}
|
||||
if (activeSort.value === 'deadline') {
|
||||
return [...base].sort((left, right) => {
|
||||
const leftValue = left.remainingDays === null ? Number.POSITIVE_INFINITY : left.remainingDays;
|
||||
const rightValue = right.remainingDays === null ? Number.POSITIVE_INFINITY : right.remainingDays;
|
||||
return leftValue - rightValue;
|
||||
});
|
||||
}
|
||||
return base;
|
||||
});
|
||||
|
||||
const currentSortLabel = computed(
|
||||
() => sortOptions.value.find(option => option.key === activeSort.value)?.label ?? '排序'
|
||||
);
|
||||
|
||||
const pagedItems = computed(() => {
|
||||
const start = (currentPage.value - 1) * PAGE_SIZE;
|
||||
return sortedItems.value.slice(start, start + PAGE_SIZE);
|
||||
});
|
||||
|
||||
watch([activeTab, activeDeadlineFilter, activeSort], () => {
|
||||
currentPage.value = 1;
|
||||
});
|
||||
|
||||
function handleSelectTab(key: WorkbenchTodoMainTab) {
|
||||
if (activeTab.value === key) return;
|
||||
activeTab.value = key;
|
||||
activeDeadlineFilter.value = null;
|
||||
if (key !== 'task' && activeSort.value === 'priority') {
|
||||
activeSort.value = 'deadline';
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectDeadlineFilter(key: Exclude<WorkbenchTodoDeadlineFilter, null>) {
|
||||
activeDeadlineFilter.value = activeDeadlineFilter.value === key ? null : key;
|
||||
}
|
||||
|
||||
function handleSelectSort(key: SortKey) {
|
||||
activeSort.value = key;
|
||||
}
|
||||
|
||||
function handleClickItem(item: WorkbenchTodoItem) {
|
||||
if (!item.routeKey) return;
|
||||
@@ -53,9 +164,7 @@ function handleClickItem(item: WorkbenchTodoItem) {
|
||||
}
|
||||
|
||||
function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
if (item.overdue || (item.remainingDays !== null && item.remainingDays < 0)) {
|
||||
return 'workbench-todo__deadline--rose';
|
||||
}
|
||||
if (isWorkbenchTodoOverdue(item)) return 'workbench-todo__deadline--rose';
|
||||
if (item.remainingDays === 0) return 'workbench-todo__deadline--amber';
|
||||
return 'workbench-todo__deadline--slate';
|
||||
}
|
||||
@@ -71,61 +180,165 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
>
|
||||
<div class="workbench-todo__tabs">
|
||||
<button
|
||||
v-for="bucket in buckets"
|
||||
:key="bucket.key"
|
||||
type="button"
|
||||
class="workbench-todo__tab"
|
||||
:class="{ 'workbench-todo__tab--active': activeBucket === bucket.key }"
|
||||
@click="activeBucket = bucket.key"
|
||||
>
|
||||
<span>{{ bucket.label }}</span>
|
||||
<span class="workbench-todo__tab-count">{{ bucketCounts[bucket.key] }}</span>
|
||||
<div class="workbench-todo__tabs-group">
|
||||
<ElTooltip
|
||||
v-for="tab in mainTabs"
|
||||
:key="tab.key"
|
||||
:content="`已逾期 ${tabOverdueCount[tab.key]} 项,建议尽快处理`"
|
||||
:disabled="tabOverdueCount[tab.key] === 0"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="workbench-todo__tab"
|
||||
:class="{ 'workbench-todo__tab--active': activeTab === tab.key }"
|
||||
@click="handleSelectTab(tab.key)"
|
||||
>
|
||||
<span>{{ tab.label }}</span>
|
||||
<span class="workbench-todo__tab-count">{{ tabCounts[tab.key] }}</span>
|
||||
<span v-if="tabOverdueCount[tab.key] > 0" class="workbench-todo__tab-dot" />
|
||||
</button>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
|
||||
<button type="button" class="workbench-todo__add" @click="handleOpenAdd">
|
||||
<SvgIcon icon="mdi:plus" class="workbench-todo__add-icon" />
|
||||
<span>个人事项</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredItems.length" class="workbench-todo__list">
|
||||
<article
|
||||
v-for="item in filteredItems"
|
||||
:key="item.id"
|
||||
class="workbench-todo__item"
|
||||
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey) }"
|
||||
@click="handleClickItem(item)"
|
||||
>
|
||||
<div class="workbench-todo__leading">
|
||||
<span class="workbench-todo__category" :class="`workbench-todo__category--${item.categoryTone}`">
|
||||
{{ item.categoryLabel }}
|
||||
</span>
|
||||
<span v-if="item.highPriority" class="workbench-todo__priority">高</span>
|
||||
</div>
|
||||
<div class="workbench-todo__filters">
|
||||
<div class="workbench-todo__filters-left">
|
||||
<button
|
||||
v-for="filter in deadlineFilters"
|
||||
:key="filter.key"
|
||||
type="button"
|
||||
class="workbench-todo__filter"
|
||||
:class="{ 'workbench-todo__filter--active': activeDeadlineFilter === filter.key }"
|
||||
@click="handleSelectDeadlineFilter(filter.key)"
|
||||
>
|
||||
{{ filter.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="workbench-todo__body">
|
||||
<h4 class="workbench-todo__item-title">{{ item.title }}</h4>
|
||||
<div class="workbench-todo__meta">
|
||||
<span class="workbench-todo__source">{{ item.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workbench-todo__trailing">
|
||||
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
|
||||
{{ item.deadlineLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
<ElDropdown trigger="click" placement="bottom-end" @command="handleSelectSort">
|
||||
<span class="workbench-todo__sort">
|
||||
排序:{{ currentSortLabel }}
|
||||
<SvgIcon icon="mdi:chevron-down" class="workbench-todo__sort-icon" />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="option in sortOptions"
|
||||
:key="option.key"
|
||||
:command="option.key"
|
||||
:class="{ 'is-active': activeSort === option.key }"
|
||||
>
|
||||
{{ option.label }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
<ElEmpty v-else description="当前筛选下暂无待办" :image-size="72" />
|
||||
|
||||
<div class="workbench-todo__content">
|
||||
<div v-if="pagedItems.length" class="workbench-todo__list">
|
||||
<article
|
||||
v-for="item in pagedItems"
|
||||
:key="item.id"
|
||||
class="workbench-todo__item"
|
||||
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey) }"
|
||||
@click="handleClickItem(item)"
|
||||
>
|
||||
<div class="workbench-todo__leading">
|
||||
<span class="workbench-todo__category" :class="`workbench-todo__category--${item.categoryTone}`">
|
||||
{{ item.categoryLabel }}
|
||||
</span>
|
||||
<span v-if="item.priority === 'high'" class="workbench-todo__priority">高</span>
|
||||
</div>
|
||||
|
||||
<div class="workbench-todo__body">
|
||||
<h4 class="workbench-todo__item-title">{{ item.title }}</h4>
|
||||
<div class="workbench-todo__meta">
|
||||
<span class="workbench-todo__source">{{ item.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workbench-todo__trailing">
|
||||
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
|
||||
{{ item.deadlineLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<ElEmpty v-else description="当前筛选下暂无待办" :image-size="72" />
|
||||
</div>
|
||||
|
||||
<div class="workbench-todo__pager">
|
||||
<ElPagination
|
||||
v-if="filteredItems.length > PAGE_SIZE"
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="PAGE_SIZE"
|
||||
:total="filteredItems.length"
|
||||
background
|
||||
small
|
||||
layout="prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PersonalItemOperateDialog
|
||||
v-model:visible="addDialogVisible"
|
||||
operate-type="add"
|
||||
:row-data="null"
|
||||
@submitted="handleAddSubmitted"
|
||||
/>
|
||||
</WorkbenchModuleCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workbench-todo__tabs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workbench-todo__tabs-group {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.workbench-todo__add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 12px;
|
||||
border: 1px solid rgb(14 116 144 / 60%);
|
||||
border-radius: 999px;
|
||||
background-color: rgb(240 253 250 / 80%);
|
||||
color: rgb(14 116 144 / 96%);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 160ms ease;
|
||||
}
|
||||
|
||||
.workbench-todo__add:hover {
|
||||
background-color: rgb(14 116 144 / 96%);
|
||||
border-color: rgb(14 116 144 / 96%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.workbench-todo__add-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workbench-todo__tab {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@@ -168,6 +381,101 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.workbench-todo__tab-dot {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 6px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background-color: rgb(225 29 72 / 96%);
|
||||
box-shadow: 0 0 0 2px rgb(255 255 255 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__tab--active .workbench-todo__tab-dot {
|
||||
box-shadow: 0 0 0 2px rgb(14 116 144 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.workbench-todo__filters-left {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workbench-todo__sort {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
color: rgb(100 116 139 / 92%);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 160ms ease;
|
||||
}
|
||||
|
||||
.workbench-todo__sort:hover {
|
||||
background-color: rgb(241 245 249 / 96%);
|
||||
color: rgb(14 116 144 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__sort-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active) {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workbench-todo__filter {
|
||||
padding: 3px 10px;
|
||||
border: 1px dashed rgb(203 213 225 / 92%);
|
||||
border-radius: 999px;
|
||||
background-color: transparent;
|
||||
color: rgb(100 116 139 / 92%);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 160ms ease;
|
||||
}
|
||||
|
||||
.workbench-todo__filter:hover {
|
||||
border-style: solid;
|
||||
border-color: rgb(14 116 144 / 60%);
|
||||
color: rgb(14 116 144 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__filter--active {
|
||||
border-style: solid;
|
||||
border-color: rgb(190 18 60 / 80%);
|
||||
background-color: rgb(255 228 230 / 96%);
|
||||
color: rgb(190 18 60 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__filter--active:hover {
|
||||
border-color: rgb(190 18 60 / 92%);
|
||||
color: rgb(190 18 60 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__content {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.workbench-todo__content :deep(.el-empty) {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.workbench-todo__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -313,6 +621,14 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
color: rgb(190 18 60 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.workbench-todo__item {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user