Compare commits
2 Commits
2a4251f9db
...
5178b6379f
| Author | SHA1 | Date | |
|---|---|---|---|
| 5178b6379f | |||
| d771462a30 |
@ -1,5 +1,6 @@
|
|||||||
import { axiosInstance } from '@/api/rbac'
|
import { axiosInstance } from '@/api/rbac'
|
||||||
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
|
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
|
||||||
|
import * as XLSX from 'xlsx'
|
||||||
|
|
||||||
function sanitizePayload(body?: Record<string, any>) {
|
function sanitizePayload(body?: Record<string, any>) {
|
||||||
if (!body || typeof body !== 'object') return body
|
if (!body || typeof body !== 'object') return body
|
||||||
@ -119,37 +120,127 @@ export type KpiAreaInfoImportResult = {
|
|||||||
failures: Array<{ rowNumber: number; reason: string }>
|
failures: Array<{ rowNumber: number; reason: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /channel/kpi-area/info/import-template */
|
export type KpiAreaInfoExcelRow = {
|
||||||
export async function downloadKpiAreaInfoImportTemplate(
|
国家代码: string
|
||||||
|
匹配维度: string
|
||||||
|
匹配方式: string
|
||||||
|
'州/省': string
|
||||||
|
城市: string
|
||||||
|
起始邮编: string
|
||||||
|
截止邮编: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const KPI_AREA_ENUM_ROWS = [
|
||||||
|
{ 枚举类型: '匹配维度', 编码: 'country', 名称: '仅国家' },
|
||||||
|
{ 枚举类型: '匹配维度', 编码: 'zip', 名称: '邮编' },
|
||||||
|
{ 枚举类型: '匹配维度', 编码: 'state', 名称: '州/省' },
|
||||||
|
{ 枚举类型: '匹配维度', 编码: 'city', 名称: '城市' },
|
||||||
|
{ 枚举类型: '匹配方式', 编码: 'prefix', 名称: '前缀' },
|
||||||
|
{ 枚举类型: '匹配方式', 编码: 'exact', 名称: '精确' },
|
||||||
|
{ 枚举类型: '匹配方式', 编码: 'range', 名称: '区间' },
|
||||||
|
{ 枚举类型: '匹配方式', 编码: 'all', 名称: '不限' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const KPI_AREA_TEMPLATE_SAMPLE_ROWS: KpiAreaInfoExcelRow[] = [
|
||||||
|
{
|
||||||
|
国家代码: 'US',
|
||||||
|
匹配维度: 'zip',
|
||||||
|
匹配方式: 'range',
|
||||||
|
'州/省': '',
|
||||||
|
城市: '',
|
||||||
|
起始邮编: '90000',
|
||||||
|
截止邮编: '90199',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
国家代码: 'US',
|
||||||
|
匹配维度: 'zip',
|
||||||
|
匹配方式: 'prefix',
|
||||||
|
'州/省': '',
|
||||||
|
城市: '',
|
||||||
|
起始邮编: '90',
|
||||||
|
截止邮编: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
国家代码: 'US',
|
||||||
|
匹配维度: 'state',
|
||||||
|
匹配方式: 'exact',
|
||||||
|
'州/省': 'CA',
|
||||||
|
城市: '',
|
||||||
|
起始邮编: '',
|
||||||
|
截止邮编: '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function writeKpiAreaInfoWorkbook(rows: KpiAreaInfoExcelRow[], fileName: string) {
|
||||||
|
const dataRows =
|
||||||
|
rows.length > 0
|
||||||
|
? rows
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
国家代码: '',
|
||||||
|
匹配维度: '',
|
||||||
|
匹配方式: '',
|
||||||
|
'州/省': '',
|
||||||
|
城市: '',
|
||||||
|
起始邮编: '',
|
||||||
|
截止邮编: '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const wb = XLSX.utils.book_new()
|
||||||
|
const templateSheet = XLSX.utils.json_to_sheet(dataRows)
|
||||||
|
templateSheet['!cols'] = [
|
||||||
|
{ wch: 12 },
|
||||||
|
{ wch: 12 },
|
||||||
|
{ wch: 12 },
|
||||||
|
{ wch: 12 },
|
||||||
|
{ wch: 12 },
|
||||||
|
{ wch: 12 },
|
||||||
|
{ wch: 12 },
|
||||||
|
]
|
||||||
|
const enumSheet = XLSX.utils.json_to_sheet(KPI_AREA_ENUM_ROWS)
|
||||||
|
enumSheet['!cols'] = [{ wch: 12 }, { wch: 12 }, { wch: 12 }]
|
||||||
|
XLSX.utils.book_append_sheet(wb, templateSheet, '导入模板')
|
||||||
|
XLSX.utils.book_append_sheet(wb, enumSheet, '枚举值')
|
||||||
|
XLSX.writeFile(wb, fileName.endsWith('.xlsx') ? fileName : `${fileName}.xlsx`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 前端生成导入模板(Sheet1 导入模板 / Sheet2 枚举值) */
|
||||||
|
export function downloadKpiAreaInfoImportTemplate(
|
||||||
areaType: 'blacklist' | 'remote' | 'price',
|
areaType: 'blacklist' | 'remote' | 'price',
|
||||||
fileName?: string
|
fileName?: string
|
||||||
) {
|
) {
|
||||||
const res = await axiosInstance.get(omsUrl('/channel/kpi-area/info/import-template'), {
|
|
||||||
params: { areaType },
|
|
||||||
responseType: 'blob',
|
|
||||||
})
|
|
||||||
const blob = res.data as Blob
|
|
||||||
const ct = (res.headers['content-type'] || '').toString()
|
|
||||||
if (ct.includes('application/json')) {
|
|
||||||
const text = await blob.text()
|
|
||||||
const json = JSON.parse(text)
|
|
||||||
throw new Error(json.msg ?? json.message ?? json.Message ?? '下载失败')
|
|
||||||
}
|
|
||||||
const defaultNames: Record<string, string> = {
|
const defaultNames: Record<string, string> = {
|
||||||
blacklist: '黑名单分区明细导入模板.xlsx',
|
blacklist: '黑名单分区明细导入模板.xlsx',
|
||||||
remote: '偏远分区明细导入模板.xlsx',
|
remote: '偏远分区明细导入模板.xlsx',
|
||||||
price: '价格分区明细导入模板.xlsx',
|
price: '价格分区明细导入模板.xlsx',
|
||||||
}
|
}
|
||||||
const name = fileName || defaultNames[areaType] || '分区明细导入模板.xlsx'
|
writeKpiAreaInfoWorkbook(
|
||||||
const url = URL.createObjectURL(blob)
|
KPI_AREA_TEMPLATE_SAMPLE_ROWS,
|
||||||
const a = document.createElement('a')
|
fileName || defaultNames[areaType] || '分区明细导入模板.xlsx'
|
||||||
a.href = url
|
)
|
||||||
a.download = name.endsWith('.xlsx') ? name : `${name}.xlsx`
|
}
|
||||||
a.style.display = 'none'
|
|
||||||
document.body.appendChild(a)
|
export function exportKpiAreaInfoExcel(rows: KpiAreaInfoExcelRow[], fileName: string) {
|
||||||
a.click()
|
writeKpiAreaInfoWorkbook(rows, fileName)
|
||||||
document.body.removeChild(a)
|
}
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
|
export function mapKpiAreaInfoToExcelRow(item: Record<string, unknown>): KpiAreaInfoExcelRow {
|
||||||
|
const pick = (...keys: string[]) => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = item[key]
|
||||||
|
if (value != null && String(value).trim()) return String(value).trim()
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
国家代码: pick('countryCode', 'CountryCode'),
|
||||||
|
匹配维度: pick('matchDim', 'MatchDim'),
|
||||||
|
匹配方式: pick('matchMode', 'MatchMode'),
|
||||||
|
'州/省': pick('province', 'Province'),
|
||||||
|
城市: pick('city', 'City'),
|
||||||
|
起始邮编: pick('leftPostcode', 'leftPostCode', 'LeftPostcode', 'LeftPostCode'),
|
||||||
|
截止邮编: pick('rightPostcode', 'rightPostCode', 'RightPostcode', 'RightPostCode'),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST /channel/kpi-area/info/import multipart */
|
/** POST /channel/kpi-area/info/import multipart */
|
||||||
|
|||||||
@ -2511,6 +2511,8 @@ export default {
|
|||||||
'order.priceCheck.card.showFee': 'Details',
|
'order.priceCheck.card.showFee': 'Details',
|
||||||
'order.priceCheck.card.hideFee': 'Hide details',
|
'order.priceCheck.card.hideFee': 'Hide details',
|
||||||
'order.priceCheck.card.switchRecalcHint': 'Changing this will recalculate this product quote',
|
'order.priceCheck.card.switchRecalcHint': 'Changing this will recalculate this product quote',
|
||||||
|
'order.priceCheck.card.switchLockedHint':
|
||||||
|
'Set in More filters — clear or change it above to switch here',
|
||||||
'order.priceCheck.card.missingProductDescription': 'Product description missing',
|
'order.priceCheck.card.missingProductDescription': 'Product description missing',
|
||||||
'order.priceCheck.tradeTerms.ddp': 'DDP (duties included)',
|
'order.priceCheck.tradeTerms.ddp': 'DDP (duties included)',
|
||||||
'order.priceCheck.tradeTerms.ddu': 'DDU (duties excluded)',
|
'order.priceCheck.tradeTerms.ddu': 'DDU (duties excluded)',
|
||||||
@ -5458,6 +5460,8 @@ export default {
|
|||||||
'channel.area.actions.addDetail': 'Add area detail',
|
'channel.area.actions.addDetail': 'Add area detail',
|
||||||
'channel.area.actions.edit': 'Edit',
|
'channel.area.actions.edit': 'Edit',
|
||||||
'channel.area.actions.delete': 'Delete',
|
'channel.area.actions.delete': 'Delete',
|
||||||
|
'channel.area.actions.import': 'Import',
|
||||||
|
'channel.area.actions.export': 'Export',
|
||||||
'channel.area.pagination.total': 'Total {count} records',
|
'channel.area.pagination.total': 'Total {count} records',
|
||||||
'channel.area.pagination.prev': 'Previous',
|
'channel.area.pagination.prev': 'Previous',
|
||||||
'channel.area.pagination.next': 'Next',
|
'channel.area.pagination.next': 'Next',
|
||||||
@ -5552,6 +5556,23 @@ export default {
|
|||||||
'channel.area.message.destCountryQuickTagMiss': 'Country not matched. Please select from the dropdown.',
|
'channel.area.message.destCountryQuickTagMiss': 'Country not matched. Please select from the dropdown.',
|
||||||
'channel.area.message.editMissingRow': 'Record not found. Please refresh and try again.',
|
'channel.area.message.editMissingRow': 'Record not found. Please refresh and try again.',
|
||||||
'channel.area.message.deleteFailed': 'Delete failed. Please try again later.',
|
'channel.area.message.deleteFailed': 'Delete failed. Please try again later.',
|
||||||
|
'channel.area.import.modalTitle': 'Import area details',
|
||||||
|
'channel.area.import.tip':
|
||||||
|
'Download the template first. Country code is required (country name is filled from the code). Put match dimension/mode codes from the Enum sheet. One row, one dimension: postcode rules if filled; state only = state match; state+city = city match; empty = country fallback.',
|
||||||
|
'channel.area.import.downloadTemplate': 'Download template',
|
||||||
|
'channel.area.import.submit': 'Import',
|
||||||
|
'channel.area.import.downloadSuccess': 'Template downloaded',
|
||||||
|
'channel.area.import.downloadFailed': 'Failed to download template',
|
||||||
|
'channel.area.import.needSelectArea': 'Select an area on the left first',
|
||||||
|
'channel.area.import.needFile': 'Please choose an Excel file',
|
||||||
|
'channel.area.import.success': 'Import succeeded',
|
||||||
|
'channel.area.import.failed': 'Import failed',
|
||||||
|
'channel.area.import.partialSuccess': 'Some rows failed. See details below.',
|
||||||
|
'channel.area.import.allFailed': 'Import failed. Please check the file.',
|
||||||
|
'channel.area.import.resultSummary': '{success} succeeded, {fail} failed',
|
||||||
|
'channel.area.import.failRow': 'Row {row}: {reason}',
|
||||||
|
'channel.area.export.success': 'Exported {count} row(s)',
|
||||||
|
'channel.area.export.failed': 'Export failed',
|
||||||
|
|
||||||
// Channel - Weight Rules
|
// Channel - Weight Rules
|
||||||
'channel.weightRules.listTitle': 'List',
|
'channel.weightRules.listTitle': 'List',
|
||||||
|
|||||||
@ -2390,6 +2390,7 @@ export default {
|
|||||||
'order.priceCheck.card.showFee': '明细',
|
'order.priceCheck.card.showFee': '明细',
|
||||||
'order.priceCheck.card.hideFee': '收起明细',
|
'order.priceCheck.card.hideFee': '收起明细',
|
||||||
'order.priceCheck.card.switchRecalcHint': '切换后将重算该产品报价',
|
'order.priceCheck.card.switchRecalcHint': '切换后将重算该产品报价',
|
||||||
|
'order.priceCheck.card.switchLockedHint': '已在更多条件中指定,请到上方修改或清除后再切换',
|
||||||
'order.priceCheck.card.missingProductDescription': '缺少产品说明',
|
'order.priceCheck.card.missingProductDescription': '缺少产品说明',
|
||||||
'order.priceCheck.tradeTerms.ddp': 'DDP(包税)',
|
'order.priceCheck.tradeTerms.ddp': 'DDP(包税)',
|
||||||
'order.priceCheck.tradeTerms.ddu': 'DDU(不包税)',
|
'order.priceCheck.tradeTerms.ddu': 'DDU(不包税)',
|
||||||
@ -5568,7 +5569,7 @@ export default {
|
|||||||
'channel.area.search.button.query': '查询',
|
'channel.area.search.button.query': '查询',
|
||||||
'channel.area.search.button.reset': '重置',
|
'channel.area.search.button.reset': '重置',
|
||||||
'channel.area.import.modalTitle': '导入分区明细',
|
'channel.area.import.modalTitle': '导入分区明细',
|
||||||
'channel.area.import.tip': '请先下载模板。国家代码必填。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。',
|
'channel.area.import.tip': '请先下载模板。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。',
|
||||||
'channel.area.import.downloadTemplate': '下载模板',
|
'channel.area.import.downloadTemplate': '下载模板',
|
||||||
'channel.area.import.submit': '开始导入',
|
'channel.area.import.submit': '开始导入',
|
||||||
'channel.area.import.downloadSuccess': '模板下载成功',
|
'channel.area.import.downloadSuccess': '模板下载成功',
|
||||||
@ -5581,7 +5582,10 @@ export default {
|
|||||||
'channel.area.import.allFailed': '导入失败,请检查文件内容',
|
'channel.area.import.allFailed': '导入失败,请检查文件内容',
|
||||||
'channel.area.import.resultSummary': '成功 {success} 条,失败 {fail} 条',
|
'channel.area.import.resultSummary': '成功 {success} 条,失败 {fail} 条',
|
||||||
'channel.area.import.failRow': '第 {row} 行:{reason}',
|
'channel.area.import.failRow': '第 {row} 行:{reason}',
|
||||||
|
'channel.area.export.success': '已导出 {count} 条',
|
||||||
|
'channel.area.export.failed': '导出失败',
|
||||||
'channel.area.actions.import': '导入',
|
'channel.area.actions.import': '导入',
|
||||||
|
'channel.area.actions.export': '导出',
|
||||||
|
|
||||||
// 渠道配置页通用
|
// 渠道配置页通用
|
||||||
'channel.config.emptySelect': '请先在左侧选择配置项',
|
'channel.config.emptySelect': '请先在左侧选择配置项',
|
||||||
|
|||||||
@ -271,12 +271,25 @@ async function fetchPreviewBlob(rawUrl: string): Promise<Blob> {
|
|||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const isCos = /^https?:\/\//i.test(raw) && isCosAbsoluteUrl(raw)
|
const isCos = /^https?:\/\//i.test(raw) && isCosAbsoluteUrl(raw)
|
||||||
|
|
||||||
// 1) 鉴权 stream(含签名 URL 服务端代拉)— 生产主路径,无浏览器 CORS
|
// 开发:测试 Admin 可能尚未部署 /file-storage,先走同源 COS 代理
|
||||||
|
if (import.meta.env.DEV && isCos) {
|
||||||
|
try {
|
||||||
|
const blob = await fetchBlobViaCosProxy(raw)
|
||||||
|
const bytes = await normalizePdfBytes(blob, raw)
|
||||||
|
return new Blob([bytes], {
|
||||||
|
type: isPrintPreviewPdf(raw, blob) ? 'application/pdf' : blob.type || guessMime(raw),
|
||||||
|
})
|
||||||
|
} catch (e: unknown) {
|
||||||
|
errors.push(extractOmsRequestError(e) || 'COS 同源代理下载失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鉴权 stream(含签名 URL 服务端代拉)— 生产主路径,无浏览器 CORS
|
||||||
const streamed = await fetchViaFileStorageStream(raw, errors)
|
const streamed = await fetchViaFileStorageStream(raw, errors)
|
||||||
if (streamed) return streamed
|
if (streamed) return streamed
|
||||||
|
|
||||||
// 2) 同源 /__cos__ 代理(开发 Vite;生产需 nginx 配好)
|
// 同源 /__cos__ 代理(生产 nginx;开发上面已试过则跳过)
|
||||||
if (isCos) {
|
if (isCos && !import.meta.env.DEV) {
|
||||||
try {
|
try {
|
||||||
const blob = await fetchBlobViaCosProxy(raw)
|
const blob = await fetchBlobViaCosProxy(raw)
|
||||||
const bytes = await normalizePdfBytes(blob, raw)
|
const bytes = await normalizePdfBytes(blob, raw)
|
||||||
|
|||||||
@ -13,7 +13,10 @@
|
|||||||
<div class="kpi-area-import-modal">
|
<div class="kpi-area-import-modal">
|
||||||
<div class="import-tip">{{ t('channel.area.import.tip') }}</div>
|
<div class="import-tip">{{ t('channel.area.import.tip') }}</div>
|
||||||
<div class="import-actions">
|
<div class="import-actions">
|
||||||
<a-button type="outline" :loading="downloading" @click="handleDownloadTemplate">
|
<a-button class="app-btn-primary-soft" :loading="downloading" @click="handleDownloadTemplate">
|
||||||
|
<template #icon>
|
||||||
|
<IconDownload />
|
||||||
|
</template>
|
||||||
{{ t('channel.area.import.downloadTemplate') }}
|
{{ t('channel.area.import.downloadTemplate') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@ -50,6 +53,7 @@
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import type { FileItem } from '@arco-design/web-vue';
|
import type { FileItem } from '@arco-design/web-vue';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { IconDownload } from '@arco-design/web-vue/es/icon';
|
||||||
import {
|
import {
|
||||||
downloadKpiAreaInfoImportTemplate,
|
downloadKpiAreaInfoImportTemplate,
|
||||||
importKpiAreaInfo,
|
importKpiAreaInfo,
|
||||||
@ -91,7 +95,7 @@
|
|||||||
async function handleDownloadTemplate() {
|
async function handleDownloadTemplate() {
|
||||||
downloading.value = true;
|
downloading.value = true;
|
||||||
try {
|
try {
|
||||||
await downloadKpiAreaInfoImportTemplate(props.areaType);
|
downloadKpiAreaInfoImportTemplate(props.areaType);
|
||||||
Message.success(t('channel.area.import.downloadSuccess'));
|
Message.success(t('channel.area.import.downloadSuccess'));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.error(e?.message || t('channel.area.import.downloadFailed'));
|
Message.error(e?.message || t('channel.area.import.downloadFailed'));
|
||||||
|
|||||||
@ -81,6 +81,9 @@
|
|||||||
<a-button :disabled="!currentId?.id" @click="openImportModal">
|
<a-button :disabled="!currentId?.id" @click="openImportModal">
|
||||||
{{ t('channel.area.actions.import') }}
|
{{ t('channel.area.actions.import') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button :disabled="!currentId?.id" :loading="exporting" @click="handleExport">
|
||||||
|
{{ t('channel.area.actions.export') }}
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -204,6 +207,8 @@
|
|||||||
onSubmitRight,
|
onSubmitRight,
|
||||||
openImportModal,
|
openImportModal,
|
||||||
onImportSuccess,
|
onImportSuccess,
|
||||||
|
exporting,
|
||||||
|
handleExport,
|
||||||
} = useKpiAreaPage(props.areaType);
|
} = useKpiAreaPage(props.areaType);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import {
|
|||||||
postForewarningServiceKpiAreaModifyKpiAreaInfo,
|
postForewarningServiceKpiAreaModifyKpiAreaInfo,
|
||||||
postForewarningServiceKpiAreaModifyKpiArea,
|
postForewarningServiceKpiAreaModifyKpiArea,
|
||||||
postForewarningServiceKpiAreaGetKpiAreas,
|
postForewarningServiceKpiAreaGetKpiAreas,
|
||||||
|
exportKpiAreaInfoExcel,
|
||||||
|
mapKpiAreaInfoToExcelRow,
|
||||||
} from '@/api/channel/kpiArea';
|
} from '@/api/channel/kpiArea';
|
||||||
import { getCountryList } from '@/api/system/country';
|
import { getCountryList } from '@/api/system/country';
|
||||||
import {
|
import {
|
||||||
@ -497,6 +499,7 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
|||||||
const modalFormLeftRef = ref();
|
const modalFormLeftRef = ref();
|
||||||
const modalFormRightRef = ref();
|
const modalFormRightRef = ref();
|
||||||
const importModalRef = ref<{ open: (id: number) => void } | null>(null);
|
const importModalRef = ref<{ open: (id: number) => void } | null>(null);
|
||||||
|
const exporting = ref(false);
|
||||||
const tableData = ref<any[]>([]);
|
const tableData = ref<any[]>([]);
|
||||||
|
|
||||||
const leftModalMode = ref<DialogType>('add');
|
const leftModalMode = ref<DialogType>('add');
|
||||||
@ -823,6 +826,52 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
|||||||
loadTableData();
|
loadTableData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchAllAreaInfos(areaId: number) {
|
||||||
|
const pageSizeAll = 200;
|
||||||
|
const all: Record<string, unknown>[] = [];
|
||||||
|
let page = 1;
|
||||||
|
let total = Number.POSITIVE_INFINITY;
|
||||||
|
while (all.length < total && page <= 200) {
|
||||||
|
const response: any = await postForewarningServiceKpiAreaGetKpiAreaInfos({
|
||||||
|
areaId,
|
||||||
|
pageIndex: page,
|
||||||
|
pageSize: pageSizeAll,
|
||||||
|
});
|
||||||
|
const items = filterByAreaType(response?.items ?? response?.data?.items ?? []);
|
||||||
|
total = Number(response?.totalCount ?? response?.total ?? items.length ?? 0);
|
||||||
|
all.push(...items);
|
||||||
|
if (items.length < pageSizeAll) break;
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
if (!guardAreaSelected()) {
|
||||||
|
$baseMessage(t('channel.area.import.needSelectArea'), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exporting.value = true;
|
||||||
|
try {
|
||||||
|
const items = await fetchAllAreaInfos(currentId.value!.id);
|
||||||
|
const rows = items.map((item) => {
|
||||||
|
const inferred = inferGeoMatch(item);
|
||||||
|
return mapKpiAreaInfoToExcelRow({
|
||||||
|
...item,
|
||||||
|
matchDim: item.matchDim || inferred.matchDim,
|
||||||
|
matchMode: item.matchMode || inferred.matchMode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const areaName = String(currentId.value?.name || t('channel.area.listTitle')).trim();
|
||||||
|
exportKpiAreaInfoExcel(rows, `${areaName}-分区明细.xlsx`);
|
||||||
|
$baseMessage(t('channel.area.export.success', { count: rows.length }), 'success');
|
||||||
|
} catch (e: any) {
|
||||||
|
$baseMessage(e?.message || t('channel.area.export.failed'), 'error');
|
||||||
|
} finally {
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => currentId.value,
|
() => currentId.value,
|
||||||
(val) => {
|
(val) => {
|
||||||
@ -888,6 +937,8 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
|||||||
onSubmitRight,
|
onSubmitRight,
|
||||||
openImportModal,
|
openImportModal,
|
||||||
onImportSuccess,
|
onImportSuccess,
|
||||||
|
exporting,
|
||||||
|
handleExport,
|
||||||
guardAreaSelected,
|
guardAreaSelected,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,8 +50,17 @@ export default {
|
|||||||
'h5.price.tag.priceOptimal': 'Best price',
|
'h5.price.tag.priceOptimal': 'Best price',
|
||||||
'h5.price.tag.timeOptimal': 'Fastest',
|
'h5.price.tag.timeOptimal': 'Fastest',
|
||||||
'h5.price.tag.best': 'Best',
|
'h5.price.tag.best': 'Best',
|
||||||
'h5.price.dimensionsToggle': 'Size (optional)',
|
'h5.price.dimensionsToggle': 'More',
|
||||||
'h5.price.dimensionsHint': 'L×W×H for volumetric weight',
|
'h5.price.moreOptional': 'Optional',
|
||||||
|
'h5.price.moreActive': '{n} filled',
|
||||||
|
'h5.price.moreDimsLabel': 'Size (cm)',
|
||||||
|
'h5.price.moreGeoLabel': 'State / City',
|
||||||
|
'h5.price.dimensionsHint': 'Size for volumetric weight; state/city helps zoning without postal code',
|
||||||
|
'h5.price.lengthPh': 'L',
|
||||||
|
'h5.price.widthPh': 'W',
|
||||||
|
'h5.price.heightPh': 'H',
|
||||||
|
'h5.price.provincePh': 'State',
|
||||||
|
'h5.price.cityPh': 'City',
|
||||||
'h5.price.accuracy.postalCta': 'Estimate only — add a postal code for better accuracy',
|
'h5.price.accuracy.postalCta': 'Estimate only — add a postal code for better accuracy',
|
||||||
'h5.price.accuracy.fillPostal': 'Add postal code',
|
'h5.price.accuracy.fillPostal': 'Add postal code',
|
||||||
'h5.price.accuracy.dismiss': 'Dismiss',
|
'h5.price.accuracy.dismiss': 'Dismiss',
|
||||||
|
|||||||
@ -50,8 +50,17 @@ export default {
|
|||||||
'h5.price.tag.priceOptimal': '价格最优',
|
'h5.price.tag.priceOptimal': '价格最优',
|
||||||
'h5.price.tag.timeOptimal': '时效最优',
|
'h5.price.tag.timeOptimal': '时效最优',
|
||||||
'h5.price.tag.best': '最优',
|
'h5.price.tag.best': '最优',
|
||||||
'h5.price.dimensionsToggle': '尺寸(选填)',
|
'h5.price.dimensionsToggle': '更多',
|
||||||
'h5.price.dimensionsHint': '填齐长宽高可算体积重',
|
'h5.price.moreOptional': '选填',
|
||||||
|
'h5.price.moreActive': '已填 {n} 项',
|
||||||
|
'h5.price.moreDimsLabel': '尺寸 (cm)',
|
||||||
|
'h5.price.moreGeoLabel': '州省 / 城市',
|
||||||
|
'h5.price.dimensionsHint': '尺寸可算体积重;无邮编时可用州省/城市辅助分区',
|
||||||
|
'h5.price.lengthPh': '长',
|
||||||
|
'h5.price.widthPh': '宽',
|
||||||
|
'h5.price.heightPh': '高',
|
||||||
|
'h5.price.provincePh': '州/省',
|
||||||
|
'h5.price.cityPh': '城市',
|
||||||
'h5.price.accuracy.postalCta': '当前为估算报价,补上邮编可提高准确性',
|
'h5.price.accuracy.postalCta': '当前为估算报价,补上邮编可提高准确性',
|
||||||
'h5.price.accuracy.fillPostal': '去填邮编',
|
'h5.price.accuracy.fillPostal': '去填邮编',
|
||||||
'h5.price.accuracy.dismiss': '关闭',
|
'h5.price.accuracy.dismiss': '关闭',
|
||||||
|
|||||||
@ -69,24 +69,6 @@
|
|||||||
autocomplete="postal-code"
|
autocomplete="postal-code"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="line-field">
|
|
||||||
<span class="line-field__label">{{ t('order.priceCheck.form.destinationProvince') }}</span>
|
|
||||||
<input
|
|
||||||
v-model="form.destinationProvince"
|
|
||||||
class="line-field__input"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('order.priceCheck.form.destinationProvince.placeholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="line-field">
|
|
||||||
<span class="line-field__label">{{ t('order.priceCheck.form.destinationCity') }}</span>
|
|
||||||
<input
|
|
||||||
v-model="form.destinationCity"
|
|
||||||
class="line-field__input"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('order.priceCheck.form.destinationCity.placeholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pkg-row">
|
<div class="pkg-row">
|
||||||
<label class="pkg-cell">
|
<label class="pkg-cell">
|
||||||
@ -113,25 +95,84 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="dim-toggle" @click="showDimensions = !showDimensions">
|
<div class="more-block" :class="{ 'is-open': showMore }">
|
||||||
<span>{{ t('h5.price.dimensionsToggle') }}</span>
|
<button
|
||||||
<em>{{ showDimensions ? '−' : '+' }}</em>
|
type="button"
|
||||||
|
class="more-toggle"
|
||||||
|
:aria-expanded="showMore"
|
||||||
|
@click="showMore = !showMore"
|
||||||
|
>
|
||||||
|
<span class="more-toggle__left">
|
||||||
|
<span class="more-toggle__title">{{ t('h5.price.dimensionsToggle') }}</span>
|
||||||
|
<span v-if="moreActiveCount" class="more-toggle__badge">
|
||||||
|
{{ t('h5.price.moreActive', { n: moreActiveCount }) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="more-toggle__hint">{{ t('h5.price.moreOptional') }}</span>
|
||||||
|
</span>
|
||||||
|
<icon-down class="more-toggle__chevron" :class="{ 'is-open': showMore }" />
|
||||||
</button>
|
</button>
|
||||||
<div v-if="showDimensions" class="dim-row">
|
|
||||||
|
<div v-show="showMore" class="more-panel">
|
||||||
|
<div class="more-group">
|
||||||
|
<p class="more-group__label">{{ t('h5.price.moreDimsLabel') }}</p>
|
||||||
|
<div class="dim-row">
|
||||||
<label class="dim-cell">
|
<label class="dim-cell">
|
||||||
<span>{{ t('h5.price.length') }}</span>
|
<input
|
||||||
<input v-model="form.length" type="text" inputmode="decimal" placeholder="cm" />
|
v-model="form.length"
|
||||||
|
type="text"
|
||||||
|
inputmode="decimal"
|
||||||
|
:placeholder="t('h5.price.lengthPh')"
|
||||||
|
aria-label="length"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<span class="dim-sep" aria-hidden="true">×</span>
|
||||||
<label class="dim-cell">
|
<label class="dim-cell">
|
||||||
<span>{{ t('h5.price.width') }}</span>
|
<input
|
||||||
<input v-model="form.width" type="text" inputmode="decimal" placeholder="cm" />
|
v-model="form.width"
|
||||||
|
type="text"
|
||||||
|
inputmode="decimal"
|
||||||
|
:placeholder="t('h5.price.widthPh')"
|
||||||
|
aria-label="width"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<span class="dim-sep" aria-hidden="true">×</span>
|
||||||
<label class="dim-cell">
|
<label class="dim-cell">
|
||||||
<span>{{ t('h5.price.height') }}</span>
|
<input
|
||||||
<input v-model="form.height" type="text" inputmode="decimal" placeholder="cm" />
|
v-model="form.height"
|
||||||
|
type="text"
|
||||||
|
inputmode="decimal"
|
||||||
|
:placeholder="t('h5.price.heightPh')"
|
||||||
|
aria-label="height"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="showDimensions" class="dim-hint">{{ t('h5.price.dimensionsHint') }}</p>
|
</div>
|
||||||
|
|
||||||
|
<div class="more-group">
|
||||||
|
<p class="more-group__label">{{ t('h5.price.moreGeoLabel') }}</p>
|
||||||
|
<div class="dim-row dim-row--geo">
|
||||||
|
<label class="dim-cell">
|
||||||
|
<input
|
||||||
|
v-model="form.destinationProvince"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('h5.price.provincePh')"
|
||||||
|
:aria-label="t('order.priceCheck.form.destinationProvince')"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="dim-cell">
|
||||||
|
<input
|
||||||
|
v-model="form.destinationCity"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('h5.price.cityPh')"
|
||||||
|
:aria-label="t('order.priceCheck.form.destinationCity')"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="dim-hint">{{ t('h5.price.dimensionsHint') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -232,9 +273,6 @@
|
|||||||
<span class="ref-label">
|
<span class="ref-label">
|
||||||
{{ tradeTermsLabel(item.tradeTerms) || t('h5.price.referencePrice') }}
|
{{ tradeTermsLabel(item.tradeTerms) || t('h5.price.referencePrice') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="item.tradeTerms === 'DDU'" class="ref-label ref-label--warn">
|
|
||||||
{{ t('h5.price.hint.dduExtra') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -254,11 +292,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="h5-quote__action-btns">
|
<div class="h5-quote__action-btns">
|
||||||
<button type="button" class="link-btn" @click="openDetail(item)">
|
<button
|
||||||
{{ t('h5.price.detailShort') }}
|
type="button"
|
||||||
</button>
|
class="advisor-icon-btn"
|
||||||
<button type="button" class="link-btn" @click="askAdvisor(item)">
|
:aria-label="t('h5.price.askAdvisor')"
|
||||||
{{ t('h5.price.askAdvisor') }}
|
@click="askAdvisor(item)"
|
||||||
|
>
|
||||||
|
<IconTabService class="advisor-icon-btn__icon" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="order-btn" @click="goOrder(item)">
|
<button type="button" class="order-btn" @click="goOrder(item)">
|
||||||
{{ t('h5.price.goOrder') }}
|
{{ t('h5.price.goOrder') }}
|
||||||
@ -490,19 +530,28 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="h5-detail__guide">
|
<section class="h5-detail__guide">
|
||||||
|
<header class="h5-detail__guide-head">
|
||||||
<h4>{{ t('h5.price.routeGuide') }}</h4>
|
<h4>{{ t('h5.price.routeGuide') }}</h4>
|
||||||
<div v-if="guideHtml" class="h5-md" v-html="guideHtml" />
|
</header>
|
||||||
|
<div v-if="guideContent" class="h5-detail__guide-body">
|
||||||
|
<MarkdownViewer :content="guideContent" />
|
||||||
|
</div>
|
||||||
<a-empty v-else-if="!detailLoading" :description="t('h5.price.guideEmpty')" />
|
<a-empty v-else-if="!detailLoading" :description="t('h5.price.guideEmpty')" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="h5-detail__footer">
|
<footer class="h5-detail__footer">
|
||||||
<a-button long @click="askAdvisor(detailItem)">
|
<button
|
||||||
{{ t('h5.price.askAdvisor') }}
|
type="button"
|
||||||
</a-button>
|
class="h5-detail__icon-btn"
|
||||||
<a-button type="primary" long @click="goOrder(detailItem)">
|
:aria-label="t('h5.price.askAdvisor')"
|
||||||
|
@click="askAdvisor(detailItem)"
|
||||||
|
>
|
||||||
|
<IconTabService class="h5-detail__icon" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="h5-detail__order-btn" @click="goOrder(detailItem)">
|
||||||
{{ t('h5.price.goOrder') }}
|
{{ t('h5.price.goOrder') }}
|
||||||
</a-button>
|
</button>
|
||||||
</div>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
@ -521,12 +570,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onMounted, reactive, ref, type ComponentPublicInstance } from 'vue';
|
import { computed, nextTick, onMounted, reactive, ref, watch, type ComponentPublicInstance } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { IconDown } from '@arco-design/web-vue/es/icon';
|
import { IconDown } from '@arco-design/web-vue/es/icon';
|
||||||
import IconRoutePackage from '@/assets/icons/h5/route-package.svg';
|
import IconRoutePackage from '@/assets/icons/h5/route-package.svg';
|
||||||
|
import IconTabService from '@/assets/icons/h5/tab-service.svg';
|
||||||
|
import MarkdownViewer from '@/components/MarkdownViewer/index.vue';
|
||||||
import type { CustomerProfileCompleteness } from '@/api/customer';
|
import type { CustomerProfileCompleteness } from '@/api/customer';
|
||||||
import { postOrderPriceCheck, type PriceCheckQueryHint } from '@/api/order';
|
import { postOrderPriceCheck, type PriceCheckQueryHint } from '@/api/order';
|
||||||
import { getCountryList } from '@/api/system/country';
|
import { getCountryList } from '@/api/system/country';
|
||||||
@ -574,8 +625,29 @@ const form = reactive({
|
|||||||
height: '',
|
height: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const showDimensions = ref(false);
|
const showMore = ref(false);
|
||||||
const formCollapsed = ref(false);
|
const formCollapsed = ref(false);
|
||||||
|
|
||||||
|
const moreActiveCount = computed(() => {
|
||||||
|
let n = 0;
|
||||||
|
if (String(form.length || '').trim()) n += 1;
|
||||||
|
if (String(form.width || '').trim()) n += 1;
|
||||||
|
if (String(form.height || '').trim()) n += 1;
|
||||||
|
if (String(form.destinationProvince || '').trim()) n += 1;
|
||||||
|
if (String(form.destinationCity || '').trim()) n += 1;
|
||||||
|
return n;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
moreActiveCount,
|
||||||
|
(count, prev) => {
|
||||||
|
// 首次有值或从无到有时自动展开;已展开后允许用户手动收起
|
||||||
|
if (count > 0 && (prev == null || prev === 0)) {
|
||||||
|
showMore.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
const aiExpanded = ref(false);
|
const aiExpanded = ref(false);
|
||||||
const quoteElMap = new Map<string, HTMLElement>();
|
const quoteElMap = new Map<string, HTMLElement>();
|
||||||
|
|
||||||
@ -610,7 +682,7 @@ const pickerType = ref<'country' | 'warehouse' | 'cargo'>('country');
|
|||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const detailLoading = ref(false);
|
const detailLoading = ref(false);
|
||||||
const detailItem = ref<any>(null);
|
const detailItem = ref<any>(null);
|
||||||
const guideHtml = ref('');
|
const guideContent = ref('');
|
||||||
|
|
||||||
function parseOptionalDimension(value: unknown) {
|
function parseOptionalDimension(value: unknown) {
|
||||||
const n = parseFloat(String(value ?? '').trim());
|
const n = parseFloat(String(value ?? '').trim());
|
||||||
@ -799,26 +871,6 @@ function parseTotalCost(cost: unknown) {
|
|||||||
return Number.isNaN(num) ? Infinity : num;
|
return Number.isNaN(num) ? Infinity : num;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text: string) {
|
|
||||||
return text
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markdownToHtml(md: string) {
|
|
||||||
const text = String(md || '').trim();
|
|
||||||
if (!text) return '';
|
|
||||||
try {
|
|
||||||
const { marked } = await import('marked');
|
|
||||||
marked.setOptions({ gfm: true, breaks: true });
|
|
||||||
return String(marked.parse(text));
|
|
||||||
} catch {
|
|
||||||
return `<pre>${escapeHtml(text)}</pre>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRow(raw: any, index: number) {
|
function normalizeRow(raw: any, index: number) {
|
||||||
const deliveryTime = String(raw.deliveryTime ?? raw.DeliveryTime ?? '');
|
const deliveryTime = String(raw.deliveryTime ?? raw.DeliveryTime ?? '');
|
||||||
const match = deliveryTime.match(/(\d+)-(\d+)/);
|
const match = deliveryTime.match(/(\d+)-(\d+)/);
|
||||||
@ -1120,7 +1172,7 @@ function selectAiAlternative(rowKey: string) {
|
|||||||
async function openDetail(item: any) {
|
async function openDetail(item: any) {
|
||||||
detailItem.value = item;
|
detailItem.value = item;
|
||||||
detailVisible.value = true;
|
detailVisible.value = true;
|
||||||
guideHtml.value = '';
|
guideContent.value = '';
|
||||||
const productId = item?.productId ?? item?.id;
|
const productId = item?.productId ?? item?.id;
|
||||||
if (productId == null || productId === '') {
|
if (productId == null || productId === '') {
|
||||||
Message.warning(t('h5.price.guideMissing'));
|
Message.warning(t('h5.price.guideMissing'));
|
||||||
@ -1129,10 +1181,9 @@ async function openDetail(item: any) {
|
|||||||
detailLoading.value = true;
|
detailLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const detail = await getSalesProductDetail(productId);
|
const detail = await getSalesProductDetail(productId);
|
||||||
const md = String(
|
guideContent.value = String(
|
||||||
detail?.customerGuideMarkdown ?? detail?.CustomerGuideMarkdown ?? '',
|
detail?.customerGuideMarkdown ?? detail?.CustomerGuideMarkdown ?? '',
|
||||||
).trim();
|
).trim();
|
||||||
guideHtml.value = await markdownToHtml(md);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.error(e?.message || t('h5.price.guideFailed'));
|
Message.error(e?.message || t('h5.price.guideFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
@ -1487,67 +1538,148 @@ onMounted(() => {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dim-toggle {
|
.more-block {
|
||||||
|
margin: 4px 0 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
background: #f7f8fa;
|
||||||
|
padding: 0 10px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 0 8px;
|
min-height: 40px;
|
||||||
padding: 8px 0 0;
|
margin: 0;
|
||||||
|
padding: 8px 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
.more-block.is-open & {
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-toggle__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-toggle__title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #4e5969;
|
color: #4e5969;
|
||||||
cursor: pointer;
|
}
|
||||||
|
|
||||||
em {
|
.more-toggle__hint {
|
||||||
font-style: normal;
|
font-size: 12px;
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
|
color: #c9cdd4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-toggle__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-toggle__chevron {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #c9cdd4;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
transform: rotate(180deg);
|
||||||
color: #86909c;
|
color: #86909c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-group__label {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #86909c;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
.dim-row {
|
.dim-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: 1fr auto 1fr auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&--geo {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 4px;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dim-sep {
|
||||||
|
color: #c9cdd4;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dim-cell {
|
.dim-cell {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #86909c;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
height: 38px;
|
||||||
border: 1px solid #e5e6eb;
|
border: 1px solid #e5e6eb;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 0 8px;
|
padding: 0 10px;
|
||||||
font-size: 15px;
|
background: #fff;
|
||||||
font-weight: 600;
|
font-size: 14px;
|
||||||
color: #1d2129;
|
font-weight: 500;
|
||||||
|
color: #4e5969;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #c9cdd4;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: color-mix(in srgb, var(--app-primary-color) 50%, #e5e6eb);
|
border-color: color-mix(in srgb, var(--app-primary-color) 45%, #e5e6eb);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--app-primary-color) 12%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dim-hint {
|
.dim-hint {
|
||||||
margin: 0 0 12px;
|
margin: 0;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #86909c;
|
color: #c9cdd4;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2217,11 +2349,6 @@ onMounted(() => {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #c9cdd4;
|
color: #c9cdd4;
|
||||||
|
|
||||||
&--warn {
|
|
||||||
color: #f53f3f;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-quote__price {
|
.h5-quote__price {
|
||||||
@ -2266,13 +2393,30 @@ onMounted(() => {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-btn {
|
.advisor-icon-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 28px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0 4px;
|
color: var(--app-primary-color);
|
||||||
font-size: 12px;
|
|
||||||
color: #86909c;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advisor-icon-btn__icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-btn {
|
.order-btn {
|
||||||
@ -2328,7 +2472,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h5-detail {
|
.h5-detail {
|
||||||
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
|
padding-bottom: calc(86px + env(safe-area-inset-bottom, 0px));
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2382,41 +2526,183 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h5-detail__guide {
|
.h5-detail__guide {
|
||||||
padding: 16px 0;
|
margin: 16px 0 8px;
|
||||||
|
padding: 14px 14px 4px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-detail__guide-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #e5e6eb;
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0 0 10px;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
color: #1d2129;
|
color: #1d2129;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 3px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--app-primary-color);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-md {
|
.h5-detail__guide-body {
|
||||||
font-size: 14px;
|
margin: 0 -4px;
|
||||||
line-height: 1.65;
|
|
||||||
|
:deep(.markdown-viewer) {
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
color: #4e5969;
|
color: #4e5969;
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
:deep(h1),
|
h1 {
|
||||||
:deep(h2),
|
margin: 0 0 12px;
|
||||||
:deep(h3) {
|
padding-bottom: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 650;
|
||||||
color: #1d2129;
|
color: #1d2129;
|
||||||
margin: 12px 0 8px;
|
border-bottom: 1px solid #e5e6eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(p) {
|
h2 {
|
||||||
margin: 0 0 8px;
|
margin: 18px 0 8px;
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: #1d2129;
|
||||||
|
border-left: 3px solid var(--app-primary-color);
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(ul),
|
h2:first-child {
|
||||||
:deep(ol) {
|
margin-top: 0;
|
||||||
padding-left: 1.2em;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(img) {
|
h3 {
|
||||||
|
margin: 14px 0 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 12px 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding-left: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li + li {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 650;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border-left: 3px solid var(--app-primary-color);
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 10px 0 12px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f2f3f5;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 14px 0;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px dashed #e5e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
|
margin: 4px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #1e1e1e;
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2426,11 +2712,60 @@ onMounted(() => {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 0.9fr 1.3fr;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
|
padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-top: 1px solid #e5e6eb;
|
border-top: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-detail__icon-btn {
|
||||||
|
flex: 0 0 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
color: #4e5969;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #f7f8fa;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-detail__icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-detail__order-btn {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
height: 48px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--app-primary-color);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 8px 18px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -98,10 +98,10 @@
|
|||||||
<a-spin :loading="loading && rows.length > 0" class="h5-so-spin">
|
<a-spin :loading="loading && rows.length > 0" class="h5-so-spin">
|
||||||
<H5EmptyState
|
<H5EmptyState
|
||||||
v-if="!loading && !rows.length"
|
v-if="!loading && !rows.length"
|
||||||
:icon="IconList"
|
:icon="IconTabPackage"
|
||||||
:text="t('h5.storeOrders.empty')"
|
:text="t('h5.storeOrders.empty')"
|
||||||
>
|
>
|
||||||
<a-button type="outline" size="small" @click="pullVisible = true">
|
<a-button class="app-btn-primary-soft" @click="pullVisible = true">
|
||||||
{{ t('h5.storeOrders.pull.entry') }}
|
{{ t('h5.storeOrders.pull.entry') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</H5EmptyState>
|
</H5EmptyState>
|
||||||
@ -248,7 +248,7 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Message, Modal } from '@arco-design/web-vue';
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
import { IconCheck, IconCopy, IconDown, IconLeft, IconList, IconSearch } from '@arco-design/web-vue/es/icon';
|
import { IconCheck, IconCopy, IconDown, IconLeft, IconSearch } from '@arco-design/web-vue/es/icon';
|
||||||
import {
|
import {
|
||||||
getPlatformStoreOrderList,
|
getPlatformStoreOrderList,
|
||||||
getPlatformStoreOrderSummary,
|
getPlatformStoreOrderSummary,
|
||||||
@ -263,6 +263,7 @@ import {
|
|||||||
} from '@/utils/warehouseListMapper';
|
} from '@/utils/warehouseListMapper';
|
||||||
import { h5Navigate } from '@/utils/device';
|
import { h5Navigate } from '@/utils/device';
|
||||||
import { getStoreOrderStatusTagStyle } from '@/constants/storeOrderStatus';
|
import { getStoreOrderStatusTagStyle } from '@/constants/storeOrderStatus';
|
||||||
|
import IconTabPackage from '@/assets/icons/h5/tab-package.svg';
|
||||||
import H5EmptyState from '../components/H5EmptyState.vue';
|
import H5EmptyState from '../components/H5EmptyState.vue';
|
||||||
import H5StoreOrderPullSheet from '../components/H5StoreOrderPullSheet.vue';
|
import H5StoreOrderPullSheet from '../components/H5StoreOrderPullSheet.vue';
|
||||||
import H5StoreOrderCreateSheet, {
|
import H5StoreOrderCreateSheet, {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { ref, computed, onMounted, reactive, nextTick, watch } from 'vue';
|
import { ref, computed, onMounted, reactive, nextTick, watch } from 'vue';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
IconExport,
|
IconDownload,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
@ -4624,7 +4624,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
:title="t('order.placeOrder.template.download')"
|
:title="t('order.placeOrder.template.download')"
|
||||||
@click="handleDownloadTemplate(template)"
|
@click="handleDownloadTemplate(template)"
|
||||||
>
|
>
|
||||||
<template #icon><icon-export /></template>
|
<template #icon><icon-download /></template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -4677,7 +4677,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
:title="t('order.placeOrder.template.download')"
|
:title="t('order.placeOrder.template.download')"
|
||||||
@click="handleDownloadTemplate(template)"
|
@click="handleDownloadTemplate(template)"
|
||||||
>
|
>
|
||||||
<template #icon><icon-export /></template>
|
<template #icon><icon-download /></template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@ -10,6 +10,10 @@
|
|||||||
selected?: boolean
|
selected?: boolean
|
||||||
recommended?: boolean
|
recommended?: boolean
|
||||||
recalcLoading?: boolean
|
recalcLoading?: boolean
|
||||||
|
/** 更多条件已指定货物属性时锁定,卡片内不可切换 */
|
||||||
|
lockCargoType?: boolean
|
||||||
|
/** 更多条件已指定仓库时锁定,卡片内不可切换 */
|
||||||
|
lockWarehouse?: boolean
|
||||||
cargoOptions?: Array<{ value: string; label: string }>
|
cargoOptions?: Array<{ value: string; label: string }>
|
||||||
warehouseOptions?: Array<{ value: number; label: string }>
|
warehouseOptions?: Array<{ value: number; label: string }>
|
||||||
}>()
|
}>()
|
||||||
@ -49,6 +53,12 @@
|
|||||||
return byLabel?.value || code
|
return byLabel?.value || code
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedCargoLabel = computed(() => {
|
||||||
|
const code = selectedCargoType.value
|
||||||
|
const hit = cargoOptions.value.find((o) => o.value === code)
|
||||||
|
return hit?.label || String(props.record.cargoTypeName || code || '-').trim() || '-'
|
||||||
|
})
|
||||||
|
|
||||||
const selectedWarehouseId = computed(() => {
|
const selectedWarehouseId = computed(() => {
|
||||||
const id = Number(props.record.warehouseId)
|
const id = Number(props.record.warehouseId)
|
||||||
if (Number.isFinite(id) && id > 0) return id
|
if (Number.isFinite(id) && id > 0) return id
|
||||||
@ -57,6 +67,12 @@
|
|||||||
return hit?.value
|
return hit?.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedWarehouseLabel = computed(() => {
|
||||||
|
const id = selectedWarehouseId.value
|
||||||
|
const hit = id != null ? warehouseOptions.value.find((o) => o.value === id) : undefined
|
||||||
|
return hit?.label || String(props.record.warehouse || '-').trim() || '-'
|
||||||
|
})
|
||||||
|
|
||||||
const tradeTermsLabel = computed(() => {
|
const tradeTermsLabel = computed(() => {
|
||||||
const tradeTerms = String(props.record.tradeTerms || '').trim().toUpperCase()
|
const tradeTerms = String(props.record.tradeTerms || '').trim().toUpperCase()
|
||||||
if (tradeTerms === 'DDP') return t('order.priceCheck.tradeTerms.ddp')
|
if (tradeTerms === 'DDP') return t('order.priceCheck.tradeTerms.ddp')
|
||||||
@ -106,17 +122,28 @@
|
|||||||
emit('select')
|
emit('select')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCargoChange(value: string | number) {
|
function resolveRecalcWarehouseId() {
|
||||||
|
const fromSelect = Number(selectedWarehouseId.value)
|
||||||
|
if (Number.isFinite(fromSelect) && fromSelect > 0) return fromSelect
|
||||||
|
const fromRecord = Number(props.record.warehouseId ?? props.record.WarehouseId)
|
||||||
|
return Number.isFinite(fromRecord) && fromRecord > 0 ? fromRecord : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRecalcCargoType() {
|
||||||
|
return String(selectedCargoType.value || props.record.cargoType || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCargoChange(value: string | number | boolean | Record<string, unknown> | undefined) {
|
||||||
const cargoType = String(value ?? '').trim()
|
const cargoType = String(value ?? '').trim()
|
||||||
const warehouseId = selectedWarehouseId.value
|
const warehouseId = resolveRecalcWarehouseId()
|
||||||
if (!cargoType || warehouseId == null) return
|
if (!cargoType || warehouseId == null) return
|
||||||
if (cargoType === selectedCargoType.value) return
|
if (cargoType === selectedCargoType.value) return
|
||||||
emit('recalc', { cargoType, warehouseId })
|
emit('recalc', { cargoType, warehouseId })
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWarehouseChange(value: string | number) {
|
function onWarehouseChange(value: string | number | boolean | Record<string, unknown> | undefined) {
|
||||||
const warehouseId = Number(value)
|
const warehouseId = Number(value)
|
||||||
const cargoType = selectedCargoType.value
|
const cargoType = resolveRecalcCargoType()
|
||||||
if (!cargoType || !Number.isFinite(warehouseId) || warehouseId <= 0) return
|
if (!cargoType || !Number.isFinite(warehouseId) || warehouseId <= 0) return
|
||||||
if (warehouseId === selectedWarehouseId.value) return
|
if (warehouseId === selectedWarehouseId.value) return
|
||||||
emit('recalc', { cargoType, warehouseId })
|
emit('recalc', { cargoType, warehouseId })
|
||||||
@ -138,22 +165,32 @@
|
|||||||
<div class="quote-card__info">
|
<div class="quote-card__info">
|
||||||
<div class="quote-card__title-row">
|
<div class="quote-card__title-row">
|
||||||
<h3 class="quote-card__name">{{ record.shippingMethod || '-' }}</h3>
|
<h3 class="quote-card__name">{{ record.shippingMethod || '-' }}</h3>
|
||||||
<p v-if="metaLine" class="quote-card__meta">{{ metaLine }}</p>
|
|
||||||
<span v-if="recommended" class="quote-card__badge">
|
<span v-if="recommended" class="quote-card__badge">
|
||||||
{{ t('order.priceCheck.card.recommendTag') }}
|
{{ t('order.priceCheck.card.recommendTag') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="metaLine" class="quote-card__meta">{{ metaLine }}</p>
|
||||||
|
|
||||||
<div class="quote-card__switches" @click.stop>
|
<div class="quote-card__switches" @click.stop>
|
||||||
<label class="quote-card__switch">
|
<div
|
||||||
|
class="quote-card__switch"
|
||||||
|
:class="{ 'is-locked': lockCargoType }"
|
||||||
|
:title="
|
||||||
|
lockCargoType
|
||||||
|
? t('order.priceCheck.card.switchLockedHint')
|
||||||
|
: t('order.priceCheck.card.switchRecalcHint')
|
||||||
|
"
|
||||||
|
>
|
||||||
<span class="quote-card__switch-label">{{ t('order.priceCheck.card.cargoType') }}</span>
|
<span class="quote-card__switch-label">{{ t('order.priceCheck.card.cargoType') }}</span>
|
||||||
|
<span v-if="lockCargoType" class="quote-card__switch-value">{{ selectedCargoLabel }}</span>
|
||||||
<a-select
|
<a-select
|
||||||
|
v-else
|
||||||
:model-value="selectedCargoType"
|
:model-value="selectedCargoType"
|
||||||
size="mini"
|
size="mini"
|
||||||
class="quote-card__select"
|
class="quote-card__select"
|
||||||
|
popup-container="body"
|
||||||
:placeholder="t('order.priceCheck.card.cargoType')"
|
:placeholder="t('order.priceCheck.card.cargoType')"
|
||||||
:disabled="!!recalcLoading || !cargoOptions.length"
|
:disabled="!!recalcLoading || !cargoOptions.length"
|
||||||
:title="t('order.priceCheck.card.switchRecalcHint')"
|
|
||||||
@change="onCargoChange"
|
@change="onCargoChange"
|
||||||
>
|
>
|
||||||
<a-option
|
<a-option
|
||||||
@ -164,16 +201,26 @@
|
|||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</a-option>
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</label>
|
</div>
|
||||||
<label class="quote-card__switch">
|
<div
|
||||||
|
class="quote-card__switch"
|
||||||
|
:class="{ 'is-locked': lockWarehouse }"
|
||||||
|
:title="
|
||||||
|
lockWarehouse
|
||||||
|
? t('order.priceCheck.card.switchLockedHint')
|
||||||
|
: t('order.priceCheck.card.switchRecalcHint')
|
||||||
|
"
|
||||||
|
>
|
||||||
<span class="quote-card__switch-label">{{ t('order.priceCheck.card.warehouse') }}</span>
|
<span class="quote-card__switch-label">{{ t('order.priceCheck.card.warehouse') }}</span>
|
||||||
|
<span v-if="lockWarehouse" class="quote-card__switch-value">{{ selectedWarehouseLabel }}</span>
|
||||||
<a-select
|
<a-select
|
||||||
|
v-else
|
||||||
:model-value="selectedWarehouseId"
|
:model-value="selectedWarehouseId"
|
||||||
size="mini"
|
size="mini"
|
||||||
class="quote-card__select"
|
class="quote-card__select"
|
||||||
|
popup-container="body"
|
||||||
:placeholder="t('order.priceCheck.card.warehouse')"
|
:placeholder="t('order.priceCheck.card.warehouse')"
|
||||||
:disabled="!!recalcLoading || !warehouseOptions.length"
|
:disabled="!!recalcLoading || !warehouseOptions.length"
|
||||||
:title="t('order.priceCheck.card.switchRecalcHint')"
|
|
||||||
@change="onWarehouseChange"
|
@change="onWarehouseChange"
|
||||||
>
|
>
|
||||||
<a-option
|
<a-option
|
||||||
@ -184,7 +231,7 @@
|
|||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</a-option>
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -398,7 +445,6 @@
|
|||||||
.quote-card__meta {
|
.quote-card__meta {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
@ -448,6 +494,10 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.is-locked {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote-card__switch-label {
|
.quote-card__switch-label {
|
||||||
@ -458,6 +508,17 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-card__switch-value {
|
||||||
|
max-width: 110px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.quote-card__select {
|
.quote-card__select {
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 72px;
|
min-width: 72px;
|
||||||
|
|||||||
@ -98,6 +98,8 @@
|
|||||||
if (formData.cargoType) n += 1;
|
if (formData.cargoType) n += 1;
|
||||||
if (formData.warehouseId != null) n += 1;
|
if (formData.warehouseId != null) n += 1;
|
||||||
if (hasFullDimensions() || hasPartialDimensions()) n += 1;
|
if (hasFullDimensions() || hasPartialDimensions()) n += 1;
|
||||||
|
if (String(formData.destinationProvince || '').trim()) n += 1;
|
||||||
|
if (String(formData.destinationCity || '').trim()) n += 1;
|
||||||
if (couponInstanceId.value) n += 1;
|
if (couponInstanceId.value) n += 1;
|
||||||
return n;
|
return n;
|
||||||
});
|
});
|
||||||
@ -117,7 +119,14 @@
|
|||||||
type AdvancedConditionChip = {
|
type AdvancedConditionChip = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
kind: 'dimensions' | 'cargoType' | 'warehouse' | 'shipping' | 'coupon';
|
kind:
|
||||||
|
| 'dimensions'
|
||||||
|
| 'destinationProvince'
|
||||||
|
| 'destinationCity'
|
||||||
|
| 'cargoType'
|
||||||
|
| 'warehouse'
|
||||||
|
| 'shipping'
|
||||||
|
| 'coupon';
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,6 +141,26 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (formData.cargoType) {
|
||||||
const hit = cargoTypeOptions.value.find((c) => c.value === formData.cargoType);
|
const hit = cargoTypeOptions.value.find((c) => c.value === formData.cargoType);
|
||||||
chips.push({
|
chips.push({
|
||||||
@ -184,6 +213,14 @@
|
|||||||
formData.height = '';
|
formData.height = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (chip.kind === 'destinationProvince') {
|
||||||
|
formData.destinationProvince = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (chip.kind === 'destinationCity') {
|
||||||
|
formData.destinationCity = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (chip.kind === 'cargoType') {
|
if (chip.kind === 'cargoType') {
|
||||||
formData.cargoType = '';
|
formData.cargoType = '';
|
||||||
return;
|
return;
|
||||||
@ -205,6 +242,8 @@
|
|||||||
formData.length = '';
|
formData.length = '';
|
||||||
formData.width = '';
|
formData.width = '';
|
||||||
formData.height = '';
|
formData.height = '';
|
||||||
|
formData.destinationProvince = '';
|
||||||
|
formData.destinationCity = '';
|
||||||
formData.cargoType = '';
|
formData.cargoType = '';
|
||||||
formData.warehouseId = undefined;
|
formData.warehouseId = undefined;
|
||||||
formData.shippingMethod = '';
|
formData.shippingMethod = '';
|
||||||
@ -1016,26 +1055,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 class="form-item button-group">
|
<div class="form-item button-group">
|
||||||
<label class="form-label form-label-spacer" aria-hidden="true"> </label>
|
<label class="form-label form-label-spacer" aria-hidden="true"> </label>
|
||||||
<div class="button-group-row">
|
<div class="button-group-row">
|
||||||
@ -1124,6 +1143,26 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div v-if="availableCoupons.length" class="form-item form-item-coupon">
|
||||||
<label class="form-label">{{ $t('marketing.priceCheck.couponLabel') }}</label>
|
<label class="form-label">{{ $t('marketing.priceCheck.couponLabel') }}</label>
|
||||||
<a-select
|
<a-select
|
||||||
@ -1266,6 +1305,8 @@
|
|||||||
:recommended="isRecommendStamp(record)"
|
:recommended="isRecommendStamp(record)"
|
||||||
:cargo-options="cargoTypeOptions"
|
:cargo-options="cargoTypeOptions"
|
||||||
:warehouse-options="warehouseOptions"
|
:warehouse-options="warehouseOptions"
|
||||||
|
:lock-cargo-type="!!formData.cargoType"
|
||||||
|
:lock-warehouse="formData.warehouseId != null"
|
||||||
:recalc-loading="!!recalcLoadingKeys[record.cardKey || record.rowKey]"
|
:recalc-loading="!!recalcLoadingKeys[record.cardKey || record.rowKey]"
|
||||||
@select="handleSelectGuide(record)"
|
@select="handleSelectGuide(record)"
|
||||||
@order="handleOrder(record)"
|
@order="handleOrder(record)"
|
||||||
|
|||||||
@ -282,14 +282,28 @@ export default defineConfig(({ mode }) => {
|
|||||||
target: 'https://cos.ideship.com',
|
target: 'https://cos.ideship.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
|
xfwd: false,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
proxyTimeout: 30000,
|
proxyTimeout: 30000,
|
||||||
router: (req) => {
|
router: (req) => {
|
||||||
const path = req.url?.split('?')[0] ?? '';
|
const raw = String(req.originalUrl || req.url || '').split('?')[0];
|
||||||
const host = path.replace(/^\/__cos__\//, '').split('/')[0];
|
const host = raw.replace(/^\/__cos__\//, '').split('/')[0];
|
||||||
return host ? `https://${host}` : 'https://cos.ideship.com';
|
return host ? `https://${host}` : 'https://cos.ideship.com';
|
||||||
},
|
},
|
||||||
rewrite: (path) => path.replace(/^\/__cos__\/[^/]+/, '') || '/',
|
rewrite: (path) => path.replace(/^\/__cos__\/[^/]+/, '') || '/',
|
||||||
|
configure: (proxy) => {
|
||||||
|
proxy.on('proxyReq', (proxyReq, req) => {
|
||||||
|
proxyReq.removeHeader('authorization');
|
||||||
|
proxyReq.removeHeader('cookie');
|
||||||
|
proxyReq.removeHeader('origin');
|
||||||
|
proxyReq.removeHeader('referer');
|
||||||
|
const raw = String(req.originalUrl || req.url || '').split('?')[0];
|
||||||
|
const host = raw.replace(/^\/__cos__\//, '').split('/')[0];
|
||||||
|
if (host) {
|
||||||
|
proxyReq.setHeader('Host', host);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'/marketing': {
|
'/marketing': {
|
||||||
target: omsAdminTarget,
|
target: omsAdminTarget,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user