feat(M08-B): 补保洁员绩效排行
This commit is contained in:
@@ -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)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -168,6 +168,8 @@ export interface CleaningStatistics {
|
||||
cleanerUserId: string;
|
||||
cleanerName: string;
|
||||
taskCount: number;
|
||||
completedTaskCount: number;
|
||||
rejectedTaskCount: number;
|
||||
pendingSettlementCents: number;
|
||||
settledRewardCents: number;
|
||||
}>;
|
||||
|
||||
Reference in New Issue
Block a user