提交
This commit is contained in:
1 parent
c8cec132bf
commit
19c5aaa8f5
95 files changed
+309129
-9838
No files matched your search
@@ -0,0 +1,28 @@
|
||||
/** 门户首页侧栏选中模块(会话级,关标签页清空) */
|
||||
const STORAGE_KEY = 'portal-home-active-module'
|
||||
|
||||
export function getPortalHomeActiveModule(): string {
|
||||
try {
|
||||
return String(sessionStorage.getItem(STORAGE_KEY) || '').trim()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function setPortalHomeActiveModule(key: string): void {
|
||||
const v = String(key || '').trim()
|
||||
if (!v) return
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, v)
|
||||
} catch {
|
||||
/* ignore quota / private mode */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearPortalHomeActiveModule(): void {
|
||||
try {
|
||||
sessionStorage.removeItem(STORAGE_KEY)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,37 @@ import type { ContractQtyReminder, ContractQtyReminderItem } from '@/api/pl'
|
||||
|
||||
export const CONTRACT_QTY_VIOLATION_CODE = 'CONTRACT_QTY_OVERFLOW'
|
||||
|
||||
/** 弹窗明细行(超量 / 提醒共用) */
|
||||
export type ContractQtyDialogRow = {
|
||||
contractId?: string
|
||||
contractNo?: string
|
||||
customerName?: string
|
||||
stopKind?: string
|
||||
productId?: string
|
||||
productName?: string
|
||||
model?: string
|
||||
unit?: string
|
||||
contractQty?: number | null
|
||||
arrangedTotalQty?: number | null
|
||||
remainingQty?: number | null
|
||||
/** framework 无固定量 */
|
||||
framework?: boolean
|
||||
overflow?: boolean
|
||||
}
|
||||
|
||||
function formatQty(n: number | undefined | null): string {
|
||||
if (n == null || Number.isNaN(Number(n))) return '—'
|
||||
return Number(n).toFixed(3)
|
||||
}
|
||||
|
||||
function escapeHtml(raw: string): string {
|
||||
return String(raw)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function stopKindLabel(kind?: string): string {
|
||||
if (kind === 'delivery') return '送货'
|
||||
if (kind === 'load') return '装货'
|
||||
@@ -32,60 +58,142 @@ export function extractContractQtyViolationDetail(error: unknown): {
|
||||
}
|
||||
}
|
||||
|
||||
export function formatContractQtyViolationMessage(
|
||||
items: Array<Record<string, unknown>>
|
||||
): string {
|
||||
if (!items.length) return '合同产品安排数量超过合同数量,请调整后再试。'
|
||||
const lines = items.map((it) => {
|
||||
const name = String(it.productName || it.productId || '产品')
|
||||
const contractNo = String(it.contractNo || it.contractId || '')
|
||||
const side = stopKindLabel(String(it.stopKind || ''))
|
||||
const contractQty = formatQty(Number(it.contractQty))
|
||||
const effective = formatQty(Number(it.effectiveTotal ?? it.arrangedTotalQty))
|
||||
const overflow = formatQty(Number(it.overflowQty))
|
||||
const meta = [contractNo, side].filter(Boolean).join(' · ')
|
||||
return `「${name}」安排后合计 ${effective},超过合同数量 ${contractQty}(超 ${overflow})${meta ? `;${meta}` : ''}`
|
||||
function normalizeDialogRows(
|
||||
items: Array<Record<string, unknown> | ContractQtyReminderItem>
|
||||
): ContractQtyDialogRow[] {
|
||||
return (items || []).map((raw) => {
|
||||
const it = raw as Record<string, unknown>
|
||||
const mode = String(it.mode || '')
|
||||
const overflowQty = it.overflowQty != null ? Number(it.overflowQty) : null
|
||||
const remaining =
|
||||
it.remainingQty != null
|
||||
? Number(it.remainingQty)
|
||||
: it.effectiveTotal != null && it.contractQty != null
|
||||
? Number(it.contractQty) - Number(it.effectiveTotal)
|
||||
: null
|
||||
return {
|
||||
contractId: String(it.contractId || ''),
|
||||
contractNo: it.contractNo != null ? String(it.contractNo) : '',
|
||||
customerName: it.customerName != null ? String(it.customerName) : '',
|
||||
stopKind: it.stopKind != null ? String(it.stopKind) : '',
|
||||
productId: it.productId != null ? String(it.productId) : '',
|
||||
productName: it.productName != null ? String(it.productName) : '',
|
||||
model: it.model != null ? String(it.model) : '',
|
||||
unit: it.unit != null ? String(it.unit) : '',
|
||||
contractQty: it.contractQty != null ? Number(it.contractQty) : null,
|
||||
arrangedTotalQty:
|
||||
it.arrangedTotalQty != null
|
||||
? Number(it.arrangedTotalQty)
|
||||
: it.effectiveTotal != null
|
||||
? Number(it.effectiveTotal)
|
||||
: null,
|
||||
remainingQty: remaining,
|
||||
framework: mode === 'framework_cumulative',
|
||||
overflow: overflowQty != null ? overflowQty > 0 : remaining != null && remaining < 0
|
||||
}
|
||||
})
|
||||
return `以下产品「已安排合计 + 本次安排」超过合同数量,请调整后再试:\n\n${lines.join('\n')}`
|
||||
}
|
||||
|
||||
export function formatContractQtyReminderMessage(reminder: ContractQtyReminder): string {
|
||||
const items = reminder?.items || []
|
||||
if (!items.length) return ''
|
||||
const remaining = items.filter((i) => i.mode === 'remaining')
|
||||
const framework = items.filter((i) => i.mode === 'framework_cumulative')
|
||||
const blocks: string[] = []
|
||||
|
||||
if (remaining.length) {
|
||||
const lines = remaining.map((it: ContractQtyReminderItem) => {
|
||||
const name = it.productName || it.productId
|
||||
const meta = [it.contractNo, stopKindLabel(it.stopKind)].filter(Boolean).join(' · ')
|
||||
return `「${name}」已安排合计(含本次)${formatQty(it.arrangedTotalQty)},剩余可安排 ${formatQty(it.remainingQty)}${meta ? `;${meta}` : ''}`
|
||||
})
|
||||
blocks.push(`合同数量校验通过。各产品剩余可安排量:\n${lines.join('\n')}`)
|
||||
}
|
||||
|
||||
if (framework.length) {
|
||||
const lines = framework.map((it: ContractQtyReminderItem) => {
|
||||
const name = it.productName || it.productId
|
||||
const meta = [it.contractNo, stopKindLabel(it.stopKind)].filter(Boolean).join(' · ')
|
||||
return `「${name}」累计已安排 ${formatQty(it.arrangedTotalQty)}(本次 ${formatQty(it.thisPlanQty)})${meta ? `;${meta}` : ''}`
|
||||
})
|
||||
blocks.push(
|
||||
`关联框架合同无固定数量上限。本次安排后累计已安排:\n${lines.join('\n')}`
|
||||
)
|
||||
}
|
||||
|
||||
return blocks.join('\n\n')
|
||||
function groupKey(row: ContractQtyDialogRow): string {
|
||||
return `${row.contractId || row.contractNo || ''}::${row.stopKind || ''}`
|
||||
}
|
||||
|
||||
/** @returns true 表示已按合同超量处理(调用方勿再抛出/重复提示) */
|
||||
function groupTitle(row: ContractQtyDialogRow): string {
|
||||
const customer = (row.customerName || '').trim() || '客户'
|
||||
const contractNo = (row.contractNo || '').trim() || '—'
|
||||
const side = stopKindLabel(row.stopKind)
|
||||
const base = `${customer} - ${contractNo}`
|
||||
return side ? `${base}(${side})` : base
|
||||
}
|
||||
|
||||
/** 按合同分组的 HTML 表格(超量 / 剩余 / 框架共用) */
|
||||
export function buildContractQtyDialogHtml(
|
||||
items: Array<Record<string, unknown> | ContractQtyReminderItem>,
|
||||
options?: { intro?: string }
|
||||
): string {
|
||||
const rows = normalizeDialogRows(items)
|
||||
if (!rows.length) {
|
||||
return `<p style="margin:0;color:#64748b;font-size:13px;">暂无明细</p>`
|
||||
}
|
||||
|
||||
const order: string[] = []
|
||||
const groups = new Map<string, ContractQtyDialogRow[]>()
|
||||
for (const row of rows) {
|
||||
const key = groupKey(row)
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, [])
|
||||
order.push(key)
|
||||
}
|
||||
groups.get(key)!.push(row)
|
||||
}
|
||||
|
||||
const blocks = order.map((key) => {
|
||||
const list = groups.get(key) || []
|
||||
const title = groupTitle(list[0])
|
||||
const body = list
|
||||
.map((row) => {
|
||||
const name = escapeHtml(row.productName || row.productId || '—')
|
||||
const model = escapeHtml(row.model || '—')
|
||||
const unit = escapeHtml(row.unit || '—')
|
||||
const contractQty = row.framework
|
||||
? '按客户需求'
|
||||
: escapeHtml(formatQty(row.contractQty))
|
||||
const arranged = escapeHtml(formatQty(row.arrangedTotalQty))
|
||||
const remainingRaw = row.framework ? '—' : formatQty(row.remainingQty)
|
||||
const remainingStyle =
|
||||
row.overflow || (row.remainingQty != null && row.remainingQty < 0)
|
||||
? 'color:#dc2626;font-weight:600;'
|
||||
: 'color:#059669;font-weight:600;'
|
||||
return `<tr>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:left;">${name}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:left;">${model}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-variant-numeric:tabular-nums;">${contractQty}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:center;">${unit}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-variant-numeric:tabular-nums;">${arranged}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-variant-numeric:tabular-nums;${remainingStyle}">${escapeHtml(remainingRaw)}</td>
|
||||
</tr>`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `<div style="margin:0 0 14px;">
|
||||
<div style="font-size:14px;font-weight:600;color:#1e293b;margin:0 0 8px;padding-left:8px;border-left:3px solid #10b981;">
|
||||
${escapeHtml(title)}
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;color:#1e293b;">
|
||||
<thead>
|
||||
<tr style="background:#f8fafc;">
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:left;font-weight:600;">产品名称</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:left;font-weight:600;">规格型号</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-weight:600;">合同数量</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:center;font-weight:600;">单位</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-weight:600;">已安排</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-weight:600;">剩余</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${body}</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
|
||||
const intro = options?.intro
|
||||
? `<p style="margin:0 0 12px;font-size:13px;color:#64748b;">${escapeHtml(options.intro)}</p>`
|
||||
: ''
|
||||
return `<div style="max-width:720px;text-align:left;">${intro}${blocks.join('')}</div>`
|
||||
}
|
||||
|
||||
/** @returns true 表示已按合同超量处理 */
|
||||
export async function showContractQtyViolationIfMatch(error: unknown): Promise<boolean> {
|
||||
const parsed = extractContractQtyViolationDetail(error)
|
||||
if (!parsed) return false
|
||||
await ElMessageBox.alert(formatContractQtyViolationMessage(parsed.items), '无法安排运输', {
|
||||
const html = buildContractQtyDialogHtml(parsed.items, {
|
||||
intro: '以下产品「已安排 + 本次」超过合同数量,请调整后再安排:'
|
||||
})
|
||||
await ElMessageBox.alert(html, '无法安排运输', {
|
||||
type: 'error',
|
||||
confirmButtonText: '知道了'
|
||||
confirmButtonText: '知道了',
|
||||
dangerouslyUseHTMLString: true,
|
||||
customClass: 'pl-contract-qty-msgbox',
|
||||
customStyle: { width: '780px', maxWidth: '92vw' }
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -95,14 +203,23 @@ export async function showContractQtyReminderIfAny(
|
||||
): Promise<void> {
|
||||
const items = reminder?.items || []
|
||||
if (!items.length) return
|
||||
const message = formatContractQtyReminderMessage(reminder!)
|
||||
if (!message) return
|
||||
const hasFramework = items.some((i) => i.mode === 'framework_cumulative')
|
||||
const title = hasFramework && !items.some((i) => i.mode === 'remaining')
|
||||
? '安排成功 · 框架合同累计'
|
||||
: '安排成功 · 合同剩余数量'
|
||||
await ElMessageBox.alert(message, title, {
|
||||
const hasRemaining = items.some((i) => i.mode === 'remaining')
|
||||
let title = '安排成功 · 合同数量提醒'
|
||||
let intro = '运输安排已成功。各关联合同产品数量如下:'
|
||||
if (hasFramework && !hasRemaining) {
|
||||
title = '安排成功 · 框架合同累计'
|
||||
intro = '运输安排已成功。框架合同无固定数量上限,累计已安排如下:'
|
||||
} else if (hasRemaining && !hasFramework) {
|
||||
title = '安排成功 · 合同剩余数量'
|
||||
intro = '运输安排已成功。各产品剩余可安排量如下:'
|
||||
}
|
||||
const html = buildContractQtyDialogHtml(items, { intro })
|
||||
await ElMessageBox.alert(html, title, {
|
||||
type: 'success',
|
||||
confirmButtonText: '知道了'
|
||||
confirmButtonText: '知道了',
|
||||
dangerouslyUseHTMLString: true,
|
||||
customClass: 'pl-contract-qty-msgbox',
|
||||
customStyle: { width: '780px', maxWidth: '92vw' }
|
||||
})
|
||||
}
|
||||
@@ -77,6 +77,87 @@ export function flattenPlanListRows(plans: Plan[]): PlPlanListDisplayRow[] {
|
||||
return rows
|
||||
}
|
||||
|
||||
function stopHasProductId(
|
||||
stop: PlanLoad | PlanDelivery | null | undefined,
|
||||
productId: string
|
||||
): boolean {
|
||||
const pid = String(productId || '').trim()
|
||||
if (!pid || !stop) return false
|
||||
return (stop.products || []).some((p) => String(p.productId || '') === pid)
|
||||
}
|
||||
|
||||
/** 仅保留指定产品(计划产品行 + 各段产品行);不含该产品的送货段丢弃。 */
|
||||
export function filterPlanToProductId(plan: Plan, productId: string): Plan {
|
||||
const pid = String(productId || '').trim()
|
||||
if (!pid) return plan
|
||||
const match = (p: { productId?: string }) => String(p.productId || '') === pid
|
||||
const products = (plan.products || []).filter(match)
|
||||
const load = plan.load
|
||||
? { ...plan.load, products: (plan.load.products || []).filter(match) }
|
||||
: plan.load
|
||||
const deliveries = (plan.deliveries || [])
|
||||
.map((d) => ({ ...d, products: (d.products || []).filter(match) }))
|
||||
.filter((d) => (d.products || []).length > 0)
|
||||
return { ...plan, products, load, deliveries }
|
||||
}
|
||||
|
||||
/**
|
||||
* 展平运输计划列表行,但只保留含指定产品的段次(不展示同计划其他商品)。
|
||||
* 用于合同用量等「单产品」弹窗列表。
|
||||
*/
|
||||
export function flattenPlanListRowsForProduct(
|
||||
plans: Plan[],
|
||||
productId: string
|
||||
): PlPlanListDisplayRow[] {
|
||||
const pid = String(productId || '').trim()
|
||||
if (!pid) return flattenPlanListRows(plans)
|
||||
|
||||
const rows: PlPlanListDisplayRow[] = []
|
||||
let planListIndex = 0
|
||||
for (const raw of plans || []) {
|
||||
if (!raw?.id) continue
|
||||
const plan = filterPlanToProductId(raw, pid)
|
||||
const { load, deliveries } = resolvePlanStopsForList(plan)
|
||||
type StopSlot = {
|
||||
kind: PlPlanListDisplayRowKind
|
||||
stop: PlanLoad | PlanDelivery
|
||||
deliverySeq?: number
|
||||
}
|
||||
const stops: StopSlot[] = []
|
||||
if (stopHasProductId(load, pid)) {
|
||||
stops.push({ kind: 'load', stop: load })
|
||||
}
|
||||
deliveries.forEach((delivery, index) => {
|
||||
if (!stopHasProductId(delivery, pid)) return
|
||||
stops.push({
|
||||
kind: 'delivery',
|
||||
stop: delivery,
|
||||
deliverySeq: delivery.seq ?? index + 1
|
||||
})
|
||||
})
|
||||
if (!stops.length) continue
|
||||
const planRowSpan = stops.length
|
||||
stops.forEach((slot, index) => {
|
||||
const isLoad = slot.kind === 'load'
|
||||
rows.push({
|
||||
rowKey: isLoad
|
||||
? `${plan.id}-load`
|
||||
: `${plan.id}-delivery-${slot.deliverySeq ?? index + 1}`,
|
||||
plan,
|
||||
kind: slot.kind,
|
||||
stop: slot.stop,
|
||||
deliverySeq: slot.deliverySeq,
|
||||
planListIndex,
|
||||
isPlanFirstRow: index === 0,
|
||||
planRowSpan,
|
||||
isPlanLastRow: index === stops.length - 1
|
||||
})
|
||||
})
|
||||
planListIndex += 1
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export function getPlanListDisplayIndex(
|
||||
displayRows: PlPlanListDisplayRow[],
|
||||
displayRowIndex: number,
|
||||
@@ -102,14 +183,16 @@ export function isPlPlanListPlanMergeColumn(
|
||||
}
|
||||
|
||||
export function buildPlPlanListSpanMethod(
|
||||
visiblePlanProps: string[]
|
||||
visiblePlanProps: string[],
|
||||
fixedMergeColCount: number = PLAN_FIXED_MERGE_COL_COUNT
|
||||
): (args: {
|
||||
row: PlPlanListDisplayRow
|
||||
column: { type?: string; property?: string }
|
||||
rowIndex: number
|
||||
columnIndex: number
|
||||
}) => { rowspan: number; colspan: number } | undefined {
|
||||
const planMergeCount = PLAN_FIXED_MERGE_COL_COUNT + visiblePlanProps.length
|
||||
const fixed = Math.max(0, fixedMergeColCount)
|
||||
const planMergeCount = fixed + visiblePlanProps.length
|
||||
return ({ row, columnIndex }) => {
|
||||
if (columnIndex >= planMergeCount) return undefined
|
||||
if (row.isPlanFirstRow) {
|
||||
@@ -120,7 +203,10 @@ export function buildPlPlanListSpanMethod(
|
||||
}
|
||||
|
||||
export function getPlPlanListRowClassName({ row }: { row: PlPlanListDisplayRow }): string {
|
||||
const classes = [row.kind === 'load' ? 'pl-plan-list-row--load' : 'pl-plan-list-row--delivery']
|
||||
const classes = [
|
||||
'pl-plan-list-row',
|
||||
row.kind === 'load' ? 'pl-plan-list-row--load' : 'pl-plan-list-row--delivery'
|
||||
]
|
||||
if (row.planListIndex % 2 === 1) classes.push('pl-plan-list-row--plan-striped')
|
||||
if (row.isPlanLastRow) classes.push('pl-plan-list-row--plan-end')
|
||||
return classes.join(' ')
|
||||
|
||||
@@ -111,7 +111,7 @@ export interface PlanDeliveryForm {
|
||||
products: PlanStopProductFormRow[]
|
||||
}
|
||||
|
||||
/** 地磅默认勾选以本段客户是否为内部客户(山峡/云栋)为准。 */
|
||||
/** 地磅勾选仅对内部客户(山峡/云栋)生效:默认勾选且不可改;其他客户默认不勾且不可勾。 */
|
||||
export function resolveStopDibangDefaults(
|
||||
customerName?: string | null,
|
||||
stopType: 'load' | 'delivery' = 'load'
|
||||
@@ -132,6 +132,10 @@ function normalizeStopDibangFieldsFromApi(
|
||||
dibangDirection?: string | null
|
||||
): { generateDibang: number; dibangDirection: string } {
|
||||
const defaults = resolveStopDibangDefaults(customerName, stopType)
|
||||
// 非山峡/云栋:禁止生成地磅单(编辑回填也强制清掉历史勾选)
|
||||
if (!isPlInternalCustomerName(customerName)) {
|
||||
return { generateDibang: 0, dibangDirection: 'none' }
|
||||
}
|
||||
const gen = Number(generateDibang ?? defaults.generateDibang) === 1 ? 1 : 0
|
||||
let direction = (dibangDirection || '').trim()
|
||||
if (gen) {
|
||||
|
||||
Reference in new issue
Block a user