接入前端P0页面真实接口

This commit is contained in:
andy
2026-07-08 16:20:06 +08:00
parent 5c5b8e33b0
commit 66a613d6cd
45 changed files with 8719 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
<template>
<div class="th-page task-list-view">
<header class="page-heading">
<div>
<h1>{{ t('taskList.title') }}</h1>
<p>{{ t('taskList.subtitle') }}</p>
</div>
</header>
<section class="th-section filter-section">
<div class="th-section-header">
<h2 class="th-section-title">
{{ t('workbench.filters') }}
</h2>
<button
type="button"
class="text-button"
@click="resetFilters"
>
{{ t('workbench.reset') }}
</button>
</div>
<div class="filter-grid">
<label>
<span>{{ t('taskList.keyword') }}</span>
<input
v-model="filters.keyword"
type="search"
placeholder="GRP-001 / Booking"
>
</label>
<label>
<span>{{ t('taskList.taskType') }}</span>
<select v-model="filters.task_type">
<option value="">All</option>
<option value="NEW_BOOKING">{{ t('cardType.NEW_BOOKING') }}</option>
<option value="UPDATE_BOOKING">{{ t('cardType.UPDATE_BOOKING') }}</option>
<option value="CANCEL_BOOKING">{{ t('cardType.CANCEL_BOOKING') }}</option>
<option value="MANUAL_REVIEW">{{ t('cardType.MANUAL_REVIEW') }}</option>
<option value="INFORMATIONAL_MESSAGE">{{ t('cardType.MESSAGE_NOTIFICATION') }}</option>
</select>
</label>
<label>
<span>{{ t('workbench.taskStatus') }}</span>
<select v-model="filters.task_status">
<option value="">All</option>
<option value="PENDING_CONFIRM">{{ t('status.PENDING_CONFIRM') }}</option>
<option value="READY">{{ t('status.READY') }}</option>
<option value="BLOCKED">{{ t('status.BLOCKED') }}</option>
<option value="COMPLETED">{{ t('status.COMPLETED') }}</option>
</select>
</label>
</div>
</section>
<section class="th-section table-section">
<div class="th-section-header">
<div>
<h2 class="th-section-title">
{{ t('taskList.tableTitle') }}
</h2>
<p class="th-muted">
{{ t('workbench.totalPrefix') }} {{ tasks.length }} {{ t('taskList.totalSuffix') }}
</p>
</div>
</div>
<div
v-if="loading"
class="empty-state"
>
{{ t('common.loading') }}
</div>
<div
v-else-if="errorMessage"
class="empty-state empty-state--error"
>
{{ errorMessage }}
</div>
<div
v-else-if="!tasks.length"
class="empty-state"
>
{{ t('common.noData') }}
</div>
<div
v-else
class="table-wrap"
>
<table>
<thead>
<tr>
<th>{{ t('taskList.columns.task') }}</th>
<th>{{ t('taskList.columns.order') }}</th>
<th>{{ t('taskList.columns.card') }}</th>
<th>{{ t('taskList.columns.status') }}</th>
<th>{{ t('taskList.columns.source') }}</th>
<th>{{ t('taskList.columns.queue') }}</th>
<th>{{ t('workbench.columns.operation') }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="task in tasks"
:key="task.task_id"
>
<td>
<strong class="th-code">#{{ task.task_id }}</strong>
<small>{{ task.task_type }} / {{ task.task_subtype ?? '-' }}</small>
</td>
<td>
<strong>{{ task.display_order_key ?? task.temporary_order_no ?? task.order_id }}</strong>
<small class="th-code">{{ task.order_id }}</small>
</td>
<td>{{ task.card_name ?? task.task_type }}</td>
<td><ReservationStatusBadge :status="task.task_status" /></td>
<td>
<span>{{ task.source_subject ?? t('taskList.noSourceSubject') }}</span>
<small>{{ task.source_sender_summary ?? task.source_message_id ?? '-' }}</small>
</td>
<td>
<span>{{ task.queue_sequence ?? '-' }}</span>
<small>{{ task.can_process ? t('taskList.canProcess') : task.readonly_reason_code ?? '-' }}</small>
</td>
<td>
<div class="row-actions">
<RouterLink
class="row-action"
:to="`/reservation/tasks/${task.task_id}`"
>
{{ t('taskList.viewTask') }}
</RouterLink>
<RouterLink
class="row-action"
:to="`/reservation/orders/${task.order_id}`"
>
{{ t('taskList.viewOrder') }}
</RouterLink>
<span
v-if="!task.source_message_id || !task.external_conversation_id"
class="pending-link"
>
{{ t('common.endpointMissing') }}
</span>
<RouterLink
v-else
class="row-action"
:to="`/reservation/source-messages/${task.source_message_id}/conversation`"
>
{{ t('taskList.viewConversation') }}
</RouterLink>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { 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'
const { t } = useI18n()
const loading = ref(false)
const errorMessage = ref('')
const tasks = ref<ReservationTaskListItem[]>([])
let taskListRequestSequence = 0
const filters = reactive<ReservationTaskListFilters>({
keyword: '',
task_type: '',
task_status: '',
page_num: 1,
page_size: 20,
})
onMounted(() => {
void loadTasks()
})
watch(filters, () => {
void loadTasks()
})
async function loadTasks(): Promise<void> {
const requestSequence = ++taskListRequestSequence
loading.value = true
errorMessage.value = ''
try {
const result = await fetchReservationTaskList({ ...filters })
if (requestSequence !== taskListRequestSequence) {
return
}
tasks.value = result.items
} catch (error) {
if (requestSequence !== taskListRequestSequence) {
return
}
tasks.value = []
errorMessage.value = error instanceof Error ? error.message : String(error)
} finally {
if (requestSequence === taskListRequestSequence) {
loading.value = false
}
}
}
function resetFilters(): void {
filters.keyword = ''
filters.task_type = ''
filters.task_status = ''
}
</script>
<style scoped>
.task-list-view {
max-width: 1320px;
}
.page-heading h1,
.page-heading p {
margin: 0;
}
.page-heading h1 {
color: var(--th-color-slate-900);
font-size: 28px;
}
.page-heading p {
margin-top: 8px;
color: var(--th-color-slate-500);
font-size: 14px;
}
.filter-section {
padding-bottom: 20px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
padding: 18px 20px 0;
}
.filter-grid label {
display: grid;
gap: 7px;
}
.filter-grid span {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 700;
}
.filter-grid input,
.filter-grid select {
min-height: 38px;
min-width: 0;
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: 8px 10px;
}
.text-button,
.row-action {
border: 0;
background: transparent;
color: var(--th-color-blue-600);
cursor: pointer;
font-size: 13px;
font-weight: 800;
}
.empty-state {
min-height: 220px;
display: grid;
place-items: center;
color: var(--th-color-slate-500);
font-size: 13px;
padding: 24px;
}
.empty-state--error {
color: var(--th-color-danger);
}
.table-section {
overflow: hidden;
}
.table-wrap {
overflow-x: auto;
padding: 14px 20px 20px;
}
table {
width: 100%;
min-width: 980px;
border-collapse: collapse;
}
th,
td {
border-bottom: 1px solid var(--th-color-slate-200);
padding: 14px 10px;
text-align: left;
vertical-align: middle;
}
th {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 800;
}
td {
color: var(--th-color-slate-900);
font-size: 13px;
}
td strong,
td small {
display: block;
}
td small {
margin-top: 4px;
color: var(--th-color-slate-500);
font-size: 12px;
}
.row-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.pending-link {
color: var(--th-color-slate-500);
font-size: 12px;
font-weight: 800;
}
@media (max-width: 900px) {
.filter-grid {
grid-template-columns: 1fr;
}
}
</style>