491 lines
15 KiB
Vue
491 lines
15 KiB
Vue
<script lang="ts" setup>
|
||
import { ref, reactive, computed } from 'vue';
|
||
import { IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { useRouter } from 'vue-router';
|
||
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
||
import PaginationPro from '@/components/PaginationPro/index.vue';
|
||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
||
import { mockPriceResults } from './mockPriceResults';
|
||
|
||
defineOptions({
|
||
name: 'PriceCheck',
|
||
});
|
||
|
||
const { t } = useI18n();
|
||
const { tableContainerRef, tableHeight } = useTableContainerHeight();
|
||
const router = useRouter();
|
||
|
||
const formData = reactive({
|
||
destinationCountry: '',
|
||
shippingMethod: '',
|
||
cargoWeight: '',
|
||
length: '',
|
||
width: '',
|
||
height: '',
|
||
cargoType: '普货',
|
||
destinationProvince: '',
|
||
postalCode: '',
|
||
});
|
||
|
||
const processQueryResults = (results: any[]) => {
|
||
const parseDeliveryTime = (timeStr: string) => {
|
||
const match = timeStr.match(/(\d+)-(\d+)/);
|
||
return match ? parseInt(match[2]) : 0;
|
||
};
|
||
const parseTotalCost = (cost: string | number) => {
|
||
if (typeof cost === 'number') return cost;
|
||
const num = parseFloat(cost.toString().replace(/[^\d.]/g, ''));
|
||
return isNaN(num) ? Infinity : num;
|
||
};
|
||
const processed = results.map((item) => ({
|
||
...item,
|
||
maxDeliveryDays: parseDeliveryTime(item.deliveryTime || '0-0'),
|
||
totalCostNum: parseTotalCost(item.totalCost),
|
||
}));
|
||
const minCost = Math.min(...processed.map((item) => item.totalCostNum));
|
||
const minDays = Math.min(...processed.map((item) => item.maxDeliveryDays));
|
||
processed.forEach((item) => {
|
||
item.isPriceOptimal = item.totalCostNum === minCost;
|
||
item.isTimeOptimal = item.maxDeliveryDays === minDays;
|
||
});
|
||
return processed.sort((a, b) => {
|
||
if (a.isPriceOptimal && !b.isPriceOptimal) return -1;
|
||
if (!a.isPriceOptimal && b.isPriceOptimal) return 1;
|
||
if (a.isTimeOptimal && !b.isTimeOptimal) return -1;
|
||
if (!a.isTimeOptimal && b.isTimeOptimal) return 1;
|
||
return 0;
|
||
});
|
||
};
|
||
|
||
const queryResults = ref(processQueryResults([...mockPriceResults]));
|
||
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(30);
|
||
const totalCount = computed(() => queryResults.value.length);
|
||
const processedTableData = computed(() => {
|
||
const start = (currentPage.value - 1) * pageSize.value;
|
||
return queryResults.value.slice(start, start + pageSize.value);
|
||
});
|
||
|
||
const hotColumns = computed(() => [
|
||
{ key: 'shippingMethod', label: t('order.priceCheck.table.shippingMethod'), width: 220 },
|
||
{ key: 'warehouse', label: t('order.priceCheck.table.warehouse'), width: 120 },
|
||
{ key: 'deliveryTime', label: t('order.priceCheck.table.deliveryTime'), width: 100 },
|
||
{ key: 'volumetricWeight', label: t('order.priceCheck.table.volumetricWeight'), width: 100 },
|
||
{ key: 'chargeableWeight', label: t('order.priceCheck.table.chargeableWeight'), width: 100 },
|
||
{ key: 'costFormula', label: t('order.priceCheck.table.costFormula'), width: 150 },
|
||
{ key: 'freight', label: t('order.priceCheck.table.freight'), width: 100 },
|
||
{ key: 'otherFees', label: t('order.priceCheck.table.otherFees'), width: 120 },
|
||
{ key: 'totalCost', label: t('order.priceCheck.table.totalCost'), width: 120 },
|
||
]);
|
||
|
||
/** 序号列占 col 0,数据列从 col 1 起(操作列在末尾,由 HandsontablePro 处理) */
|
||
const customCellRenderer = (row: number, col: number, value: any, rowData: any): string | null => {
|
||
const dataColIndex = col - 1;
|
||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null;
|
||
const colKey = hotColumns.value[dataColIndex].key;
|
||
|
||
if (colKey === 'shippingMethod') {
|
||
let text = rowData?.shippingMethod || '';
|
||
const tags: string[] = [];
|
||
if (rowData?.isPriceOptimal) {
|
||
tags.push(
|
||
'<span style="display:inline-block;padding:1px 6px;margin-left:4px;border-radius:2px;font-size:11px;color:#00b42a;background-color:#e8f8e8;">价格最优</span>'
|
||
);
|
||
}
|
||
if (rowData?.isTimeOptimal) {
|
||
tags.push(
|
||
'<span style="display:inline-block;padding:1px 6px;margin-left:4px;border-radius:2px;font-size:11px;color:#165dff;background-color:#e8f3ff;">时效最优</span>'
|
||
);
|
||
}
|
||
return tags.length > 0 ? text + tags.join('') : text;
|
||
}
|
||
if (colKey === 'freight') {
|
||
return value != null ? `<span style="color:#595959;font-weight:500;">¥${value}</span>` : null;
|
||
}
|
||
if (colKey === 'otherFees') {
|
||
if (value === '-') return `<span style="color:#bfbfbf;">${value}</span>`;
|
||
return value != null ? `<span class="other-fees-cell">¥${value}</span>` : null;
|
||
}
|
||
if (colKey === 'totalCost') {
|
||
return value != null ? `<span class="total-cost-cell">¥${value}</span>` : null;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const actionRenderer = (row: any, rowIndex: number): string => {
|
||
return `<a href="javascript:void(0)" class="order-action-link" data-action="order" data-row-index="${rowIndex}">${t('order.priceCheck.table.orderNow')}</a>`;
|
||
};
|
||
|
||
const countryOptions = ref([
|
||
{ labelKey: 'order.priceCheck.country.US', value: 'US' },
|
||
{ labelKey: 'order.priceCheck.country.UK', value: 'UK' },
|
||
{ labelKey: 'order.priceCheck.country.DE', value: 'DE' },
|
||
{ labelKey: 'order.priceCheck.country.FR', value: 'FR' },
|
||
]);
|
||
|
||
const shippingMethodOptions = ref([
|
||
{ labelKey: 'order.priceCheck.shippingMethod.air', value: 'air' },
|
||
{ labelKey: 'order.priceCheck.shippingMethod.sea', value: 'sea' },
|
||
{ labelKey: 'order.priceCheck.shippingMethod.land', value: 'land' },
|
||
]);
|
||
|
||
const cargoTypeOptions = ref([
|
||
{ labelKey: 'order.priceCheck.cargoType.general', value: '普货' },
|
||
{ labelKey: 'order.priceCheck.cargoType.sensitive', value: '敏感货' },
|
||
{ labelKey: 'order.priceCheck.cargoType.dangerous', value: '危险品' },
|
||
]);
|
||
|
||
const provinceOptions = ref([
|
||
{ labelKey: 'order.priceCheck.province.CA', value: 'CA' },
|
||
{ labelKey: 'order.priceCheck.province.NY', value: 'NY' },
|
||
{ labelKey: 'order.priceCheck.province.TX', value: 'TX' },
|
||
]);
|
||
|
||
const handleQuery = () => {
|
||
// TODO: 替换为接口结果后:queryResults.value = processQueryResults(res.items)
|
||
};
|
||
|
||
const handleReset = () => {
|
||
Object.assign(formData, {
|
||
destinationCountry: '',
|
||
shippingMethod: '',
|
||
cargoWeight: '',
|
||
length: '',
|
||
width: '',
|
||
height: '',
|
||
cargoType: '普货',
|
||
destinationProvince: '',
|
||
postalCode: '',
|
||
});
|
||
};
|
||
|
||
const handleOrder = (item: any) => {
|
||
router.push('/order/create');
|
||
};
|
||
|
||
const handleActionClick = (row: any) => {
|
||
handleOrder(row);
|
||
};
|
||
|
||
const handlePageChange = (page: number) => {
|
||
currentPage.value = page;
|
||
};
|
||
|
||
const handlePageSizeChange = (size: number) => {
|
||
pageSize.value = size;
|
||
currentPage.value = 1;
|
||
};
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<div class="price-check-container">
|
||
<div class="query-form-section">
|
||
<div class="form-container">
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<label class="form-label required">{{ $t('order.priceCheck.form.destinationCountry') }}</label>
|
||
<a-select
|
||
v-model="formData.destinationCountry"
|
||
:placeholder="$t('order.priceCheck.form.destinationCountry.placeholder')"
|
||
class="form-select"
|
||
>
|
||
<a-option v-for="option in countryOptions" :key="option.value" :value="option.value">
|
||
{{ $t(option.labelKey) }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label required">{{ $t('order.priceCheck.form.shippingMethod') }}</label>
|
||
<a-select
|
||
v-model="formData.shippingMethod"
|
||
:placeholder="$t('order.priceCheck.form.shippingMethod.placeholder')"
|
||
class="form-select"
|
||
>
|
||
<a-option v-for="option in shippingMethodOptions" :key="option.value" :value="option.value">
|
||
{{ $t(option.labelKey) }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label required">{{ $t('order.priceCheck.form.cargoWeight') }}</label>
|
||
<a-input
|
||
v-model="formData.cargoWeight"
|
||
:placeholder="$t('order.priceCheck.form.cargoWeight.placeholder')"
|
||
class="form-input"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-item dimensions-group">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.dimensions') }}</label>
|
||
<div class="dimensions-inputs">
|
||
<a-input
|
||
v-model="formData.length"
|
||
:placeholder="$t('order.priceCheck.form.dimensions.length')"
|
||
class="dimension-input"
|
||
/>
|
||
<a-input
|
||
v-model="formData.width"
|
||
:placeholder="$t('order.priceCheck.form.dimensions.width')"
|
||
class="dimension-input"
|
||
/>
|
||
<a-input
|
||
v-model="formData.height"
|
||
:placeholder="$t('order.priceCheck.form.dimensions.height')"
|
||
class="dimension-input"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-item">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.cargoType') }}</label>
|
||
<a-select v-model="formData.cargoType" class="form-select">
|
||
<a-option v-for="option in cargoTypeOptions" :key="option.value" :value="option.value">
|
||
{{ $t(option.labelKey) }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.destinationProvince') }}</label>
|
||
<a-select
|
||
v-model="formData.destinationProvince"
|
||
:placeholder="$t('order.priceCheck.form.destinationProvince.placeholder')"
|
||
class="form-select"
|
||
>
|
||
<a-option v-for="option in provinceOptions" :key="option.value" :value="option.value">
|
||
{{ $t(option.labelKey) }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.postalCode') }}</label>
|
||
<a-input
|
||
v-model="formData.postalCode"
|
||
:placeholder="$t('order.priceCheck.form.postalCode.placeholder')"
|
||
class="form-input"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-item button-group">
|
||
<a-button type="primary" class="query-btn" @click="handleQuery">
|
||
<template #icon>
|
||
<icon-search />
|
||
</template>
|
||
{{ $t('order.priceCheck.actions.query') }}
|
||
</a-button>
|
||
<a-button class="reset-btn" @click="handleReset">
|
||
<template #icon>
|
||
<icon-refresh />
|
||
</template>
|
||
{{ $t('order.priceCheck.actions.reset') }}
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-section">
|
||
<div class="table-toolbar">
|
||
<div class="toolbar-left" />
|
||
<div class="toolbar-right">
|
||
<div class="pagination-controls">
|
||
<PaginationPro
|
||
:total-count="totalCount"
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:page-sizes="[10, 30, 50, 100]"
|
||
@page-change="handlePageChange"
|
||
@page-size-change="handlePageSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div ref="tableContainerRef" class="list-wrap">
|
||
<HandsontablePro
|
||
:columns="hotColumns"
|
||
:data="processedTableData"
|
||
:total="totalCount"
|
||
:current-page="currentPage"
|
||
:page-size="pageSize"
|
||
:show-action-column="true"
|
||
action-column-position="end"
|
||
:action-column-width="120"
|
||
:action-renderer="actionRenderer"
|
||
:custom-cell-renderer="customCellRenderer"
|
||
:height="tableHeight"
|
||
:stretch-columns="true"
|
||
@action-click="handleActionClick"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="less" scoped>
|
||
.price-check-container {
|
||
padding: 16px;
|
||
background-color: #f0f3f4;
|
||
min-height: 100vh;
|
||
height: auto;
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.query-form-section {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 16px 20px;
|
||
margin-bottom: 16px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.form-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: flex-end;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.form-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
flex: 0 0 200px;
|
||
|
||
&.dimensions-group {
|
||
flex: 0 0 300px;
|
||
}
|
||
|
||
&.button-group {
|
||
flex: 0 0 300px;
|
||
flex-direction: row;
|
||
gap: 8px;
|
||
align-items: flex-end;
|
||
}
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #262626;
|
||
|
||
&.required::after {
|
||
content: '*';
|
||
color: #f53f3f;
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
|
||
.form-select,
|
||
.form-input {
|
||
width: 100%;
|
||
}
|
||
|
||
.dimensions-inputs {
|
||
display: flex;
|
||
gap: 8px;
|
||
|
||
.dimension-input {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.query-btn,
|
||
.reset-btn {
|
||
height: 32px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.reset-btn {
|
||
border-color: #d9d9d9;
|
||
color: #262626;
|
||
|
||
&:hover {
|
||
border-color: var(--app-primary-color);
|
||
color: var(--app-primary-color);
|
||
}
|
||
}
|
||
|
||
.table-section {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
flex: 0 0 auto;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.table-toolbar {
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.list-wrap {
|
||
width: 100%;
|
||
flex: 0 0 auto;
|
||
height: 900px;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
overscroll-behavior: contain;
|
||
touch-action: pan-x pan-y;
|
||
}
|
||
|
||
:deep(.order-action-link) {
|
||
color: var(--app-primary-color);
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
padding: 4px 12px;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
|
||
&:hover {
|
||
opacity: 0.85;
|
||
}
|
||
}
|
||
|
||
:deep(.total-cost-cell) {
|
||
color: #f53f3f;
|
||
font-weight: 600;
|
||
font-size: 15px;
|
||
}
|
||
|
||
:deep(.other-fees-cell) {
|
||
color: var(--app-primary-color);
|
||
font-weight: 500;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.form-item {
|
||
flex: 1 1 calc(50% - 10px);
|
||
min-width: 180px;
|
||
}
|
||
}
|
||
</style>
|