feat(projects): 工作台小组件设计
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
<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 WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchTeamLoad' });
|
||||
@@ -10,24 +14,18 @@ interface Props {
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>();
|
||||
|
||||
interface LoadRow {
|
||||
name: string;
|
||||
inProgress: number;
|
||||
}
|
||||
const view = computed(() => buildWorkbenchTeamLoadView(workbenchTeamLoadMock));
|
||||
|
||||
const rows: LoadRow[] = [
|
||||
{ name: '张三', inProgress: 5 },
|
||||
{ name: '李四', inProgress: 3 },
|
||||
{ name: '王五', inProgress: 7 },
|
||||
{ name: '赵六', inProgress: 2 },
|
||||
{ name: '钱七', inProgress: 5 }
|
||||
];
|
||||
const LEVEL_LABEL: Record<WorkbenchTeamLoadLevel, string> = {
|
||||
high: '高负载',
|
||||
mid: '中负载',
|
||||
normal: '正常'
|
||||
};
|
||||
|
||||
const MAX = 10;
|
||||
function level(n: number): 'ok' | 'warn' | 'over' {
|
||||
if (n >= 6) return 'over';
|
||||
if (n >= 4) return 'warn';
|
||||
return 'ok';
|
||||
function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
if (dueSoon > 0 && overdue > 0) return `临期 ${dueSoon} · 逾期 ${overdue}`;
|
||||
if (overdue > 0) return `逾期 ${overdue}`;
|
||||
return `临期 ${dueSoon}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -40,69 +38,243 @@ function level(n: number): 'ok' | 'warn' | 'over' {
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
>
|
||||
<ul class="load-list">
|
||||
<li v-for="row in rows" :key="row.name" class="load-item">
|
||||
<span class="load-name">{{ row.name }}</span>
|
||||
<div class="load-bar">
|
||||
<div
|
||||
class="load-bar-inner"
|
||||
:class="`is-${level(row.inProgress)}`"
|
||||
:style="{ width: `${(row.inProgress / MAX) * 100}%` }"
|
||||
/>
|
||||
<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="load-n" :class="{ 'text-danger': level(row.inProgress) === 'over' }">
|
||||
{{ row.inProgress }}{{ level(row.inProgress) === 'over' ? ' ⚠' : '' }}
|
||||
<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="load-hint">阈值 ≥6 高负载 · ≥4 中负载</div>
|
||||
|
||||
<div class="tl-hint">高 = 进行中 ≥ 6 或 临期+逾期 ≥ 2 · 中 = 进行中 ≥ 4 或 临期+逾期 ≥ 1</div>
|
||||
</WorkbenchModuleCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.load-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.load-item {
|
||||
.tl-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr 50px;
|
||||
align-items: center;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.load-bar {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--el-fill-color);
|
||||
overflow: hidden;
|
||||
.tl-kpi {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 10px 12px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.load-bar-inner {
|
||||
height: 100%;
|
||||
}
|
||||
.load-bar-inner.is-ok {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
.load-bar-inner.is-warn {
|
||||
background: var(--el-color-warning);
|
||||
}
|
||||
.load-bar-inner.is-over {
|
||||
background: var(--el-color-danger);
|
||||
}
|
||||
.load-n {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
.load-hint {
|
||||
margin-top: 8px;
|
||||
.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;
|
||||
max-height: 240px;
|
||||
overflow-y: 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>
|
||||
|
||||
Reference in New Issue
Block a user