620 lines
20 KiB
Vue
620 lines
20 KiB
Vue
<template>
|
|
<section class="cleaning-workspace">
|
|
<section class="metric-grid" aria-label="保洁概览">
|
|
<div class="metric">
|
|
<span>待验收</span>
|
|
<strong>{{ statistics.summary.pendingReview }}</strong>
|
|
</div>
|
|
<div class="metric">
|
|
<span>已完成</span>
|
|
<strong>{{ statistics.summary.completed }}</strong>
|
|
</div>
|
|
<div class="metric">
|
|
<span>待发放</span>
|
|
<strong>{{ money(statistics.summary.confirmedSettlementCents) }}</strong>
|
|
</div>
|
|
<div class="metric">
|
|
<span>待结算</span>
|
|
<strong>{{ money(statistics.summary.pendingSettlementCents) }}</strong>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="action-board" aria-label="运营待处理">
|
|
<button class="action-tile" type="button" @click="jumpToTaskStatus('SUBMITTED')">
|
|
<ClipboardCheck :size="20" />
|
|
<span>待验收任务</span>
|
|
<strong>{{ statistics.summary.pendingReview }}</strong>
|
|
</button>
|
|
<button class="action-tile" type="button" @click="jumpToTaskStatus('REJECTED')">
|
|
<RotateCcw :size="20" />
|
|
<span>驳回补做</span>
|
|
<strong>{{ statistics.summary.rejected }}</strong>
|
|
</button>
|
|
<button class="action-tile" type="button" @click="jumpToSettlementStatus('DRAFT')">
|
|
<ListTodo :size="20" />
|
|
<span>待确认结算</span>
|
|
<strong>{{ draftSettlementTotal }}</strong>
|
|
</button>
|
|
<button class="action-tile" type="button" @click="jumpToSettlementStatus('CONFIRMED')">
|
|
<Send :size="20" />
|
|
<span>待发放金额</span>
|
|
<strong>{{ money(statistics.summary.confirmedSettlementCents) }}</strong>
|
|
</button>
|
|
<button class="action-tile" type="button" @click="exportOperationalQueue">
|
|
<Download :size="20" />
|
|
<span>导出待处理</span>
|
|
<strong>{{ operationalQueueTotal }}</strong>
|
|
</button>
|
|
</section>
|
|
|
|
<el-alert
|
|
v-if="lastError"
|
|
class="alert-line"
|
|
:title="lastError"
|
|
type="error"
|
|
show-icon
|
|
closable
|
|
@close="lastError = ''"
|
|
/>
|
|
<el-alert
|
|
v-if="lastMessage"
|
|
class="alert-line"
|
|
:title="lastMessage"
|
|
type="success"
|
|
show-icon
|
|
closable
|
|
@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"
|
|
:cleaners="cleaners.items"
|
|
:total="tasks.total"
|
|
:page="tasks.page"
|
|
:page-size="tasks.pageSize"
|
|
:status="taskStatus"
|
|
:filters="taskFilters"
|
|
@refresh="loadTasks"
|
|
@status-change="changeTaskStatus"
|
|
@filter="changeTaskFilters"
|
|
@page-change="changeTaskPage"
|
|
@assign="handleAssign"
|
|
@complete="handleComplete"
|
|
@complete-many="handleCompleteMany"
|
|
@reject="handleReject"
|
|
@reject-many="handleRejectMany"
|
|
@exempt="handleExempt"
|
|
@exempt-many="handleExemptMany"
|
|
@reclaim="handleReclaim"
|
|
/>
|
|
</el-tab-pane>
|
|
<el-tab-pane label="结算" name="settlements">
|
|
<CleaningSettlementsPanel
|
|
:session="session"
|
|
:loading="loading.settlements"
|
|
:items="settlements.items"
|
|
:cleaners="cleaners.items"
|
|
:total="settlements.total"
|
|
:page="settlements.page"
|
|
:page-size="settlements.pageSize"
|
|
:status="settlementStatus"
|
|
:filters="settlementFilters"
|
|
@refresh="loadSettlements"
|
|
@status-change="changeSettlementStatus"
|
|
@filter="changeSettlementFilters"
|
|
@page-change="changeSettlementPage"
|
|
@generate="handleGenerateSettlement"
|
|
@confirm="handleConfirmSettlement"
|
|
@transfer="handleTransfer"
|
|
@sync-transfer="handleSyncTransfer"
|
|
@sync-transfer-many="handleSyncTransferMany"
|
|
@failure="handleFailure"
|
|
/>
|
|
</el-tab-pane>
|
|
<el-tab-pane label="统计" name="statistics">
|
|
<CleaningStatisticsPanel
|
|
:loading="loading.statistics"
|
|
:statistics="statistics"
|
|
:filters="statisticsFilters"
|
|
:cleaners="cleaners.items"
|
|
@refresh="loadStatistics"
|
|
@filter="changeStatisticsFilters"
|
|
/>
|
|
</el-tab-pane>
|
|
<el-tab-pane label="保洁员" name="cleaners">
|
|
<CleanersPanel
|
|
:loading="loading.cleaners"
|
|
:items="cleaners.items"
|
|
:total="cleaners.total"
|
|
:page="cleaners.page"
|
|
:page-size="cleaners.pageSize"
|
|
:status="cleanerStatus"
|
|
:search="cleanerSearch"
|
|
@refresh="loadCleaners"
|
|
@status-change="changeCleanerStatus"
|
|
@search-change="changeCleanerSearch"
|
|
@page-change="changeCleanerPage"
|
|
@create="handleCreateCleaner"
|
|
@update="handleUpdateCleaner"
|
|
@status-toggle="handleToggleCleanerStatus"
|
|
@reset-sessions="handleResetCleanerSessions"
|
|
/>
|
|
</el-tab-pane>
|
|
<el-tab-pane label="清洁规则" name="rules">
|
|
<CleaningRulesPanel :session="session" />
|
|
</el-tab-pane>
|
|
<el-tab-pane label="联调" name="handoff">
|
|
<CleaningFieldHandoffPanel />
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, ref, toRef } from 'vue';
|
|
import { ElMessage } from 'element-plus';
|
|
import { ClipboardCheck, Download, ListTodo, RotateCcw, Send } from '@lucide/vue';
|
|
import CleaningTasksPanel from './CleaningTasksPanel.vue';
|
|
import CleaningSettlementsPanel from './CleaningSettlementsPanel.vue';
|
|
import CleaningStatisticsPanel from './CleaningStatisticsPanel.vue';
|
|
import CleanersPanel from './CleanersPanel.vue';
|
|
import CleaningRulesPanel from './CleaningRulesPanel.vue';
|
|
import CleaningFieldHandoffPanel from './CleaningFieldHandoffPanel.vue';
|
|
import {
|
|
ApiError,
|
|
assignCleaningTask,
|
|
completeCleaningTask,
|
|
confirmCleaningSettlement,
|
|
createStaffUser,
|
|
exemptCleaningTask,
|
|
executeWechatTransfer,
|
|
generateCleaningSettlement,
|
|
getCleaningStatistics,
|
|
listCleaningSettlements,
|
|
listCleaningTasks,
|
|
listStaffUsers,
|
|
reclaimCleaningTimeouts,
|
|
recordPayoutFailure,
|
|
rejectCleaningTask,
|
|
resetStaffSessions,
|
|
syncWechatTransfer,
|
|
updateStaffUser,
|
|
type ApiSession
|
|
} from '../api';
|
|
import type {
|
|
CleaningSettlement,
|
|
CleaningStatistics,
|
|
CleaningTask,
|
|
ManagedUser,
|
|
PageResult,
|
|
PayoutStateFilter,
|
|
SettlementStatus,
|
|
TaskStatus,
|
|
TransferMode,
|
|
UserStatus
|
|
} from '../types';
|
|
import { money } from '../format';
|
|
|
|
const props = defineProps<{ session: ApiSession }>();
|
|
const session = toRef(props, 'session');
|
|
const activeTab = ref('tasks');
|
|
const lastError = ref('');
|
|
const lastMessage = ref('');
|
|
const taskStatus = ref<TaskStatus | ''>('SUBMITTED');
|
|
const settlementStatus = ref<SettlementStatus | ''>('CONFIRMED');
|
|
const cleanerStatus = ref<UserStatus | ''>('ACTIVE');
|
|
const cleanerSearch = ref('');
|
|
const loading = reactive({ tasks: false, settlements: false, statistics: false, cleaners: 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 cleaners = reactive<PageResult<ManagedUser>>({ items: [], total: 0, page: 1, pageSize: 20 });
|
|
const taskFilters = reactive({ storeId: '', cleanerUserId: '' });
|
|
const settlementFilters = reactive<{
|
|
storeId: string;
|
|
cleanerUserId: string;
|
|
payoutState: PayoutStateFilter | '';
|
|
}>({ storeId: '', cleanerUserId: '', payoutState: '' });
|
|
const statisticsFilters = reactive({ from: '', to: '', storeId: '', cleanerUserId: '' });
|
|
const statistics = reactive<CleaningStatistics>({
|
|
summary: {
|
|
taskTotal: 0,
|
|
pendingReview: 0,
|
|
active: 0,
|
|
rejected: 0,
|
|
exempted: 0,
|
|
completed: 0,
|
|
rewardCents: 0,
|
|
pendingSettlementCents: 0,
|
|
confirmedSettlementCents: 0,
|
|
paidSettlementCents: 0
|
|
},
|
|
byStatus: [],
|
|
byStore: [],
|
|
settlements: [],
|
|
members: [],
|
|
trend: []
|
|
});
|
|
const draftSettlementTotal = computed(() => statistics.settlements
|
|
.find((item) => item.status === 'DRAFT')?.total || 0);
|
|
const operationalQueueTotal = computed(() => (
|
|
statistics.summary.pendingReview
|
|
+ statistics.summary.rejected
|
|
+ draftSettlementTotal.value
|
|
));
|
|
|
|
function loadCleaningWorkspace() {
|
|
return Promise.all([loadTasks(), loadSettlements(), loadStatistics(), loadCleaners()]);
|
|
}
|
|
|
|
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 () => {
|
|
Object.assign(tasks, await listCleaningTasks(session.value, {
|
|
page: tasks.page,
|
|
pageSize: tasks.pageSize,
|
|
status: taskStatus.value || undefined,
|
|
storeId: taskFilters.storeId || undefined,
|
|
cleanerUserId: taskFilters.cleanerUserId || undefined
|
|
}));
|
|
}, '任务已刷新');
|
|
loading.tasks = false;
|
|
}
|
|
|
|
async function loadSettlements() {
|
|
loading.settlements = true;
|
|
await runAction(async () => {
|
|
Object.assign(settlements, await listCleaningSettlements(session.value, {
|
|
page: settlements.page,
|
|
pageSize: settlements.pageSize,
|
|
status: settlementStatus.value || undefined,
|
|
payoutState: settlementFilters.payoutState || undefined,
|
|
storeId: settlementFilters.storeId || undefined,
|
|
cleanerUserId: settlementFilters.cleanerUserId || undefined
|
|
}));
|
|
}, '结算已刷新');
|
|
loading.settlements = false;
|
|
}
|
|
|
|
async function loadStatistics() {
|
|
loading.statistics = true;
|
|
await runAction(async () => {
|
|
const result = await getCleaningStatistics(session.value, {
|
|
from: statisticsFilters.from || undefined,
|
|
to: statisticsFilters.to || undefined,
|
|
storeId: statisticsFilters.storeId || undefined,
|
|
cleanerUserId: statisticsFilters.cleanerUserId || undefined
|
|
});
|
|
Object.assign(statistics.summary, result.summary);
|
|
statistics.byStatus = result.byStatus;
|
|
statistics.byStore = result.byStore;
|
|
statistics.settlements = result.settlements;
|
|
statistics.members = result.members;
|
|
statistics.trend = result.trend;
|
|
}, '统计已刷新');
|
|
loading.statistics = false;
|
|
}
|
|
|
|
async function loadCleaners() {
|
|
loading.cleaners = true;
|
|
await runAction(async () => {
|
|
const result = await listStaffUsers(session.value, {
|
|
page: cleaners.page,
|
|
pageSize: cleaners.pageSize,
|
|
role: 'CLEANER',
|
|
status: cleanerStatus.value || undefined,
|
|
search: cleanerSearch.value || undefined
|
|
});
|
|
Object.assign(cleaners, {
|
|
items: result.items,
|
|
total: result.total,
|
|
page: cleaners.page,
|
|
pageSize: cleaners.pageSize
|
|
});
|
|
}, '保洁员已刷新');
|
|
loading.cleaners = false;
|
|
}
|
|
|
|
function changeTaskStatus(status: TaskStatus | '') {
|
|
taskStatus.value = status;
|
|
tasks.page = 1;
|
|
void loadTasks();
|
|
}
|
|
|
|
function jumpToTaskStatus(status: TaskStatus) {
|
|
activeTab.value = 'tasks';
|
|
taskStatus.value = status;
|
|
tasks.page = 1;
|
|
void loadTasks();
|
|
}
|
|
|
|
function changeTaskFilters(input: { storeId: string; cleanerUserId: string }) {
|
|
Object.assign(taskFilters, input);
|
|
tasks.page = 1;
|
|
void loadTasks();
|
|
}
|
|
|
|
function changeSettlementStatus(status: SettlementStatus | '') {
|
|
settlementStatus.value = status;
|
|
settlements.page = 1;
|
|
void loadSettlements();
|
|
}
|
|
|
|
function jumpToSettlementStatus(status: SettlementStatus) {
|
|
activeTab.value = 'settlements';
|
|
settlementStatus.value = status;
|
|
settlementFilters.payoutState = '';
|
|
settlements.page = 1;
|
|
void loadSettlements();
|
|
}
|
|
|
|
function exportOperationalQueue() {
|
|
downloadCsv(`cleaning-operational-queue-${Date.now()}.csv`, [
|
|
['队列', '数量', '金额', '入口'],
|
|
['待验收任务', String(statistics.summary.pendingReview), '', '任务/SUBMITTED'],
|
|
['驳回补做', String(statistics.summary.rejected), '', '任务/REJECTED'],
|
|
['待确认结算', String(draftSettlementTotal.value), '', '结算/DRAFT'],
|
|
['待发放金额', '', money(statistics.summary.confirmedSettlementCents), '结算/CONFIRMED'],
|
|
['待结算金额', '', money(statistics.summary.pendingSettlementCents), '统计/待结算'],
|
|
['已完成任务', String(statistics.summary.completed), '', '统计/已完成']
|
|
]);
|
|
}
|
|
|
|
function downloadCsv(filename: string, rows: string[][]) {
|
|
const content = rows.map((row) => row.map(csvCell).join(',')).join('\r\n');
|
|
const blob = new Blob([`\uFEFF${content}`], { type: 'text/csv;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function csvCell(value: string) {
|
|
const normalized = value.replace(/\r?\n/g, ' ');
|
|
return /[",\r\n]/.test(normalized) ? `"${normalized.replace(/"/g, '""')}"` : normalized;
|
|
}
|
|
|
|
function changeSettlementFilters(input: {
|
|
storeId: string;
|
|
cleanerUserId: string;
|
|
payoutState: PayoutStateFilter | '';
|
|
}) {
|
|
Object.assign(settlementFilters, input);
|
|
settlements.page = 1;
|
|
void loadSettlements();
|
|
}
|
|
|
|
function changeTaskPage(page: number) {
|
|
tasks.page = page;
|
|
void loadTasks();
|
|
}
|
|
|
|
function changeSettlementPage(page: number) {
|
|
settlements.page = page;
|
|
void loadSettlements();
|
|
}
|
|
|
|
function changeStatisticsFilters(input: {
|
|
from: string;
|
|
to: string;
|
|
storeId: string;
|
|
cleanerUserId: string;
|
|
}) {
|
|
Object.assign(statisticsFilters, input);
|
|
void loadStatistics();
|
|
}
|
|
|
|
function changeCleanerStatus(status: UserStatus | '') {
|
|
cleanerStatus.value = status;
|
|
cleaners.page = 1;
|
|
void loadCleaners();
|
|
}
|
|
|
|
function changeCleanerSearch(search: string) {
|
|
cleanerSearch.value = search;
|
|
cleaners.page = 1;
|
|
void loadCleaners();
|
|
}
|
|
|
|
function changeCleanerPage(page: number) {
|
|
cleaners.page = page;
|
|
void loadCleaners();
|
|
}
|
|
|
|
async function handleAssign(input: { taskId: string; cleanerUserId: string; note?: string }) {
|
|
await runAction(async () => {
|
|
await assignCleaningTask(session.value, input.taskId, input);
|
|
await Promise.all([loadTasks(), loadStatistics()]);
|
|
}, '已指派');
|
|
}
|
|
|
|
async function handleComplete(input: { taskId: string; note?: string }) {
|
|
await runAction(async () => {
|
|
await completeCleaningTask(session.value, input.taskId, input.note);
|
|
await Promise.all([loadTasks(), loadStatistics()]);
|
|
}, '已验收');
|
|
}
|
|
|
|
async function handleCompleteMany(input: { taskIds: string[]; note?: string }) {
|
|
await runAction(async () => {
|
|
for (const taskId of input.taskIds) await completeCleaningTask(session.value, taskId, input.note);
|
|
await Promise.all([loadTasks(), loadStatistics()]);
|
|
}, `已批量验收 ${input.taskIds.length} 个任务`);
|
|
}
|
|
|
|
async function handleReject(input: { taskId: string; reason: string }) {
|
|
await runAction(async () => {
|
|
await rejectCleaningTask(session.value, input.taskId, input.reason);
|
|
await Promise.all([loadTasks(), loadStatistics()]);
|
|
}, '已驳回');
|
|
}
|
|
|
|
async function handleRejectMany(input: { taskIds: string[]; reason: string }) {
|
|
await runAction(async () => {
|
|
for (const taskId of input.taskIds) await rejectCleaningTask(session.value, taskId, input.reason);
|
|
await Promise.all([loadTasks(), loadStatistics()]);
|
|
}, `已批量驳回 ${input.taskIds.length} 个任务`);
|
|
}
|
|
|
|
async function handleExempt(input: { taskId: string; note?: string }) {
|
|
await runAction(async () => {
|
|
await exemptCleaningTask(session.value, input.taskId, input.note);
|
|
await Promise.all([loadTasks(), loadStatistics()]);
|
|
}, '已标记免清洁');
|
|
}
|
|
|
|
async function handleExemptMany(input: { taskIds: string[]; note?: string }) {
|
|
await runAction(async () => {
|
|
for (const taskId of input.taskIds) await exemptCleaningTask(session.value, taskId, input.note);
|
|
await Promise.all([loadTasks(), loadStatistics()]);
|
|
}, `已批量标记免清洁 ${input.taskIds.length} 个任务`);
|
|
}
|
|
|
|
async function handleReclaim(input: { olderThanMinutes: number; limit: number }) {
|
|
await runAction(async () => {
|
|
await reclaimCleaningTimeouts(session.value, input);
|
|
await Promise.all([loadTasks(), loadStatistics()]);
|
|
}, '已回收超时任务');
|
|
}
|
|
|
|
async function handleGenerateSettlement(input: {
|
|
cleanerUserId: string;
|
|
storeId?: string;
|
|
note?: string;
|
|
}) {
|
|
await runAction(async () => {
|
|
await generateCleaningSettlement(session.value, input);
|
|
await Promise.all([loadTasks(), loadSettlements(), loadStatistics()]);
|
|
}, '已生成结算单');
|
|
}
|
|
|
|
async function handleConfirmSettlement(input: { settlementId: string; note?: string }) {
|
|
await runAction(async () => {
|
|
await confirmCleaningSettlement(session.value, input.settlementId, input.note);
|
|
await Promise.all([loadSettlements(), loadStatistics()]);
|
|
}, '已确认结算单');
|
|
}
|
|
|
|
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 Promise.all([loadSettlements(), loadStatistics()]);
|
|
}, '已提交微信转账');
|
|
}
|
|
|
|
async function handleSyncTransfer(input: { settlementId: string; note?: string }) {
|
|
await runAction(async () => {
|
|
await syncWechatTransfer(session.value, input.settlementId, input.note);
|
|
await Promise.all([loadSettlements(), loadStatistics()]);
|
|
}, '已同步微信状态');
|
|
}
|
|
|
|
async function handleSyncTransferMany(input: { settlementIds: string[]; note?: string }) {
|
|
await runAction(async () => {
|
|
for (const settlementId of input.settlementIds) {
|
|
await syncWechatTransfer(session.value, settlementId, input.note);
|
|
}
|
|
await Promise.all([loadSettlements(), loadStatistics()]);
|
|
}, `已批量同步 ${input.settlementIds.length} 个结算单`);
|
|
}
|
|
|
|
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 Promise.all([loadSettlements(), loadStatistics()]);
|
|
}, '已记录失败');
|
|
}
|
|
|
|
async function handleCreateCleaner(input: {
|
|
nickname: string;
|
|
phone: string;
|
|
storeIds: string[];
|
|
note?: string;
|
|
}) {
|
|
await runAction(async () => {
|
|
await createStaffUser(session.value, {
|
|
nickname: input.nickname,
|
|
phone: input.phone,
|
|
roles: ['CLEANER'],
|
|
storeIds: input.storeIds,
|
|
note: input.note
|
|
});
|
|
await loadCleaners();
|
|
}, '已新增保洁员');
|
|
}
|
|
|
|
async function handleUpdateCleaner(input: {
|
|
userId: string;
|
|
nickname: string;
|
|
phone?: string;
|
|
storeIds: string[];
|
|
note?: string;
|
|
}) {
|
|
await runAction(async () => {
|
|
await updateStaffUser(session.value, input.userId, {
|
|
nickname: input.nickname,
|
|
phone: input.phone,
|
|
roles: ['CLEANER'],
|
|
storeIds: input.storeIds,
|
|
note: input.note
|
|
});
|
|
await loadCleaners();
|
|
}, '已更新保洁员');
|
|
}
|
|
|
|
async function handleToggleCleanerStatus(input: { userId: string; status: UserStatus }) {
|
|
await runAction(async () => {
|
|
await updateStaffUser(session.value, input.userId, { status: input.status });
|
|
await loadCleaners();
|
|
}, input.status === 'ACTIVE' ? '已启用保洁员' : '已停用保洁员');
|
|
}
|
|
|
|
async function handleResetCleanerSessions(userId: string) {
|
|
await runAction(async () => {
|
|
await resetStaffSessions(session.value, userId);
|
|
await loadCleaners();
|
|
}, '已重置保洁员会话');
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (session.value.token) void loadCleaningWorkspace();
|
|
});
|
|
</script>
|