113 lines
2.6 KiB
Vue
113 lines
2.6 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||
|
|
|
||
|
|
defineOptions({ name: 'WorkbenchTeamWorklog' });
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
editing?: boolean;
|
||
|
|
collapsed?: boolean;
|
||
|
|
}
|
||
|
|
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||
|
|
defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>();
|
||
|
|
|
||
|
|
interface MemberRow {
|
||
|
|
name: string;
|
||
|
|
hours: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
const members: MemberRow[] = [
|
||
|
|
{ name: '张三', hours: 38 },
|
||
|
|
{ name: '李四', hours: 42 },
|
||
|
|
{ name: '王五', hours: 30 },
|
||
|
|
{ name: '赵六', hours: 48 },
|
||
|
|
{ name: '钱七', hours: 25 }
|
||
|
|
];
|
||
|
|
|
||
|
|
const maxHours = 48;
|
||
|
|
const avg = (members.reduce((s, m) => s + m.hours, 0) / members.length).toFixed(1);
|
||
|
|
const lowest = members.reduce((a, b) => (a.hours < b.hours ? a : b));
|
||
|
|
const highest = members.reduce((a, b) => (a.hours > b.hours ? a : b));
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<WorkbenchModuleCard
|
||
|
|
title="团队工时分布"
|
||
|
|
icon="mdi:chart-bar"
|
||
|
|
:editing="editing"
|
||
|
|
:collapsed="collapsed"
|
||
|
|
@hide="$emit('hide')"
|
||
|
|
@toggle-collapse="$emit('toggle-collapse')"
|
||
|
|
>
|
||
|
|
<div class="bars">
|
||
|
|
<div v-for="m in members" :key="m.name" class="bar-col">
|
||
|
|
<div class="bar-value">{{ m.hours }}h</div>
|
||
|
|
<div class="bar" :style="{ height: `${(m.hours / maxHours) * 100}%` }" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="bars-x">
|
||
|
|
<div v-for="m in members" :key="m.name">{{ m.name }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="bars-hint">
|
||
|
|
平均 {{ avg }}h / 人 ·
|
||
|
|
<span class="text-danger">{{ lowest.name }}</span>
|
||
|
|
工时偏低 ·
|
||
|
|
<span class="text-warn">{{ highest.name }}</span>
|
||
|
|
超 40h
|
||
|
|
</div>
|
||
|
|
</WorkbenchModuleCard>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.bars {
|
||
|
|
display: flex;
|
||
|
|
align-items: flex-end;
|
||
|
|
gap: 10px;
|
||
|
|
height: 120px;
|
||
|
|
padding: 18px 4px 0;
|
||
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
|
|
}
|
||
|
|
.bar-col {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: flex-end;
|
||
|
|
height: 100%;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
.bar-value {
|
||
|
|
position: absolute;
|
||
|
|
top: -16px;
|
||
|
|
font-size: 10px;
|
||
|
|
color: var(--el-text-color-secondary);
|
||
|
|
}
|
||
|
|
.bar {
|
||
|
|
width: 100%;
|
||
|
|
background: linear-gradient(180deg, var(--el-color-primary), var(--el-color-primary-light-7));
|
||
|
|
border-radius: 4px 4px 0 0;
|
||
|
|
min-height: 6px;
|
||
|
|
}
|
||
|
|
.bars-x {
|
||
|
|
display: flex;
|
||
|
|
gap: 10px;
|
||
|
|
margin-top: 4px;
|
||
|
|
}
|
||
|
|
.bars-x div {
|
||
|
|
flex: 1;
|
||
|
|
text-align: center;
|
||
|
|
font-size: 11px;
|
||
|
|
color: var(--el-text-color-secondary);
|
||
|
|
}
|
||
|
|
.bars-hint {
|
||
|
|
margin-top: 10px;
|
||
|
|
font-size: 12px;
|
||
|
|
color: var(--el-text-color-secondary);
|
||
|
|
}
|
||
|
|
.text-danger {
|
||
|
|
color: var(--el-color-danger);
|
||
|
|
}
|
||
|
|
.text-warn {
|
||
|
|
color: var(--el-color-warning);
|
||
|
|
}
|
||
|
|
</style>
|