feat(M08-B): 建立保洁管理后台

This commit is contained in:
Codex
2026-06-29 11:06:54 +08:00
parent a1bace2dac
commit 1fada09249
22 changed files with 3136 additions and 15 deletions
@@ -0,0 +1,266 @@
<template>
<section class="panel">
<div class="panel-toolbar">
<el-segmented
:model-value="status"
:options="statusOptions"
@update:model-value="$emit('status-change', $event as SettlementStatus | '')"
/>
<div class="toolbar-actions">
<el-button :icon="FilePlus2" type="primary" @click="generateDialog.open = true">
生成
</el-button>
<el-tooltip content="刷新结算">
<el-button :icon="RefreshCw" :loading="loading" circle @click="$emit('refresh')" />
</el-tooltip>
</div>
</div>
<el-table :data="items" :loading="loading" class="data-table" row-key="id">
<el-table-column prop="settlementNo" label="结算单" min-width="190" fixed>
<template #default="{ row }">
<div class="stack">
<strong>{{ row.settlementNo }}</strong>
<span>{{ row.cleanerName }} · {{ compactText(row.storeName) }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="115">
<template #default="{ row }">
<el-tag :type="settlementTag(row.status)" effect="light">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="taskCount" label="任务" width="80" />
<el-table-column prop="totalRewardCents" label="金额" width="110">
<template #default="{ row }">{{ money(row.totalRewardCents) }}</template>
</el-table-column>
<el-table-column prop="payoutState" label="转账状态" width="130">
<template #default="{ row }">{{ compactText(row.payoutState) }}</template>
</el-table-column>
<el-table-column prop="payoutReference" label="流水" min-width="150">
<template #default="{ row }">{{ compactText(row.payoutReference) }}</template>
</el-table-column>
<el-table-column prop="payoutError" label="失败原因" min-width="150">
<template #default="{ row }">{{ compactText(row.payoutError) }}</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建" width="130">
<template #default="{ row }">{{ shortDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }">
<div class="row-actions">
<el-button
size="small"
:icon="BadgeCheck"
:disabled="row.status !== 'DRAFT'"
@click="$emit('confirm', { settlementId: row.id, note: '后台确认结算' })"
>
确认
</el-button>
<el-button
size="small"
type="primary"
:icon="Send"
:disabled="row.status !== 'CONFIRMED'"
@click="openTransfer(row)"
>
转账
</el-button>
<el-button
size="small"
:icon="RefreshCw"
:disabled="row.status !== 'CONFIRMED'"
@click="$emit('sync-transfer', { settlementId: row.id, note: '后台同步微信转账状态' })"
>
同步
</el-button>
<el-button
size="small"
type="danger"
:icon="CircleAlert"
:disabled="row.status !== 'CONFIRMED'"
@click="openFailure(row)"
>
失败
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
layout="prev, pager, next, total"
:current-page="page"
:page-size="pageSize"
:total="total"
@current-change="$emit('page-change', $event)"
/>
</div>
</section>
<el-dialog v-model="generateDialog.open" title="生成结算单" width="420px">
<el-form label-position="top">
<el-form-item label="保洁员用户 ID">
<el-input v-model="generateDialog.cleanerUserId" inputmode="numeric" />
</el-form-item>
<el-form-item label="门店 ID">
<el-input v-model="generateDialog.storeId" inputmode="numeric" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="generateDialog.note" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="generateDialog.open = false">取消</el-button>
<el-button type="primary" @click="submitGenerate">生成</el-button>
</template>
</el-dialog>
<el-dialog v-model="transferDialog.open" title="微信转账" width="420px">
<el-form label-position="top">
<el-form-item label="模式">
<el-radio-group v-model="transferDialog.mode">
<el-radio-button label="API">API</el-radio-button>
<el-radio-button label="MOCK">MOCK</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="transferDialog.note" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="transferDialog.open = false">取消</el-button>
<el-button type="primary" @click="submitTransfer">发起</el-button>
</template>
</el-dialog>
<el-dialog v-model="failureDialog.open" title="记录失败" width="420px">
<el-form label-position="top">
<el-form-item label="失败原因">
<el-input v-model="failureDialog.error" />
</el-form-item>
<el-form-item label="外部流水">
<el-input v-model="failureDialog.payoutReference" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="failureDialog.note" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="failureDialog.open = false">取消</el-button>
<el-button type="danger" @click="submitFailure">记录</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import {
BadgeCheck,
CircleAlert,
FilePlus2,
RefreshCw,
Send
} from '@lucide/vue';
import { compactText, money, shortDate } from '../format';
import type {
CleaningSettlement,
PageResult,
SettlementStatus,
TransferMode
} from '../types';
defineProps<{
session: { token: string };
loading: boolean;
items: PageResult<CleaningSettlement>['items'];
total: number;
page: number;
pageSize: number;
status: SettlementStatus | '';
}>();
const emit = defineEmits<{
refresh: [];
'status-change': [SettlementStatus | ''];
'page-change': [number];
generate: [{ cleanerUserId: string; storeId?: string; note?: string }];
confirm: [{ settlementId: string; note?: string }];
transfer: [{ settlementId: string; mode: TransferMode; note?: string }];
'sync-transfer': [{ settlementId: string; note?: string }];
failure: [{ settlementId: string; error: string; payoutReference?: string; note?: string }];
}>();
const statusOptions = [
{ label: '全部', value: '' },
{ label: '草稿', value: 'DRAFT' },
{ label: '待发', value: 'CONFIRMED' },
{ label: '已发', value: 'PAID' },
{ label: '取消', value: 'CANCELLED' }
];
const generateDialog = reactive({ open: false, cleanerUserId: '', storeId: '', note: '' });
const transferDialog = reactive({
open: false,
settlementId: '',
mode: 'API' as TransferMode,
note: ''
});
const failureDialog = reactive({
open: false,
settlementId: '',
error: '',
payoutReference: '',
note: ''
});
function settlementTag(status: SettlementStatus) {
if (status === 'PAID') return 'success';
if (status === 'CONFIRMED') return 'warning';
if (status === 'CANCELLED') return 'info';
return 'primary';
}
function submitGenerate() {
emit('generate', {
cleanerUserId: generateDialog.cleanerUserId,
storeId: generateDialog.storeId || undefined,
note: generateDialog.note
});
generateDialog.open = false;
}
function openTransfer(row: CleaningSettlement) {
transferDialog.open = true;
transferDialog.settlementId = row.id;
transferDialog.mode = 'API';
transferDialog.note = '';
}
function submitTransfer() {
emit('transfer', {
settlementId: transferDialog.settlementId,
mode: transferDialog.mode,
note: transferDialog.note
});
transferDialog.open = false;
}
function openFailure(row: CleaningSettlement) {
failureDialog.open = true;
failureDialog.settlementId = row.id;
failureDialog.error = row.payoutError || '';
failureDialog.payoutReference = row.payoutReference || '';
failureDialog.note = '';
}
function submitFailure() {
emit('failure', {
settlementId: failureDialog.settlementId,
error: failureDialog.error,
payoutReference: failureDialog.payoutReference,
note: failureDialog.note
});
failureDialog.open = false;
}
</script>