完善空运锁舱与海运报价单单选回填,并增强模板预览与协同字段盖写。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,7 +17,16 @@ import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
||||
import AdminPagination from "/@/views/inquiry/components/AdminPagination.vue";
|
||||
|
||||
type TicketQuote = { source: string; price: string; time?: string; highlight?: boolean };
|
||||
/** 单条费用明细:名称 + 带币种金额,对齐企微价格明细。 */
|
||||
type TicketQuoteFee = { name: string; amount: string };
|
||||
type TicketQuote = {
|
||||
source: string;
|
||||
price: string;
|
||||
time?: string;
|
||||
highlight?: boolean;
|
||||
/** TMS/手工报价费用明细行;无明细时为空数组 */
|
||||
fees: TicketQuoteFee[];
|
||||
};
|
||||
type TicketAttach = { name: string; uploadedAt: string; uploader: string };
|
||||
type TicketTimeline = { status: string; remark: string; time: string };
|
||||
|
||||
@@ -108,6 +117,110 @@ function quoteSourceLabel(q: any) {
|
||||
return label && label !== "报价" ? "手工报价" : "报价";
|
||||
}
|
||||
|
||||
/** JSON 字符串或对象都解开;解不开当没有。 */
|
||||
function asObject(raw: any): Record<string, any> | null {
|
||||
if (raw == null) return null;
|
||||
if (typeof raw === "object" && !Array.isArray(raw)) return raw as Record<string, any>;
|
||||
if (typeof raw === "string" && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, any>;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 费用数组:List / JSON 字符串 / 二次编码都认。 */
|
||||
function asFeeArray(raw: any): any[] {
|
||||
if (raw == null) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
if (typeof raw === "string" && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
if (typeof parsed === "string") {
|
||||
const again = JSON.parse(parsed);
|
||||
return Array.isArray(again) ? again : [];
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把一条费用行收成「名称 + 金额」。
|
||||
* TMS 字段:name/chargeName、amount/unitPrice、currency。
|
||||
*/
|
||||
function feeRowToDisplay(item: any): TicketQuoteFee | null {
|
||||
if (!item || typeof item !== "object") return null;
|
||||
const name = String(
|
||||
item.name || item.chargeName || item.feeName || item.label || ""
|
||||
).trim();
|
||||
if (!name) return null;
|
||||
let amount = String(item.amount ?? item.totalPrice ?? item.value ?? "").trim();
|
||||
const unitPrice = String(item.unitPrice ?? item.unit_price ?? "").trim();
|
||||
if (!amount || amount === "-") {
|
||||
amount = unitPrice;
|
||||
}
|
||||
if (!amount || amount === "-") return null;
|
||||
const ccy = String(item.currency || "").trim().toUpperCase();
|
||||
const amountUpper = amount.toUpperCase();
|
||||
if (ccy && !amountUpper.startsWith(ccy) && !/^[A-Z]{3}\s/.test(amountUpper)) {
|
||||
amount = `${ccy} ${amount}`;
|
||||
}
|
||||
return { name, amount };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从报价 payload 抽费用明细,供后台「价格与利润」展示。
|
||||
* 优先 fee_lines / fee_rows;空运再看 departureCharges;海运再看三段桶。
|
||||
* 副作用:无。不写库。
|
||||
*/
|
||||
function parseQuoteFees(q: any): TicketQuoteFee[] {
|
||||
const payload = asObject(q?.payloadJson) || asObject(q?.payload) || {};
|
||||
const out: TicketQuoteFee[] = [];
|
||||
const pushRows = (rows: any[]) => {
|
||||
for (const row of rows) {
|
||||
const one = feeRowToDisplay(row);
|
||||
if (one) out.push(one);
|
||||
}
|
||||
};
|
||||
pushRows(asFeeArray(payload.fee_lines ?? payload.fee_rows ?? q?.fee_lines ?? q?.fee_rows));
|
||||
if (!out.length) {
|
||||
const selected =
|
||||
asObject(payload.selected_air_option) || asObject(payload.selectedAirOption);
|
||||
if (selected) {
|
||||
pushRows(
|
||||
asFeeArray(
|
||||
selected.departureCharges || selected.departure_charges || selected.feeItems
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!out.length) {
|
||||
const opts = asFeeArray(payload.airOptions || payload.air_options);
|
||||
const first = opts.find((x) => x && typeof x === "object") || null;
|
||||
if (first) {
|
||||
pushRows(
|
||||
asFeeArray(first.departureCharges || first.departure_charges || first.feeItems)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!out.length) {
|
||||
for (const bucket of ["polCostItems", "oceanCostItems", "podCostItems"] as const) {
|
||||
const key = `fee_rows_${bucket}`;
|
||||
pushRows(asFeeArray(payload[key] || payload[bucket]));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主账 inq_ticket_status_log.action 是内部码,禁止当备注展示。
|
||||
* transition 与「已报价」等 toStatus 重复,无用户备注时不再跟一段英文。
|
||||
@@ -260,6 +373,7 @@ function mapTicket(t: any, quotes: any[] = [], attachments: any[] = [], timeline
|
||||
price: q.priceText || "-",
|
||||
time: fmtTime(q.createTime),
|
||||
highlight: q.highlighted === 1 || q.highlighted === true,
|
||||
fees: parseQuoteFees(q),
|
||||
})),
|
||||
attachments: attachRows.map((a) => a.name),
|
||||
attachRows,
|
||||
@@ -556,6 +670,13 @@ function refresh() {
|
||||
style="margin-bottom: 8px"
|
||||
>
|
||||
<div class="quote-source">{{ q.source }}</div>
|
||||
<div
|
||||
v-for="(fee, fi) in q.fees"
|
||||
:key="q.source + '-fee-' + fi"
|
||||
class="quote-fee-line"
|
||||
>
|
||||
{{ fee.name }}:{{ fee.amount }}
|
||||
</div>
|
||||
<div class="quote-price" :style="q.highlight ? { color: '#1d4ed8' } : undefined">
|
||||
{{ q.price }}
|
||||
</div>
|
||||
@@ -688,9 +809,30 @@ function refresh() {
|
||||
<div class="ticket-detail-item"><label>利润点</label><span>{{ current.profit }}</span></div>
|
||||
</div>
|
||||
<div v-if="current.quotes.length" class="ticket-detail-demand">
|
||||
<div v-for="q in current.quotes" :key="q.source + q.price + (q.time || '')" class="quote-row" style="margin-bottom: 6px">
|
||||
<div class="quote-source">{{ q.source }}<span v-if="q.time && q.time !== '-'"> · {{ q.time }}</span></div>
|
||||
<div class="quote-price" :style="q.highlight ? { color: '#1d4ed8' } : undefined">{{ q.price }}</div>
|
||||
<div
|
||||
v-for="q in current.quotes"
|
||||
:key="q.source + q.price + (q.time || '')"
|
||||
class="quote-row ticket-detail-quote"
|
||||
>
|
||||
<div class="quote-source">
|
||||
{{ q.source }}<span v-if="q.time && q.time !== '-'"> · {{ q.time }}</span>
|
||||
</div>
|
||||
<div v-if="q.fees.length" class="quote-fee-list">
|
||||
<div
|
||||
v-for="(fee, fi) in q.fees"
|
||||
:key="q.source + '-detail-fee-' + fi"
|
||||
class="quote-fee-line"
|
||||
>
|
||||
<span class="quote-fee-name">{{ fee.name }}</span>
|
||||
<span class="quote-fee-amount">{{ fee.amount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="quote-price"
|
||||
:style="q.highlight ? { color: '#1d4ed8' } : undefined"
|
||||
>
|
||||
合计 {{ q.price }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user