1819 lines
64 KiB
Vue
1819 lines
64 KiB
Vue
<script lang="ts" setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue';
|
||
import { IconSearch, IconDown, IconUp, IconInfoCircle, IconQuestionCircle, IconClose } from '@arco-design/web-vue/es/icon';
|
||
import { Message } from '@arco-design/web-vue';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { useRouter } from 'vue-router';
|
||
import PaginationPro from '@/components/PaginationPro/index.vue';
|
||
import { postOrderPriceCheck, getOrderPriceCheckShippingOptions, type PriceCheckQueryHint, type PriceCheckShippingGroup, type CouponAvailableOutput } from '@/api/order';
|
||
import { getCountryList } from '@/api/system/country';
|
||
import { getDictListAll } from '@/api/system/dict';
|
||
import { getWarehouseList } from '@/api/system/warehouse';
|
||
import { useCurrentWarehouseStore } from '@/store';
|
||
import ChannelGuidePanel from './components/ChannelGuidePanel.vue';
|
||
import ShippingMethodTabSelect from './components/ShippingMethodTabSelect.vue';
|
||
import AiRecommendCard from './components/AiRecommendCard.vue';
|
||
import QuoteResultCard from './components/QuoteResultCard.vue';
|
||
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||
import { recommendWithAi, type QuoteCandidate, type RecommendAlternative, type ShipVerdict } from './recommendChannel';
|
||
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute';
|
||
import { openPriceAskAdvisor, type PriceAskIntent } from '@/composables/priceAskAdvisor';
|
||
import {
|
||
formatPriceCheckFailures,
|
||
normalizePriceCheckFailures,
|
||
type PriceCheckFailureItem,
|
||
} from './priceCheckFailures';
|
||
|
||
defineOptions({
|
||
name: 'PriceCheck',
|
||
});
|
||
|
||
const PRICE_CHECK_ORDER_DRAFT_KEY = 'oms_price_check_order_draft';
|
||
|
||
const { t } = useI18n();
|
||
const router = useRouter();
|
||
const currentWarehouseStore = useCurrentWarehouseStore();
|
||
|
||
/** 首屏常用国精简,避免芯片墙;其余靠搜索 */
|
||
const countryShortcutCodes: string[] = ['US', 'GB', 'DE', 'CA', 'FR', 'ES'];
|
||
|
||
const formData = reactive({
|
||
destinationCountry: '',
|
||
shippingMethod: '',
|
||
cargoWeight: '1',
|
||
length: '',
|
||
width: '',
|
||
height: '',
|
||
/** 货物属性(单选;空表示全部) */
|
||
cargoType: '' as string,
|
||
/** 仓库(单选;空表示全部) */
|
||
warehouseId: undefined as number | undefined,
|
||
postalCode: '',
|
||
destinationProvince: '',
|
||
destinationCity: '',
|
||
});
|
||
|
||
/** 是否已发起过查询(用于空态 / 结果区切换) */
|
||
const hasQueried = ref(false);
|
||
const postalInputRef = ref<HTMLElement | null>(null);
|
||
|
||
function parseOptionalDimension(value: unknown) {
|
||
const n = parseFloat(String(value ?? '').trim());
|
||
return Number.isFinite(n) && n > 0 ? n : undefined;
|
||
}
|
||
|
||
function hasFullDimensions() {
|
||
return [formData.length, formData.width, formData.height].every((v) => parseOptionalDimension(v) != null);
|
||
}
|
||
|
||
function hasPartialDimensions() {
|
||
const filled = [formData.length, formData.width, formData.height].filter((v) => String(v ?? '').trim() !== '').length;
|
||
return filled > 0 && !hasFullDimensions();
|
||
}
|
||
|
||
/** 更多条件默认折叠,各项空值表示「全部」 */
|
||
const showAdvanced = ref(false);
|
||
const sortMode = ref<'recommend' | 'price' | 'time'>('recommend');
|
||
const sortTabItems = computed(() => [
|
||
{ key: 'recommend', label: t('order.priceCheck.sort.recommend') },
|
||
{ key: 'price', label: t('order.priceCheck.sort.price') },
|
||
{ key: 'time', label: t('order.priceCheck.sort.time') },
|
||
]);
|
||
const selectedGuideRecord = ref<Record<string, any> | null>(null);
|
||
const recommendedRowKey = ref('');
|
||
const recommendReason = ref('');
|
||
const recommendVerdict = ref<ShipVerdict | ''>('');
|
||
const recommendEvidence = ref<string[]>([]);
|
||
const recommendCargoName = ref('');
|
||
const recommendAlternatives = ref<RecommendAlternative[]>([]);
|
||
const aiRecommendLoading = ref(false);
|
||
const queryHint = ref<PriceCheckQueryHint | null>(null);
|
||
const hintBannerDismissed = ref(false);
|
||
const couponInstanceId = ref<number | undefined>(undefined);
|
||
const availableCoupons = ref<CouponAvailableOutput[]>([]);
|
||
|
||
const needPostalHint = computed(() => {
|
||
if (!hasQueried.value) return false;
|
||
if (String(formData.postalCode || '').trim()) return false;
|
||
const missing = queryHint.value?.missingFieldCodes ?? [];
|
||
return (
|
||
queryHint.value?.quoteLevel === 'estimated' ||
|
||
missing.includes('postalCode') ||
|
||
missing.includes('PostalCode')
|
||
);
|
||
});
|
||
|
||
/** 仅「更多条件」里的筛选项摘要(主表单字段不再重复展示) */
|
||
type AdvancedConditionChip = {
|
||
key: string;
|
||
label: string;
|
||
kind:
|
||
| 'dimensions'
|
||
| 'destinationProvince'
|
||
| 'destinationCity'
|
||
| 'cargoType'
|
||
| 'warehouse'
|
||
| 'coupon';
|
||
value?: string | number;
|
||
};
|
||
|
||
const advancedConditionChips = computed((): AdvancedConditionChip[] => {
|
||
const chips: AdvancedConditionChip[] = [];
|
||
|
||
if (hasFullDimensions()) {
|
||
chips.push({
|
||
key: 'dimensions',
|
||
kind: 'dimensions',
|
||
label: `${formData.length}×${formData.width}×${formData.height}cm`,
|
||
});
|
||
}
|
||
|
||
const province = String(formData.destinationProvince || '').trim();
|
||
if (province) {
|
||
chips.push({
|
||
key: 'destinationProvince',
|
||
kind: 'destinationProvince',
|
||
value: province,
|
||
label: `${t('order.priceCheck.form.destinationProvince')}: ${province}`,
|
||
});
|
||
}
|
||
|
||
const city = String(formData.destinationCity || '').trim();
|
||
if (city) {
|
||
chips.push({
|
||
key: 'destinationCity',
|
||
kind: 'destinationCity',
|
||
value: city,
|
||
label: `${t('order.priceCheck.form.destinationCity')}: ${city}`,
|
||
});
|
||
}
|
||
|
||
if (formData.cargoType) {
|
||
const hit = cargoTypeOptions.value.find((c) => c.value === formData.cargoType);
|
||
chips.push({
|
||
key: `cargoType:${formData.cargoType}`,
|
||
kind: 'cargoType',
|
||
value: formData.cargoType,
|
||
label: hit?.label || formData.cargoType,
|
||
});
|
||
}
|
||
|
||
if (formData.warehouseId != null) {
|
||
const hit = warehouseOptions.value.find((w) => w.value === formData.warehouseId);
|
||
chips.push({
|
||
key: `warehouse:${formData.warehouseId}`,
|
||
kind: 'warehouse',
|
||
value: formData.warehouseId,
|
||
label: hit?.label || String(formData.warehouseId),
|
||
});
|
||
}
|
||
|
||
if (couponInstanceId.value) {
|
||
const c = availableCoupons.value.find((x) => (x.id ?? x.Id) === couponInstanceId.value);
|
||
const name = String(c?.templateName ?? c?.TemplateName ?? '').trim();
|
||
chips.push({
|
||
key: 'coupon',
|
||
kind: 'coupon',
|
||
value: couponInstanceId.value,
|
||
label: name || t('marketing.priceCheck.couponLabel'),
|
||
});
|
||
}
|
||
|
||
return chips;
|
||
});
|
||
|
||
function removeAdvancedChip(chip: AdvancedConditionChip) {
|
||
if (chip.kind === 'dimensions') {
|
||
formData.length = '';
|
||
formData.width = '';
|
||
formData.height = '';
|
||
return;
|
||
}
|
||
if (chip.kind === 'destinationProvince') {
|
||
formData.destinationProvince = '';
|
||
return;
|
||
}
|
||
if (chip.kind === 'destinationCity') {
|
||
formData.destinationCity = '';
|
||
return;
|
||
}
|
||
if (chip.kind === 'cargoType') {
|
||
formData.cargoType = '';
|
||
return;
|
||
}
|
||
if (chip.kind === 'warehouse') {
|
||
formData.warehouseId = undefined;
|
||
return;
|
||
}
|
||
if (chip.kind === 'coupon') {
|
||
couponInstanceId.value = undefined;
|
||
}
|
||
}
|
||
|
||
function clearAdvancedConditions() {
|
||
formData.length = '';
|
||
formData.width = '';
|
||
formData.height = '';
|
||
formData.destinationProvince = '';
|
||
formData.destinationCity = '';
|
||
formData.cargoType = '';
|
||
formData.warehouseId = undefined;
|
||
couponInstanceId.value = undefined;
|
||
}
|
||
|
||
function openAdvancedFilters() {
|
||
showAdvanced.value = true;
|
||
}
|
||
|
||
/** 仓库默认取顶栏当前仓;货物属性默认普货 */
|
||
function applyDefaultFilters() {
|
||
const id = currentWarehouseStore.resolveDefaultWarehouseId();
|
||
if (!id) {
|
||
formData.warehouseId = undefined;
|
||
} else {
|
||
const hit = warehouseOptions.value.find((w) => w.value === id);
|
||
formData.warehouseId = hit?.value;
|
||
}
|
||
|
||
if (!formData.cargoType && cargoTypeOptions.value.length) {
|
||
const general = cargoTypeOptions.value.find(
|
||
(c) => /普货|general/i.test(c.label) || /^(general|general_cargo)$/i.test(c.value),
|
||
);
|
||
formData.cargoType = (general || cargoTypeOptions.value[0]).value;
|
||
}
|
||
}
|
||
|
||
function focusPostalAndHint() {
|
||
hintBannerDismissed.value = false;
|
||
postalInputRef.value?.scrollIntoView?.({ behavior: 'smooth', block: 'center' });
|
||
const el = postalInputRef.value?.querySelector?.('input') as HTMLInputElement | null;
|
||
el?.focus?.();
|
||
}
|
||
|
||
const showQueryHintBanner = computed(
|
||
() =>
|
||
Boolean(queryHint.value?.disclaimers?.length) ||
|
||
queryHint.value?.quoteLevel === 'estimated',
|
||
);
|
||
|
||
const queryHintDisclaimers = computed(() => {
|
||
const fromApi = queryHint.value?.disclaimers ?? [];
|
||
if (fromApi.length) return fromApi;
|
||
const codes = queryHint.value?.missingFieldCodes ?? [];
|
||
return codes.map((code) => t(`order.priceCheck.hint.missing.${code}`)).filter(Boolean);
|
||
});
|
||
|
||
const queryHintSummary = computed(() => {
|
||
if (!showQueryHintBanner.value) return '';
|
||
return t('order.priceCheck.hint.summaryInline');
|
||
});
|
||
|
||
function couponOptionLabel(c: CouponAvailableOutput) {
|
||
const id = c.id ?? c.Id;
|
||
const name = c.templateName ?? c.TemplateName ?? `#${id}`;
|
||
const face = c.faceValue ?? c.FaceValue;
|
||
const reason = c.unavailableReason ?? c.UnavailableReason;
|
||
const available = c.available ?? c.Available ?? !reason;
|
||
const suffix = available ? (face != null ? ` (-¥${face})` : '') : ` (${reason || t('marketing.priceCheck.couponUnavailable')})`;
|
||
return `${name}${suffix}`;
|
||
}
|
||
|
||
async function handleCouponChange(val: number | undefined) {
|
||
couponInstanceId.value = val;
|
||
if (formData.destinationCountry && String(formData.cargoWeight ?? '').trim()) {
|
||
await handleQuery();
|
||
if (val) Message.info(t('marketing.priceCheck.couponChanged'));
|
||
}
|
||
}
|
||
|
||
function normalizeStringList(raw: unknown): string[] {
|
||
if (!Array.isArray(raw)) return [];
|
||
return raw.map((x) => String(x ?? '').trim()).filter(Boolean);
|
||
}
|
||
|
||
/** 兼容查价接口 camelCase / PascalCase */
|
||
type PriceCheckOtherFee = {
|
||
costItem: string;
|
||
name: string;
|
||
amount: number;
|
||
formulaPart?: string;
|
||
};
|
||
|
||
function formatMoneyAmount(value: string | number | null | undefined) {
|
||
if (value == null || value === '') return '';
|
||
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
||
if (Number.isNaN(num)) return String(value);
|
||
if (Number.isInteger(num)) return String(num);
|
||
return num.toFixed(4).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '');
|
||
}
|
||
|
||
function resolveCargoTypeLabel(code?: string, fallbackName?: string) {
|
||
const normalizedCode = String(code ?? '').trim();
|
||
const fromDict = cargoTypeOptions.value.find(
|
||
(option) => option.value.toLowerCase() === normalizedCode.toLowerCase()
|
||
)?.label;
|
||
if (fromDict) return fromDict;
|
||
const fallback = String(fallbackName ?? '').trim();
|
||
if (fallback) return fallback;
|
||
return normalizedCode;
|
||
}
|
||
|
||
function normalizeOtherFeeList(raw: any): PriceCheckOtherFee[] {
|
||
const list = raw?.otherFeeList ?? raw?.OtherFeeList ?? [];
|
||
if (!Array.isArray(list)) return [];
|
||
return list
|
||
.map((item) => ({
|
||
costItem: String(item?.costItem ?? item?.CostItem ?? '').trim(),
|
||
name: String(item?.name ?? item?.Name ?? item?.costItem ?? item?.CostItem ?? '').trim(),
|
||
amount: Number(item?.amount ?? item?.Amount ?? 0),
|
||
formulaPart: String(item?.formulaPart ?? item?.FormulaPart ?? '').trim(),
|
||
}))
|
||
.filter((item) => item.name || item.costItem);
|
||
}
|
||
|
||
function buildOtherFeesTooltip(fees: PriceCheckOtherFee[]) {
|
||
if (!fees.length) return '';
|
||
return fees
|
||
.map((fee) => `${fee.name || fee.costItem}:¥${formatMoneyAmount(fee.amount)}`)
|
||
.join('\n');
|
||
}
|
||
|
||
function normalizePriceRow(raw: any) {
|
||
const cargoType = raw.cargoType ?? raw.CargoType ?? '';
|
||
const cargoTypeNameRaw = raw.cargoTypeName ?? raw.CargoTypeName ?? raw.cargoType ?? raw.CargoType ?? '';
|
||
const otherFeeList = normalizeOtherFeeList(raw);
|
||
return {
|
||
...raw,
|
||
productId: raw.id ?? raw.Id ?? raw.productId ?? raw.ProductId,
|
||
productCode: raw.productCode ?? raw.ProductCode ?? '',
|
||
cargoType,
|
||
cargoTypeName: resolveCargoTypeLabel(String(cargoType), String(cargoTypeNameRaw)),
|
||
shippingMethod: raw.shippingMethod ?? raw.ShippingMethod ?? '',
|
||
warehouseId: raw.warehouseId ?? raw.WarehouseId,
|
||
warehouse: raw.warehouse ?? raw.Warehouse ?? '',
|
||
deliveryTime: raw.deliveryTime ?? raw.DeliveryTime ?? '',
|
||
tradeTerms: String(raw.tradeTerms ?? raw.TradeTerms ?? '')
|
||
.trim()
|
||
.toUpperCase(),
|
||
productGroup: String(raw.productGroup ?? raw.ProductGroup ?? '').trim(),
|
||
productGroupName: String(
|
||
raw.productGroupName ?? raw.ProductGroupName ?? raw.productGroup ?? raw.ProductGroup ?? '',
|
||
).trim(),
|
||
productDescription: String(
|
||
raw.productDescription ?? raw.ProductDescription ?? '',
|
||
).trim(),
|
||
volumetricWeight: raw.volumetricWeight ?? raw.VolumetricWeight ?? '',
|
||
chargeableWeight: raw.chargeableWeight ?? raw.ChargeableWeight ?? '',
|
||
costFormula: raw.costFormula ?? raw.CostFormula ?? '',
|
||
freight: raw.freight ?? raw.Freight ?? '',
|
||
otherFees: raw.otherFees ?? raw.OtherFees ?? '',
|
||
otherFeeList,
|
||
otherFeesTooltip: buildOtherFeesTooltip(otherFeeList),
|
||
totalCost: raw.totalCost ?? raw.TotalCost ?? '',
|
||
quoteLevel: String(raw.quoteLevel ?? raw.QuoteLevel ?? 'exact').toLowerCase(),
|
||
missingFieldCodes: normalizeStringList(raw.missingFieldCodes ?? raw.MissingFieldCodes),
|
||
disclaimers: normalizeStringList(raw.disclaimers ?? raw.Disclaimers),
|
||
zoneName: raw.zoneName ?? raw.ZoneName ?? '',
|
||
};
|
||
}
|
||
|
||
const parseDeliveryTime = (timeStr: string) => {
|
||
const match = String(timeStr || '').match(/(\d+)-(\d+)/);
|
||
return match ? parseInt(match[2], 10) : 0;
|
||
};
|
||
|
||
const parseTotalCost = (cost: string | number | undefined | null) => {
|
||
if (cost == null || cost === '') return Infinity;
|
||
if (typeof cost === 'number') return cost;
|
||
const num = parseFloat(String(cost).replace(/[^\d.]/g, ''));
|
||
return Number.isNaN(num) ? Infinity : num;
|
||
};
|
||
|
||
function applyOptimalFlags(list: any[]) {
|
||
if (!list.length) return list;
|
||
const minCost = Math.min(...list.map((item) => item.totalCostNum));
|
||
const minDays = Math.min(...list.map((item) => item.maxDeliveryDays));
|
||
list.forEach((item) => {
|
||
item.isPriceOptimal = item.totalCostNum === minCost;
|
||
item.isTimeOptimal = item.maxDeliveryDays === minDays;
|
||
});
|
||
return list;
|
||
}
|
||
|
||
const processQueryResults = (results: any[]) => {
|
||
if (!results?.length) return [];
|
||
const processed = results.map((item, index) => {
|
||
const row = normalizePriceRow(item);
|
||
const cardKey = [
|
||
row.productId ?? row.id ?? 'p',
|
||
row.productCode || 'code',
|
||
row.cargoType ?? 'c',
|
||
row.warehouseId ?? row.warehouse ?? 'w',
|
||
index,
|
||
].join('-');
|
||
return {
|
||
...row,
|
||
maxDeliveryDays: parseDeliveryTime(row.deliveryTime || '0-0'),
|
||
totalCostNum: parseTotalCost(row.totalCost as any),
|
||
cardKey,
|
||
rowKey: cardKey,
|
||
};
|
||
});
|
||
applyOptimalFlags(processed);
|
||
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<any[]>([]);
|
||
const queryLoading = ref(false);
|
||
const queryFailureText = ref('');
|
||
const queryFailureItems = ref<PriceCheckFailureItem[]>([]);
|
||
/** 单卡切换货物属性 / 仓库时的加载态 */
|
||
const recalcLoadingKeys = ref<Record<string, boolean>>({});
|
||
|
||
function failureStepLabel(step: string) {
|
||
const key = `order.priceCheck.guide.failureStep.${step}`;
|
||
const label = t(key);
|
||
return label === key ? step : label;
|
||
}
|
||
|
||
function applyPriceCheckFailures(failures: unknown[]) {
|
||
queryFailureItems.value = normalizePriceCheckFailures((failures ?? []) as any);
|
||
queryFailureText.value =
|
||
formatPriceCheckFailures((failures ?? []) as any, failureStepLabel) ||
|
||
t('order.priceCheck.table.empty');
|
||
}
|
||
|
||
function clearPriceCheckFailures() {
|
||
queryFailureItems.value = [];
|
||
queryFailureText.value = '';
|
||
}
|
||
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(10);
|
||
const totalCount = computed(() => queryResults.value.length);
|
||
|
||
const sortedResults = computed(() => {
|
||
const list = [...queryResults.value];
|
||
if (sortMode.value === 'price') {
|
||
return list.sort((a, b) => a.totalCostNum - b.totalCostNum);
|
||
}
|
||
if (sortMode.value === 'time') {
|
||
return list.sort((a, b) => a.maxDeliveryDays - b.maxDeliveryDays || a.totalCostNum - b.totalCostNum);
|
||
}
|
||
return list;
|
||
});
|
||
|
||
const processedTableData = computed(() => {
|
||
const start = (currentPage.value - 1) * pageSize.value;
|
||
return sortedResults.value.slice(start, start + pageSize.value);
|
||
});
|
||
|
||
type SelectOption = { value: string; label: string };
|
||
|
||
const countryOptions = ref<SelectOption[]>([]);
|
||
const shippingMethodGroups = ref<PriceCheckShippingGroup[]>([]);
|
||
const cargoTypeOptions = ref<SelectOption[]>([]);
|
||
const warehouseOptions = ref<Array<{ value: number; label: string }>>([]);
|
||
const optionsLoading = ref(false);
|
||
|
||
function normalizeCountryRow(row: any) {
|
||
const code = String(row?.countryCode ?? row?.CountryCode ?? '').trim();
|
||
const name = String(row?.chineseName ?? row?.ChineseName ?? '').trim();
|
||
return { code, name };
|
||
}
|
||
|
||
function normalizeDictRow(row: any) {
|
||
const code = String(row?.dictCode ?? row?.DictCode ?? '').trim();
|
||
const name = String(row?.dictName ?? row?.DictName ?? '').trim();
|
||
const sort = Number(row?.dictSort ?? row?.DictSort ?? 0);
|
||
return { code, name, sort: Number.isFinite(sort) ? sort : 0 };
|
||
}
|
||
|
||
function normalizeWarehouseRow(row: any) {
|
||
const id = Number(row?.id ?? row?.Id);
|
||
const name = String(row?.warehouseName ?? row?.WarehouseName ?? '').trim();
|
||
return { id, name };
|
||
}
|
||
|
||
async function loadPriceCheckOptions() {
|
||
optionsLoading.value = true;
|
||
try {
|
||
const [countryRes, shippingGroups, dictRows, warehouseRes] = await Promise.all([
|
||
getCountryList(),
|
||
getOrderPriceCheckShippingOptions(),
|
||
getDictListAll({ dictType: 'cargo_attribute' }),
|
||
getWarehouseList({ page: 1, pageSize: 500 }),
|
||
]);
|
||
const countries = (countryRes.items ?? [])
|
||
.map(normalizeCountryRow)
|
||
.filter((c) => c.code)
|
||
.map((c) => ({
|
||
value: c.code,
|
||
label: c.name ? `${c.name} ${c.code}` : c.code,
|
||
}));
|
||
countryOptions.value = countries;
|
||
|
||
shippingMethodGroups.value = shippingGroups ?? [];
|
||
|
||
const cargoOpts = (dictRows ?? [])
|
||
.map(normalizeDictRow)
|
||
.filter((d) => d.code)
|
||
.sort((a, b) => a.sort - b.sort || a.code.localeCompare(b.code))
|
||
.map((d) => ({
|
||
value: d.code,
|
||
label: d.name || d.code,
|
||
}));
|
||
cargoTypeOptions.value = cargoOpts;
|
||
|
||
warehouseOptions.value = (warehouseRes.items ?? [])
|
||
.map(normalizeWarehouseRow)
|
||
.filter((w) => Number.isFinite(w.id) && w.id > 0)
|
||
.map((w) => ({
|
||
value: w.id,
|
||
label: w.name || String(w.id),
|
||
}));
|
||
applyDefaultFilters();
|
||
} catch (e: any) {
|
||
Message.error(e?.message || '加载筛选项失败');
|
||
countryOptions.value = [];
|
||
shippingMethodGroups.value = [];
|
||
cargoTypeOptions.value = [];
|
||
warehouseOptions.value = [];
|
||
} finally {
|
||
optionsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadPriceCheckOptions();
|
||
});
|
||
|
||
const handleQuery = async (e?: Event) => {
|
||
e?.preventDefault?.();
|
||
e?.stopPropagation?.();
|
||
if (!formData.destinationCountry || String(formData.cargoWeight ?? '').trim() === '') {
|
||
Message.warning(t('order.priceCheck.validate.requiredBasic'));
|
||
return;
|
||
}
|
||
if (hasPartialDimensions()) {
|
||
Message.warning(t('order.priceCheck.validate.partialDimensions'));
|
||
return;
|
||
}
|
||
hasQueried.value = true;
|
||
queryLoading.value = true;
|
||
clearPriceCheckFailures();
|
||
queryHint.value = null;
|
||
hintBannerDismissed.value = false;
|
||
try {
|
||
const length = parseOptionalDimension(formData.length);
|
||
const width = parseOptionalDimension(formData.width);
|
||
const height = parseOptionalDimension(formData.height);
|
||
const { items, failures, queryHint: hint, availableCoupons: coupons } = await postOrderPriceCheck({
|
||
destinationCountry: formData.destinationCountry,
|
||
shippingMethod: formData.shippingMethod || '',
|
||
cargoWeight: String(formData.cargoWeight).trim(),
|
||
...(length != null && width != null && height != null
|
||
? { length: String(length), width: String(width), height: String(height) }
|
||
: {}),
|
||
cargoTypes: formData.cargoType ? [formData.cargoType] : undefined,
|
||
warehouseIds: formData.warehouseId != null ? [formData.warehouseId] : undefined,
|
||
destinationProvince: formData.destinationProvince?.trim() || '',
|
||
destinationCity: formData.destinationCity?.trim() || '',
|
||
postalCode: formData.postalCode?.trim() || '',
|
||
couponInstanceId: couponInstanceId.value,
|
||
});
|
||
queryHint.value = hint;
|
||
availableCoupons.value = coupons ?? [];
|
||
hintBannerDismissed.value = false;
|
||
queryResults.value = processQueryResults(items || []);
|
||
resetRecommendState();
|
||
sortMode.value = 'recommend';
|
||
currentPage.value = 1;
|
||
recalcLoadingKeys.value = {};
|
||
if (!queryResults.value.length) {
|
||
selectedGuideRecord.value = null;
|
||
applyPriceCheckFailures(failures ?? []);
|
||
} else {
|
||
selectedGuideRecord.value = sortedResults.value[0] ?? queryResults.value[0];
|
||
clearPriceCheckFailures();
|
||
Message.success(t('order.priceCheck.message.querySuccess', { count: queryResults.value.length }));
|
||
}
|
||
} catch (e: any) {
|
||
const errText = e?.message || t('order.priceCheck.message.queryFailed');
|
||
queryFailureItems.value = [];
|
||
queryFailureText.value = errText;
|
||
queryResults.value = [];
|
||
selectedGuideRecord.value = null;
|
||
resetRecommendState();
|
||
} finally {
|
||
queryLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const handleReset = () => {
|
||
Object.assign(formData, {
|
||
destinationCountry: '',
|
||
shippingMethod: '',
|
||
cargoWeight: '1',
|
||
length: '',
|
||
width: '',
|
||
height: '',
|
||
cargoType: '',
|
||
warehouseId: undefined as number | undefined,
|
||
postalCode: '',
|
||
destinationProvince: '',
|
||
destinationCity: '',
|
||
});
|
||
applyDefaultFilters();
|
||
hasQueried.value = false;
|
||
queryResults.value = [];
|
||
queryHint.value = null;
|
||
hintBannerDismissed.value = false;
|
||
selectedGuideRecord.value = null;
|
||
resetRecommendState();
|
||
currentPage.value = 1;
|
||
clearPriceCheckFailures();
|
||
sortMode.value = 'recommend';
|
||
couponInstanceId.value = undefined;
|
||
availableCoupons.value = [];
|
||
recalcLoadingKeys.value = {};
|
||
};
|
||
|
||
/** 单卡切换货物属性 / 仓库后,仅重算该销售产品报价 */
|
||
async function handleCardRecalc(
|
||
record: Record<string, any>,
|
||
payload: { cargoType: string; warehouseId: number },
|
||
) {
|
||
if (!formData.destinationCountry || String(formData.cargoWeight ?? '').trim() === '') {
|
||
Message.warning(t('order.priceCheck.validate.requiredBasic'));
|
||
return;
|
||
}
|
||
if (hasPartialDimensions()) {
|
||
Message.warning(t('order.priceCheck.validate.partialDimensions'));
|
||
return;
|
||
}
|
||
const cardKey = String(record.cardKey || record.rowKey || '');
|
||
const productCode = String(record.productCode || '').trim();
|
||
if (!cardKey || !productCode) {
|
||
Message.warning(t('order.priceCheck.message.recalcFailed'));
|
||
return;
|
||
}
|
||
if (recalcLoadingKeys.value[cardKey]) return;
|
||
|
||
recalcLoadingKeys.value = { ...recalcLoadingKeys.value, [cardKey]: true };
|
||
try {
|
||
const length = parseOptionalDimension(formData.length);
|
||
const width = parseOptionalDimension(formData.width);
|
||
const height = parseOptionalDimension(formData.height);
|
||
const { items, failures } = await postOrderPriceCheck({
|
||
destinationCountry: formData.destinationCountry,
|
||
shippingMethod: productCode,
|
||
cargoWeight: String(formData.cargoWeight).trim(),
|
||
...(length != null && width != null && height != null
|
||
? { length: String(length), width: String(width), height: String(height) }
|
||
: {}),
|
||
cargoTypes: [payload.cargoType],
|
||
warehouseIds: [payload.warehouseId],
|
||
destinationProvince: formData.destinationProvince?.trim() || '',
|
||
destinationCity: formData.destinationCity?.trim() || '',
|
||
postalCode: formData.postalCode?.trim() || '',
|
||
couponInstanceId: couponInstanceId.value,
|
||
});
|
||
const nextRows = processQueryResults(items || []);
|
||
const replacement =
|
||
nextRows.find((row) => String(row.productCode || '').trim() === productCode) || nextRows[0];
|
||
if (!replacement) {
|
||
const failureText = formatPriceCheckFailures(failures ?? [], failureStepLabel);
|
||
Message.warning(failureText || t('order.priceCheck.message.recalcNoQuote'));
|
||
return;
|
||
}
|
||
replacement.cardKey = cardKey;
|
||
replacement.rowKey = cardKey;
|
||
|
||
const idx = queryResults.value.findIndex(
|
||
(item) => String(item.cardKey || item.rowKey) === cardKey,
|
||
);
|
||
if (idx < 0) return;
|
||
|
||
const updated = [...queryResults.value];
|
||
updated[idx] = replacement;
|
||
applyOptimalFlags(updated);
|
||
queryResults.value = updated;
|
||
|
||
if (
|
||
selectedGuideRecord.value &&
|
||
String(selectedGuideRecord.value.cardKey || selectedGuideRecord.value.rowKey) === cardKey
|
||
) {
|
||
selectedGuideRecord.value = replacement;
|
||
}
|
||
if (recommendedRowKey.value === cardKey || recommendedRowKey.value === record.rowKey) {
|
||
recommendedRowKey.value = cardKey;
|
||
}
|
||
Message.success(t('order.priceCheck.message.recalcSuccess'));
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('order.priceCheck.message.recalcFailed'));
|
||
} finally {
|
||
const next = { ...recalcLoadingKeys.value };
|
||
delete next[cardKey];
|
||
recalcLoadingKeys.value = next;
|
||
}
|
||
}
|
||
|
||
function buildAskContext(item: any) {
|
||
const cargoTypeName =
|
||
cargoTypeOptions.value.find((o) => o.value === String(item?.cargoType ?? ''))?.label ||
|
||
String(item?.cargoTypeName ?? item?.cargoType ?? '').trim();
|
||
return {
|
||
destinationCountry: String(formData.destinationCountry || '').trim(),
|
||
cargoWeight: formData.cargoWeight,
|
||
postalCode: formData.postalCode,
|
||
length: formData.length,
|
||
width: formData.width,
|
||
height: formData.height,
|
||
productCode: String(item?.productCode ?? '').trim(),
|
||
shippingMethod: String(item?.shippingMethod ?? '').trim(),
|
||
cargoType: String(item?.cargoType ?? '').trim(),
|
||
cargoTypeName,
|
||
warehouse: String(item?.warehouse ?? '').trim(),
|
||
totalCost: item?.totalCost,
|
||
deliveryTime: item?.deliveryTime,
|
||
quoteLevel: String(item?.quoteLevel ?? item?.QuoteLevel ?? queryHint.value?.quoteLevel ?? '').trim(),
|
||
};
|
||
}
|
||
|
||
function handleAskAi(item: any, intent: PriceAskIntent) {
|
||
openPriceAskAdvisor(router, intent, buildAskContext(item));
|
||
}
|
||
|
||
const handleOrder = (item: any) => {
|
||
const rowDisclaimers = normalizeStringList(item?.disclaimers ?? item?.Disclaimers);
|
||
const hintDisclaimers = normalizeStringList(queryHint.value?.disclaimers);
|
||
const disclaimers = [...new Set([...rowDisclaimers, ...hintDisclaimers])];
|
||
const quoteLevel = String(item?.quoteLevel ?? item?.QuoteLevel ?? queryHint.value?.quoteLevel ?? '')
|
||
.trim()
|
||
.toLowerCase();
|
||
const draft = {
|
||
productCode: String(item?.productCode ?? '').trim(),
|
||
productId: item?.productId ?? item?.id ?? null,
|
||
shippingMethod: String(item?.shippingMethod ?? '').trim(),
|
||
cargoAttribute: normalizeCargoAttributeCode(item?.cargoType ?? item?.cargoAttribute),
|
||
warehouseId: item?.warehouseId != null && item?.warehouseId !== '' ? String(item.warehouseId) : '',
|
||
countryCode: String(formData.destinationCountry || '').trim(),
|
||
actualWeight: String(formData.cargoWeight || '').trim(),
|
||
length: String(formData.length || '').trim(),
|
||
width: String(formData.width || '').trim(),
|
||
height: String(formData.height || '').trim(),
|
||
zipCode: String(formData.postalCode || '').trim(),
|
||
totalCost: item?.totalCost ?? null,
|
||
quoteLevel: quoteLevel || undefined,
|
||
disclaimers,
|
||
};
|
||
try {
|
||
sessionStorage.setItem(PRICE_CHECK_ORDER_DRAFT_KEY, JSON.stringify(draft));
|
||
} catch {
|
||
// ignore quota
|
||
}
|
||
router
|
||
.push({
|
||
name: 'PlaceOrder',
|
||
query: {
|
||
from: 'priceCheck',
|
||
mode: 'single',
|
||
productCode: draft.productCode || undefined,
|
||
countryCode: draft.countryCode || undefined,
|
||
},
|
||
})
|
||
.catch(() => undefined);
|
||
};
|
||
|
||
function clearRecommendInsight() {
|
||
recommendReason.value = '';
|
||
recommendVerdict.value = '';
|
||
recommendEvidence.value = [];
|
||
recommendCargoName.value = '';
|
||
recommendAlternatives.value = [];
|
||
}
|
||
|
||
function resetRecommendState() {
|
||
recommendedRowKey.value = '';
|
||
clearRecommendInsight();
|
||
}
|
||
|
||
function applyRecommendInsight(result: {
|
||
rowKey?: string;
|
||
reason: string;
|
||
verdict: ShipVerdict;
|
||
evidence: string[];
|
||
cargoName: string;
|
||
alternatives: RecommendAlternative[];
|
||
}) {
|
||
recommendedRowKey.value = result.rowKey || '';
|
||
recommendReason.value = result.reason;
|
||
recommendVerdict.value = result.verdict;
|
||
recommendEvidence.value = result.evidence || [];
|
||
recommendCargoName.value = result.cargoName || '';
|
||
recommendAlternatives.value = result.alternatives || [];
|
||
}
|
||
|
||
function isRecommendStamp(record: Record<string, any>) {
|
||
if (recommendedRowKey.value) return record.rowKey === recommendedRowKey.value;
|
||
return sortMode.value === 'recommend' && !!record.isPriceOptimal;
|
||
}
|
||
|
||
const handleSelectGuide = (record: any) => {
|
||
if (!record) return;
|
||
selectedGuideRecord.value = record;
|
||
clearRecommendInsight();
|
||
};
|
||
|
||
const handleCloseGuide = () => {
|
||
selectedGuideRecord.value = null;
|
||
clearRecommendInsight();
|
||
};
|
||
|
||
const focusQuoteRow = (rowKey: string) => {
|
||
const hit = queryResults.value.find((item) => item.rowKey === rowKey);
|
||
if (!hit) return null;
|
||
selectedGuideRecord.value = hit;
|
||
const sorted = sortedResults.value;
|
||
const idx = sorted.findIndex((item) => item.rowKey === rowKey);
|
||
if (idx >= 0) {
|
||
currentPage.value = Math.floor(idx / pageSize.value) + 1;
|
||
}
|
||
return hit;
|
||
};
|
||
|
||
const handleSelectAlternative = (rowKey: string) => {
|
||
const hit = focusQuoteRow(rowKey);
|
||
if (!hit) {
|
||
Message.warning(t('order.priceCheck.ai.noMatch', { name: recommendCargoName.value || '' }));
|
||
return;
|
||
}
|
||
const alt = recommendAlternatives.value.find((a) => a.rowKey === rowKey);
|
||
recommendedRowKey.value = rowKey;
|
||
recommendVerdict.value = alt?.verdict || recommendVerdict.value || 'unknown';
|
||
recommendReason.value = alt?.blurb
|
||
? t('order.priceCheck.ai.altSelected', { blurb: alt.blurb })
|
||
: recommendReason.value;
|
||
recommendAlternatives.value = recommendAlternatives.value.filter((a) => a.rowKey !== rowKey);
|
||
Message.success(t('order.priceCheck.ai.altSwitched'));
|
||
};
|
||
|
||
const handleRecommendPlaceOrder = () => {
|
||
if (!selectedGuideRecord.value) return;
|
||
handleOrder(selectedGuideRecord.value);
|
||
};
|
||
|
||
const handleRecommendAskAdvisor = () => {
|
||
if (!selectedGuideRecord.value) return;
|
||
const cargoName = recommendCargoName.value.trim();
|
||
openPriceAskAdvisor(
|
||
router,
|
||
'canShip',
|
||
{
|
||
...buildAskContext(selectedGuideRecord.value),
|
||
cargoName,
|
||
},
|
||
{ autoAsk: !!cargoName },
|
||
);
|
||
};
|
||
|
||
const handleAiRecommend = async (productName: string) => {
|
||
const name = String(productName || '').trim();
|
||
if (!name) {
|
||
Message.warning(t('order.priceCheck.ai.validate.productName'));
|
||
return;
|
||
}
|
||
if (!queryResults.value.length) {
|
||
Message.warning(t('order.priceCheck.ai.validate.needQuotes'));
|
||
return;
|
||
}
|
||
|
||
aiRecommendLoading.value = true;
|
||
queryFailureText.value = '';
|
||
try {
|
||
const candidates: QuoteCandidate[] = queryResults.value.map((item) => ({
|
||
rowKey: String(item.rowKey),
|
||
productId: item.productId,
|
||
productCode: String(item.productCode || ''),
|
||
shippingMethod: String(item.shippingMethod || ''),
|
||
cargoTypeName: String(item.cargoTypeName || ''),
|
||
warehouse: String(item.warehouse || ''),
|
||
deliveryTime: String(item.deliveryTime || ''),
|
||
totalCostNum: Number(item.totalCostNum),
|
||
maxDeliveryDays: Number(item.maxDeliveryDays),
|
||
isPriceOptimal: !!item.isPriceOptimal,
|
||
isTimeOptimal: !!item.isTimeOptimal,
|
||
}));
|
||
const result = await recommendWithAi(name, candidates);
|
||
if (!result) {
|
||
resetRecommendState();
|
||
selectedGuideRecord.value = null;
|
||
queryFailureText.value = t('order.priceCheck.ai.noMatch', { name });
|
||
Message.warning(t('order.priceCheck.ai.noMatch', { name }));
|
||
return;
|
||
}
|
||
const hit = focusQuoteRow(result.rowKey);
|
||
if (!hit) {
|
||
Message.warning(t('order.priceCheck.ai.noMatch', { name }));
|
||
return;
|
||
}
|
||
applyRecommendInsight(result);
|
||
Message.success(
|
||
result.source === 'ai'
|
||
? t('order.priceCheck.ai.successAi')
|
||
: result.source === 'mcp'
|
||
? t('order.priceCheck.ai.successMcp')
|
||
: t('order.priceCheck.ai.successLocal'),
|
||
);
|
||
} catch (e: any) {
|
||
Message.error(e?.message || t('order.priceCheck.ai.failed'));
|
||
} finally {
|
||
aiRecommendLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const handleSortChange = (mode: string) => {
|
||
if (mode !== 'recommend' && mode !== 'price' && mode !== 'time') return;
|
||
sortMode.value = mode;
|
||
currentPage.value = 1;
|
||
};
|
||
|
||
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="price-check-layout">
|
||
<div class="price-check-main">
|
||
<section class="liquid-panel query-form-section">
|
||
<div class="query-form-layout liquid-panel__content">
|
||
<div class="form-container">
|
||
<div class="form-head">
|
||
<h2 class="form-head__title">{{ $t('order.priceCheck.form.title') }}</h2>
|
||
<a-tooltip :content="$t('order.priceCheck.form.subtitle')" position="right">
|
||
<button type="button" class="form-head__help" :aria-label="$t('order.priceCheck.form.subtitle')">
|
||
<icon-question-circle />
|
||
</button>
|
||
</a-tooltip>
|
||
</div>
|
||
|
||
<div class="form-row form-row-primary">
|
||
<div class="form-item form-item-channel">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.channel') }}</label>
|
||
<ShippingMethodTabSelect
|
||
v-model="formData.shippingMethod"
|
||
:groups="shippingMethodGroups"
|
||
:loading="optionsLoading"
|
||
:placeholder="$t('order.priceCheck.form.channel.placeholder')"
|
||
class="form-select"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-item form-item-country">
|
||
<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"
|
||
allow-search
|
||
:loading="optionsLoading"
|
||
>
|
||
<template #header>
|
||
<div
|
||
class="country-shortcuts"
|
||
role="group"
|
||
:aria-label="$t('order.priceCheck.shortcuts.destCountryAria')"
|
||
>
|
||
<button
|
||
v-for="code in countryShortcutCodes"
|
||
:key="code"
|
||
type="button"
|
||
class="country-chip"
|
||
:class="{ 'is-active': formData.destinationCountry === code }"
|
||
@mousedown.prevent
|
||
@click="formData.destinationCountry = code"
|
||
>
|
||
{{ t(`order.priceCheck.shortcut.${code}`) }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
<a-option v-for="option in countryOptions" :key="option.value" :value="option.value">
|
||
{{ option.label }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<div ref="postalInputRef" class="form-item form-item-compact">
|
||
<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"
|
||
@press-enter="handleQuery"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-item form-item-compact">
|
||
<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"
|
||
@press-enter="handleQuery"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-item button-group">
|
||
<label class="form-label form-label-spacer" aria-hidden="true"> </label>
|
||
<div class="button-group-row">
|
||
<a-button
|
||
html-type="button"
|
||
type="primary"
|
||
class="query-btn"
|
||
:loading="queryLoading"
|
||
@click="handleQuery"
|
||
>
|
||
{{ $t('order.priceCheck.actions.query') }}
|
||
</a-button>
|
||
<a-button html-type="button" class="reset-btn" @click="handleReset">
|
||
{{ $t('order.priceCheck.actions.reset') }}
|
||
</a-button>
|
||
<button type="button" class="advanced-toggle-btn" @click="showAdvanced = !showAdvanced">
|
||
<span>{{ $t('order.priceCheck.form.moreFilters') }}</span>
|
||
<icon-up v-if="showAdvanced" />
|
||
<icon-down v-else />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="advancedConditionChips.length && !showAdvanced"
|
||
class="filter-summary-bar"
|
||
>
|
||
<span class="filter-summary-bar__label">
|
||
{{ $t('order.priceCheck.form.activeFilters', { count: advancedConditionChips.length }) }}
|
||
</span>
|
||
<div class="filter-summary-bar__chips">
|
||
<button
|
||
v-for="chip in advancedConditionChips"
|
||
:key="chip.key"
|
||
type="button"
|
||
class="filter-chip"
|
||
:title="chip.label"
|
||
@click="openAdvancedFilters"
|
||
>
|
||
<span class="filter-chip__text">{{ chip.label }}</span>
|
||
<span
|
||
class="filter-chip__remove"
|
||
role="button"
|
||
tabindex="0"
|
||
:aria-label="$t('order.priceCheck.form.removeFilter')"
|
||
@click.stop="removeAdvancedChip(chip)"
|
||
@keydown.enter.prevent.stop="removeAdvancedChip(chip)"
|
||
>
|
||
<icon-close />
|
||
</span>
|
||
</button>
|
||
<button type="button" class="filter-summary-bar__clear" @click="clearAdvancedConditions">
|
||
{{ $t('order.priceCheck.form.clearFilters') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-show="showAdvanced" class="form-row form-row-advanced">
|
||
<div class="form-item dimensions-group">
|
||
<label class="form-label" :title="$t('order.priceCheck.form.dimensionsHintOptional')">
|
||
{{ $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>
|
||
<p v-if="hasPartialDimensions()" class="form-inline-error">
|
||
{{ $t('order.priceCheck.validate.partialDimensions') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.destinationProvince') }}</label>
|
||
<a-input
|
||
v-model="formData.destinationProvince"
|
||
:placeholder="$t('order.priceCheck.form.destinationProvince.placeholder')"
|
||
class="form-input"
|
||
@press-enter="handleQuery"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.destinationCity') }}</label>
|
||
<a-input
|
||
v-model="formData.destinationCity"
|
||
:placeholder="$t('order.priceCheck.form.destinationCity.placeholder')"
|
||
class="form-input"
|
||
@press-enter="handleQuery"
|
||
/>
|
||
</div>
|
||
|
||
<div v-if="availableCoupons.length" class="form-item form-item-coupon">
|
||
<label class="form-label">{{ $t('marketing.priceCheck.couponLabel') }}</label>
|
||
<a-select
|
||
:model-value="couponInstanceId"
|
||
allow-clear
|
||
:placeholder="$t('marketing.priceCheck.couponPlaceholder')"
|
||
class="form-select"
|
||
@change="handleCouponChange"
|
||
>
|
||
<a-option
|
||
v-for="c in availableCoupons"
|
||
:key="c.id ?? c.Id"
|
||
:value="(c.id ?? c.Id) as number"
|
||
:disabled="!(c.available ?? c.Available ?? !(c.unavailableReason ?? c.UnavailableReason))"
|
||
>
|
||
{{ couponOptionLabel(c) }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.cargoType') }}</label>
|
||
<a-select
|
||
v-model="formData.cargoType"
|
||
class="form-select"
|
||
allow-search
|
||
allow-clear
|
||
:placeholder="$t('order.priceCheck.form.cargoType.placeholder')"
|
||
:loading="optionsLoading"
|
||
>
|
||
<a-option v-for="option in cargoTypeOptions" :key="option.value" :value="option.value">
|
||
{{ option.label }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<div class="form-item">
|
||
<label class="form-label">{{ $t('order.priceCheck.form.warehouse') }}</label>
|
||
<a-select
|
||
v-model="formData.warehouseId"
|
||
class="form-select"
|
||
allow-search
|
||
allow-clear
|
||
:placeholder="$t('order.priceCheck.form.warehouse.placeholder')"
|
||
:loading="optionsLoading"
|
||
>
|
||
<a-option v-for="option in warehouseOptions" :key="option.value" :value="option.value">
|
||
{{ option.label }}
|
||
</a-option>
|
||
</a-select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="liquid-panel table-section">
|
||
<div class="table-section__inner liquid-panel__content">
|
||
<AiRecommendCard
|
||
v-if="queryResults.length"
|
||
variant="bar"
|
||
:loading="aiRecommendLoading"
|
||
:show-postal-hint="needPostalHint && !hintBannerDismissed"
|
||
@recommend="handleAiRecommend"
|
||
@dismiss-hint="hintBannerDismissed = true"
|
||
/>
|
||
|
||
<div class="table-toolbar">
|
||
<div class="toolbar-left">
|
||
<SlidingTabs
|
||
:model-value="sortMode"
|
||
:items="sortTabItems"
|
||
aria-label="Sort tabs"
|
||
@change="handleSortChange"
|
||
/>
|
||
<template v-if="showQueryHintBanner && !needPostalHint && !hintBannerDismissed">
|
||
<a-popover
|
||
v-if="queryHintDisclaimers.length"
|
||
position="bl"
|
||
trigger="hover"
|
||
:content-style="{ maxWidth: '320px', padding: '8px 12px' }"
|
||
>
|
||
<button type="button" class="hint-inline-chip">
|
||
<icon-info-circle />
|
||
<span>{{ queryHintSummary }}</span>
|
||
</button>
|
||
<template #content>
|
||
<ul class="hint-popover-list">
|
||
<li v-for="(line, idx) in queryHintDisclaimers" :key="idx">{{ line }}</li>
|
||
</ul>
|
||
</template>
|
||
</a-popover>
|
||
<span v-else class="hint-inline-chip hint-inline-chip--static">
|
||
<icon-info-circle />
|
||
<span>{{ queryHintSummary }}</span>
|
||
</span>
|
||
<button
|
||
type="button"
|
||
class="hint-inline-dismiss"
|
||
:title="$t('order.priceCheck.hint.dismiss')"
|
||
@click="hintBannerDismissed = true"
|
||
>
|
||
×
|
||
</button>
|
||
</template>
|
||
</div>
|
||
<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 class="list-wrap">
|
||
<a-spin :loading="queryLoading" class="price-check-spin">
|
||
<div v-if="processedTableData.length" class="quote-card-list">
|
||
<QuoteResultCard
|
||
v-for="record in processedTableData"
|
||
:key="record.cardKey || record.rowKey"
|
||
:record="record"
|
||
:selected="selectedGuideRecord?.rowKey === record.rowKey"
|
||
:recommended="isRecommendStamp(record)"
|
||
:cargo-options="cargoTypeOptions"
|
||
:warehouse-options="warehouseOptions"
|
||
:lock-cargo-type="!!formData.cargoType"
|
||
:lock-warehouse="formData.warehouseId != null"
|
||
:recalc-loading="!!recalcLoadingKeys[record.cardKey || record.rowKey]"
|
||
@select="handleSelectGuide(record)"
|
||
@order="handleOrder(record)"
|
||
@ask="(intent) => handleAskAi(record, intent)"
|
||
@recalc="(payload) => handleCardRecalc(record, payload)"
|
||
/>
|
||
</div>
|
||
<div v-else-if="!queryLoading && hasQueried" class="quote-empty">
|
||
<a-empty
|
||
:description="
|
||
queryFailureItems.length
|
||
? t('order.priceCheck.table.emptyWithFailures', { count: queryFailureItems.length })
|
||
: queryFailureText || t('order.priceCheck.table.empty')
|
||
"
|
||
>
|
||
<template #image />
|
||
<p class="empty-hint">{{ t('order.priceCheck.table.emptyHint') }}</p>
|
||
</a-empty>
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div class="price-check-aside">
|
||
<ChannelGuidePanel
|
||
:record="selectedGuideRecord"
|
||
:failure-items="!queryResults.length ? queryFailureItems : []"
|
||
:failure-text="!queryResults.length ? queryFailureText : ''"
|
||
:recommend-reason="recommendReason"
|
||
:recommend-verdict="recommendVerdict"
|
||
:recommend-evidence="recommendEvidence"
|
||
:recommend-cargo-name="recommendCargoName"
|
||
:alternatives="recommendAlternatives"
|
||
@close="handleCloseGuide"
|
||
@select-alternative="handleSelectAlternative"
|
||
@place-order="handleRecommendPlaceOrder"
|
||
@ask-advisor="handleRecommendAskAdvisor"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="less" scoped>
|
||
.price-check-container {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 16px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
background-color: var(--app-page-bg);
|
||
}
|
||
.price-check-layout { display: flex; gap: 12px; flex: 1; min-height: 0; min-width: 0; }
|
||
.price-check-main { flex: 5 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
|
||
.price-check-aside { flex: 3 1 0; min-width: 340px; min-height: 0; display: flex; flex-direction: column; > * { flex: 1; min-height: 0; } }
|
||
.liquid-panel {
|
||
position: relative;
|
||
z-index: 0;
|
||
overflow: hidden;
|
||
border: 1px solid var(--app-card-border);
|
||
border-radius: 12px;
|
||
background: var(--app-card-bg);
|
||
}
|
||
|
||
.liquid-panel__content {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.query-form-section { padding: 20px 24px 16px; flex-shrink: 0; }
|
||
.query-form-layout {
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 14px;
|
||
min-width: 0;
|
||
}
|
||
.form-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
.form-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.form-head__title {
|
||
margin: 0;
|
||
color: var(--color-text-1);
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.02em;
|
||
line-height: 1.3;
|
||
}
|
||
.form-head__help {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 20px;
|
||
height: 20px;
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 50%;
|
||
background: transparent;
|
||
color: var(--color-text-3);
|
||
font-size: 14px;
|
||
cursor: help;
|
||
transition: color 0.15s ease;
|
||
|
||
&:hover {
|
||
color: var(--app-primary-color, #007979);
|
||
}
|
||
}
|
||
.form-label-tip {
|
||
margin-left: 6px;
|
||
color: var(--color-text-3);
|
||
font-size: 11px;
|
||
font-weight: 400;
|
||
}
|
||
.form-inline-error {
|
||
margin: 0;
|
||
color: #f53f3f;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
.form-row { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
|
||
.form-row-primary, .form-row-advanced { overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; }
|
||
.form-row-primary { flex-wrap: nowrap; }
|
||
.form-row-advanced {
|
||
margin-top: 12px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid var(--color-border-2);
|
||
}
|
||
.filter-summary-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 8px;
|
||
min-width: 0;
|
||
}
|
||
.filter-summary-bar__label {
|
||
flex-shrink: 0;
|
||
color: var(--color-text-3);
|
||
font-size: 12px;
|
||
line-height: 1.2;
|
||
white-space: nowrap;
|
||
}
|
||
.filter-summary-bar__chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
.filter-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
max-width: 180px;
|
||
height: 24px;
|
||
padding: 0 4px 0 8px;
|
||
border: none;
|
||
border-radius: 999px;
|
||
background: var(--color-fill-2);
|
||
color: var(--color-text-2);
|
||
font-size: 12px;
|
||
line-height: 22px;
|
||
cursor: pointer;
|
||
transition: color 0.15s ease, background 0.15s ease;
|
||
|
||
&:hover {
|
||
background: color-mix(in srgb, var(--app-primary-color, #007979) 10%, var(--color-fill-2));
|
||
color: var(--app-primary-color, #007979);
|
||
}
|
||
}
|
||
.filter-chip__text {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.filter-chip__remove {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
color: var(--color-text-3);
|
||
font-size: 10px;
|
||
flex-shrink: 0;
|
||
|
||
&:hover {
|
||
background: var(--color-fill-2);
|
||
color: var(--color-text-1);
|
||
}
|
||
}
|
||
.filter-summary-bar__clear {
|
||
flex-shrink: 0;
|
||
height: 24px;
|
||
padding: 0 6px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
background: transparent;
|
||
color: var(--color-text-3);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
|
||
&:hover {
|
||
color: var(--app-primary-color, #007979);
|
||
}
|
||
}
|
||
.advanced-toggle-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-shrink: 0;
|
||
height: 32px;
|
||
padding: 0 4px;
|
||
border: none;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
color: var(--color-text-2);
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
&:hover { color: var(--app-primary-color, #007979); }
|
||
}
|
||
.form-hint { margin: 0; color: var(--color-text-3); font-size: 12px; line-height: 1.3; }
|
||
.form-item {
|
||
display: flex; flex-direction: column; gap: 8px; flex: 0 0 auto; width: 148px; min-width: 132px;
|
||
&.dimensions-group { width: 260px; min-width: 220px; }
|
||
&.button-group { width: auto; min-width: 0; flex-shrink: 0; }
|
||
.button-group-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
flex-wrap: nowrap;
|
||
}
|
||
&.form-item-country { width: 240px; min-width: 200px; flex-shrink: 0; }
|
||
&.form-item-channel { width: 220px; min-width: 180px; flex-shrink: 0; }
|
||
&.form-item-compact { width: 112px; min-width: 100px; }
|
||
}
|
||
.country-shortcuts {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
padding: 8px 12px 4px;
|
||
max-width: 100%;
|
||
}
|
||
.country-chip {
|
||
height: 24px;
|
||
padding: 0 8px;
|
||
border: 1px solid transparent;
|
||
border-radius: 4px;
|
||
background: var(--color-fill-2);
|
||
color: var(--color-text-2);
|
||
font-size: 12px;
|
||
line-height: 22px;
|
||
cursor: pointer;
|
||
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
||
&:hover { color: var(--app-primary-color, #007979); background: rgba(0, 121, 121, 0.06); }
|
||
&.is-active {
|
||
background: rgba(0, 121, 121, 0.08);
|
||
border-color: color-mix(in srgb, var(--app-primary-color, #007979) 35%, transparent);
|
||
color: var(--app-primary-color, #007979);
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
.form-label {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--color-text-2);
|
||
min-height: 18px;
|
||
line-height: 18px;
|
||
flex-shrink: 0;
|
||
letter-spacing: 0.01em;
|
||
&.required::after { content: '*'; color: #f53f3f; margin-left: 4px; }
|
||
&.form-label-spacer { visibility: hidden; user-select: none; pointer-events: none; }
|
||
}
|
||
.form-select, .form-input { width: 100%; }
|
||
.form-row-primary :deep(.arco-input-wrapper),
|
||
.form-row-advanced :deep(.arco-input-wrapper),
|
||
.form-row-primary :deep(.arco-select-view),
|
||
.form-row-advanced :deep(.arco-select-view) {
|
||
border-radius: 6px;
|
||
background: var(--app-card-bg);
|
||
border-color: var(--color-border-2);
|
||
}
|
||
.form-row-primary :deep(.arco-input-wrapper .arco-input),
|
||
.form-row-advanced :deep(.arco-input-wrapper .arco-input) { min-height: 32px; }
|
||
.form-row-primary :deep(.arco-select .arco-select-view),
|
||
.form-row-advanced :deep(.arco-select .arco-select-view) { min-height: 32px; }
|
||
.dimensions-inputs { display: flex; gap: 8px; .dimension-input { flex: 1; } }
|
||
.query-btn,
|
||
.reset-btn {
|
||
height: 32px;
|
||
border-radius: 4px;
|
||
}
|
||
.query-btn {
|
||
min-width: 88px;
|
||
}
|
||
.table-section { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }
|
||
.table-section__inner { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; gap: 10px; }
|
||
.table-section__inner > .ai-rec {
|
||
margin: 12px 16px 0;
|
||
flex-shrink: 0;
|
||
}
|
||
.quote-card-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
/* 列表本身可滚动 */
|
||
padding: 14px 16px 16px 18px;
|
||
box-sizing: border-box;
|
||
}
|
||
.quote-empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 240px;
|
||
padding: 24px;
|
||
}
|
||
.table-toolbar {
|
||
padding: 4px 16px 8px;
|
||
margin-bottom: 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
min-width: 0;
|
||
}
|
||
.hint-inline-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
max-width: 220px;
|
||
height: 24px;
|
||
padding: 0 8px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
background: rgba(255, 125, 0, 0.08);
|
||
color: #d48806;
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
cursor: default;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
:deep(svg) { flex-shrink: 0; font-size: 14px; }
|
||
&--static { cursor: default; }
|
||
}
|
||
.hint-inline-dismiss {
|
||
width: 20px;
|
||
height: 20px;
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 4px;
|
||
background: transparent;
|
||
color: var(--color-text-3);
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
&:hover { color: var(--color-text-2); background: var(--color-fill-2); }
|
||
}
|
||
.hint-popover-list {
|
||
margin: 0;
|
||
padding-left: 16px;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
color: var(--color-text-2);
|
||
li + li { margin-top: 4px; }
|
||
}
|
||
.quote-disclaimer-popover {
|
||
max-width: 320px;
|
||
white-space: pre-wrap;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
.list-wrap {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
.price-check-spin {
|
||
width: 100%;
|
||
flex: 1 0 auto;
|
||
min-height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
:deep(.arco-spin) {
|
||
width: 100%;
|
||
flex: 1 0 auto;
|
||
min-height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.arco-spin-children) {
|
||
flex: 1 0 auto;
|
||
min-height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: visible;
|
||
}
|
||
}
|
||
.price-check-table {
|
||
--row-hover-bg: var(--color-fill-2);
|
||
--row-selected-bg: color-mix(in srgb, var(--app-primary-color, #2d8c8c) 10%, var(--app-card-bg));
|
||
|
||
:deep(.arco-table-container),
|
||
:deep(.arco-table-content) {
|
||
background: transparent;
|
||
}
|
||
|
||
:deep(.arco-table-th) {
|
||
background: var(--color-fill-2);
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
/* 普通列 + 固定列统一底色,避免 Arco 固定列 ::before 造成色差 */
|
||
:deep(.arco-table-td),
|
||
:deep(.arco-table-td.arco-table-col-fixed-left),
|
||
:deep(.arco-table-td.arco-table-col-fixed-right) {
|
||
background-color: var(--app-card-bg) !important;
|
||
}
|
||
|
||
:deep(.arco-table-td.arco-table-col-fixed-left::before),
|
||
:deep(.arco-table-td.arco-table-col-fixed-right::before) {
|
||
display: none !important;
|
||
content: none !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
:deep(.arco-table-tr) {
|
||
cursor: pointer;
|
||
}
|
||
|
||
:deep(.arco-table-tr:hover .arco-table-td),
|
||
:deep(.arco-table-tr:hover .arco-table-td.arco-table-col-fixed-left),
|
||
:deep(.arco-table-tr:hover .arco-table-td.arco-table-col-fixed-right) {
|
||
background-color: var(--row-hover-bg) !important;
|
||
}
|
||
|
||
:deep(.price-check-row-selected .arco-table-td),
|
||
:deep(.price-check-row-selected .arco-table-td.arco-table-col-fixed-left),
|
||
:deep(.price-check-row-selected .arco-table-td.arco-table-col-fixed-right),
|
||
:deep(.price-check-row-selected:hover .arco-table-td),
|
||
:deep(.price-check-row-selected:hover .arco-table-td.arco-table-col-fixed-left),
|
||
:deep(.price-check-row-selected:hover .arco-table-td.arco-table-col-fixed-right) {
|
||
background-color: var(--row-selected-bg) !important;
|
||
}
|
||
}
|
||
.shipping-method-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
.shipping-method-name {
|
||
font-weight: 650;
|
||
font-size: 14px;
|
||
color: var(--color-text-1);
|
||
line-height: 1.4;
|
||
}
|
||
.optimal-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
.delivery-time-cell { font-weight: 600; color: var(--color-text-1); }
|
||
.delivery-time-unit { margin-left: 4px; color: var(--color-text-3); font-size: 12px; }
|
||
.weight-cell { font-size: 13px; line-height: 1.5; color: var(--color-text-2); }
|
||
.weight-label { color: var(--color-text-3); margin-right: 4px; }
|
||
.weight-sub { margin-top: 2px; font-size: 12px; }
|
||
.fee-detail-cell { display: flex; flex-direction: column; gap: 2px; font-size: 12px; line-height: 1.5; }
|
||
.fee-label { margin-right: 4px; color: var(--color-text-3); }
|
||
.money-freight { color: var(--color-text-2); font-weight: 500; }
|
||
.money-other { color: var(--app-primary-color); font-weight: 500; }
|
||
.total-cost-cell { display: flex; flex-direction: column; gap: 2px; }
|
||
.money-total { color: #f53f3f; font-weight: 700; font-size: 20px; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1.2; }
|
||
.money-total-hover { cursor: help; }
|
||
.empty-hint { margin: 8px 0 0; color: var(--color-text-3); font-size: 12px; }
|
||
.ops-actions { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }
|
||
.ops-btn {
|
||
display: inline-flex; align-items: center; justify-content: center; height: 30px; padding: 0 14px;
|
||
border-radius: 8px; font-size: 13px; font-weight: 500; line-height: 1; cursor: pointer;
|
||
transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
|
||
&--ghost {
|
||
border: 1px solid var(--color-border-2); background: var(--app-elevated-bg); color: var(--color-text-2);
|
||
&:hover { border-color: var(--app-primary-color, #2d8c8c); color: var(--app-primary-color, #2d8c8c); background: rgba(45, 140, 140, 0.06); }
|
||
}
|
||
&--primary {
|
||
border: 1px solid var(--app-primary-color, #2d8c8c); background: var(--app-primary-color, #2d8c8c); color: #fff;
|
||
font-weight: 600; box-shadow: 0 3px 10px rgba(45, 140, 140, 0.22);
|
||
&:hover { filter: brightness(1.06); box-shadow: 0 4px 14px rgba(45, 140, 140, 0.3); }
|
||
&:active { filter: brightness(0.96); box-shadow: none; }
|
||
}
|
||
}
|
||
.cost-formula-popover {
|
||
max-width: 320px;
|
||
.cost-formula-label { margin-bottom: 6px; font-size: 12px; font-weight: 600; color: var(--color-text-3); }
|
||
.cost-formula-text { font-size: 13px; line-height: 1.6; color: var(--color-text-1); white-space: pre-wrap; word-break: break-word; }
|
||
}
|
||
.money-empty { color: var(--color-text-4); }
|
||
.other-fee-popover {
|
||
min-width: 180px;
|
||
.other-fee-row {
|
||
display: flex; justify-content: space-between; gap: 16px; padding: 4px 0; font-size: 13px; line-height: 1.5;
|
||
&:not(:last-child) { border-bottom: 1px dashed var(--color-border-2); }
|
||
}
|
||
}
|
||
</style>
|
||
|