284 lines
7.0 KiB
Vue
284 lines
7.0 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue';
|
|
import { getWorkbenchItemColor } from '../composables/use-workbench-colors';
|
|
import { type WorkbenchTeamLoadLevel, buildWorkbenchTeamLoadView } from '../homepage';
|
|
import { workbenchTeamLoadMock } from '../mock';
|
|
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
|
import WorkbenchModuleCard from './workbench-module-card.vue';
|
|
|
|
defineOptions({ name: 'WorkbenchTeamLoad' });
|
|
|
|
interface Props {
|
|
editing?: boolean;
|
|
}
|
|
withDefaults(defineProps<Props>(), { editing: false });
|
|
defineEmits<{ (e: 'hide'): void }>();
|
|
|
|
const { loading, refresh } = useWorkbenchRefresh();
|
|
|
|
const view = computed(() => buildWorkbenchTeamLoadView(workbenchTeamLoadMock));
|
|
|
|
const LEVEL_LABEL: Record<WorkbenchTeamLoadLevel, string> = {
|
|
high: '高负载',
|
|
mid: '中负载',
|
|
normal: '正常'
|
|
};
|
|
|
|
function urgentTooltip(dueSoon: number, overdue: number) {
|
|
if (dueSoon > 0 && overdue > 0) return `临期 ${dueSoon} · 逾期 ${overdue}`;
|
|
if (overdue > 0) return `逾期 ${overdue}`;
|
|
return `临期 ${dueSoon}`;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<WorkbenchModuleCard
|
|
v-loading="loading"
|
|
title="团队负载"
|
|
icon="mdi:scale-balance"
|
|
:editing="editing"
|
|
@hide="$emit('hide')"
|
|
@refresh="refresh"
|
|
>
|
|
<div class="tl-kpis">
|
|
<div class="tl-kpi">
|
|
<span class="tl-kpi__label">高负载</span>
|
|
<span class="tl-kpi__value" :class="{ 'is-danger': view.highCount > 0 }">
|
|
{{ view.highCount }}
|
|
<span class="tl-kpi__unit">人</span>
|
|
</span>
|
|
</div>
|
|
<div class="tl-kpi">
|
|
<span class="tl-kpi__label">中负载</span>
|
|
<span class="tl-kpi__value" :class="{ 'is-warn': view.midCount > 0 }">
|
|
{{ view.midCount }}
|
|
<span class="tl-kpi__unit">人</span>
|
|
</span>
|
|
</div>
|
|
<div class="tl-kpi">
|
|
<span class="tl-kpi__label">临期 + 逾期</span>
|
|
<span class="tl-kpi__value" :class="{ 'is-danger': view.urgentTotal > 0 }">
|
|
{{ view.urgentTotal }}
|
|
<span class="tl-kpi__unit">条</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<ul class="tl-list">
|
|
<li v-for="m in view.members" :key="m.memberId" class="tl-row">
|
|
<span class="tl-row__dot" :class="`is-${m.level}`" :title="LEVEL_LABEL[m.level]" />
|
|
<span class="tl-row__name">{{ m.memberName }}</span>
|
|
<div class="tl-row__bar-wrap">
|
|
<div class="tl-row__bar" :style="{ width: `${m.barWidthPercent}%` }">
|
|
<ElTooltip
|
|
v-for="seg in m.segments"
|
|
:key="seg.key"
|
|
:content="`${seg.label} · ${seg.count} 个`"
|
|
placement="top"
|
|
>
|
|
<div
|
|
class="tl-row__seg"
|
|
:style="{
|
|
width: `${seg.widthPercent}%`,
|
|
background: getWorkbenchItemColor(seg.key, seg.kind)
|
|
}"
|
|
/>
|
|
</ElTooltip>
|
|
</div>
|
|
<span v-if="m.overflowExtra > 0" class="tl-row__overflow">+{{ m.overflowExtra }}</span>
|
|
</div>
|
|
<span class="tl-row__metrics">
|
|
<span class="tl-row__metric" :class="`is-${m.level}`">
|
|
<b>{{ m.inProgress }}</b>
|
|
进行
|
|
</span>
|
|
<span v-if="m.urgent > 0" class="tl-row__metric is-urgent">
|
|
<ElTooltip :content="urgentTooltip(m.dueSoon, m.overdue)" placement="top">
|
|
<span>
|
|
<b>{{ m.urgent }}</b>
|
|
临期
|
|
<SvgIcon v-if="m.overdue > 0" icon="mdi:alert" class="tl-row__warn-icon" />
|
|
</span>
|
|
</ElTooltip>
|
|
</span>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tl-hint">高 = 进行中 ≥ 6 或 临期+逾期 ≥ 2 · 中 = 进行中 ≥ 4 或 临期+逾期 ≥ 1</div>
|
|
</WorkbenchModuleCard>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.tl-kpis {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.tl-kpi {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding: 10px 12px;
|
|
background: var(--el-fill-color-lighter);
|
|
border-radius: 8px;
|
|
min-width: 0;
|
|
}
|
|
.tl-kpi__label {
|
|
font-size: 11px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
.tl-kpi__value {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
color: var(--el-text-color-primary);
|
|
line-height: 1.2;
|
|
}
|
|
.tl-kpi__value.is-danger {
|
|
color: var(--el-color-danger);
|
|
}
|
|
.tl-kpi__value.is-warn {
|
|
color: var(--el-color-warning);
|
|
}
|
|
.tl-kpi__unit {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--el-text-color-secondary);
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.tl-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
}
|
|
.tl-list::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
.tl-list::-webkit-scrollbar-thumb {
|
|
background: var(--el-fill-color-darker);
|
|
border-radius: 3px;
|
|
}
|
|
.tl-list::-webkit-scrollbar-thumb:hover {
|
|
background: var(--el-border-color);
|
|
}
|
|
.tl-row {
|
|
display: grid;
|
|
grid-template-columns: 10px 64px 1fr auto;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 7px 0;
|
|
font-size: 13px;
|
|
border-bottom: 1px dashed var(--el-border-color-lighter);
|
|
}
|
|
.tl-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.tl-row__dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--el-color-success);
|
|
}
|
|
.tl-row__dot.is-high {
|
|
background: var(--el-color-danger);
|
|
}
|
|
.tl-row__dot.is-mid {
|
|
background: var(--el-color-warning);
|
|
}
|
|
.tl-row__dot.is-normal {
|
|
background: var(--el-color-success);
|
|
}
|
|
.tl-row__name {
|
|
color: var(--el-text-color-primary);
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.tl-row__bar-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
}
|
|
.tl-row__bar {
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background: var(--el-fill-color);
|
|
overflow: hidden;
|
|
display: flex;
|
|
min-width: 0;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.tl-row__seg {
|
|
height: 100%;
|
|
transition: filter 0.15s ease;
|
|
cursor: default;
|
|
}
|
|
.tl-row__seg:hover {
|
|
filter: brightness(0.92);
|
|
}
|
|
.tl-row__seg + .tl-row__seg {
|
|
box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.75);
|
|
}
|
|
.tl-row__overflow {
|
|
flex-shrink: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 1px 6px;
|
|
border-radius: 8px;
|
|
background: var(--el-color-danger);
|
|
color: #fff;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.tl-row__metrics {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
white-space: nowrap;
|
|
}
|
|
.tl-row__metric b {
|
|
color: var(--el-text-color-primary);
|
|
font-weight: 600;
|
|
margin-right: 2px;
|
|
}
|
|
.tl-row__metric.is-high b {
|
|
color: var(--el-color-danger);
|
|
}
|
|
.tl-row__metric.is-mid b {
|
|
color: var(--el-color-warning);
|
|
}
|
|
.tl-row__metric.is-normal b {
|
|
color: var(--el-color-success);
|
|
}
|
|
.tl-row__metric.is-urgent {
|
|
color: var(--el-color-danger);
|
|
}
|
|
.tl-row__metric.is-urgent b {
|
|
color: var(--el-color-danger);
|
|
}
|
|
.tl-row__warn-icon {
|
|
vertical-align: -2px;
|
|
margin-left: 2px;
|
|
font-size: 12px;
|
|
color: var(--el-color-danger);
|
|
}
|
|
|
|
.tl-hint {
|
|
margin-top: 10px;
|
|
font-size: 11px;
|
|
color: var(--el-text-color-placeholder);
|
|
line-height: 1.5;
|
|
}
|
|
</style>
|