feat(home): implement workbench dashboard
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { HomeDocumentRanking, HomeRankingPeriod } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
rows: HomeDocumentRanking[]
|
||||
period: HomeRankingPeriod
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:period': [period: HomeRankingPeriod]
|
||||
manage: []
|
||||
}>()
|
||||
|
||||
const periodOptions: Array<{ label: string; value: HomeRankingPeriod }> = [
|
||||
{ label: '近7天', value: 7 },
|
||||
{ label: '近14天', value: 14 },
|
||||
{ label: '近30天', value: 30 },
|
||||
]
|
||||
|
||||
const rankColors = ['#0e6e8c', '#18a0c0', '#0f9d6c', '#d9821f', '#7a4dbb', '#d0534f']
|
||||
|
||||
const maxCount = computed(() => Math.max(...props.rows.map((row) => row.signedCount), 1))
|
||||
|
||||
function getBarWidth(row: HomeDocumentRanking) {
|
||||
return `${Math.max((row.signedCount / maxCount.value) * 100, 1.5)}%`
|
||||
}
|
||||
|
||||
function getRankColor(index: number) {
|
||||
return rankColors[index % rankColors.length]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="home-panel ranking-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>Top10 文档签署排名</h2>
|
||||
<span class="heading-spacer" />
|
||||
<div class="period-switch" role="group" aria-label="排名时间范围">
|
||||
<button
|
||||
v-for="option in periodOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="period-button"
|
||||
:class="{ 'period-button--active': period === option.value }"
|
||||
@click="emit('update:period', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="panel-more" @click="emit('manage')">管理 ›</button>
|
||||
</div>
|
||||
|
||||
<div v-if="rows.length" class="ranking-chart" role="list" aria-label="文档签署排名">
|
||||
<div
|
||||
v-for="(row, index) in rows"
|
||||
:key="row.id"
|
||||
class="ranking-row"
|
||||
role="listitem"
|
||||
:title="`${row.documentName}:${row.signedCount} 次(${row.department})`"
|
||||
>
|
||||
<span class="ranking-number" :style="{ color: getRankColor(index) }">{{ index + 1 }}</span>
|
||||
<span class="ranking-name">{{ row.documentName }}</span>
|
||||
<span class="ranking-bar-track">
|
||||
<span
|
||||
class="ranking-bar"
|
||||
:style="{ width: getBarWidth(row), background: getRankColor(index) }"
|
||||
/>
|
||||
</span>
|
||||
<strong class="ranking-count" :style="{ color: getRankColor(index) }">
|
||||
{{ row.signedCount }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="ranking-axis" aria-hidden="true">
|
||||
<span>0</span>
|
||||
<span>{{ Math.ceil(maxCount / 4) }}</span>
|
||||
<span>{{ Math.ceil(maxCount / 2) }}</span>
|
||||
<span>{{ Math.ceil((maxCount * 3) / 4) }}</span>
|
||||
<span>{{ maxCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="ranking-empty">暂无排名数据</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-panel {
|
||||
min-width: 0;
|
||||
padding: 16px 18px;
|
||||
background: var(--card);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.panel-heading h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 14.5px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.heading-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panel-more {
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel-more:hover {
|
||||
color: var(--brand-d);
|
||||
}
|
||||
|
||||
.period-switch {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--brand-l);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.period-button {
|
||||
padding: 5px 12px;
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--brand-l);
|
||||
}
|
||||
|
||||
.period-button:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.period-button:hover:not(.period-button--active) {
|
||||
color: var(--brand);
|
||||
background: var(--brand-l);
|
||||
}
|
||||
|
||||
.period-button--active {
|
||||
color: #fff;
|
||||
background: var(--brand);
|
||||
}
|
||||
|
||||
.ranking-chart {
|
||||
display: flex;
|
||||
min-width: 680px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ranking-row {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(170px, 220px) minmax(160px, 1fr) 42px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-height: 21px;
|
||||
}
|
||||
|
||||
.ranking-number {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ranking-name {
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ranking-bar-track {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
background-color: #f7fafb;
|
||||
background-image: linear-gradient(to right, transparent calc(25% - 1px), #eef4f6 25%);
|
||||
background-size: 25% 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ranking-bar {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-width: 2px;
|
||||
border-radius: 4px;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.ranking-count {
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ranking-axis {
|
||||
display: flex;
|
||||
grid-column: 3;
|
||||
justify-content: space-between;
|
||||
margin-left: 32px;
|
||||
padding-top: 1px;
|
||||
color: var(--mut);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.ranking-empty {
|
||||
display: grid;
|
||||
min-height: 210px;
|
||||
color: var(--mut);
|
||||
font-size: 13px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.panel-heading {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.heading-spacer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.period-switch {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.panel-more {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user