Merge remote-tracking branch 'origin/feat-tpd-2.0-20260721' into feat/deploy-uat2
| | |
| | | pointIds: string[] |
| | | // 计划级「到下车点是否停车」配置,传布尔值;不传后端默认 true。 |
| | | isStopAtDropoff?: boolean |
| | | strategyId?: number | string | null |
| | | skipInvalidEnabled?: boolean |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/add', data) |
| | | } |
| | |
| | | pointIds: string[] |
| | | // 传了则更新「到下车点是否停车」,不传保持原值。 |
| | | isStopAtDropoff?: boolean |
| | | strategyId?: number | string | null |
| | | skipInvalidEnabled?: boolean |
| | | }, |
| | | ) { |
| | | return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/test-plan-demo/edit/${id}`, data) |
| | |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/vehicle/manage', data) |
| | | } |
| | | |
| | | export function fetchTradingStrategyConfig() { |
| | | return request.Get<Service.ResponseResult<any>>('/operation/api/v1/tradingStrategy/queryById') |
| | | } |
| | | |
| | | export function updateTestPlanStrategy(planId: number | string, strategyId: number | string | null) { |
| | | return request.Put<Service.ResponseResult<any>>(`/operation/api/v1/test-plan-demo/${planId}/strategy`, { |
| | | strategyId, |
| | | }) |
| | | } |
| | | |
| | | export function startTestPlanCruise(planId: number | string, data: { |
| | | cruisePlanId: number | string |
| | | durationMinutes: number |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/test-plan-demo/${planId}/cruise/start`, data) |
| | | } |
| | | |
| | | export function stopTestPlanCruise(planId: number | string, vehicleIds?: Array<number | string>) { |
| | | return request.Post<Service.ResponseResult<any>>(`/operation/api/v1/test-plan-demo/${planId}/cruise/stop`, vehicleIds ? { vehicleIds } : {}) |
| | | } |
| | | |
| | | export function fetchTestPlanCruiseStatus(planId: number | string) { |
| | | return request.Get<Service.ResponseResult<any>>(`/operation/api/v1/test-plan-demo/${planId}/cruise/status`) |
| | | } |
| | |
| | | import noDataImg from '@/assets/element-img/no-data.png' |
| | | import { ref, computed, watch } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { NButton, NSpace, NTag, NTooltip, NForm, NFormItemGi, NRadioGroup, NRadioButton, NInput, NSwitch, NDataTable, NGrid, useDialog } from 'naive-ui' |
| | | import { NButton, NSpace, NTag, NTooltip, NForm, NFormItemGi, NRadioGroup, NRadioButton, NInput, NSwitch, NSelect, NDataTable, NGrid, useDialog } from 'naive-ui' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { useMessage } from 'naive-ui' |
| | | import waitIcon from '@/assets/pricingManagement/wait.png' |
| | |
| | | editTestPlan, |
| | | fetchTestPlanDetail, |
| | | fetchTestPlanOrderPageList, |
| | | fetchTradingStrategyConfig, |
| | | generateTestPlanPreviewOrders, |
| | | startTestPlanDispatch, |
| | | toggleTestPlanStopAtDropoff, |
| | |
| | | // ================= Step 1: 测试点位选择 ================= |
| | | const planName = ref('') |
| | | const orderGenMode = ref(1) // 1: 全排列模式, 2: 串烧模式 |
| | | const strategyId = ref<number | string | null>(null) |
| | | const skipInvalidEnabled = ref(false) |
| | | const strategyOptions = ref<Array<{ label: string; value: number | string | null }>>([ |
| | | { label: '默认FIFO', value: null }, |
| | | ]) |
| | | |
| | | async function loadStrategyOptions() { |
| | | try { |
| | | const res = await fetchTradingStrategyConfig() |
| | | if ((res.code === 200 || res.code === 0) && res.data) { |
| | | const s = res.data |
| | | strategyOptions.value = [ |
| | | { label: '默认FIFO', value: null }, |
| | | { label: s.name || '当前交易策略', value: s.id }, |
| | | ] |
| | | } |
| | | } catch (_) {} |
| | | } |
| | | loadStrategyOptions() |
| | | |
| | | interface Step1Row { |
| | | id: string |
| | |
| | | pointIds: step1Data.value.map(item => item.id), |
| | | // 计划级「到下车点是否停车」配置,传布尔值。 |
| | | isStopAtDropoff: isStopAtDropoff.value, |
| | | strategyId: strategyId.value, |
| | | skipInvalidEnabled: skipInvalidEnabled.value, |
| | | } |
| | | } |
| | | |
| | |
| | | orderGenMode.value = Number(detail.orderMode || 1) |
| | | // 回显「到下车点是否停车」,后端未返回时按默认 true 处理。 |
| | | isStopAtDropoff.value = detail.isStopAtDropoff !== false |
| | | strategyId.value = detail.strategyId ?? null |
| | | skipInvalidEnabled.value = detail.skipInvalidEnabled === true |
| | | // 编辑态回显时,点位 ID 同样按 string 处理。 |
| | | step1Data.value = Array.isArray(detail.points) |
| | | ? detail.points.map((item: any) => ({ |
| | |
| | | </div> |
| | | </n-form-item-gi> |
| | | </n-grid> |
| | | <n-grid :cols="24" :x-gap="24"> |
| | | <n-form-item-gi v-if="orderGenMode === 1" :span="12" label="分单策略"> |
| | | <n-select |
| | | v-model:value="strategyId" |
| | | :options="strategyOptions" |
| | | placeholder="默认FIFO" |
| | | clearable |
| | | /> |
| | | </n-form-item-gi> |
| | | <n-form-item-gi :span="12" label="跳过无效订单"> |
| | | <n-switch v-model:value="skipInvalidEnabled" /> |
| | | <span class="ml-8px text-12px text-[rgba(23,31,38,0.5)]">开启后派单时跳过路线不可达的订单</span> |
| | | </n-form-item-gi> |
| | | </n-grid> |
| | | </n-form> |
| | | |
| | | <!-- 第一步表格操作区 --> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, watch } from 'vue' |
| | | import { useMessage } from 'naive-ui' |
| | | import { fetchCruisePlanPageList } from '@/service/api/cruise' |
| | | import { startTestPlanCruise, stopTestPlanCruise } from '@/service/api/testPlanManagement' |
| | | |
| | | const props = defineProps<{ |
| | | visible: boolean |
| | | planId?: number | string |
| | | cruiseStatus?: number |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | (e: 'update:visible', val: boolean): void |
| | | (e: 'success'): void |
| | | }>() |
| | | |
| | | const message = useMessage() |
| | | |
| | | const show = computed({ |
| | | get: () => props.visible, |
| | | set: val => emit('update:visible', val), |
| | | }) |
| | | |
| | | const isCruising = computed(() => props.cruiseStatus === 1) |
| | | const modalTitle = computed(() => (isCruising.value ? '停止巡游' : '启动巡游')) |
| | | |
| | | const formModel = ref({ |
| | | cruisePlanId: null as number | null, |
| | | durationMinutes: null as number | null, |
| | | }) |
| | | |
| | | const cruisePlanOptions = ref<Array<{ label: string, value: number }>>([]) |
| | | const submitting = ref(false) |
| | | |
| | | async function loadCruisePlans() { |
| | | try { |
| | | const res = await fetchCruisePlanPageList({ pageNum: 1, pageSize: 100 }) |
| | | if (res.code === 200 || res.code === 0) { |
| | | const records = res.data?.records || [] |
| | | cruisePlanOptions.value = records.map((item: any) => ({ |
| | | label: item.planName || `巡游计划${item.id}`, |
| | | value: item.id, |
| | | })) |
| | | } |
| | | } |
| | | catch {} |
| | | } |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | (val) => { |
| | | if (val && !isCruising.value) { |
| | | formModel.value = { cruisePlanId: null, durationMinutes: null } |
| | | loadCruisePlans() |
| | | } |
| | | }, |
| | | ) |
| | | |
| | | async function handleConfirm() { |
| | | if (isCruising.value) { |
| | | submitting.value = true |
| | | try { |
| | | const res = await stopTestPlanCruise(props.planId!) |
| | | if (res.code !== 200 && res.code !== 0) { |
| | | throw new Error(res.msg || '停止巡游失败') |
| | | } |
| | | message.success('已停止巡游') |
| | | show.value = false |
| | | emit('success') |
| | | } |
| | | catch (e: any) { |
| | | message.error(e?.message || '停止巡游失败') |
| | | } |
| | | finally { |
| | | submitting.value = false |
| | | } |
| | | return |
| | | } |
| | | |
| | | if (!formModel.value.cruisePlanId) { |
| | | message.warning('请选择巡游计划') |
| | | return |
| | | } |
| | | if (!formModel.value.durationMinutes || formModel.value.durationMinutes < 1 || formModel.value.durationMinutes > 999) { |
| | | message.warning('暂停时长范围为 1~999 分钟') |
| | | return |
| | | } |
| | | |
| | | submitting.value = true |
| | | try { |
| | | const res = await startTestPlanCruise(props.planId!, { |
| | | cruisePlanId: formModel.value.cruisePlanId, |
| | | durationMinutes: formModel.value.durationMinutes, |
| | | }) |
| | | if (res.code !== 200 && res.code !== 0) { |
| | | throw new Error(res.msg || '启动巡游失败') |
| | | } |
| | | const data = res.data |
| | | const startedCount = data?.started?.length || 0 |
| | | const skippedCount = data?.skipped?.length || 0 |
| | | message.success(`巡游已启动,${startedCount} 辆车已下发巡游单,${skippedCount} 辆车跳过`) |
| | | show.value = false |
| | | emit('success') |
| | | } |
| | | catch (e: any) { |
| | | message.error(e?.message || '启动巡游失败') |
| | | } |
| | | finally { |
| | | submitting.value = false |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="show" |
| | | preset="dialog" |
| | | :title="modalTitle" |
| | | :mask-closable="false" |
| | | :positive-text="isCruising ? '确认停止' : '确认启动'" |
| | | negative-text="取消" |
| | | :loading="submitting" |
| | | @positive-click="handleConfirm" |
| | | @negative-click="show = false" |
| | | > |
| | | <template v-if="isCruising"> |
| | | <p>确认停止当前巡游吗?停止后将取消所有进行中的巡游订单,恢复正常乘客订单派发。</p> |
| | | </template> |
| | | <template v-else> |
| | | <n-form label-placement="left" label-width="90" :show-feedback="false" class="mt-16px"> |
| | | <n-form-item label="巡游计划"> |
| | | <n-select |
| | | v-model:value="formModel.cruisePlanId" |
| | | :options="cruisePlanOptions" |
| | | placeholder="请选择巡游计划" |
| | | clearable |
| | | filterable |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="暂停时长" class="mt-12px"> |
| | | <n-input-number |
| | | v-model:value="formModel.durationMinutes" |
| | | :min="1" |
| | | :max="999" |
| | | :precision="0" |
| | | placeholder="请输入暂停时长" |
| | | class="w-full" |
| | | > |
| | | <template #suffix> |
| | | 分钟 |
| | | </template> |
| | | </n-input-number> |
| | | </n-form-item> |
| | | </n-form> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | |
| | | closeTestPlanOrder, |
| | | deleteTestPlanOrders, |
| | | exportTestPlanDetailOrders, |
| | | fetchTestPlanCruiseStatus, |
| | | fetchTestPlanDetail, |
| | | fetchTestPlanDetailOrderPageList, |
| | | fetchTestPlanMonitorDetail, |
| | |
| | | statusColor: string |
| | | statusBg: string |
| | | actions: string[] |
| | | dispatchScore?: number | string | null |
| | | skipReason?: string |
| | | scoreDetail?: string |
| | | } |
| | | |
| | | interface VehicleRow { |
| | |
| | | statusColor: statusStyle.textColor, |
| | | statusBg: statusStyle.color, |
| | | actions: resolveOrderActions(statusCode, statusName), |
| | | dispatchScore: item.dispatchScore ?? null, |
| | | skipReason: item.skipReason || '', |
| | | scoreDetail: item.scoreDetail || '', |
| | | } |
| | | }) |
| | | orderCount.value = Number(pageData.total || 0) |
| | |
| | | fetchOrderList(), |
| | | fetchVehicleList(), |
| | | ]) |
| | | await fetchCruiseRecords() |
| | | } |
| | | |
| | | async function refreshOrderRelatedData() { |
| | |
| | | </NTag> |
| | | ), |
| | | }, |
| | | { title: '评分', key: 'dispatchScore', width: 90, render: row => row.dispatchScore != null ? String(row.dispatchScore) : '-' }, |
| | | { title: '跳过原因', key: 'skipReason', width: 160, ellipsis: { tooltip: true }, render: row => row.skipReason || '-' }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | | // ---- 巡游记录 ---- |
| | | interface CruiseRow { |
| | | cruiseId: number | string |
| | | vehicleId: number | string |
| | | plateNo: string |
| | | status: number |
| | | statusName: string |
| | | statusColor: string |
| | | statusBg: string |
| | | createTime: string |
| | | interruptReason: string |
| | | } |
| | | |
| | | const CRUISE_STATUS_MAP: Record<number, { label: string, textColor: string, color: string }> = { |
| | | 0: { label: '待执行', textColor: '#764DFF', color: '#F6F0FF' }, |
| | | 1: { label: '执行中', textColor: '#0068FF', color: '#E6F4FF' }, |
| | | 2: { label: '已中断', textColor: '#F59E0B', color: '#FFF7E6' }, |
| | | 3: { label: '已完成', textColor: '#10B981', color: '#F6FFED' }, |
| | | } |
| | | |
| | | const cruiseData = ref<CruiseRow[]>([]) |
| | | const cruiseLoading = ref(false) |
| | | |
| | | const cruiseColumns = computed<DataTableColumns<CruiseRow>>(() => [ |
| | | { title: '车辆', key: 'plateNo', width: 100, ellipsis: { tooltip: true } }, |
| | | { |
| | | title: '状态', |
| | | key: 'statusName', |
| | | width: 80, |
| | | render: row => ( |
| | | <NTag |
| | | color={{ textColor: row.statusColor, color: row.statusBg }} |
| | | bordered={false} |
| | | size="small" |
| | | style="border-radius: 4px; padding: 0 8px; height: 24px; line-height: 24px; font-weight: 400;" |
| | | > |
| | | {row.statusName} |
| | | </NTag> |
| | | ), |
| | | }, |
| | | { title: '下发时间', key: 'createTime', width: 160 }, |
| | | { title: '中断原因', key: 'interruptReason', width: 100, ellipsis: { tooltip: true }, render: row => row.interruptReason || '-' }, |
| | | ]) |
| | | |
| | | async function fetchCruiseRecords() { |
| | | if (!currentPlanId.value) |
| | | return |
| | | cruiseLoading.value = true |
| | | try { |
| | | const res = await fetchTestPlanCruiseStatus(currentPlanId.value) |
| | | if (res.code !== 200 && res.code !== 0) { |
| | | cruiseData.value = [] |
| | | return |
| | | } |
| | | const records = Array.isArray(res.data) ? res.data : [] |
| | | const vehicleMap = new Map(vehicleData.value.map(v => [String(v.id), v.plateNo])) |
| | | cruiseData.value = records.map((item: any) => { |
| | | const status = Number(item.status ?? 0) |
| | | const style = CRUISE_STATUS_MAP[status] || { label: '-', textColor: '#8C9299', color: '#F5F5F5' } |
| | | return { |
| | | cruiseId: item.cruiseId ?? item.id, |
| | | vehicleId: item.vehicleId, |
| | | plateNo: vehicleMap.get(String(item.vehicleId)) || String(item.vehicleId), |
| | | status, |
| | | statusName: style.label, |
| | | statusColor: style.textColor, |
| | | statusBg: style.color, |
| | | createTime: item.createTime || '-', |
| | | interruptReason: item.interruptReason || '', |
| | | } |
| | | }) |
| | | } |
| | | catch { |
| | | cruiseData.value = [] |
| | | } |
| | | finally { |
| | | cruiseLoading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleManageVehicle() { |
| | | isManageTestVehicleModalVisible.value = true |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 右侧:测试车辆 --> |
| | | <!-- 右侧:测试车辆 + 巡游记录 --> |
| | | <div |
| | | class="bg-white p-12px rounded-6px w-[340px] flex flex-col shrink-0 shadow-sm" |
| | | class="w-[340px] flex flex-col shrink-0 gap-12px" |
| | | > |
| | | <!-- 标题与按钮 --> |
| | | <div class="flex items-center justify-between mb-12px shrink-0"> |
| | | <div class="flex items-center"> |
| | | <div class="w-3px h-16px bg-[#0068FF] mr-8px rounded-sm" /> |
| | | <span class="font-bold text-16px text-[#171F26]">测试车辆</span> |
| | | <!-- 测试车辆 --> |
| | | <div |
| | | class="bg-white p-12px rounded-6px flex flex-col flex-1 min-h-0 shadow-sm" |
| | | > |
| | | <!-- 标题与按钮 --> |
| | | <div class="flex items-center justify-between mb-12px shrink-0"> |
| | | <div class="flex items-center"> |
| | | <div class="w-3px h-16px bg-[#0068FF] mr-8px rounded-sm" /> |
| | | <span class="font-bold text-16px text-[#171F26]">测试车辆</span> |
| | | </div> |
| | | <NButton type="primary" secondary @click="handleManageVehicle"> |
| | | 管理测试车辆 |
| | | </NButton> |
| | | </div> |
| | | <NButton type="primary" secondary @click="handleManageVehicle"> |
| | | 管理测试车辆 |
| | | </NButton> |
| | | |
| | | <!-- 表格 --> |
| | | <div class="flex-1 min-h-0"> |
| | | <NDataTable |
| | | class="h-full" |
| | | :columns="vehicleColumns" |
| | | :data="vehicleData" |
| | | :loading="vehicleLoading" |
| | | :row-key="(row) => row.id" |
| | | flex-height |
| | | :bordered="false" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | :src="noDataImg" |
| | | style="width: 123px; height: 125px" |
| | | alt="no data" |
| | | > |
| | | </div> |
| | | </template> |
| | | </NDataTable> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 表格 --> |
| | | <div class="flex-1 min-h-0"> |
| | | <NDataTable |
| | | class="h-full" |
| | | :columns="vehicleColumns" |
| | | :data="vehicleData" |
| | | :loading="vehicleLoading" |
| | | :row-key="(row) => row.id" |
| | | flex-height |
| | | :bordered="false" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | :src="noDataImg" |
| | | style="width: 123px; height: 125px" |
| | | alt="no data" |
| | | > |
| | | </div> |
| | | </template> |
| | | </NDataTable> |
| | | <!-- 巡游记录 --> |
| | | <div |
| | | v-if="cruiseData.length > 0" |
| | | class="bg-white p-12px rounded-6px flex flex-col max-h-[280px] shadow-sm" |
| | | > |
| | | <div class="flex items-center mb-12px shrink-0"> |
| | | <div class="w-3px h-16px bg-[#A034FF] mr-8px rounded-sm" /> |
| | | <span class="font-bold text-16px text-[#171F26]">巡游记录</span> |
| | | <span class="ml-8px text-12px text-[rgba(23,31,38,0.5)]">{{ cruiseData.length }}条</span> |
| | | </div> |
| | | <div class="flex-1 min-h-0"> |
| | | <NDataTable |
| | | class="h-full" |
| | | :columns="cruiseColumns" |
| | | :data="cruiseData" |
| | | :loading="cruiseLoading" |
| | | :row-key="(row) => row.cruiseId" |
| | | flex-height |
| | | :bordered="false" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | import { useBoolean } from '@/hooks' |
| | | import { closeTestPlanDispatch, deleteTestPlan, fetchTestPlanPageList, interruptTestPlanDispatch, resumeTestPlanDispatch } from '@/service/api/testPlanManagement' |
| | | import { getTestPlanStatusLabel, getTestPlanStatusStyle, TEST_PLAN_STATUS_OPTIONS } from './constants' |
| | | import CruiseDialog from './components/CruiseDialog.vue' |
| | | |
| | | const router = useRouter() |
| | | const dialog = useDialog() |
| | |
| | | vehicleNum: number |
| | | orderNum: number |
| | | status: number |
| | | cruiseStatus: number |
| | | } |
| | | |
| | | const initialModel = { |
| | |
| | | vehicleCount: Number(item.vehicleCount ?? 0), |
| | | orderCount: Number(item.orderCount ?? 0), |
| | | status: Number(item.status ?? 0), |
| | | cruiseStatus: Number(item.cruiseStatus ?? 0), |
| | | })) |
| | | count.value = Number(res.data?.total ?? records.length ?? 0) |
| | | } |
| | |
| | | }) |
| | | } |
| | | |
| | | const showCruiseDialog = ref(false) |
| | | const cruiseRow = ref<TestPlanRow | null>(null) |
| | | |
| | | function handleCruise(row: TestPlanRow) { |
| | | cruiseRow.value = row |
| | | showCruiseDialog.value = true |
| | | } |
| | | |
| | | function handleCruiseSuccess() { |
| | | getList() |
| | | } |
| | | |
| | | const columns = computed<DataTableColumns<TestPlanRow>>(() => [ |
| | | { |
| | | title: '序号', |
| | |
| | | width: 120, |
| | | align: 'left', |
| | | render: (row) => { |
| | | return <NTag color={getStatusColor(row.status)} bordered={false} size="small" style="border-radius: 4px;">{getStatusLabel(row.status)}</NTag> |
| | | const tags = [<NTag color={getStatusColor(row.status)} bordered={false} size="small" style="border-radius: 4px;">{getStatusLabel(row.status)}</NTag>] |
| | | if (row.cruiseStatus === 1) { |
| | | tags.push(<NTag type="warning" bordered={false} size="small" style="border-radius: 4px; margin-left: 4px;">巡游中</NTag>) |
| | | } |
| | | return <>{tags}</> |
| | | }, |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 200, |
| | | width: 280, |
| | | align: 'left', |
| | | fixed: 'right', |
| | | render: (row: TestPlanRow) => { |
| | |
| | | if (row.status === 2) { // 测试中 |
| | | actions.push(<NButton text type="primary" onClick={() => handleDetail(row)} v-permission="2026061523">详情</NButton>) |
| | | actions.push(<NButton text type="primary" onClick={() => handleInterrupt(row)} v-permission="2026061525">中断测试</NButton>) |
| | | actions.push(<NButton text type="primary" onClick={() => handleCruise(row)} v-permission="2026061525">{row.cruiseStatus === 1 ? '停止巡游' : '巡游策略'}</NButton>) |
| | | } |
| | | if (row.status === 3) { // 已暂停 |
| | | actions.push(<NButton text type="primary" onClick={() => handleDetail(row)} v-permission="2026061523">详情</NButton>) |
| | |
| | | </n-card> |
| | | |
| | | <OperationLogModal v-model:show="showOperationLogModal" :business-type="57" /> |
| | | <CruiseDialog |
| | | v-model:visible="showCruiseDialog" |
| | | :plan-id="cruiseRow?.id" |
| | | :cruise-status="cruiseRow?.cruiseStatus" |
| | | @success="handleCruiseSuccess" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |