feat(M08-B): 补保洁员绩效排行

This commit is contained in:
Codex
2026-07-07 14:41:35 +08:00
parent ee39e98418
commit 38410b1185
10 changed files with 183 additions and 14 deletions
@@ -63,6 +63,33 @@
</div>
</section>
<section class="stat-block cleaner-performance-board">
<header>
<div>
<h3>保洁员绩效排行</h3>
<span>任务 / 完成率 / 驳回 / 结算</span>
</div>
<el-button :icon="Download" :disabled="cleanerPerformanceRows.length === 0" @click="exportCleanerPerformance">
导出排行
</el-button>
</header>
<div class="cleaner-performance-grid">
<button
v-for="row in cleanerPerformanceRows"
:key="row.cleanerUserId"
type="button"
class="cleaner-performance-card"
@click="filterByCleaner(row.cleanerUserId)"
>
<span> {{ row.rank }} </span>
<strong>{{ row.cleanerName || compactText(row.cleanerUserId) }}</strong>
<em>任务 {{ row.taskCount }} · 完成 {{ row.completedTaskCount }} · 驳回 {{ row.rejectedTaskCount }}</em>
<small>完成率 {{ row.completionRate }}% · 待结算 {{ money(row.pendingSettlementCents) }} · 已结算 {{ money(row.settledRewardCents) }}</small>
</button>
</div>
<el-empty v-if="cleanerPerformanceRows.length === 0" description="暂无保洁员绩效数据" :image-size="72" />
</section>
<div class="stats-grid">
<section class="stat-block">
<header>
@@ -149,6 +176,8 @@
</template>
</el-table-column>
<el-table-column prop="taskCount" label="任务" width="90" />
<el-table-column prop="completedTaskCount" label="完成" width="90" />
<el-table-column prop="rejectedTaskCount" label="驳回" width="90" />
<el-table-column prop="pendingSettlementCents" label="待结算" width="130">
<template #default="{ row }">{{ money(row.pendingSettlementCents) }}</template>
</el-table-column>
@@ -186,6 +215,20 @@ const quickOptions = [
{ label: '本月', value: 'month' }
];
const maxTrendTotal = computed(() => Math.max(1, ...props.statistics.trend.map((row) => row.taskTotal)));
const cleanerPerformanceRows = computed(() => props.statistics.members
.map((row) => ({
...row,
completionRate: row.taskCount ? Math.round((row.completedTaskCount / row.taskCount) * 100) : 0,
settlementTotalCents: row.pendingSettlementCents + row.settledRewardCents
}))
.sort((a, b) => (
b.taskCount - a.taskCount
|| b.completedTaskCount - a.completedTaskCount
|| a.rejectedTaskCount - b.rejectedTaskCount
|| b.settlementTotalCents - a.settlementTotalCents
))
.slice(0, 8)
.map((row, index) => ({ ...row, rank: index + 1 })));
watch(
() => props.filters,
@@ -232,6 +275,28 @@ function trendWidth(total: number) {
return `${Math.max(6, Math.round((total / maxTrendTotal.value) * 100))}%`;
}
function filterByCleaner(cleanerUserId: string) {
filterDraft.cleanerUserId = cleanerUserId;
emitFilter();
}
function exportCleanerPerformance() {
downloadCsv(`cleaning-cleaner-performance-${Date.now()}.csv`, [
['排名', '保洁员', '保洁员ID', '任务', '完成', '驳回', '完成率', '待结算', '已结算'],
...cleanerPerformanceRows.value.map((row) => [
String(row.rank),
row.cleanerName || '',
row.cleanerUserId,
String(row.taskCount),
String(row.completedTaskCount),
String(row.rejectedTaskCount),
`${row.completionRate}%`,
money(row.pendingSettlementCents),
money(row.settledRewardCents)
])
]);
}
function exportStatistics() {
downloadCsv(`cleaning-statistics-${Date.now()}.csv`, [
['section', 'key', 'value', 'extra1', 'extra2', 'extra3', 'extra4'],
@@ -272,15 +337,25 @@ function exportStatistics() {
String(row.rejected),
money(row.rewardCents)
]),
['成员分账', '保洁员', '任务', '待结算', '已结算', '', ''],
['成员分账', '保洁员', '任务', '完成', '驳回', '待结算', '已结算'],
...props.statistics.members.map((row) => [
'成员分账',
`${row.cleanerName || ''}(${row.cleanerUserId})`,
String(row.taskCount),
String(row.completedTaskCount),
String(row.rejectedTaskCount),
money(row.pendingSettlementCents),
money(row.settledRewardCents),
'',
''
money(row.settledRewardCents)
]),
['绩效排行', '排名', '保洁员', '任务', '完成率', '驳回', '结算合计'],
...cleanerPerformanceRows.value.map((row) => [
'绩效排行',
String(row.rank),
`${row.cleanerName || ''}(${row.cleanerUserId})`,
String(row.taskCount),
`${row.completionRate}%`,
String(row.rejectedTaskCount),
money(row.settlementTotalCents)
])
]);
}
+51
View File
@@ -808,6 +808,49 @@ textarea {
font-size: 12px;
}
.cleaner-performance-board {
background: #fbfcfe;
}
.cleaner-performance-board header > div {
display: grid;
gap: 3px;
}
.cleaner-performance-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
padding: 12px;
}
.cleaner-performance-card {
display: grid;
gap: 5px;
min-width: 0;
padding: 10px;
border: 1px solid #d8e2ef;
border-radius: 8px;
background: #ffffff;
color: #172033;
text-align: left;
cursor: pointer;
}
.cleaner-performance-card span,
.cleaner-performance-card em,
.cleaner-performance-card small {
color: #60708a;
font-size: 12px;
font-style: normal;
}
.cleaner-performance-card strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.el-button {
border-radius: 8px;
}
@@ -874,6 +917,10 @@ textarea {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.cleaner-performance-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.trend-row {
grid-template-columns: 92px minmax(80px, 1fr);
}
@@ -901,6 +948,10 @@ textarea {
grid-template-columns: 1fr;
}
.cleaner-performance-grid {
grid-template-columns: 1fr;
}
.action-board {
grid-template-columns: 1fr;
}
+2
View File
@@ -168,6 +168,8 @@ export interface CleaningStatistics {
cleanerUserId: string;
cleanerName: string;
taskCount: number;
completedTaskCount: number;
rejectedTaskCount: number;
pendingSettlementCents: number;
settledRewardCents: number;
}>;