2037 lines
63 KiB
Vue
2037 lines
63 KiB
Vue
<script setup lang="ts">
|
||
import { type Component, computed, markRaw, onActivated, ref, watch } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import dayjs from 'dayjs';
|
||
import type { RouteKey } from '@elegant-router/types';
|
||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||
import { RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
|
||
import {
|
||
fetchApproveOvertimeApplication,
|
||
fetchBatchApproveOvertimeApplication,
|
||
fetchBatchRejectOvertimeApplication,
|
||
fetchChangePersonalItemStatus,
|
||
fetchChangeProjectTaskStatus,
|
||
fetchGetMonthlyReportApprovalPage,
|
||
fetchGetMyTaskPage,
|
||
fetchGetOvertimeApplicationApprovalPage,
|
||
fetchGetPersonalItemDetail,
|
||
fetchGetPersonalItemPage,
|
||
fetchGetProjectReportApprovalPage,
|
||
fetchGetProjectTask,
|
||
fetchGetWeeklyReportApprovalPage,
|
||
fetchPerformanceSheetPage,
|
||
fetchRejectOvertimeApplication
|
||
} from '@/service/api';
|
||
import { useAuthStore } from '@/store/modules/auth';
|
||
import { useRouterPush } from '@/hooks/common/router';
|
||
import { useDict } from '@/hooks/business/dict';
|
||
import type { WorklogChangedPayload } from '@/views/project/project/execution/shared';
|
||
import TaskStatusActionDialog from '@/views/project/project/execution/modules/status-action-dialog.vue';
|
||
import TaskInfoReadonly from '@/views/project/project/execution/modules/task-info-readonly.vue';
|
||
import TaskWorklogDialog from '@/views/project/project/execution/modules/task-worklog-dialog.vue';
|
||
import PersonalItemDetailDialog from '@/views/personal-center/my-item/modules/personal-item-detail-dialog.vue';
|
||
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
||
import PersonalItemStatusActionDialog from '@/views/personal-center/my-item/modules/personal-item-status-action-dialog.vue';
|
||
import PerformanceActionDialog from '@/views/personal-center/my-performance/modules/performance-action-dialog.vue';
|
||
import PerformanceExcelEditorDrawer from '@/views/personal-center/my-performance/modules/performance-excel-editor-drawer.vue';
|
||
import { PerformancePermission } from '@/views/personal-center/my-performance/modules/performance-shared';
|
||
import OvertimeApplicationBatchDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-batch-detail-dialog.vue';
|
||
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
|
||
import WorkReportPrototypePageDialog from '@/views/personal-center/work-report/shared/components/prototype-page-dialog.vue';
|
||
import {
|
||
WORK_REPORT_TYPE_LABEL,
|
||
type WorkReportRow,
|
||
type WorkReportType,
|
||
formatPeriod,
|
||
formatWeeklyPeriodLabel
|
||
} from '@/views/personal-center/work-report/shared/types';
|
||
import {
|
||
type WorkbenchTodoDeadlineFilter,
|
||
type WorkbenchTodoItem,
|
||
type WorkbenchTodoMainTab,
|
||
buildWorkbenchTodoItems,
|
||
filterWorkbenchTodoItemsByCategory,
|
||
filterWorkbenchTodoItemsByDeadline,
|
||
isWorkbenchTodoOverdue,
|
||
sortWorkbenchTodoItemsByPriority
|
||
} from '../homepage';
|
||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||
import { useWorkbenchWorklogSignal } from '../composables/use-workbench-worklog-signal';
|
||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||
import IconMdiClipboardCheckOutline from '~icons/mdi/clipboard-check-outline';
|
||
import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
|
||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||
import IconMdiPauseCircleOutline from '~icons/mdi/pause-circle-outline';
|
||
import IconMdiPlayCircleOutline from '~icons/mdi/play-circle-outline';
|
||
|
||
type SortKey = 'created' | 'priority' | 'deadline';
|
||
type OvertimeApprovalActionType = 'approve' | 'reject';
|
||
type PerformanceApprovalActionType = 'confirm' | 'reject';
|
||
type ApprovalBizType = 'overtime_application' | 'performance' | WorkReportType;
|
||
|
||
defineOptions({ name: 'WorkbenchTodoPanel' });
|
||
|
||
interface Props {
|
||
editing?: boolean;
|
||
}
|
||
|
||
withDefaults(defineProps<Props>(), { editing: false });
|
||
|
||
defineEmits<{
|
||
(e: 'hide'): void;
|
||
}>();
|
||
|
||
const router = useRouter();
|
||
const { routerPushByKey } = useRouterPush();
|
||
|
||
const authStore = useAuthStore();
|
||
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||
const buttonPermissions = computed(() => new Set(authStore.userInfo.buttons || []));
|
||
|
||
function hasButtonPermission(permission: string) {
|
||
return buttonPermissions.value.has(permission);
|
||
}
|
||
|
||
// 工时填报在工作台内弹层完成(不切路由),需广播给「我的工时」widget 重拉
|
||
const { notify: notifyWorklogChanged } = useWorkbenchWorklogSignal();
|
||
|
||
const { loading, refresh } = useWorkbenchRefresh(async () => {
|
||
await Promise.all([
|
||
loadMyTaskItems(),
|
||
loadPersonalTodoItems(),
|
||
loadOvertimeApprovalItems(),
|
||
loadPerformanceApprovalItems(),
|
||
loadWorkReportApprovalItems()
|
||
]);
|
||
});
|
||
|
||
const PAGE_SIZE = 5;
|
||
|
||
const activeTab = ref<WorkbenchTodoMainTab>('all');
|
||
const activeDeadlineFilter = ref<WorkbenchTodoDeadlineFilter>(null);
|
||
const activeApprovalBizType = ref<ApprovalBizType>('weekly');
|
||
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: '待审批' },
|
||
{ key: 'confirm', label: '待确认' }
|
||
];
|
||
|
||
const deadlineFilters: Array<{ key: Exclude<WorkbenchTodoDeadlineFilter, null>; label: string }> = [
|
||
{ key: 'overdue', label: '已逾期' },
|
||
{ key: 'today', label: '今日到期' },
|
||
{ key: 'week', label: '本周到期' }
|
||
];
|
||
|
||
const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
|
||
{ key: 'weekly', label: '周报' },
|
||
{ key: 'monthly', label: '月报' },
|
||
{ key: 'project', label: '项目半月报' },
|
||
{ key: 'overtime_application', label: '业务学习' }
|
||
];
|
||
const confirmBizTabs: Array<{ key: Extract<ApprovalBizType, 'performance'>; label: string }> = [
|
||
{ key: 'performance', label: '绩效表' }
|
||
];
|
||
|
||
const hasWorkReportApprovePermission = computed(() => hasButtonPermission('project:work-report:approve'));
|
||
const hasOvertimeApprovePermission = computed(() => hasButtonPermission('project:overtime-application:approve'));
|
||
const hasPerformanceApprovePermission = computed(
|
||
() =>
|
||
hasButtonPermission(PerformancePermission.SheetConfirm) && hasButtonPermission(PerformancePermission.SheetReject)
|
||
);
|
||
const visibleMainTabs = computed(() =>
|
||
mainTabs.filter(tab => {
|
||
if (tab.key === 'confirm') {
|
||
return hasPerformanceApprovePermission.value;
|
||
}
|
||
return true;
|
||
})
|
||
);
|
||
const visibleApprovalBizTabs = computed(() =>
|
||
approvalBizTabs.filter(tab => {
|
||
if (tab.key === 'performance') {
|
||
return hasPerformanceApprovePermission.value;
|
||
}
|
||
|
||
if (tab.key === 'overtime_application') {
|
||
return hasOvertimeApprovePermission.value;
|
||
}
|
||
|
||
return hasWorkReportApprovePermission.value;
|
||
})
|
||
);
|
||
const visibleConfirmBizTabs = computed(() => (hasPerformanceApprovePermission.value ? confirmBizTabs : []));
|
||
|
||
const myTaskItems = ref<WorkbenchTodoItem[]>([]);
|
||
// 保留任务原始行,供操作图标按 availableActions 渲染并取 projectId / executionId 调状态变更接口
|
||
const myTaskRows = ref<Api.Project.MyTaskItem[]>([]);
|
||
const personalTodoItems = ref<WorkbenchTodoItem[]>([]);
|
||
// 保留我的事项原始行,供操作图标(详情/填报/完成)按 availableActions 渲染并回传给弹层
|
||
const personalItemRows = ref<Api.PersonalItem.PersonalItem[]>([]);
|
||
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
||
const performanceApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||
const performanceApprovalRows = ref<Api.Performance.Sheet.Sheet[]>([]);
|
||
const workReportApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||
const weeklyApprovalRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||
const monthlyApprovalRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||
const projectApprovalRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
||
// 工单 tab 等工单业务上线,当前为空态
|
||
const mergedItems = computed(() => [
|
||
...myTaskItems.value,
|
||
...personalTodoItems.value,
|
||
...overtimeApprovalItems.value,
|
||
...performanceApprovalItems.value,
|
||
...workReportApprovalItems.value
|
||
]);
|
||
|
||
// 我的事项操作弹层:详情用 view 模式、新增用 add 模式,共用同一实例
|
||
const personalOperateVisible = ref(false);
|
||
const personalOperateType = ref<'add' | 'view'>('add');
|
||
const personalOperateRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||
// 我的事项填报:复用详情弹层的 worklog tab
|
||
const personalDetailVisible = ref(false);
|
||
const personalDetailRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||
// 我的事项完成:复用状态动作弹层收集原因 + 二次确认
|
||
const personalStatusVisible = ref(false);
|
||
const personalStatusRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||
const personalStatusAction = ref<Api.PersonalItem.PersonalItemLifecycleAction | null>(null);
|
||
// 任务生命周期动作:复用任务工作区的状态动作弹层收集原因
|
||
const taskStatusVisible = ref(false);
|
||
const taskStatusRow = ref<Api.Project.MyTaskItem | null>(null);
|
||
const taskStatusAction = ref<Api.Project.LifecycleAction<Api.Project.ProjectTaskActionCode> | null>(null);
|
||
const taskPreviewMap = ref<Record<string, Api.Project.ProjectTask>>({});
|
||
const taskPreviewLoadingIds = ref<string[]>([]);
|
||
// 任务填报工时:复用任务工作区的工时弹层;弹层需要完整任务详情,点击时按需拉取
|
||
const taskWorklogVisible = ref(false);
|
||
const taskWorklogTask = ref<Api.Project.ProjectTask | null>(null);
|
||
const overtimeDetailVisible = ref(false);
|
||
const overtimeActionSubmitting = ref(false);
|
||
const currentOvertimeApplication = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||
const batchDetailVisible = ref(false);
|
||
const batchSubmitting = ref(false);
|
||
const workReportDetailVisible = ref(false);
|
||
const currentWorkReport = ref<WorkReportRow | null>(null);
|
||
const currentWorkReportType = ref<WorkReportType>('weekly');
|
||
const performanceDetailVisible = ref(false);
|
||
const performanceActionVisible = ref(false);
|
||
const currentPerformanceSheet = ref<Api.Performance.Sheet.Sheet | null>(null);
|
||
const currentPerformanceActionType = ref<PerformanceApprovalActionType>('confirm');
|
||
|
||
// 批量审批选中状态(存原始业务学习 id,避免映射转换)
|
||
const selectedOvertimeIds = ref<Set<string>>(new Set());
|
||
|
||
const APPROVAL_ENTRY_ICON = markRaw(IconMdiClipboardCheckOutline);
|
||
|
||
function getApprovalCategoryLabel(bizType: ApprovalBizType) {
|
||
if (bizType === 'weekly') return '周报';
|
||
if (bizType === 'monthly') return '月报';
|
||
if (bizType === 'project') return '项目半月报';
|
||
if (bizType === 'performance') return '绩效表';
|
||
if (bizType === 'overtime_application') return '业务学习';
|
||
return '待审批';
|
||
}
|
||
|
||
const PERSONAL_ACTION_ICONS = {
|
||
detail: markRaw(IconMdiEyeOutline),
|
||
worklog: markRaw(IconMdiClipboardEditOutline),
|
||
complete: markRaw(IconMdiCheckCircleOutline)
|
||
};
|
||
|
||
// auto_start 是系统自动动作,接口不返回;无图标的未知动作不渲染按钮
|
||
const TASK_ACTION_ICONS: Partial<Record<Api.Project.ProjectTaskActionCode, Component>> = {
|
||
pause: markRaw(IconMdiPauseCircleOutline),
|
||
resume: markRaw(IconMdiPlayCircleOutline),
|
||
complete: markRaw(IconMdiCheckCircleOutline),
|
||
cancel: markRaw(IconMdiCloseCircleOutline)
|
||
};
|
||
|
||
function normalizeTodoDeadline(deadline: string | null) {
|
||
if (!deadline) return null;
|
||
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(deadline)) {
|
||
return dayjs(deadline).endOf('day').format('YYYY-MM-DD HH:mm');
|
||
}
|
||
|
||
return deadline;
|
||
}
|
||
|
||
function getTaskActionButtonType(code: Api.Project.ProjectTaskActionCode) {
|
||
if (code === 'complete') return 'success' as const;
|
||
if (code === 'cancel') return 'danger' as const;
|
||
return 'primary' as const;
|
||
}
|
||
|
||
function getTodoProgress(item: WorkbenchTodoItem) {
|
||
if (typeof item.progressRate !== 'number' || !Number.isFinite(item.progressRate)) {
|
||
return 0;
|
||
}
|
||
|
||
return Math.min(100, Math.max(0, Math.round(item.progressRate)));
|
||
}
|
||
|
||
function shouldShowTodoProgress(item: WorkbenchTodoItem) {
|
||
return (item.category === 'task' || item.category === 'personal') && typeof item.progressRate === 'number';
|
||
}
|
||
|
||
function handleOpenAdd() {
|
||
personalOperateType.value = 'add';
|
||
personalOperateRow.value = null;
|
||
personalOperateVisible.value = true;
|
||
}
|
||
|
||
async function handleAddSubmitted() {
|
||
activeTab.value = 'personal';
|
||
activeDeadlineFilter.value = null;
|
||
await loadPersonalTodoItems();
|
||
}
|
||
|
||
function findPersonalItemRow(item: WorkbenchTodoItem) {
|
||
return personalItemRows.value.find(row => `personal-${row.id}` === item.id) || null;
|
||
}
|
||
|
||
function getPersonalCompleteAction(row: Api.PersonalItem.PersonalItem) {
|
||
return row.availableActions?.find(action => action.actionCode === 'complete') || null;
|
||
}
|
||
|
||
// 仅当我的事项当前可完成(availableActions 含 complete)时才渲染完成图标
|
||
function canCompletePersonalItem(item: WorkbenchTodoItem) {
|
||
const row = findPersonalItemRow(item);
|
||
return Boolean(row && getPersonalCompleteAction(row));
|
||
}
|
||
|
||
// 详情:复用我的事项操作弹层的 view 只读模式
|
||
function openPersonalDetail(item: WorkbenchTodoItem) {
|
||
const row = findPersonalItemRow(item);
|
||
if (!row) return;
|
||
|
||
personalOperateType.value = 'view';
|
||
personalOperateRow.value = row;
|
||
personalOperateVisible.value = true;
|
||
}
|
||
|
||
// 填报:拉最新详情后打开详情弹层的 worklog tab
|
||
async function openPersonalWorklog(item: WorkbenchTodoItem) {
|
||
const row = findPersonalItemRow(item);
|
||
if (!row) return;
|
||
|
||
const { error, data } = await fetchGetPersonalItemDetail(row.id);
|
||
personalDetailRow.value = error || !data ? row : data;
|
||
personalDetailVisible.value = true;
|
||
}
|
||
|
||
// 完成:走状态动作弹层(二次确认 + 按 needReason 收集原因)
|
||
function openPersonalComplete(item: WorkbenchTodoItem) {
|
||
const row = findPersonalItemRow(item);
|
||
if (!row) return;
|
||
|
||
const completeAction = getPersonalCompleteAction(row);
|
||
if (!completeAction) return;
|
||
|
||
personalStatusRow.value = row;
|
||
personalStatusAction.value = completeAction;
|
||
personalStatusVisible.value = true;
|
||
}
|
||
|
||
async function handlePersonalStatusSubmit(reason: string | null) {
|
||
if (!personalStatusRow.value || !personalStatusAction.value) return;
|
||
|
||
const { error } = await fetchChangePersonalItemStatus(personalStatusRow.value.id, {
|
||
actionCode: personalStatusAction.value.actionCode,
|
||
reason
|
||
});
|
||
|
||
if (error) return;
|
||
|
||
personalStatusVisible.value = false;
|
||
window.$message?.success(`${personalStatusAction.value.actionName}成功`);
|
||
await loadPersonalTodoItems();
|
||
}
|
||
|
||
async function handlePersonalWorklogChanged() {
|
||
notifyWorklogChanged();
|
||
await loadPersonalTodoItems();
|
||
}
|
||
|
||
function findMyTaskRow(item: WorkbenchTodoItem) {
|
||
return myTaskRows.value.find(row => `task-${row.id}` === item.id) || null;
|
||
}
|
||
|
||
function canPreviewTaskInfo(item: WorkbenchTodoItem) {
|
||
const row = findMyTaskRow(item);
|
||
return Boolean(item.category === 'task' && row?.projectId && row.executionId);
|
||
}
|
||
|
||
function getTaskPreviewTask(item: WorkbenchTodoItem) {
|
||
const row = findMyTaskRow(item);
|
||
if (!row) return null;
|
||
return taskPreviewMap.value[row.id] || null;
|
||
}
|
||
|
||
function getTaskPreviewTaskOptions(item: WorkbenchTodoItem) {
|
||
const task = getTaskPreviewTask(item);
|
||
return task ? [task] : [];
|
||
}
|
||
|
||
function isTaskPreviewLoading(item: WorkbenchTodoItem) {
|
||
const row = findMyTaskRow(item);
|
||
if (!row) return false;
|
||
return taskPreviewLoadingIds.value.includes(row.id);
|
||
}
|
||
|
||
async function handleTaskPreviewShow(item: WorkbenchTodoItem) {
|
||
const row = findMyTaskRow(item);
|
||
if (!row?.executionId) return;
|
||
|
||
if (taskPreviewMap.value[row.id] || taskPreviewLoadingIds.value.includes(row.id)) {
|
||
return;
|
||
}
|
||
|
||
taskPreviewLoadingIds.value = [...taskPreviewLoadingIds.value, row.id];
|
||
|
||
const { error, data } = await fetchGetProjectTask(row.projectId, row.executionId, row.id);
|
||
|
||
taskPreviewLoadingIds.value = taskPreviewLoadingIds.value.filter(id => id !== row.id);
|
||
|
||
if (error || !data) {
|
||
return;
|
||
}
|
||
|
||
taskPreviewMap.value = {
|
||
...taskPreviewMap.value,
|
||
[row.id]: data
|
||
};
|
||
}
|
||
|
||
// 状态变更接口挂在执行路径下,未挂执行的任务不渲染动作按钮
|
||
function getTaskActions(item: WorkbenchTodoItem) {
|
||
const row = findMyTaskRow(item);
|
||
if (!row?.executionId) return [];
|
||
return row.availableActions.filter(action => Boolean(TASK_ACTION_ICONS[action.actionCode]));
|
||
}
|
||
|
||
// 填报工时同样依赖执行路径(工时接口挂在 project/execution/task 下)
|
||
function canReportTaskWorklog(item: WorkbenchTodoItem) {
|
||
return Boolean(findMyTaskRow(item)?.executionId);
|
||
}
|
||
|
||
async function refreshTaskWorklogTaskDetail(
|
||
target?:
|
||
| Pick<Api.Project.ProjectTask, 'id' | 'projectId' | 'executionId'>
|
||
| Pick<Api.Project.MyTaskItem, 'id' | 'projectId' | 'executionId'>
|
||
| null
|
||
) {
|
||
const task = target ?? taskWorklogTask.value;
|
||
if (!task?.executionId) return;
|
||
|
||
const { error, data } = await fetchGetProjectTask(task.projectId, task.executionId, task.id);
|
||
if (error || !data) return;
|
||
|
||
if (taskWorklogTask.value?.id === data.id) {
|
||
taskWorklogTask.value = data;
|
||
}
|
||
}
|
||
|
||
// 填报:工时弹层需要完整任务详情(负责人/状态/日期),按需拉一次详情再打开
|
||
async function openTaskWorklog(item: WorkbenchTodoItem) {
|
||
const row = findMyTaskRow(item);
|
||
if (!row?.executionId) return;
|
||
|
||
const { error, data } = await fetchGetProjectTask(row.projectId, row.executionId, row.id);
|
||
if (error || !data) return;
|
||
|
||
taskWorklogTask.value = data;
|
||
taskWorklogVisible.value = true;
|
||
}
|
||
|
||
// 填报会联动任务进度/状态(如 auto_start),变更后刷新任务列表
|
||
async function handleTaskWorklogChanged(payload: WorklogChangedPayload) {
|
||
notifyWorklogChanged();
|
||
await loadMyTaskItems();
|
||
if (taskWorklogTask.value?.id === payload.taskId) {
|
||
await refreshTaskWorklogTaskDetail();
|
||
}
|
||
|
||
// 与任务工作区联动一致:进度填到 100 且我是任务负责人时提示完成(仅单任务,不做级联)
|
||
if (payload.mode === 'delete' || payload.progressRate !== 100) return;
|
||
|
||
const task = taskWorklogTask.value;
|
||
if (!task || task.id !== payload.taskId || task.ownerId !== currentUserId.value) return;
|
||
|
||
// 以刷新后的行为准:后端按进度/负责人口径返回了 complete 才提示
|
||
const row = myTaskRows.value.find(item => item.id === payload.taskId);
|
||
const completeAction = row?.availableActions.find(action => action.actionCode === 'complete');
|
||
if (!row || !completeAction) return;
|
||
|
||
taskStatusRow.value = row;
|
||
taskStatusAction.value = completeAction;
|
||
|
||
try {
|
||
await window.$messageBox?.confirm('任务进度已达 100%,是否完成当前任务?', '完成确认', {
|
||
confirmButtonText: '完成任务',
|
||
cancelButtonText: '仅保留工时',
|
||
type: 'info'
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
|
||
await submitTaskStatusChange(null);
|
||
}
|
||
|
||
const taskStatusActionTitle = computed(() =>
|
||
taskStatusAction.value ? `任务状态变更:${taskStatusAction.value.actionName}` : '任务状态变更'
|
||
);
|
||
|
||
async function handleTaskAction(
|
||
item: WorkbenchTodoItem,
|
||
action: Api.Project.LifecycleAction<Api.Project.ProjectTaskActionCode>
|
||
) {
|
||
const row = findMyTaskRow(item);
|
||
if (!row?.executionId) return;
|
||
|
||
taskStatusRow.value = row;
|
||
taskStatusAction.value = action;
|
||
|
||
// 完成动作:二次确认后直接提交(与任务工作区同口径;有未完成子任务时后端会拒绝,按业务错误提示)
|
||
if (action.actionCode === 'complete') {
|
||
try {
|
||
await window.$messageBox?.confirm(`确定要完成任务“${row.taskTitle}”吗?`, '完成确认', {
|
||
confirmButtonText: '完成任务',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
await submitTaskStatusChange(null);
|
||
return;
|
||
}
|
||
|
||
// 其他非必填原因的动作(暂停/恢复)直接提交,不弹原因弹层
|
||
if (!action.needReason) {
|
||
await submitTaskStatusChange(null);
|
||
return;
|
||
}
|
||
|
||
taskStatusVisible.value = true;
|
||
}
|
||
|
||
async function submitTaskStatusChange(reason: string | null) {
|
||
const row = taskStatusRow.value;
|
||
const action = taskStatusAction.value;
|
||
if (!row?.executionId || !action) return;
|
||
|
||
const { error } = await fetchChangeProjectTaskStatus(row.projectId, row.executionId, {
|
||
taskId: row.id,
|
||
data: { actionCode: action.actionCode, reason }
|
||
});
|
||
|
||
if (error) return;
|
||
|
||
taskStatusVisible.value = false;
|
||
window.$message?.success(`${action.actionName}成功`);
|
||
await loadMyTaskItems();
|
||
if (taskWorklogTask.value?.id === row.id) {
|
||
await refreshTaskWorklogTaskDetail(row);
|
||
}
|
||
}
|
||
|
||
const tabCounts = computed(() => {
|
||
const counts: Record<WorkbenchTodoMainTab, number> = {
|
||
all: mergedItems.value.length,
|
||
task: 0,
|
||
ticket: 0,
|
||
personal: 0,
|
||
approval: 0,
|
||
confirm: 0
|
||
};
|
||
mergedItems.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,
|
||
confirm: 0
|
||
};
|
||
mergedItems.value.forEach(item => {
|
||
if (!isWorkbenchTodoOverdue(item)) return;
|
||
map.all += 1;
|
||
map[item.category] += 1;
|
||
});
|
||
return map;
|
||
});
|
||
|
||
const itemsInTab = computed(() => filterWorkbenchTodoItemsByCategory(mergedItems.value, activeTab.value));
|
||
|
||
const filteredItems = computed(() => {
|
||
if (activeTab.value === 'approval') {
|
||
return itemsInTab.value.filter(item => item.approvalBizType === activeApprovalBizType.value);
|
||
}
|
||
|
||
if (activeTab.value === 'confirm') {
|
||
return itemsInTab.value.filter(item => item.approvalBizType === 'performance');
|
||
}
|
||
|
||
return filterWorkbenchTodoItemsByDeadline(itemsInTab.value, activeDeadlineFilter.value);
|
||
});
|
||
|
||
const approvalBizTabCounts = computed(() => {
|
||
const counts: Record<ApprovalBizType, number> = {
|
||
overtime_application: 0,
|
||
performance: 0,
|
||
weekly: 0,
|
||
monthly: 0,
|
||
project: 0
|
||
};
|
||
|
||
itemsInTab.value.forEach(item => {
|
||
if (item.approvalBizType === 'overtime_application') {
|
||
counts.overtime_application += 1;
|
||
return;
|
||
}
|
||
|
||
if (item.approvalBizType === 'performance') {
|
||
counts.performance += 1;
|
||
return;
|
||
}
|
||
|
||
if (isWorkReportApprovalBizType(item.approvalBizType)) {
|
||
counts[item.approvalBizType] += 1;
|
||
}
|
||
});
|
||
|
||
return counts;
|
||
});
|
||
|
||
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;
|
||
});
|
||
|
||
watch(
|
||
visibleApprovalBizTabs,
|
||
tabs => {
|
||
if (!tabs.length) return;
|
||
if (!tabs.some(tab => tab.key === activeApprovalBizType.value)) {
|
||
activeApprovalBizType.value = tabs[0].key;
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
watch(
|
||
visibleMainTabs,
|
||
tabs => {
|
||
if (!tabs.some(tab => tab.key === activeTab.value)) {
|
||
activeTab.value = tabs[0]?.key ?? 'all';
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
function handleSelectTab(key: WorkbenchTodoMainTab) {
|
||
if (key === 'confirm' && !hasPerformanceApprovePermission.value) return;
|
||
if (activeTab.value === key) return;
|
||
activeTab.value = key;
|
||
activeDeadlineFilter.value = null;
|
||
if (key === 'confirm') {
|
||
activeApprovalBizType.value = 'performance';
|
||
} else if (key === 'approval' && activeApprovalBizType.value === 'performance') {
|
||
activeApprovalBizType.value = visibleApprovalBizTabs.value[0]?.key ?? 'weekly';
|
||
}
|
||
if (key !== 'task' && activeSort.value === 'priority') {
|
||
activeSort.value = 'deadline';
|
||
}
|
||
}
|
||
|
||
function handleSelectDeadlineFilter(key: Exclude<WorkbenchTodoDeadlineFilter, null>) {
|
||
activeDeadlineFilter.value = activeDeadlineFilter.value === key ? null : key;
|
||
}
|
||
|
||
function handleSelectApprovalBizType(key: ApprovalBizType) {
|
||
activeApprovalBizType.value = key;
|
||
}
|
||
|
||
function handleSelectSort(key: SortKey) {
|
||
activeSort.value = key;
|
||
}
|
||
|
||
function handleClickItem(item: WorkbenchTodoItem) {
|
||
if (item.approvalBizType === 'overtime_application') {
|
||
openOvertimeDetail(item);
|
||
return;
|
||
}
|
||
|
||
if (item.approvalBizType === 'performance') {
|
||
openPerformanceDetail(item);
|
||
return;
|
||
}
|
||
|
||
if (isWorkReportApprovalBizType(item.approvalBizType)) {
|
||
openWorkReportDetail(item);
|
||
return;
|
||
}
|
||
|
||
// 任务条目:带项目对象上下文跳进该项目的任务导航(执行池页)
|
||
if (item.category === 'task' && item.projectId) {
|
||
router.push({
|
||
path: '/project/project/execution',
|
||
query: { [OBJECT_CONTEXT_QUERY_KEY]: item.projectId }
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!item.routeKey) return;
|
||
routerPushByKey(item.routeKey as RouteKey);
|
||
}
|
||
|
||
function isWorkReportApprovalBizType(value?: string): value is WorkReportType {
|
||
return value === 'weekly' || value === 'monthly' || value === 'project';
|
||
}
|
||
|
||
function findOvertimeApprovalRow(item: WorkbenchTodoItem) {
|
||
if (!item.approvalBizId) {
|
||
return null;
|
||
}
|
||
|
||
return overtimeApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||
}
|
||
|
||
function findPerformanceApprovalRow(item: WorkbenchTodoItem) {
|
||
if (!item.approvalBizId) {
|
||
return null;
|
||
}
|
||
|
||
return performanceApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||
}
|
||
|
||
function openOvertimeDetail(item: WorkbenchTodoItem) {
|
||
const row = findOvertimeApprovalRow(item);
|
||
if (!row) return;
|
||
|
||
currentOvertimeApplication.value = row;
|
||
overtimeDetailVisible.value = true;
|
||
}
|
||
|
||
function openPerformanceDetail(item: WorkbenchTodoItem) {
|
||
const row = findPerformanceApprovalRow(item);
|
||
if (!row) return;
|
||
|
||
currentPerformanceSheet.value = row;
|
||
performanceDetailVisible.value = true;
|
||
}
|
||
|
||
function openCurrentPerformanceAction(actionType: PerformanceApprovalActionType) {
|
||
if (!currentPerformanceSheet.value) return;
|
||
|
||
currentPerformanceActionType.value = actionType;
|
||
performanceActionVisible.value = true;
|
||
}
|
||
|
||
function findWorkReportApprovalRow(item: WorkbenchTodoItem) {
|
||
if (!item.approvalBizId || !isWorkReportApprovalBizType(item.approvalBizType)) {
|
||
return null;
|
||
}
|
||
|
||
if (item.approvalBizType === 'weekly') {
|
||
return weeklyApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||
}
|
||
|
||
if (item.approvalBizType === 'monthly') {
|
||
return monthlyApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||
}
|
||
|
||
return projectApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||
}
|
||
|
||
function openWorkReportDetail(item: WorkbenchTodoItem) {
|
||
const row = findWorkReportApprovalRow(item);
|
||
if (!row || !isWorkReportApprovalBizType(item.approvalBizType)) return;
|
||
|
||
currentWorkReport.value = row;
|
||
currentWorkReportType.value = item.approvalBizType;
|
||
workReportDetailVisible.value = true;
|
||
}
|
||
|
||
async function handleOvertimeActionSubmit(payload: { actionType: OvertimeApprovalActionType; reason: string | null }) {
|
||
if (!currentOvertimeApplication.value) {
|
||
return;
|
||
}
|
||
|
||
overtimeActionSubmitting.value = true;
|
||
const result =
|
||
payload.actionType === 'approve'
|
||
? await fetchApproveOvertimeApplication(currentOvertimeApplication.value.id, { reason: payload.reason })
|
||
: await fetchRejectOvertimeApplication(currentOvertimeApplication.value.id, { reason: payload.reason });
|
||
overtimeActionSubmitting.value = false;
|
||
|
||
if (result.error) {
|
||
return;
|
||
}
|
||
|
||
overtimeDetailVisible.value = false;
|
||
window.$message?.success(payload.actionType === 'approve' ? '业务学习已通过' : '业务学习已退回');
|
||
await loadOvertimeApprovalItems();
|
||
}
|
||
|
||
async function handleWorkReportSubmitted() {
|
||
workReportDetailVisible.value = false;
|
||
await loadWorkReportApprovalItems();
|
||
}
|
||
|
||
async function handlePerformanceActionSubmitted() {
|
||
performanceActionVisible.value = false;
|
||
performanceDetailVisible.value = false;
|
||
await loadPerformanceApprovalItems();
|
||
}
|
||
|
||
// 优先级角标用字典 label 原样回显(rdms_req_priority:P0~P3),不翻译成高/中/低
|
||
const { getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
||
|
||
// 任务优先级字典 value("0" P0 ~ "3" P3,数字越小越高)映射待办优先级(仅用于排序与高亮权重)
|
||
function mapTaskTodoPriority(priority: string) {
|
||
if (priority === '0') return 'high' as const;
|
||
if (priority === '1') return 'mid' as const;
|
||
return 'low' as const;
|
||
}
|
||
|
||
// 我的任务:跨项目单接口聚合(负责/协办并集、只返回非终态,过滤排序、进度与可执行动作均由后端完成)
|
||
async function loadMyTaskItems() {
|
||
const { error, data } = await fetchGetMyTaskPage({ pageNo: 1, pageSize: -1 });
|
||
|
||
taskPreviewMap.value = {};
|
||
taskPreviewLoadingIds.value = [];
|
||
|
||
if (error || !data) {
|
||
myTaskRows.value = [];
|
||
myTaskItems.value = [];
|
||
return;
|
||
}
|
||
|
||
myTaskRows.value = data.list;
|
||
myTaskItems.value = buildWorkbenchTodoItems(
|
||
data.list.map(task => ({
|
||
id: `task-${task.id}`,
|
||
category: 'task' as const,
|
||
title: task.taskTitle,
|
||
createdTime: task.createTime,
|
||
deadline: normalizeTodoDeadline(task.plannedEndDate),
|
||
source: task.projectName,
|
||
progressRate: task.progressRate,
|
||
priority: mapTaskTodoPriority(task.priority),
|
||
priorityLabel: getPriorityLabel(task.priority) || task.priority,
|
||
projectId: task.projectId
|
||
}))
|
||
);
|
||
}
|
||
|
||
// 待办口径:未到终态的我的事项(pending / active / paused),terminal 态(completed / cancelled)不进待办
|
||
const PERSONAL_TODO_STATUSES: Api.PersonalItem.PersonalItemStatusCode[] = ['pending', 'active', 'paused'];
|
||
|
||
async function loadPersonalTodoItems() {
|
||
const { error, data } = await fetchGetPersonalItemPage({ pageNo: 1, pageSize: 100 });
|
||
|
||
if (error || !data) {
|
||
personalItemRows.value = [];
|
||
personalTodoItems.value = [];
|
||
return;
|
||
}
|
||
|
||
const rows = data.list.filter(item => PERSONAL_TODO_STATUSES.includes(item.statusCode));
|
||
personalItemRows.value = rows;
|
||
personalTodoItems.value = buildWorkbenchTodoItems(
|
||
rows.map(item => ({
|
||
id: `personal-${item.id}`,
|
||
category: 'personal',
|
||
title: item.taskTitle,
|
||
createdTime: item.createTime,
|
||
deadline: normalizeTodoDeadline(item.plannedEndDate),
|
||
source: '我的事项',
|
||
progressRate: item.progressRate,
|
||
routeKey: 'personal-center_my-item'
|
||
}))
|
||
);
|
||
}
|
||
|
||
// 批量审批选中状态管理
|
||
function isOvertimeItemSelected(item: WorkbenchTodoItem) {
|
||
return item.approvalBizId ? selectedOvertimeIds.value.has(item.approvalBizId) : false;
|
||
}
|
||
|
||
function toggleOvertimeItem(item: WorkbenchTodoItem, checked: boolean) {
|
||
if (!item.approvalBizId) return;
|
||
if (checked) {
|
||
selectedOvertimeIds.value.add(item.approvalBizId);
|
||
} else {
|
||
selectedOvertimeIds.value.delete(item.approvalBizId);
|
||
}
|
||
}
|
||
|
||
function toggleSelectAllOvertimeItems(checked: boolean) {
|
||
if (checked) {
|
||
overtimeApprovalItems.value.forEach(item => {
|
||
if (item.approvalBizId) selectedOvertimeIds.value.add(item.approvalBizId);
|
||
});
|
||
} else {
|
||
selectedOvertimeIds.value.clear();
|
||
}
|
||
}
|
||
|
||
function clearOvertimeSelection() {
|
||
selectedOvertimeIds.value.clear();
|
||
}
|
||
|
||
// 切换审批子页签时清空选中
|
||
watch(activeApprovalBizType, () => {
|
||
clearOvertimeSelection();
|
||
});
|
||
|
||
// 当前页业务学习是否全部选中(用于全选复选框状态)
|
||
const allOvertimeItemsSelected = computed(() => {
|
||
const items = overtimeApprovalItems.value;
|
||
if (items.length === 0) return false;
|
||
return items.every(item => item.approvalBizId && selectedOvertimeIds.value.has(item.approvalBizId));
|
||
});
|
||
|
||
// 当前页是否有部分选中
|
||
const someOvertimeItemsSelected = computed(() => {
|
||
return overtimeApprovalItems.value.some(
|
||
item => item.approvalBizId && selectedOvertimeIds.value.has(item.approvalBizId)
|
||
);
|
||
});
|
||
|
||
// 批量审批选中的 id 数组(用于传给批量详情弹窗)
|
||
const batchSelectedIds = computed(() => Array.from(selectedOvertimeIds.value));
|
||
|
||
// 打开批量审批详情弹窗
|
||
function handleBatchReview() {
|
||
batchDetailVisible.value = true;
|
||
}
|
||
|
||
// 批量审批提交(对所有选中项执行批量 API)
|
||
async function handleBatchActionSubmit(payload: { actionType: OvertimeApprovalActionType; reason: string | null }) {
|
||
const ids = Array.from(selectedOvertimeIds.value);
|
||
if (ids.length === 0) return;
|
||
|
||
const fn =
|
||
payload.actionType === 'approve' ? fetchBatchApproveOvertimeApplication : fetchBatchRejectOvertimeApplication;
|
||
|
||
batchSubmitting.value = true;
|
||
const { error, data } = await fn({ ids, reason: payload.reason });
|
||
batchSubmitting.value = false;
|
||
|
||
if (error || !data) return;
|
||
|
||
batchDetailVisible.value = false;
|
||
clearOvertimeSelection();
|
||
|
||
if (data.failCount > 0) {
|
||
window.$message?.warning(`成功 ${data.successCount} 条,失败 ${data.failCount} 条`);
|
||
} else {
|
||
window.$message?.success(`已批量处理 ${data.successCount} 条`);
|
||
}
|
||
|
||
await loadOvertimeApprovalItems();
|
||
}
|
||
|
||
async function loadOvertimeApprovalItems() {
|
||
if (!hasOvertimeApprovePermission.value) {
|
||
overtimeApprovalRows.value = [];
|
||
overtimeApprovalItems.value = [];
|
||
return;
|
||
}
|
||
|
||
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
|
||
pageNo: 1,
|
||
pageSize: 20,
|
||
statusCode: 'pending',
|
||
keyword: undefined,
|
||
applicantName: undefined,
|
||
approverId: undefined,
|
||
approverName: undefined,
|
||
overtimeDate: undefined,
|
||
createTime: undefined
|
||
});
|
||
|
||
if (error || !data) {
|
||
overtimeApprovalRows.value = [];
|
||
overtimeApprovalItems.value = [];
|
||
return;
|
||
}
|
||
|
||
overtimeApprovalRows.value = data.list;
|
||
overtimeApprovalItems.value = buildWorkbenchTodoItems(
|
||
data.list.map(item => ({
|
||
id: `overtime-application-${item.id}`,
|
||
category: 'approval',
|
||
categoryLabel: '业务学习',
|
||
title: `${item.applicantName} · ${item.overtimeDate.slice(5, 7)} 月学习 ${item.overtimeDuration} 申请待审批`,
|
||
createdTime: item.submitTime || item.createTime,
|
||
deadline: item.submitTime || item.createTime,
|
||
source: `业务学习 · ${item.applicantName}`,
|
||
priority: 'mid',
|
||
approvalBizType: 'overtime_application',
|
||
approvalBizId: item.id
|
||
}))
|
||
);
|
||
}
|
||
|
||
async function loadPerformanceApprovalItems() {
|
||
if (!hasPerformanceApprovePermission.value) {
|
||
performanceApprovalRows.value = [];
|
||
performanceApprovalItems.value = [];
|
||
return;
|
||
}
|
||
|
||
const { error, data } = await fetchPerformanceSheetPage({
|
||
pageNo: 1,
|
||
pageSize: 20,
|
||
statusCode: 'sent'
|
||
});
|
||
|
||
if (error || !data) {
|
||
performanceApprovalRows.value = [];
|
||
performanceApprovalItems.value = [];
|
||
return;
|
||
}
|
||
|
||
performanceApprovalRows.value = data.list;
|
||
performanceApprovalItems.value = buildWorkbenchTodoItems(
|
||
data.list.map(item => ({
|
||
id: `performance-${item.id}`,
|
||
category: 'confirm',
|
||
categoryLabel: '绩效表',
|
||
title: `${item.periodMonth} 绩效表待确认`,
|
||
createdTime: item.sentTime || item.createTime || item.updateTime || '',
|
||
deadline: item.sentTime || item.createTime || item.updateTime || null,
|
||
source: `绩效表 · ${item.managerName || '--'}`,
|
||
priority: 'mid',
|
||
approvalBizType: 'performance',
|
||
approvalBizId: item.id
|
||
}))
|
||
);
|
||
}
|
||
|
||
function buildWorkReportApprovalItems<T extends WorkReportRow>(
|
||
bizType: WorkReportType,
|
||
rows: T[]
|
||
): WorkbenchTodoItem[] {
|
||
const reportTypeLabel = WORK_REPORT_TYPE_LABEL[bizType];
|
||
|
||
return buildWorkbenchTodoItems(
|
||
rows.map(item => ({
|
||
id: `${bizType}-${item.id}`,
|
||
category: 'approval',
|
||
categoryLabel: getApprovalCategoryLabel(bizType),
|
||
title: `${reportTypeLabel} · ${bizType === 'weekly' ? formatWeeklyPeriodLabel(item) : formatPeriod(item)} 待审批`,
|
||
createdTime: item.submitTime || item.createTime || '',
|
||
deadline: item.submitTime || item.createTime || null,
|
||
source: `${reportTypeLabel} · ${'projectName' in item ? item.projectName : item.reporterName}`,
|
||
priority: 'mid',
|
||
approvalBizType: bizType,
|
||
approvalBizId: item.id
|
||
}))
|
||
);
|
||
}
|
||
|
||
async function loadWorkReportApprovalItems() {
|
||
if (!hasWorkReportApprovePermission.value) {
|
||
weeklyApprovalRows.value = [];
|
||
monthlyApprovalRows.value = [];
|
||
projectApprovalRows.value = [];
|
||
workReportApprovalItems.value = [];
|
||
return;
|
||
}
|
||
|
||
const [weeklyResult, monthlyResult, projectResult] = await Promise.all([
|
||
fetchGetWeeklyReportApprovalPage({
|
||
pageNo: 1,
|
||
pageSize: 20,
|
||
statusCode: 'pending_approval',
|
||
keyword: undefined,
|
||
periodStartDate: undefined,
|
||
submitTime: undefined,
|
||
supervisorName: undefined,
|
||
isBusinessTrip: undefined
|
||
}),
|
||
fetchGetMonthlyReportApprovalPage({
|
||
pageNo: 1,
|
||
pageSize: 20,
|
||
statusCode: 'pending_approval',
|
||
keyword: undefined,
|
||
periodStartDate: undefined,
|
||
submitTime: undefined,
|
||
supervisorName: undefined
|
||
}),
|
||
fetchGetProjectReportApprovalPage({
|
||
pageNo: 1,
|
||
pageSize: 20,
|
||
statusCode: 'pending_approval',
|
||
keyword: undefined,
|
||
periodStartDate: undefined,
|
||
submitTime: undefined,
|
||
supervisorName: undefined,
|
||
projectId: undefined,
|
||
flag: undefined
|
||
})
|
||
]);
|
||
|
||
weeklyApprovalRows.value = weeklyResult.error || !weeklyResult.data ? [] : weeklyResult.data.list;
|
||
monthlyApprovalRows.value = monthlyResult.error || !monthlyResult.data ? [] : monthlyResult.data.list;
|
||
projectApprovalRows.value = projectResult.error || !projectResult.data ? [] : projectResult.data.list;
|
||
workReportApprovalItems.value = [
|
||
...buildWorkReportApprovalItems('weekly', weeklyApprovalRows.value),
|
||
...buildWorkReportApprovalItems('monthly', monthlyApprovalRows.value),
|
||
...buildWorkReportApprovalItems('project', projectApprovalRows.value)
|
||
];
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
// 工作台路由 keepAlive:切回不重挂,用 onActivated 替代 onMounted,每次激活经 refresh 重拉(首挂也会触发,不漏首屏)
|
||
onActivated(refresh);
|
||
</script>
|
||
|
||
<template>
|
||
<WorkbenchModuleCard
|
||
v-loading="loading"
|
||
title="我的待办"
|
||
icon="mdi:clipboard-text-clock-outline"
|
||
:editing="editing"
|
||
@hide="$emit('hide')"
|
||
@refresh="refresh"
|
||
>
|
||
<div class="workbench-todo__tabs">
|
||
<div class="workbench-todo__tabs-group">
|
||
<ElTooltip
|
||
v-for="tab in visibleMainTabs"
|
||
: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' && activeTab !== 'confirm'" 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-if="activeTab === 'approval'" class="workbench-todo__filters-left">
|
||
<button
|
||
v-for="tab in visibleApprovalBizTabs"
|
||
:key="tab.key"
|
||
type="button"
|
||
class="workbench-todo__filter"
|
||
:class="{ 'workbench-todo__filter--active': activeApprovalBizType === tab.key }"
|
||
@click="handleSelectApprovalBizType(tab.key)"
|
||
>
|
||
{{ tab.label }}
|
||
<span class="workbench-todo__filter-count">{{ approvalBizTabCounts[tab.key] }}</span>
|
||
</button>
|
||
</div>
|
||
<div v-else class="workbench-todo__filters-left">
|
||
<button
|
||
v-for="tab in visibleConfirmBizTabs"
|
||
:key="tab.key"
|
||
type="button"
|
||
class="workbench-todo__filter"
|
||
:class="{ 'workbench-todo__filter--active': activeApprovalBizType === tab.key }"
|
||
@click="handleSelectApprovalBizType(tab.key)"
|
||
>
|
||
{{ tab.label }}
|
||
<span class="workbench-todo__filter-count">{{ approvalBizTabCounts[tab.key] }}</span>
|
||
</button>
|
||
</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="
|
||
activeTab === 'approval' &&
|
||
activeApprovalBizType === 'overtime_application' &&
|
||
overtimeApprovalItems.length > 0
|
||
"
|
||
class="workbench-todo__batch-bar"
|
||
:class="{ 'workbench-todo__batch-bar--active': selectedOvertimeIds.size > 0 }"
|
||
>
|
||
<div class="workbench-todo__batch-bar-left">
|
||
<ElCheckbox
|
||
:model-value="allOvertimeItemsSelected"
|
||
:indeterminate="someOvertimeItemsSelected && !allOvertimeItemsSelected"
|
||
@change="val => toggleSelectAllOvertimeItems(Boolean(val))"
|
||
@click.stop
|
||
>
|
||
全选
|
||
</ElCheckbox>
|
||
<span v-if="selectedOvertimeIds.size > 0" class="workbench-todo__batch-bar-count">
|
||
已选择 {{ selectedOvertimeIds.size }} 项
|
||
</span>
|
||
</div>
|
||
<div v-if="selectedOvertimeIds.size > 0" class="workbench-todo__batch-bar-right">
|
||
<ElButton size="small" type="primary" :loading="batchSubmitting" @click.stop="handleBatchReview">
|
||
批量审批
|
||
</ElButton>
|
||
<ElButton size="small" link @click.stop="clearOvertimeSelection">取消选择</ElButton>
|
||
</div>
|
||
</div>
|
||
|
||
<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--selected': isOvertimeItemSelected(item)
|
||
}"
|
||
>
|
||
<div class="workbench-todo__leading">
|
||
<!-- 业务学习待审批时显示复选框 -->
|
||
<ElCheckbox
|
||
v-if="
|
||
activeTab === 'approval' &&
|
||
activeApprovalBizType === 'overtime_application' &&
|
||
item.approvalBizType === 'overtime_application'
|
||
"
|
||
:model-value="isOvertimeItemSelected(item)"
|
||
@change="val => toggleOvertimeItem(item, Boolean(val))"
|
||
@click.stop
|
||
/>
|
||
<span class="workbench-todo__category" :class="`workbench-todo__category--${item.categoryTone}`">
|
||
{{ item.categoryLabel }}
|
||
</span>
|
||
<span
|
||
v-if="item.priorityLabel"
|
||
class="workbench-todo__priority"
|
||
:class="{ 'workbench-todo__priority--high': item.priority === 'high' }"
|
||
>
|
||
{{ item.priorityLabel }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="workbench-todo__body">
|
||
<h4 class="workbench-todo__item-title">
|
||
<ElPopover
|
||
v-if="canPreviewTaskInfo(item)"
|
||
placement="right-start"
|
||
trigger="hover"
|
||
:width="960"
|
||
popper-class="workbench-task-preview-popover"
|
||
@show="handleTaskPreviewShow(item)"
|
||
>
|
||
<template #reference>
|
||
<span
|
||
class="workbench-todo__item-title-text"
|
||
:class="{
|
||
'workbench-todo__item-title-text--clickable': Boolean(
|
||
item.routeKey || item.approvalBizType || item.projectId
|
||
)
|
||
}"
|
||
@click="handleClickItem(item)"
|
||
>
|
||
{{ item.title }}
|
||
</span>
|
||
</template>
|
||
|
||
<div class="workbench-task-preview">
|
||
<div class="workbench-task-preview__header">任务信息</div>
|
||
<div v-loading="isTaskPreviewLoading(item)" class="workbench-task-preview__body">
|
||
<TaskInfoReadonly
|
||
v-if="getTaskPreviewTask(item)"
|
||
:task="getTaskPreviewTask(item)"
|
||
:task-options="getTaskPreviewTaskOptions(item)"
|
||
/>
|
||
<ElEmpty v-else description="暂无任务信息" :image-size="56" />
|
||
</div>
|
||
</div>
|
||
</ElPopover>
|
||
<span
|
||
v-else
|
||
class="workbench-todo__item-title-text"
|
||
:class="{
|
||
'workbench-todo__item-title-text--clickable': Boolean(
|
||
item.routeKey || item.approvalBizType || item.projectId
|
||
)
|
||
}"
|
||
@click="handleClickItem(item)"
|
||
>
|
||
{{ item.title }}
|
||
</span>
|
||
</h4>
|
||
<div class="workbench-todo__meta">
|
||
<span class="workbench-todo__source">{{ item.source }}</span>
|
||
</div>
|
||
<div
|
||
v-if="shouldShowTodoProgress(item)"
|
||
class="workbench-todo__progress"
|
||
:aria-label="`进度 ${getTodoProgress(item)}%`"
|
||
>
|
||
<span class="workbench-todo__progress-label">进度:</span>
|
||
<ElProgress
|
||
class="workbench-todo__progress-bar"
|
||
:percentage="getTodoProgress(item)"
|
||
:stroke-width="6"
|
||
:show-text="false"
|
||
/>
|
||
<span class="workbench-todo__progress-text">{{ getTodoProgress(item) }}%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="workbench-todo__trailing">
|
||
<div v-if="item.approvalBizType === 'overtime_application'" class="workbench-todo__actions" @click.stop>
|
||
<ElTooltip content="审批">
|
||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openOvertimeDetail(item)">
|
||
<component :is="APPROVAL_ENTRY_ICON" class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
</div>
|
||
<div v-else-if="item.approvalBizType === 'performance'" class="workbench-todo__actions" @click.stop>
|
||
<ElTooltip content="审批">
|
||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPerformanceDetail(item)">
|
||
<component :is="APPROVAL_ENTRY_ICON" class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
</div>
|
||
<div
|
||
v-else-if="isWorkReportApprovalBizType(item.approvalBizType)"
|
||
class="workbench-todo__actions"
|
||
@click.stop
|
||
>
|
||
<ElTooltip content="审批">
|
||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openWorkReportDetail(item)">
|
||
<component :is="APPROVAL_ENTRY_ICON" class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
</div>
|
||
<div
|
||
v-else-if="item.category === 'task' && canReportTaskWorklog(item)"
|
||
class="workbench-todo__actions"
|
||
@click.stop
|
||
>
|
||
<ElTooltip content="填报">
|
||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openTaskWorklog(item)">
|
||
<component :is="PERSONAL_ACTION_ICONS.worklog" class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
<ElTooltip v-for="action in getTaskActions(item)" :key="action.actionCode" :content="action.actionName">
|
||
<ElButton
|
||
link
|
||
:type="getTaskActionButtonType(action.actionCode)"
|
||
class="workbench-todo__action-btn"
|
||
@click="handleTaskAction(item, action)"
|
||
>
|
||
<component :is="TASK_ACTION_ICONS[action.actionCode]" class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
</div>
|
||
<div v-else-if="item.category === 'personal'" class="workbench-todo__actions" @click.stop>
|
||
<ElTooltip content="详情">
|
||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPersonalDetail(item)">
|
||
<component :is="PERSONAL_ACTION_ICONS.detail" class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
<ElTooltip content="填报工时">
|
||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPersonalWorklog(item)">
|
||
<component :is="PERSONAL_ACTION_ICONS.worklog" class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
<ElTooltip v-if="canCompletePersonalItem(item)" content="完成">
|
||
<ElButton link type="success" class="workbench-todo__action-btn" @click="openPersonalComplete(item)">
|
||
<component :is="PERSONAL_ACTION_ICONS.complete" class="text-15px" />
|
||
</ElButton>
|
||
</ElTooltip>
|
||
</div>
|
||
<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>
|
||
|
||
<!-- append-to-body:脱离 grid item 的 transform 容器,弹窗才能正常全屏居中 -->
|
||
<PersonalItemOperateDialog
|
||
v-model:visible="personalOperateVisible"
|
||
:operate-type="personalOperateType"
|
||
:row-data="personalOperateRow"
|
||
append-to-body
|
||
@submitted="handleAddSubmitted"
|
||
/>
|
||
|
||
<PersonalItemDetailDialog
|
||
v-model:visible="personalDetailVisible"
|
||
:row-data="personalDetailRow"
|
||
default-tab="worklog"
|
||
append-to-body
|
||
@changed="handlePersonalWorklogChanged"
|
||
/>
|
||
|
||
<PersonalItemStatusActionDialog
|
||
v-model:visible="personalStatusVisible"
|
||
:action="personalStatusAction"
|
||
append-to-body
|
||
@submit="handlePersonalStatusSubmit"
|
||
/>
|
||
|
||
<TaskStatusActionDialog
|
||
v-model:visible="taskStatusVisible"
|
||
:title="taskStatusActionTitle"
|
||
:action="taskStatusAction"
|
||
append-to-body
|
||
@submit="submitTaskStatusChange"
|
||
/>
|
||
|
||
<TaskWorklogDialog
|
||
v-model:visible="taskWorklogVisible"
|
||
:task="taskWorklogTask"
|
||
append-to-body
|
||
@changed="handleTaskWorklogChanged"
|
||
/>
|
||
|
||
<OvertimeApplicationDetailDialog
|
||
v-model:visible="overtimeDetailVisible"
|
||
:row-data="currentOvertimeApplication"
|
||
show-approval-actions
|
||
:action-loading="overtimeActionSubmitting"
|
||
append-to-body
|
||
@submit="handleOvertimeActionSubmit"
|
||
/>
|
||
|
||
<!-- 批量审批详情弹窗(左右箭头切换 + 通过/退回按钮 + 意见输入) -->
|
||
<OvertimeApplicationBatchDetailDialog
|
||
v-model:visible="batchDetailVisible"
|
||
:selected-ids="batchSelectedIds"
|
||
:rows="overtimeApprovalRows"
|
||
:action-loading="batchSubmitting"
|
||
append-to-body
|
||
@submit="handleBatchActionSubmit"
|
||
/>
|
||
|
||
<WorkReportPrototypePageDialog
|
||
v-model:visible="workReportDetailVisible"
|
||
mode="detail"
|
||
scene="approval"
|
||
:report-type="currentWorkReportType"
|
||
:row-data="currentWorkReport"
|
||
@submitted="handleWorkReportSubmitted"
|
||
/>
|
||
|
||
<PerformanceExcelEditorDrawer
|
||
v-model:visible="performanceDetailVisible"
|
||
:row-data="currentPerformanceSheet"
|
||
mode="view"
|
||
show-approval-footer
|
||
@start-approval="openCurrentPerformanceAction('confirm')"
|
||
/>
|
||
|
||
<PerformanceActionDialog
|
||
v-model:visible="performanceActionVisible"
|
||
:row-data="currentPerformanceSheet"
|
||
:action-type="currentPerformanceActionType"
|
||
selectable-action-type
|
||
@submitted="handlePerformanceActionSubmitted"
|
||
/>
|
||
</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__filter-count {
|
||
margin-left: 4px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.workbench-todo__content {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: auto;
|
||
}
|
||
|
||
.workbench-todo__content :deep(.el-empty) {
|
||
margin: auto;
|
||
}
|
||
|
||
/* 批量操作栏 */
|
||
.workbench-todo__batch-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 8px 14px;
|
||
margin-bottom: 10px;
|
||
border: 1px solid rgb(226 232 240 / 90%);
|
||
border-radius: 12px;
|
||
background-color: rgb(248 250 252 / 96%);
|
||
font-size: 13px;
|
||
color: rgb(71 85 105 / 94%);
|
||
transition: all 160ms ease;
|
||
}
|
||
|
||
.workbench-todo__batch-bar--active {
|
||
border-color: rgb(14 116 144 / 40%);
|
||
background-color: rgb(240 253 250 / 80%);
|
||
color: rgb(14 116 144 / 96%);
|
||
}
|
||
|
||
.workbench-todo__batch-bar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.workbench-todo__batch-bar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.workbench-todo__batch-bar-count {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.workbench-todo__list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.workbench-todo__item {
|
||
display: grid;
|
||
grid-template-columns: max-content 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--selected {
|
||
border-color: rgb(14 116 144 / 60%);
|
||
background-color: rgb(240 253 250 / 90%);
|
||
}
|
||
|
||
.workbench-todo__leading {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: max-content;
|
||
}
|
||
|
||
.workbench-todo__category {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 3px 10px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.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;
|
||
min-width: 20px;
|
||
height: 20px;
|
||
padding: 0 4px;
|
||
border-radius: 6px;
|
||
background-color: rgb(241 245 249 / 96%);
|
||
color: rgb(71 85 105 / 96%);
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.workbench-todo__priority--high {
|
||
background-color: rgb(254 226 226 / 96%);
|
||
color: rgb(220 38 38 / 96%);
|
||
}
|
||
|
||
.workbench-todo__body {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.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__item-title-text--clickable {
|
||
cursor: pointer;
|
||
transition: color 160ms ease;
|
||
}
|
||
|
||
.workbench-todo__item-title-text--clickable:hover {
|
||
color: rgb(14 116 144 / 96%);
|
||
text-decoration: underline;
|
||
}
|
||
|
||
:deep(.workbench-task-preview-popover) {
|
||
padding: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.workbench-task-preview {
|
||
background: var(--el-bg-color);
|
||
}
|
||
|
||
.workbench-task-preview__header {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 48px;
|
||
padding: 0 18px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.workbench-task-preview__body {
|
||
max-height: min(72vh, 760px);
|
||
padding: 18px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.workbench-todo__meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.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__progress {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
width: min(100%, 260px);
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.workbench-todo__progress-label {
|
||
flex: 0 0 auto;
|
||
color: rgb(100 116 139 / 92%);
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.workbench-todo__progress-bar {
|
||
flex: 1 1 auto;
|
||
min-width: 96px;
|
||
}
|
||
|
||
.workbench-todo__progress-text {
|
||
flex: 0 0 34px;
|
||
color: rgb(71 85 105 / 94%);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
text-align: right;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.workbench-todo__progress :deep(.el-progress-bar__outer) {
|
||
background-color: rgb(226 232 240 / 96%);
|
||
}
|
||
|
||
.workbench-todo__trailing {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.workbench-todo__actions {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.workbench-todo__actions :deep(.el-button + .el-button) {
|
||
margin-left: 0;
|
||
}
|
||
|
||
:deep(.workbench-todo__action-btn) {
|
||
min-width: auto;
|
||
height: auto;
|
||
padding: 3px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.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: 72px minmax(0, 1fr);
|
||
}
|
||
|
||
.workbench-todo__trailing {
|
||
grid-column: 1 / -1;
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
</style>
|