feat(M08-B): 建立保洁管理后台
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<el-config-provider>
|
||||
<main class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="brand-mark">棋</div>
|
||||
<div>
|
||||
<h1>自助棋牌室</h1>
|
||||
<p>运营后台</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav-list" aria-label="后台模块">
|
||||
<button class="nav-item active" type="button">
|
||||
<Sparkles :size="18" />
|
||||
<span>保洁运营</span>
|
||||
</button>
|
||||
<button class="nav-item" type="button" disabled>
|
||||
<Store :size="18" />
|
||||
<span>门店</span>
|
||||
</button>
|
||||
<button class="nav-item" type="button" disabled>
|
||||
<RadioTower :size="18" />
|
||||
<span>设备</span>
|
||||
</button>
|
||||
<button class="nav-item" type="button" disabled>
|
||||
<WalletCards :size="18" />
|
||||
<span>支付</span>
|
||||
</button>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<section class="workspace">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow">M08-B</p>
|
||||
<h2>保洁任务与结算</h2>
|
||||
</div>
|
||||
<div class="token-box">
|
||||
<el-input
|
||||
v-model="tokenDraft"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="后台访问令牌"
|
||||
autocomplete="off"
|
||||
@keyup.enter="saveToken"
|
||||
/>
|
||||
<el-tooltip content="保存令牌" placement="bottom">
|
||||
<el-button :icon="Save" type="primary" @click="saveToken" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="metric-grid" aria-label="保洁概览">
|
||||
<div class="metric">
|
||||
<span>待验收</span>
|
||||
<strong>{{ metrics.submitted }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>已完成</span>
|
||||
<strong>{{ metrics.completed }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>待发放</span>
|
||||
<strong>{{ metrics.confirmed }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>本页金额</span>
|
||||
<strong>{{ money(metrics.amountCents) }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-alert
|
||||
v-if="lastError"
|
||||
class="alert-line"
|
||||
:title="lastError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="true"
|
||||
@close="lastError = ''"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="lastMessage"
|
||||
class="alert-line"
|
||||
:title="lastMessage"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="true"
|
||||
@close="lastMessage = ''"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="activeTab" class="work-tabs">
|
||||
<el-tab-pane label="任务" name="tasks">
|
||||
<CleaningTasksPanel
|
||||
:session="session"
|
||||
:loading="loading.tasks"
|
||||
:items="tasks.items"
|
||||
:total="tasks.total"
|
||||
:page="tasks.page"
|
||||
:page-size="tasks.pageSize"
|
||||
:status="taskStatus"
|
||||
@refresh="loadTasks"
|
||||
@status-change="changeTaskStatus"
|
||||
@page-change="changeTaskPage"
|
||||
@assign="handleAssign"
|
||||
@complete="handleComplete"
|
||||
@reject="handleReject"
|
||||
@reclaim="handleReclaim"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="结算" name="settlements">
|
||||
<CleaningSettlementsPanel
|
||||
:session="session"
|
||||
:loading="loading.settlements"
|
||||
:items="settlements.items"
|
||||
:total="settlements.total"
|
||||
:page="settlements.page"
|
||||
:page-size="settlements.pageSize"
|
||||
:status="settlementStatus"
|
||||
@refresh="loadSettlements"
|
||||
@status-change="changeSettlementStatus"
|
||||
@page-change="changeSettlementPage"
|
||||
@generate="handleGenerateSettlement"
|
||||
@confirm="handleConfirmSettlement"
|
||||
@transfer="handleTransfer"
|
||||
@sync-transfer="handleSyncTransfer"
|
||||
@failure="handleFailure"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
</main>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {
|
||||
RadioTower,
|
||||
Save,
|
||||
Sparkles,
|
||||
Store,
|
||||
WalletCards
|
||||
} from '@lucide/vue';
|
||||
import CleaningTasksPanel from './components/CleaningTasksPanel.vue';
|
||||
import CleaningSettlementsPanel from './components/CleaningSettlementsPanel.vue';
|
||||
import {
|
||||
ApiError,
|
||||
assignCleaningTask,
|
||||
completeCleaningTask,
|
||||
confirmCleaningSettlement,
|
||||
executeWechatTransfer,
|
||||
generateCleaningSettlement,
|
||||
listCleaningSettlements,
|
||||
listCleaningTasks,
|
||||
reclaimCleaningTimeouts,
|
||||
recordPayoutFailure,
|
||||
rejectCleaningTask,
|
||||
syncWechatTransfer
|
||||
} from './api';
|
||||
import type {
|
||||
CleaningSettlement,
|
||||
CleaningTask,
|
||||
PageResult,
|
||||
SettlementStatus,
|
||||
TaskStatus,
|
||||
TransferMode
|
||||
} from './types';
|
||||
import { money } from './format';
|
||||
|
||||
const tokenDraft = ref(localStorage.getItem('qipai.admin.token') || '');
|
||||
const activeTab = ref('tasks');
|
||||
const lastError = ref('');
|
||||
const lastMessage = ref('');
|
||||
const taskStatus = ref<TaskStatus | ''>('SUBMITTED');
|
||||
const settlementStatus = ref<SettlementStatus | ''>('CONFIRMED');
|
||||
const session = computed(() => ({ token: tokenDraft.value }));
|
||||
const loading = reactive({ tasks: false, settlements: false });
|
||||
const tasks = reactive<PageResult<CleaningTask>>({ items: [], total: 0, page: 1, pageSize: 20 });
|
||||
const settlements = reactive<PageResult<CleaningSettlement>>({
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
const metrics = computed(() => ({
|
||||
submitted: tasks.items.filter((item) => item.status === 'SUBMITTED').length,
|
||||
completed: tasks.items.filter((item) => item.status === 'COMPLETED').length,
|
||||
confirmed: settlements.items.filter((item) => item.status === 'CONFIRMED').length,
|
||||
amountCents: settlements.items.reduce((sum, item) => sum + Number(item.totalRewardCents || 0), 0)
|
||||
}));
|
||||
|
||||
function saveToken() {
|
||||
localStorage.setItem('qipai.admin.token', tokenDraft.value.trim());
|
||||
ElMessage.success('已保存');
|
||||
}
|
||||
|
||||
async function runAction(work: () => Promise<void>, success: string) {
|
||||
lastError.value = '';
|
||||
lastMessage.value = '';
|
||||
try {
|
||||
await work();
|
||||
lastMessage.value = success;
|
||||
ElMessage.success(success);
|
||||
} catch (error) {
|
||||
lastError.value = error instanceof ApiError
|
||||
? `${error.code}${error.traceId ? ` · ${error.traceId}` : ''}`
|
||||
: error instanceof Error ? error.message : '操作失败';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
loading.tasks = true;
|
||||
await runAction(async () => {
|
||||
const result = await listCleaningTasks(session.value, {
|
||||
page: tasks.page,
|
||||
pageSize: tasks.pageSize,
|
||||
status: taskStatus.value || undefined
|
||||
});
|
||||
Object.assign(tasks, result);
|
||||
}, '任务已刷新');
|
||||
loading.tasks = false;
|
||||
}
|
||||
|
||||
async function loadSettlements() {
|
||||
loading.settlements = true;
|
||||
await runAction(async () => {
|
||||
const result = await listCleaningSettlements(session.value, {
|
||||
page: settlements.page,
|
||||
pageSize: settlements.pageSize,
|
||||
status: settlementStatus.value || undefined
|
||||
});
|
||||
Object.assign(settlements, result);
|
||||
}, '结算已刷新');
|
||||
loading.settlements = false;
|
||||
}
|
||||
|
||||
function changeTaskStatus(status: TaskStatus | '') {
|
||||
taskStatus.value = status;
|
||||
tasks.page = 1;
|
||||
void loadTasks();
|
||||
}
|
||||
|
||||
function changeSettlementStatus(status: SettlementStatus | '') {
|
||||
settlementStatus.value = status;
|
||||
settlements.page = 1;
|
||||
void loadSettlements();
|
||||
}
|
||||
|
||||
function changeTaskPage(page: number) {
|
||||
tasks.page = page;
|
||||
void loadTasks();
|
||||
}
|
||||
|
||||
function changeSettlementPage(page: number) {
|
||||
settlements.page = page;
|
||||
void loadSettlements();
|
||||
}
|
||||
|
||||
async function handleAssign(input: { taskId: string; cleanerUserId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await assignCleaningTask(session.value, input.taskId, input);
|
||||
await loadTasks();
|
||||
}, '已指派');
|
||||
}
|
||||
|
||||
async function handleComplete(input: { taskId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await completeCleaningTask(session.value, input.taskId, input.note);
|
||||
await loadTasks();
|
||||
}, '已验收');
|
||||
}
|
||||
|
||||
async function handleReject(input: { taskId: string; reason: string }) {
|
||||
await runAction(async () => {
|
||||
await rejectCleaningTask(session.value, input.taskId, input.reason);
|
||||
await loadTasks();
|
||||
}, '已驳回');
|
||||
}
|
||||
|
||||
async function handleReclaim(input: { olderThanMinutes: number; limit: number }) {
|
||||
await runAction(async () => {
|
||||
await reclaimCleaningTimeouts(session.value, input);
|
||||
await loadTasks();
|
||||
}, '已回收超时任务');
|
||||
}
|
||||
|
||||
async function handleGenerateSettlement(
|
||||
input: { cleanerUserId: string; storeId?: string; note?: string }
|
||||
) {
|
||||
await runAction(async () => {
|
||||
await generateCleaningSettlement(session.value, input);
|
||||
await Promise.all([loadTasks(), loadSettlements()]);
|
||||
}, '已生成结算单');
|
||||
}
|
||||
|
||||
async function handleConfirmSettlement(input: { settlementId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await confirmCleaningSettlement(session.value, input.settlementId, input.note);
|
||||
await loadSettlements();
|
||||
}, '已确认结算单');
|
||||
}
|
||||
|
||||
async function handleTransfer(input: { settlementId: string; mode: TransferMode; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await executeWechatTransfer(session.value, input.settlementId, {
|
||||
mode: input.mode,
|
||||
note: input.note
|
||||
});
|
||||
await loadSettlements();
|
||||
}, '已提交微信转账');
|
||||
}
|
||||
|
||||
async function handleSyncTransfer(input: { settlementId: string; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await syncWechatTransfer(session.value, input.settlementId, input.note);
|
||||
await loadSettlements();
|
||||
}, '已同步微信状态');
|
||||
}
|
||||
|
||||
async function handleFailure(
|
||||
input: { settlementId: string; error: string; payoutReference?: string; note?: string }
|
||||
) {
|
||||
await runAction(async () => {
|
||||
await recordPayoutFailure(session.value, input.settlementId, {
|
||||
error: input.error,
|
||||
payoutChannel: 'WECHAT_TRANSFER',
|
||||
payoutReference: input.payoutReference,
|
||||
note: input.note
|
||||
});
|
||||
await loadSettlements();
|
||||
}, '已记录失败');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (session.value.token) {
|
||||
void Promise.all([loadTasks(), loadSettlements()]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user