feat(M08-B): 建立保洁管理后台
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<section class="panel">
|
||||
<div class="panel-toolbar">
|
||||
<el-segmented
|
||||
:model-value="status"
|
||||
:options="statusOptions"
|
||||
@update:model-value="$emit('status-change', $event as TaskStatus | '')"
|
||||
/>
|
||||
<div class="toolbar-actions">
|
||||
<el-popover trigger="click" width="280">
|
||||
<template #reference>
|
||||
<el-button :icon="RotateCcw">回收</el-button>
|
||||
</template>
|
||||
<el-form label-position="top" class="compact-form">
|
||||
<el-form-item label="分钟">
|
||||
<el-input-number v-model="reclaimForm.olderThanMinutes" :min="5" :max="1440" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数量">
|
||||
<el-input-number v-model="reclaimForm.limit" :min="1" :max="100" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :icon="Check" @click="$emit('reclaim', reclaimForm)">
|
||||
执行
|
||||
</el-button>
|
||||
</el-form>
|
||||
</el-popover>
|
||||
<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="taskNo" label="任务" min-width="180" fixed>
|
||||
<template #default="{ row }">
|
||||
<div class="stack">
|
||||
<strong>{{ row.taskNo }}</strong>
|
||||
<span>{{ row.storeName }} · {{ row.roomName || row.roomNo }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="taskTag(row.status)" effect="light">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cleanerUserId" label="保洁员" width="110">
|
||||
<template #default="{ row }">{{ compactText(row.cleanerUserId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="memberCount" label="成员" width="90" />
|
||||
<el-table-column prop="rewardCents" label="奖励" width="110">
|
||||
<template #default="{ row }">{{ money(row.rewardCents) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submittedAt" label="提交" width="130">
|
||||
<template #default="{ row }">{{ shortDate(row.submittedAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="照片" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-badge :value="row.photoUrls?.length || 0" :hidden="!row.photoUrls?.length">
|
||||
<Image :size="18" />
|
||||
</el-badge>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="270" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button size="small" :icon="UserPlus" @click="openAssign(row)">指派</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
:icon="BadgeCheck"
|
||||
:disabled="row.status !== 'SUBMITTED'"
|
||||
@click="$emit('complete', { taskId: row.id, note: '后台验收通过' })"
|
||||
>
|
||||
验收
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
:icon="Undo2"
|
||||
:disabled="row.status !== 'SUBMITTED'"
|
||||
@click="openReject(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="assignDialog.open" title="指派保洁员" width="420px">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="保洁员用户 ID">
|
||||
<el-input v-model="assignDialog.cleanerUserId" inputmode="numeric" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="assignDialog.note" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="assignDialog.open = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitAssign">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="rejectDialog.open" title="驳回任务" width="420px">
|
||||
<el-input v-model="rejectDialog.reason" type="textarea" :rows="4" maxlength="512" show-word-limit />
|
||||
<template #footer>
|
||||
<el-button @click="rejectDialog.open = false">取消</el-button>
|
||||
<el-button type="danger" @click="submitReject">驳回</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue';
|
||||
import {
|
||||
BadgeCheck,
|
||||
Check,
|
||||
Image,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Undo2,
|
||||
UserPlus
|
||||
} from '@lucide/vue';
|
||||
import { compactText, money, shortDate } from '../format';
|
||||
import type { CleaningTask, PageResult, TaskStatus } from '../types';
|
||||
|
||||
defineProps<{
|
||||
session: { token: string };
|
||||
loading: boolean;
|
||||
items: PageResult<CleaningTask>['items'];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
status: TaskStatus | '';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
'status-change': [TaskStatus | ''];
|
||||
'page-change': [number];
|
||||
assign: [{ taskId: string; cleanerUserId: string; note?: string }];
|
||||
complete: [{ taskId: string; note?: string }];
|
||||
reject: [{ taskId: string; reason: string }];
|
||||
reclaim: [{ olderThanMinutes: number; limit: number }];
|
||||
}>();
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '待抢', value: 'WAITING' },
|
||||
{ label: '已抢', value: 'CLAIMED' },
|
||||
{ label: '进行', value: 'STARTED' },
|
||||
{ label: '待验', value: 'SUBMITTED' },
|
||||
{ label: '完成', value: 'COMPLETED' },
|
||||
{ label: '驳回', value: 'REJECTED' }
|
||||
];
|
||||
const reclaimForm = reactive({ olderThanMinutes: 60, limit: 20 });
|
||||
const assignDialog = reactive({ open: false, taskId: '', cleanerUserId: '', note: '' });
|
||||
const rejectDialog = reactive({ open: false, taskId: '', reason: '' });
|
||||
|
||||
function taskTag(status: TaskStatus) {
|
||||
if (status === 'SUBMITTED') return 'warning';
|
||||
if (status === 'COMPLETED' || status === 'SETTLED') return 'success';
|
||||
if (status === 'REJECTED') return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function openAssign(row: CleaningTask) {
|
||||
assignDialog.open = true;
|
||||
assignDialog.taskId = row.id;
|
||||
assignDialog.cleanerUserId = row.cleanerUserId || '';
|
||||
assignDialog.note = '';
|
||||
}
|
||||
|
||||
function submitAssign() {
|
||||
emit('assign', {
|
||||
taskId: assignDialog.taskId,
|
||||
cleanerUserId: assignDialog.cleanerUserId,
|
||||
note: assignDialog.note
|
||||
});
|
||||
assignDialog.open = false;
|
||||
}
|
||||
|
||||
function openReject(row: CleaningTask) {
|
||||
rejectDialog.open = true;
|
||||
rejectDialog.taskId = row.id;
|
||||
rejectDialog.reason = row.rejectReason || '';
|
||||
}
|
||||
|
||||
function submitReject() {
|
||||
emit('reject', { taskId: rejectDialog.taskId, reason: rejectDialog.reason });
|
||||
rejectDialog.open = false;
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user