feat(M09-B): 完成清洁规则与照片验收闭环
This commit is contained in:
@@ -9,8 +9,12 @@ import type {
|
||||
CleaningSettlementDetail,
|
||||
CleaningStatistics,
|
||||
CleaningTask,
|
||||
CleaningTemplate,
|
||||
CleaningTemplateScope,
|
||||
CleaningExemptPolicy,
|
||||
CleaningTaskEvent,
|
||||
CleaningTaskMember,
|
||||
CleaningTaskSubmission,
|
||||
DecorationComponent,
|
||||
DecorationVersion,
|
||||
FranchiseApplication,
|
||||
@@ -449,6 +453,29 @@ export function getCleaningStatistics(
|
||||
return request<CleaningStatistics>(session, `/cleaning/statistics${query ? `?${query}` : ''}`);
|
||||
}
|
||||
|
||||
export function listCleaningTemplates(session: ApiSession) {
|
||||
return request<CleaningTemplate[]>(session, '/cleaning/templates');
|
||||
}
|
||||
|
||||
export function upsertCleaningTemplate(
|
||||
session: ApiSession,
|
||||
input: {
|
||||
scopeType: CleaningTemplateScope;
|
||||
scopeId?: string;
|
||||
name: string;
|
||||
requirement: string;
|
||||
photoRequired: boolean;
|
||||
minPhotoCount: number;
|
||||
maxPhotoCount: number;
|
||||
exemptPolicy: CleaningExemptPolicy;
|
||||
status: 'ACTIVE' | 'DISABLED';
|
||||
}
|
||||
) {
|
||||
return request<CleaningTemplate>(session, '/cleaning/templates', {
|
||||
method: 'PUT', body: JSON.stringify(input)
|
||||
});
|
||||
}
|
||||
|
||||
export function listStaffUsers(
|
||||
session: ApiSession,
|
||||
input: {
|
||||
@@ -729,6 +756,13 @@ export function listCleaningTaskEvents(session: ApiSession, taskId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function listCleaningTaskSubmissions(session: ApiSession, taskId: string) {
|
||||
return request<CleaningTaskSubmission[]>(
|
||||
session,
|
||||
`/cleaning/tasks/${encodeURIComponent(taskId)}/submissions`
|
||||
);
|
||||
}
|
||||
|
||||
export function addCleaningTaskMember(
|
||||
session: ApiSession,
|
||||
taskId: string,
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<section class="rules-panel">
|
||||
<header class="panel-heading">
|
||||
<div>
|
||||
<p class="eyebrow">M09-B · 清洁规则</p>
|
||||
<h3>门店与房间清洁模板</h3>
|
||||
<p>模板按房间、门店、租户依次匹配;保存后只影响新任务,既有任务继续使用创建时快照。</p>
|
||||
</div>
|
||||
<el-button :loading="loading" @click="loadTemplates">刷新</el-button>
|
||||
</header>
|
||||
|
||||
<el-alert
|
||||
title="配置变更不会静默改写在途任务;每次保存都会记录审计。"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<el-form class="rule-form" label-position="top" @submit.prevent>
|
||||
<el-form-item label="作用范围">
|
||||
<el-select v-model="draft.scopeType" @change="handleScopeChange">
|
||||
<el-option label="租户默认" value="TENANT" />
|
||||
<el-option label="指定门店" value="STORE" />
|
||||
<el-option label="指定房间" value="ROOM" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="draft.scopeType === 'STORE'" label="门店">
|
||||
<el-select v-model="draft.scopeId" filterable placeholder="请选择门店">
|
||||
<el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="draft.scopeType === 'ROOM'" label="所属门店">
|
||||
<el-select v-model="draft.roomStoreId" filterable placeholder="先选择门店" @change="handleRoomStoreChange">
|
||||
<el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="draft.scopeType === 'ROOM'" label="房间">
|
||||
<el-select v-model="draft.scopeId" filterable placeholder="请选择房间" :disabled="!draft.roomStoreId">
|
||||
<el-option
|
||||
v-for="room in rooms"
|
||||
:key="room.id"
|
||||
:label="`${room.roomNo} · ${room.name}`"
|
||||
:value="room.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板名称">
|
||||
<el-input v-model.trim="draft.name" maxlength="128" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="draft.status">
|
||||
<el-option label="启用" value="ACTIVE" />
|
||||
<el-option label="停用" value="DISABLED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="wide-field" label="清洁要求">
|
||||
<el-input v-model.trim="draft.requirement" type="textarea" :rows="3" maxlength="512" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="必须上传照片">
|
||||
<el-switch v-model="draft.photoRequired" @change="normalizePhotoRule" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最少照片">
|
||||
<el-input-number v-model="draft.minPhotoCount" :min="draft.photoRequired ? 1 : 0" :max="draft.maxPhotoCount" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最多照片">
|
||||
<el-input-number v-model="draft.maxPhotoCount" :min="Math.max(1, draft.minPhotoCount)" :max="9" />
|
||||
</el-form-item>
|
||||
<el-form-item label="免清洁策略">
|
||||
<el-select v-model="draft.exemptPolicy">
|
||||
<el-option label="不允许免清洁" value="DISABLED" />
|
||||
<el-option label="开始前允许" value="BEFORE_START" />
|
||||
<el-option label="活动阶段均允许" value="ANY_ACTIVE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="wide-field form-actions">
|
||||
<el-button type="primary" :loading="saving" @click="saveTemplate">保存模板</el-button>
|
||||
<el-button @click="resetDraft">新建配置</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="templates" class="data-table" empty-text="暂无清洁模板">
|
||||
<el-table-column prop="scopeKey" label="范围" min-width="130" />
|
||||
<el-table-column prop="name" label="模板" min-width="150" />
|
||||
<el-table-column label="照片" min-width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.photoRequired ? `${row.minPhotoCount}–${row.maxPhotoCount} 张` : `可选,最多 ${row.maxPhotoCount} 张` }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="免清洁" min-width="130">
|
||||
<template #default="{ row }">{{ exemptPolicyLabel(row.exemptPolicy) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="版本" width="88">
|
||||
<template #default="{ row }">v{{ row.version }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="88">
|
||||
<template #default="{ row }">{{ row.status === 'ACTIVE' ? '启用' : '停用' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="88" fixed="right">
|
||||
<template #default="{ row }"><el-button link type="primary" @click="editTemplate(row)">编辑</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {
|
||||
listCleaningTemplates,
|
||||
listManagedRooms,
|
||||
listManagedStores,
|
||||
upsertCleaningTemplate,
|
||||
type ApiSession
|
||||
} from '../api';
|
||||
import type {
|
||||
CleaningExemptPolicy,
|
||||
CleaningTemplate,
|
||||
CleaningTemplateScope,
|
||||
ManagedRoom,
|
||||
ManagedStore
|
||||
} from '../types';
|
||||
|
||||
const props = defineProps<{ session: ApiSession }>();
|
||||
const templates = ref<CleaningTemplate[]>([]);
|
||||
const stores = ref<ManagedStore[]>([]);
|
||||
const rooms = ref<ManagedRoom[]>([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
const draft = reactive({
|
||||
scopeType: 'TENANT' as CleaningTemplateScope,
|
||||
scopeId: '',
|
||||
roomStoreId: '',
|
||||
name: '默认清洁模板',
|
||||
requirement: '完成桌面、地面与设备周边清洁',
|
||||
photoRequired: true,
|
||||
minPhotoCount: 1,
|
||||
maxPhotoCount: 9,
|
||||
exemptPolicy: 'ANY_ACTIVE' as CleaningExemptPolicy,
|
||||
status: 'ACTIVE' as 'ACTIVE' | 'DISABLED'
|
||||
});
|
||||
|
||||
async function loadTemplates() {
|
||||
if (!props.session.token || loading.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
templates.value = await listCleaningTemplates(props.session);
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '清洁模板加载失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStores() {
|
||||
try {
|
||||
stores.value = await listManagedStores(props.session);
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '门店列表加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRooms(storeId = draft.roomStoreId) {
|
||||
if (!storeId) {
|
||||
rooms.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
rooms.value = await listManagedRooms(props.session, storeId);
|
||||
} catch (error) {
|
||||
rooms.value = [];
|
||||
ElMessage.error(error instanceof Error ? error.message : '房间列表加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (!draft.name || (draft.scopeType !== 'TENANT' && !/^[1-9]\d{0,19}$/.test(draft.scopeId))) {
|
||||
return ElMessage.warning('请填写模板名称和有效作用范围');
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await upsertCleaningTemplate(props.session, {
|
||||
scopeType: draft.scopeType,
|
||||
scopeId: draft.scopeType === 'TENANT' ? undefined : draft.scopeId,
|
||||
name: draft.name,
|
||||
requirement: draft.requirement,
|
||||
photoRequired: draft.photoRequired,
|
||||
minPhotoCount: draft.minPhotoCount,
|
||||
maxPhotoCount: draft.maxPhotoCount,
|
||||
exemptPolicy: draft.exemptPolicy,
|
||||
status: draft.status
|
||||
});
|
||||
ElMessage.success('清洁模板已保存,仅影响后续新任务');
|
||||
await loadTemplates();
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '清洁模板保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function editTemplate(template: CleaningTemplate) {
|
||||
draft.scopeType = template.scopeType;
|
||||
draft.scopeId = template.storeId || template.roomId || '';
|
||||
draft.roomStoreId = template.scopeType === 'ROOM' ? template.storeId || '' : '';
|
||||
if (draft.roomStoreId) await loadRooms();
|
||||
draft.name = template.name;
|
||||
draft.requirement = template.requirement;
|
||||
draft.photoRequired = template.photoRequired;
|
||||
draft.minPhotoCount = template.minPhotoCount;
|
||||
draft.maxPhotoCount = template.maxPhotoCount;
|
||||
draft.exemptPolicy = template.exemptPolicy;
|
||||
draft.status = template.status;
|
||||
}
|
||||
|
||||
function resetDraft() {
|
||||
Object.assign(draft, {
|
||||
scopeType: 'TENANT', scopeId: '', roomStoreId: '', name: '默认清洁模板',
|
||||
requirement: '完成桌面、地面与设备周边清洁', photoRequired: true,
|
||||
minPhotoCount: 1, maxPhotoCount: 9, exemptPolicy: 'ANY_ACTIVE', status: 'ACTIVE'
|
||||
});
|
||||
}
|
||||
|
||||
function handleScopeChange() { draft.scopeId = ''; draft.roomStoreId = ''; rooms.value = []; }
|
||||
async function handleRoomStoreChange() { draft.scopeId = ''; await loadRooms(); }
|
||||
function normalizePhotoRule() { draft.minPhotoCount = draft.photoRequired ? Math.max(1, draft.minPhotoCount) : 0; }
|
||||
function exemptPolicyLabel(policy: CleaningExemptPolicy) {
|
||||
return { DISABLED: '不允许', BEFORE_START: '开始前', ANY_ACTIVE: '活动阶段' }[policy];
|
||||
}
|
||||
|
||||
watch(() => props.session.token, (token) => {
|
||||
if (token) void Promise.all([loadTemplates(), loadStores()]);
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rules-panel { display: grid; gap: 18px; }
|
||||
.panel-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||
.panel-heading h3 { margin: 4px 0 6px; color: var(--ink); }
|
||||
.panel-heading p:last-child { margin: 0; color: var(--muted); }
|
||||
.rule-form { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0 14px; padding: 18px; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); }
|
||||
.wide-field { grid-column: 1 / -1; }
|
||||
.form-actions { display: flex; justify-content: flex-end; gap: 10px; }
|
||||
@media (max-width: 980px) { .rule-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 560px) { .panel-heading { align-items: stretch; flex-direction: column; } .rule-form { grid-template-columns: 1fr; padding: 14px; } .wide-field { grid-column: auto; } .form-actions { justify-content: stretch; } .form-actions :deep(.el-button) { flex: 1; } }
|
||||
</style>
|
||||
@@ -149,7 +149,7 @@
|
||||
size="small"
|
||||
type="warning"
|
||||
:icon="Ban"
|
||||
:disabled="!canExempt(row.status)"
|
||||
:disabled="!canExempt(row)"
|
||||
@click="$emit('exempt', { taskId: row.id, note: '\u540e\u53f0\u6807\u8bb0\u514d\u6e05\u6d01' })"
|
||||
>
|
||||
{{ '\u514d\u6e05\u6d01' }}
|
||||
@@ -323,7 +323,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailDialog.open" title="保洁任务详情" width="760px">
|
||||
<el-dialog v-model="detailDialog.open" title="保洁任务详情" width="880px">
|
||||
<div v-if="detailDialog.task" class="dialog-stack">
|
||||
<div class="detail-grid">
|
||||
<span>任务号<strong>{{ detailDialog.task.taskNo }}</strong></span>
|
||||
@@ -341,6 +341,12 @@
|
||||
<el-descriptions-item label="要求" :span="2">
|
||||
{{ compactText(detailDialog.task.requirement) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="照片规则">
|
||||
{{ detailDialog.task.photoRequired ? `${detailDialog.task.minPhotoCount}–${detailDialog.task.maxPhotoCount} 张` : `可选,最多 ${detailDialog.task.maxPhotoCount} 张` }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="任务快照">
|
||||
模板 v{{ detailDialog.task.cleaningTemplateVersion }} · 照片修订 {{ detailDialog.task.photoRevision }} · 补做 {{ detailDialog.task.reworkCount }} 次
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="驳回原因" :span="2">
|
||||
{{ compactText(detailDialog.task.rejectReason) }}
|
||||
</el-descriptions-item>
|
||||
@@ -351,6 +357,46 @@
|
||||
<span>提交<strong>{{ shortDate(detailDialog.task.submittedAt) }}</strong></span>
|
||||
<span>完成<strong>{{ shortDate(detailDialog.task.completedAt) }}</strong></span>
|
||||
</div>
|
||||
<section class="stat-block" v-loading="detailDialog.loadingSubmissions">
|
||||
<header>
|
||||
<h3>验收照片版本</h3>
|
||||
<span>{{ detailDialog.submissions.length }} 次提交</span>
|
||||
</header>
|
||||
<div v-if="detailDialog.submissions.length" class="submission-list">
|
||||
<article
|
||||
v-for="submission in detailDialog.submissions"
|
||||
:key="submission.id"
|
||||
class="submission-card"
|
||||
>
|
||||
<header>
|
||||
<strong>第 {{ submission.revision }} 版</strong>
|
||||
<el-tag
|
||||
:type="submission.status === 'ACCEPTED' ? 'success' : submission.status === 'REJECTED' ? 'danger' : 'warning'"
|
||||
effect="light"
|
||||
>
|
||||
{{ submission.status }}
|
||||
</el-tag>
|
||||
<span>{{ shortDate(submission.submittedAt) }}</span>
|
||||
</header>
|
||||
<p v-if="submission.rejectReason" class="submission-reason">
|
||||
驳回原因:{{ submission.rejectReason }}
|
||||
</p>
|
||||
<p v-if="submission.note" class="submission-note">提交备注:{{ submission.note }}</p>
|
||||
<div v-if="submission.photoUrls.length" class="photo-grid">
|
||||
<el-image
|
||||
v-for="url in submission.photoUrls"
|
||||
:key="`${submission.id}-${url}`"
|
||||
:src="url"
|
||||
fit="cover"
|
||||
:preview-src-list="submission.photoUrls"
|
||||
preview-teleported
|
||||
/>
|
||||
</div>
|
||||
<el-empty v-else description="本版无照片" :image-size="48" />
|
||||
</article>
|
||||
</div>
|
||||
<el-empty v-else description="暂无提交版本" :image-size="64" />
|
||||
</section>
|
||||
<section class="stat-block">
|
||||
<header>
|
||||
<h3>操作流水</h3>
|
||||
@@ -451,6 +497,7 @@ import {
|
||||
addCleaningTaskMember,
|
||||
listCleaningTaskEvents,
|
||||
listCleaningTaskMembers,
|
||||
listCleaningTaskSubmissions,
|
||||
removeCleaningTaskMember
|
||||
} from '../api';
|
||||
import { compactText, money, shortDate } from '../format';
|
||||
@@ -458,6 +505,7 @@ import type {
|
||||
CleaningTask,
|
||||
CleaningTaskEvent,
|
||||
CleaningTaskMember,
|
||||
CleaningTaskSubmission,
|
||||
ManagedUser,
|
||||
PageResult,
|
||||
TaskStatus
|
||||
@@ -510,16 +558,20 @@ const detailDialog = reactive<{
|
||||
open: boolean;
|
||||
loadingEvents: boolean;
|
||||
loadingMembers: boolean;
|
||||
loadingSubmissions: boolean;
|
||||
task: CleaningTask | null;
|
||||
events: CleaningTaskEvent[];
|
||||
members: CleaningTaskMember[];
|
||||
submissions: CleaningTaskSubmission[];
|
||||
}>({
|
||||
open: false,
|
||||
loadingEvents: false,
|
||||
loadingMembers: false,
|
||||
loadingSubmissions: false,
|
||||
task: null,
|
||||
events: [],
|
||||
members: []
|
||||
members: [],
|
||||
submissions: []
|
||||
});
|
||||
const memberDialog = reactive({
|
||||
open: false,
|
||||
@@ -536,7 +588,7 @@ const selectedSubmittedTasks = computed(() => selectedTasks.value
|
||||
.filter((task) => task.status === 'SUBMITTED'));
|
||||
const selectedSubmittedCount = computed(() => selectedSubmittedTasks.value.length);
|
||||
const selectedExemptableTasks = computed(() => selectedTasks.value
|
||||
.filter((task) => canExempt(task.status)));
|
||||
.filter((task) => canExempt(task)));
|
||||
const selectedExemptableCount = computed(() => selectedExemptableTasks.value.length);
|
||||
|
||||
watch(
|
||||
@@ -553,11 +605,13 @@ function taskTag(status: TaskStatus) {
|
||||
}
|
||||
|
||||
function canSelectTask(row: CleaningTask) {
|
||||
return row.status === 'SUBMITTED' || canExempt(row.status);
|
||||
return row.status === 'SUBMITTED' || canExempt(row);
|
||||
}
|
||||
|
||||
function canExempt(status: TaskStatus) {
|
||||
return ['WAITING', 'CLAIMED', 'STARTED', 'SUBMITTED', 'REJECTED'].includes(status);
|
||||
function canExempt(task: CleaningTask) {
|
||||
if (task.exemptPolicy === 'DISABLED') return false;
|
||||
if (task.exemptPolicy === 'BEFORE_START') return ['WAITING', 'CLAIMED'].includes(task.status);
|
||||
return ['WAITING', 'CLAIMED', 'STARTED', 'SUBMITTED', 'REJECTED'].includes(task.status);
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: CleaningTask[]) {
|
||||
@@ -583,20 +637,25 @@ async function openDetail(row: CleaningTask) {
|
||||
detailDialog.task = row;
|
||||
detailDialog.events = [];
|
||||
detailDialog.members = [];
|
||||
detailDialog.submissions = [];
|
||||
detailDialog.loadingEvents = true;
|
||||
detailDialog.loadingMembers = true;
|
||||
detailDialog.loadingSubmissions = true;
|
||||
try {
|
||||
const [events, members] = await Promise.all([
|
||||
const [events, members, submissions] = await Promise.all([
|
||||
listCleaningTaskEvents(props.session, row.id),
|
||||
listCleaningTaskMembers(props.session, row.id)
|
||||
listCleaningTaskMembers(props.session, row.id),
|
||||
listCleaningTaskSubmissions(props.session, row.id)
|
||||
]);
|
||||
detailDialog.events = events;
|
||||
detailDialog.members = members;
|
||||
detailDialog.submissions = submissions;
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '任务详情加载失败');
|
||||
} finally {
|
||||
detailDialog.loadingEvents = false;
|
||||
detailDialog.loadingMembers = false;
|
||||
detailDialog.loadingSubmissions = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,10 +838,17 @@ function exportTaskDetail() {
|
||||
`${event.action}:${shortDate(event.createdAt)}`,
|
||||
`${compactText(event.fromStatus)} -> ${event.toStatus} ${compactText(event.actorId)} ${compactText(event.note)}`
|
||||
]),
|
||||
...task.photoUrls.map((url, index) => [
|
||||
'photo',
|
||||
`photo_${index + 1}`,
|
||||
url
|
||||
...detailDialog.submissions.flatMap((submission) => [
|
||||
[
|
||||
'submission',
|
||||
`revision_${submission.revision}`,
|
||||
`${submission.status} ${submission.rejectReason || submission.note || ''}`
|
||||
],
|
||||
...submission.photoUrls.map((url, index) => [
|
||||
'photo',
|
||||
`revision_${submission.revision}_photo_${index + 1}`,
|
||||
url
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
@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>
|
||||
@@ -159,6 +162,7 @@ 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,
|
||||
|
||||
@@ -1291,6 +1291,43 @@ textarea {
|
||||
background: #f5f8fb;
|
||||
}
|
||||
|
||||
.submission-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.submission-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e1e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.submission-card > header {
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.submission-card > header span:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.submission-reason,
|
||||
.submission-note {
|
||||
margin: 0;
|
||||
color: #60708a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.submission-reason {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.trend-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
@@ -614,8 +614,16 @@ export interface CleaningTask {
|
||||
priority: number;
|
||||
rewardCents: number;
|
||||
requirement: string;
|
||||
cleaningTemplateId: string | null;
|
||||
cleaningTemplateVersion: number;
|
||||
photoRequired: boolean;
|
||||
minPhotoCount: number;
|
||||
maxPhotoCount: number;
|
||||
exemptPolicy: CleaningExemptPolicy;
|
||||
photoUrls: string[];
|
||||
rejectReason: string;
|
||||
reworkCount: number;
|
||||
photoRevision: number;
|
||||
claimedAt?: string;
|
||||
startedAt?: string;
|
||||
submittedAt?: string;
|
||||
@@ -625,6 +633,27 @@ export interface CleaningTask {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export type CleaningTemplateScope = 'TENANT' | 'STORE' | 'ROOM';
|
||||
export type CleaningExemptPolicy = 'DISABLED' | 'BEFORE_START' | 'ANY_ACTIVE';
|
||||
|
||||
export interface CleaningTemplate {
|
||||
id: string;
|
||||
scopeKey: string;
|
||||
scopeType: CleaningTemplateScope;
|
||||
storeId: string | null;
|
||||
roomId: string | null;
|
||||
name: string;
|
||||
requirement: string;
|
||||
photoRequired: boolean;
|
||||
minPhotoCount: number;
|
||||
maxPhotoCount: number;
|
||||
exemptPolicy: CleaningExemptPolicy;
|
||||
version: number;
|
||||
status: 'ACTIVE' | 'DISABLED';
|
||||
updatedAt: string;
|
||||
appliesToExistingTasks: false;
|
||||
}
|
||||
|
||||
export interface CleaningTaskMember {
|
||||
id: string;
|
||||
taskId: string;
|
||||
@@ -649,6 +678,20 @@ export interface CleaningTaskEvent {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CleaningTaskSubmission {
|
||||
id: string;
|
||||
taskId: string;
|
||||
revision: number;
|
||||
status: 'SUBMITTED' | 'ACCEPTED' | 'REJECTED';
|
||||
photoUrls: string[];
|
||||
note: string;
|
||||
rejectReason: string;
|
||||
submittedBy: string;
|
||||
reviewedBy: string | null;
|
||||
submittedAt: string;
|
||||
reviewedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CleaningSettlement {
|
||||
id: string;
|
||||
settlementNo: string;
|
||||
|
||||
Reference in New Issue
Block a user