接入前端真实接口并标记邮件HTML清洗

This commit is contained in:
andy
2026-07-08 23:37:03 +08:00
parent c83935a26c
commit a0cb3a05d7
25 changed files with 2581 additions and 157 deletions

View File

@@ -28,11 +28,15 @@
v-model="filters.keyword"
type="search"
placeholder="GRP-001 / Booking"
@input="resetToFirstPage"
>
</label>
<label>
<span>{{ t('taskList.taskType') }}</span>
<select v-model="filters.task_type">
<select
v-model="filters.task_type"
@change="resetToFirstPage"
>
<option value="">All</option>
<option value="NEW_BOOKING">{{ t('cardType.NEW_BOOKING') }}</option>
<option value="UPDATE_BOOKING">{{ t('cardType.UPDATE_BOOKING') }}</option>
@@ -43,7 +47,10 @@
</label>
<label>
<span>{{ t('workbench.taskStatus') }}</span>
<select v-model="filters.task_status">
<select
v-model="filters.task_status"
@change="resetToFirstPage"
>
<option value="">All</option>
<option value="PENDING_CONFIRM">{{ t('status.PENDING_CONFIRM') }}</option>
<option value="READY">{{ t('status.READY') }}</option>
@@ -51,6 +58,30 @@
<option value="COMPLETED">{{ t('status.COMPLETED') }}</option>
</select>
</label>
<label>
<span>{{ t('workbench.orderStatus') }}</span>
<select
value=""
disabled
>
<option value="">All</option>
<option value="TEMPORARY">{{ t('status.TEMPORARY') }}</option>
<option value="ACTIVE">{{ t('status.ACTIVE') }}</option>
<option value="ENDED">{{ t('status.ENDED') }}</option>
</select>
<small class="filter-hint">{{ t('taskList.orderStatusFilterPending') }}</small>
</label>
<label>
<span>{{ t('taskList.queueParticipation') }}</span>
<select
v-model="filters.queue_participation"
@change="resetToFirstPage"
>
<option :value="undefined">{{ t('taskList.queueAll') }}</option>
<option :value="true">{{ t('taskList.queueIncluded') }}</option>
<option :value="false">{{ t('taskList.queueExcluded') }}</option>
</select>
</label>
</div>
</section>
@@ -61,7 +92,7 @@
{{ t('taskList.tableTitle') }}
</h2>
<p class="th-muted">
{{ t('workbench.totalPrefix') }} {{ tasks.length }} {{ t('taskList.totalSuffix') }}
{{ t('workbench.totalPrefix') }} {{ total }} {{ t('taskList.totalSuffix') }}
</p>
</div>
</div>
@@ -139,7 +170,7 @@
{{ t('taskList.viewOrder') }}
</RouterLink>
<span
v-if="!task.source_message_id || !task.external_conversation_id"
v-if="!task.source_message_id"
class="pending-link"
>
{{ t('common.endpointMissing') }}
@@ -157,28 +188,66 @@
</tbody>
</table>
</div>
<footer class="pagination-bar">
<label class="page-size-control">
<span>{{ t('common.pageSize') }}</span>
<select
v-model.number="filters.page_size"
@change="resetToFirstPage"
>
<option :value="10">10</option>
<option :value="20">20</option>
<option :value="50">50</option>
</select>
</label>
<div class="pagination-actions">
<button
type="button"
class="secondary-button"
:disabled="currentPage <= 1 || loading"
@click="goToPage(currentPage - 1)"
>
{{ t('common.previousPage') }}
</button>
<span>{{ t('common.pageStatus', { current: currentPage, total: totalPages }) }}</span>
<button
type="button"
class="secondary-button"
:disabled="currentPage >= totalPages || loading"
@click="goToPage(currentPage + 1)"
>
{{ t('common.nextPage') }}
</button>
</div>
</footer>
</section>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n'
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
import { fetchReservationTaskList } from '@/services/reservationService'
import type { ReservationTaskListFilters, ReservationTaskListItem } from '@/types/reservation'
import type { ReservationPageResult, ReservationTaskListFilters, ReservationTaskListItem } from '@/types/reservation'
const { t } = useI18n()
const loading = ref(false)
const errorMessage = ref('')
const tasks = ref<ReservationTaskListItem[]>([])
const page = ref(createPage(1, 20, 0))
const total = computed(() => page.value.total)
const currentPage = computed(() => page.value.page_num)
const totalPages = computed(() => Math.max(1, Math.ceil(page.value.total / Math.max(page.value.page_size, 1))))
let taskListRequestSequence = 0
const filters = reactive<ReservationTaskListFilters>({
keyword: '',
task_type: '',
task_status: '',
queue_participation: undefined,
page_num: 1,
page_size: 20,
})
@@ -201,11 +270,13 @@ async function loadTasks(): Promise<void> {
return
}
tasks.value = result.items
page.value = result.page
} catch (error) {
if (requestSequence !== taskListRequestSequence) {
return
}
tasks.value = []
page.value = createPage(filters.page_num ?? 1, filters.page_size ?? 20, 0)
errorMessage.value = error instanceof Error ? error.message : String(error)
} finally {
if (requestSequence === taskListRequestSequence) {
@@ -218,6 +289,25 @@ function resetFilters(): void {
filters.keyword = ''
filters.task_type = ''
filters.task_status = ''
filters.queue_participation = undefined
filters.page_num = 1
filters.page_size = 20
}
function resetToFirstPage(): void {
filters.page_num = 1
}
function goToPage(page: number): void {
filters.page_num = Math.min(Math.max(page, 1), totalPages.value)
}
function createPage(pageNum: number, pageSize: number, total: number): ReservationPageResult {
return {
page_num: pageNum,
page_size: pageSize,
total,
}
}
</script>
@@ -248,7 +338,7 @@ function resetFilters(): void {
.filter-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 14px;
padding: 18px 20px 0;
}
@@ -275,6 +365,19 @@ function resetFilters(): void {
padding: 8px 10px;
}
.filter-grid select:disabled {
color: var(--th-color-slate-500);
cursor: not-allowed;
opacity: 0.72;
}
.filter-hint {
color: var(--th-color-warning);
font-size: 11px;
font-weight: 800;
line-height: 1.45;
}
.text-button,
.row-action {
border: 0;
@@ -355,9 +458,63 @@ td small {
font-weight: 800;
}
.pagination-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
border-top: 1px solid var(--th-color-slate-200);
padding: 14px 20px 18px;
}
.page-size-control,
.pagination-actions {
display: flex;
align-items: center;
gap: 10px;
}
.page-size-control span,
.pagination-actions span {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 800;
}
.page-size-control select {
min-height: 34px;
border: 1px solid var(--th-color-slate-200);
border-radius: var(--th-radius-sm);
background: var(--th-color-white);
color: var(--th-color-slate-900);
padding: 6px 8px;
}
.secondary-button {
min-height: 34px;
border: 1px solid var(--th-color-slate-200);
border-radius: var(--th-radius-sm);
background: var(--th-color-white);
color: var(--th-color-slate-700);
cursor: pointer;
font-size: 12px;
font-weight: 800;
padding: 6px 12px;
}
.secondary-button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
@media (max-width: 900px) {
.filter-grid {
grid-template-columns: 1fr;
}
.pagination-bar {
align-items: flex-start;
flex-direction: column;
}
}
</style>