feat(M08-B): 补任务结算筛选

This commit is contained in:
Codex
2026-06-30 17:30:33 +08:00
parent dac353d184
commit e46d8c0f0a
13 changed files with 212 additions and 35 deletions
+24 -2
View File
@@ -99,8 +99,10 @@
: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"
@@ -118,8 +120,10 @@
: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"
@@ -226,6 +230,8 @@ const settlements = reactive<PageResult<CleaningSettlement>>({
pageSize: 20
});
const cleaners = reactive<PageResult<ManagedUser>>({ items: [], total: 0, page: 1, pageSize: 20 });
const taskFilters = reactive({ storeId: '', cleanerUserId: '' });
const settlementFilters = reactive({ storeId: '', cleanerUserId: '' });
const statisticsFilters = reactive({ from: '', to: '', storeId: '' });
const statistics = reactive<CleaningStatistics>({
summary: {
@@ -271,7 +277,9 @@ async function loadTasks() {
const result = await listCleaningTasks(session.value, {
page: tasks.page,
pageSize: tasks.pageSize,
status: taskStatus.value || undefined
status: taskStatus.value || undefined,
storeId: taskFilters.storeId || undefined,
cleanerUserId: taskFilters.cleanerUserId || undefined
});
Object.assign(tasks, result);
}, '任务已刷新');
@@ -284,7 +292,9 @@ async function loadSettlements() {
const result = await listCleaningSettlements(session.value, {
page: settlements.page,
pageSize: settlements.pageSize,
status: settlementStatus.value || undefined
status: settlementStatus.value || undefined,
storeId: settlementFilters.storeId || undefined,
cleanerUserId: settlementFilters.cleanerUserId || undefined
});
Object.assign(settlements, result);
}, '结算已刷新');
@@ -335,12 +345,24 @@ function changeTaskStatus(status: TaskStatus | '') {
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 changeSettlementFilters(input: { storeId: string; cleanerUserId: string }) {
Object.assign(settlementFilters, input);
settlements.page = 1;
void loadSettlements();
}
function changeTaskPage(page: number) {
tasks.page = page;
void loadTasks();
+6 -2
View File
@@ -57,13 +57,15 @@ async function request<T>(
export function listCleaningTasks(
session: ApiSession,
input: { page: number; pageSize: number; status?: TaskStatus }
input: { page: number; pageSize: number; status?: TaskStatus; storeId?: string; cleanerUserId?: string }
) {
const params = new URLSearchParams({
page: String(input.page),
pageSize: String(input.pageSize)
});
if (input.status) params.set('status', input.status);
if (input.storeId) params.set('storeId', input.storeId);
if (input.cleanerUserId) params.set('cleanerUserId', input.cleanerUserId);
return request<PageResult<CleaningTask>>(session, `/cleaning/tasks?${params}`);
}
@@ -210,13 +212,15 @@ export function reclaimCleaningTimeouts(
export function listCleaningSettlements(
session: ApiSession,
input: { page: number; pageSize: number; status?: SettlementStatus }
input: { page: number; pageSize: number; status?: SettlementStatus; storeId?: string; cleanerUserId?: string }
) {
const params = new URLSearchParams({
page: String(input.page),
pageSize: String(input.pageSize)
});
if (input.status) params.set('status', input.status);
if (input.storeId) params.set('storeId', input.storeId);
if (input.cleanerUserId) params.set('cleanerUserId', input.cleanerUserId);
return request<PageResult<CleaningSettlement>>(session, `/cleaning/settlements?${params}`);
}
@@ -1,11 +1,38 @@
<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-segmented
:model-value="status"
:options="statusOptions"
@update:model-value="$emit('status-change', $event as SettlementStatus | '')"
/>
<el-input
v-model="filterDraft.storeId"
class="narrow-input"
placeholder="门店 ID"
clearable
@keyup.enter="submitFilters"
@clear="submitFilters"
/>
<el-select
v-model="filterDraft.cleanerUserId"
class="filter-select"
filterable
clearable
placeholder="保洁员"
@change="submitFilters"
@clear="submitFilters"
>
<el-option
v-for="cleaner in cleaners"
:key="cleaner.id"
:label="cleanerLabel(cleaner)"
:value="cleaner.id"
/>
</el-select>
<el-button :icon="Search" @click="submitFilters">筛选</el-button>
</div>
<div class="toolbar-actions">
<el-button :icon="FilePlus2" type="primary" @click="generateDialog.open = true">
生成
@@ -263,7 +290,7 @@
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import { reactive, watch } from 'vue';
import {
BadgeCheck,
CircleAlert,
@@ -271,6 +298,7 @@ import {
FilePlus2,
ListChecks,
RefreshCw,
Search,
Send
} from '@lucide/vue';
import { ElMessage } from 'element-plus';
@@ -295,11 +323,13 @@ const props = defineProps<{
page: number;
pageSize: number;
status: SettlementStatus | '';
filters: { storeId: string; cleanerUserId: string };
}>();
const emit = defineEmits<{
refresh: [];
'status-change': [SettlementStatus | ''];
filter: [{ storeId: string; cleanerUserId: string }];
'page-change': [number];
generate: [{ cleanerUserId: string; storeId?: string; note?: string }];
confirm: [{ settlementId: string; note?: string }];
@@ -315,6 +345,7 @@ const statusOptions = [
{ label: '已发', value: 'PAID' },
{ label: '取消', value: 'CANCELLED' }
];
const filterDraft = reactive({ storeId: props.filters.storeId, cleanerUserId: props.filters.cleanerUserId });
const generateDialog = reactive({ open: false, cleanerUserId: '', storeId: '', note: '' });
const transferDialog = reactive({
open: false,
@@ -322,6 +353,12 @@ const transferDialog = reactive({
mode: 'API' as TransferMode,
note: ''
});
watch(
() => props.filters,
(value) => Object.assign(filterDraft, value),
{ deep: true }
);
const preflightDialog = reactive<{
open: boolean;
result: WechatTransferPreflight | null;
@@ -366,6 +403,13 @@ function cleanerLabel(cleaner: ManagedUser) {
return `${name}${phone}${stores}`;
}
function submitFilters() {
emit('filter', {
storeId: filterDraft.storeId.trim(),
cleanerUserId: filterDraft.cleanerUserId
});
}
function submitGenerate() {
if (!generateDialog.cleanerUserId) {
ElMessage.warning('请选择保洁员');
+50 -6
View File
@@ -1,11 +1,38 @@
<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-segmented
:model-value="status"
:options="statusOptions"
@update:model-value="$emit('status-change', $event as TaskStatus | '')"
/>
<el-input
v-model="filterDraft.storeId"
class="narrow-input"
placeholder="门店 ID"
clearable
@keyup.enter="submitFilters"
@clear="submitFilters"
/>
<el-select
v-model="filterDraft.cleanerUserId"
class="filter-select"
filterable
clearable
placeholder="保洁员"
@change="submitFilters"
@clear="submitFilters"
>
<el-option
v-for="cleaner in cleaners"
:key="cleaner.id"
:label="cleanerLabel(cleaner)"
:value="cleaner.id"
/>
</el-select>
<el-button :icon="Search" @click="submitFilters">筛选</el-button>
</div>
<div class="toolbar-actions">
<el-popover trigger="click" width="280">
<template #reference>
@@ -226,13 +253,14 @@
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import { reactive, watch } from 'vue';
import {
BadgeCheck,
Check,
Image,
RefreshCw,
RotateCcw,
Search,
Undo2,
UserMinus,
UserPlus,
@@ -256,11 +284,13 @@ const props = defineProps<{
page: number;
pageSize: number;
status: TaskStatus | '';
filters: { storeId: string; cleanerUserId: string };
}>();
const emit = defineEmits<{
refresh: [];
'status-change': [TaskStatus | ''];
filter: [{ storeId: string; cleanerUserId: string }];
'page-change': [number];
assign: [{ taskId: string; cleanerUserId: string; note?: string }];
complete: [{ taskId: string; note?: string }];
@@ -277,6 +307,7 @@ const statusOptions = [
{ label: '完成', value: 'COMPLETED' },
{ label: '驳回', value: 'REJECTED' }
];
const filterDraft = reactive({ storeId: props.filters.storeId, cleanerUserId: props.filters.cleanerUserId });
const reclaimForm = reactive({ olderThanMinutes: 60, limit: 20 });
const assignDialog = reactive({ open: false, taskId: '', cleanerUserId: '', note: '' });
const rejectDialog = reactive({ open: false, taskId: '', reason: '' });
@@ -292,6 +323,12 @@ const memberDialog = reactive({
members: [] as CleaningTaskMember[]
});
watch(
() => props.filters,
(value) => Object.assign(filterDraft, value),
{ deep: true }
);
function taskTag(status: TaskStatus) {
if (status === 'SUBMITTED') return 'warning';
if (status === 'COMPLETED' || status === 'SETTLED') return 'success';
@@ -306,6 +343,13 @@ function cleanerLabel(cleaner: ManagedUser) {
return `${name}${phone}${stores}`;
}
function submitFilters() {
emit('filter', {
storeId: filterDraft.storeId.trim(),
cleanerUserId: filterDraft.cleanerUserId
});
}
function openAssign(row: CleaningTask) {
assignDialog.open = true;
assignDialog.taskId = row.id;
+8
View File
@@ -206,6 +206,14 @@ textarea {
width: min(240px, 100%);
}
.narrow-input {
width: 120px;
}
.filter-select {
width: 190px;
}
.profile-badges {
display: flex;
flex-wrap: wrap;