Files
cn-rdms-web/src/views/workbench/modules/workbench-todo-panel.vue

645 lines
16 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.

<script setup lang="ts">
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 WorkbenchTodoMainTab,
buildWorkbenchTodoItems,
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 {
editing?: boolean;
collapsed?: boolean;
}
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
defineEmits<{
(e: 'hide'): void;
(e: 'toggle-collapse'): void;
}>();
const { routerPushByKey } = useRouterPush();
const PAGE_SIZE = 5;
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: 'task', label: '任务' },
{ key: 'ticket', label: '工单' },
{ key: 'personal', label: '个人事项' },
{ key: 'approval', label: '待审批' }
];
const deadlineFilters: Array<{ key: Exclude<WorkbenchTodoDeadlineFilter, null>; label: string }> = [
{ key: 'overdue', label: '已逾期' },
{ key: 'today', label: '今日到期' },
{ key: 'week', label: '本周到期' }
];
const allItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
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,
approval: 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,
approval: 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;
if (key === 'approval') activeDeadlineFilter.value = null;
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;
routerPushByKey(item.routeKey as RouteKey);
}
function getDeadlineToneClass(item: WorkbenchTodoItem) {
if (isWorkbenchTodoOverdue(item)) return 'workbench-todo__deadline--rose';
if (item.remainingDays === 0) return 'workbench-todo__deadline--amber';
return 'workbench-todo__deadline--slate';
}
</script>
<template>
<WorkbenchModuleCard
title="我的待办"
icon="mdi:clipboard-text-clock-outline"
:editing="editing"
:collapsed="collapsed"
@hide="$emit('hide')"
@toggle-collapse="$emit('toggle-collapse')"
>
<div class="workbench-todo__tabs">
<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 class="workbench-todo__filters">
<div v-if="activeTab !== 'approval'" 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 v-else></div>
<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>
<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;
}
.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;
padding: 6px 12px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 999px;
background-color: rgb(255 255 255 / 96%);
color: rgb(71 85 105 / 94%);
font-size: 13px;
cursor: pointer;
transition: all 160ms ease;
}
.workbench-todo__tab:hover {
border-color: rgb(14 116 144 / 64%);
color: rgb(14 116 144 / 96%);
}
.workbench-todo__tab--active {
border-color: rgb(14 116 144 / 92%);
background-color: rgb(14 116 144 / 96%);
color: white;
}
.workbench-todo__tab--active:hover {
color: white;
}
.workbench-todo__tab-count {
padding: 1px 6px;
border-radius: 999px;
background-color: rgb(241 245 249 / 96%);
color: rgb(71 85 105 / 94%);
font-size: 11px;
font-weight: 600;
}
.workbench-todo__tab--active .workbench-todo__tab-count {
background-color: rgb(255 255 255 / 22%);
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;
gap: 10px;
}
.workbench-todo__item {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid rgb(226 232 240 / 90%);
border-radius: 16px;
background-color: rgb(255 255 255 / 98%);
transition:
border-color 160ms ease,
background-color 160ms ease;
}
.workbench-todo__item--clickable {
cursor: pointer;
}
.workbench-todo__item--clickable:hover {
border-color: rgb(14 116 144 / 60%);
background-color: rgb(240 253 250 / 84%);
}
.workbench-todo__leading {
display: flex;
align-items: center;
gap: 8px;
}
.workbench-todo__category {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.workbench-todo__category--sky {
background-color: rgb(224 242 254 / 96%);
color: rgb(14 116 144 / 96%);
}
.workbench-todo__category--emerald {
background-color: rgb(220 252 231 / 96%);
color: rgb(5 150 105 / 96%);
}
.workbench-todo__category--amber {
background-color: rgb(254 243 199 / 96%);
color: rgb(180 83 9 / 96%);
}
.workbench-todo__category--rose {
background-color: rgb(255 228 230 / 96%);
color: rgb(190 18 60 / 96%);
}
.workbench-todo__category--violet {
background-color: rgb(237 233 254 / 96%);
color: rgb(109 40 217 / 96%);
}
.workbench-todo__priority {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 6px;
background-color: rgb(254 226 226 / 96%);
color: rgb(220 38 38 / 96%);
font-size: 11px;
font-weight: 800;
}
.workbench-todo__body {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.workbench-todo__item-title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 14px;
font-weight: 600;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.workbench-todo__meta {
display: flex;
align-items: center;
gap: 8px;
}
.workbench-todo__source {
color: rgb(100 116 139 / 92%);
font-size: 12px;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.workbench-todo__trailing {
display: flex;
align-items: center;
}
.workbench-todo__deadline {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.workbench-todo__deadline--slate {
background-color: rgb(241 245 249 / 96%);
color: rgb(71 85 105 / 94%);
}
.workbench-todo__deadline--amber {
background-color: rgb(254 243 199 / 96%);
color: rgb(180 83 9 / 96%);
}
.workbench-todo__deadline--rose {
background-color: rgb(255 228 230 / 96%);
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);
}
.workbench-todo__trailing {
grid-column: 1 / -1;
justify-content: flex-end;
}
}
</style>