feat(pl): 装送货段增加同市标记,并同步额度证件与计划表单。

同市易制毒段只校验购买证;同步 Web/H5 表单、额度统计展示与相关回归测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-12 16:34:58 +08:00
1 parent d58f6ea5bd
commit 522b4be705
40 files changed
+576 -107

No files matched your search

+4
View File
@@ -480,6 +480,8 @@ export interface PlanLoad {
notes?: string
generateDibang?: number
dibangDirection?: string
/** 1 时本段易制毒只校验购买证 */
sameCity?: number
remark?: string
products?: PlanStopProduct[]
contracts?: PlanContractLink[]
@@ -502,6 +504,8 @@ export interface PlanDelivery {
notes?: string
generateDibang?: number
dibangDirection?: string
/** 1 时本段易制毒只校验购买证 */
sameCity?: number
remark?: string
deliveryMethod?: string
products?: PlanStopProduct[]
+2
View File
@@ -285,6 +285,8 @@ export interface QuotaSourceOption {
contractId: string
contractProductId: string
partyName?: string
/** 本段勾选同市时只要求购买证 */
sameCity?: boolean
}
export interface QuotaSourcePair {
sourceStopType: 'load' | 'delivery'
@@ -162,6 +162,15 @@
@update:model-value="patchDelivery(index, { notes: $event })"
/>
</el-form-item>
<el-form-item label="是否同市" class="form-col-span-2">
<el-checkbox
:model-value="delivery.sameCity === 1"
:disabled="readonly"
@update:model-value="(checked) => patchDelivery(index, { sameCity: checked ? 1 : 0 })"
>
同市只需购买证
</el-checkbox>
</el-form-item>
<el-form-item label="地磅单" class="form-col-span-2">
<div class="pl-plan-dibang-row">
<el-checkbox
@@ -714,6 +714,7 @@
mergePlanProductsFromLoadMirror,
shouldRetainPlanProductsWhileLoadEmpty,
resolveStopDibangDefaults,
toxicStopCertBindingMessage,
sumStopProductsPlanQty,
type PlanDeliveryForm,
type PlanLoadForm,
@@ -2619,19 +2620,7 @@
}
function toxicContractCertBindingMessage(rows: QuotaSourceOption[]) {
const missing = new Map<string, string>()
for (const row of rows) {
if (isPlInternalCustomerName(row.partyName)) continue
const certificates = row.certificates || []
const ready =
certificates.some((cert) => cert.certType === 'purchase') &&
certificates.some((cert) => cert.certType === 'transport')
if (ready) continue
const key = row.contractId || row.contractNo || row.productName || row.sourceStopProductId
missing.set(key, row.contractNo || '未编号合同')
}
if (!missing.size) return ''
return `合同${[...missing.values()].join('、')}尚未绑定购买证和运输证,请先到合同中绑定后再安排运输`
return toxicStopCertBindingMessage(rows)
}
async function performArrangeAfterSave(
@@ -110,6 +110,15 @@
@update:model-value="patch({ notes: $event })"
/>
</el-form-item>
<el-form-item label="是否同市" class="form-col-span-2">
<el-checkbox
:model-value="modelValue.sameCity === 1"
:disabled="readonly"
@update:model-value="patch({ sameCity: $event ? 1 : 0 })"
>
同市只需购买证
</el-checkbox>
</el-form-item>
<el-form-item label="地磅单" class="form-col-span-2">
<div class="pl-plan-dibang-row">
<el-checkbox
@@ -78,7 +78,15 @@
<section v-if="faceTabs.length" class="quota-face">
<h3 class="dialog-section-title">关联证件证面</h3>
<el-tabs v-model="faceTab" class="quota-face-tabs">
<el-tab-pane v-for="tab in faceTabs" :key="tab.key" :label="tab.label" :name="tab.key">
<el-tab-pane v-for="tab in faceTabs" :key="tab.key" :name="tab.key">
<template #label>
<span class="quota-kind-line">
<el-tag size="small" :type="certKindTagType(tab.kind)">{{
certKindLabel(tab.kind)
}}</el-tag>
<span>{{ tab.certNo }}</span>
</span>
</template>
<p v-if="!tab.cert" class="quota-note">没有读到这张证的证面档案。</p>
<template v-else>
<section
@@ -209,12 +217,9 @@
<div class="conversion-grid">
<section v-for="row in group.rows" :key="conversionKey(row)" class="conversion-card">
<div class="conversion-card-head">
<el-tag
size="small"
effect="plain"
:type="row.cert.certType === 'purchase' ? 'success' : 'primary'"
>{{ row.cert.certType === 'purchase' ? '购买证' : '运输证' }}</el-tag
>
<el-tag size="small" :type="certKindTagType(row.cert.certType)">{{
certKindLabel(row.cert.certType)
}}</el-tag>
<strong>{{ row.cert.certNo }}</strong>
</div>
<label>换算系数</label>
@@ -286,7 +291,12 @@
>
</div>
<div v-for="item in selected" :key="item.kind + item.cert.id" class="view-section">
<h4>{{ item.kind === 'purchase' ? '购买证' : '运输证' }} · {{ item.cert.certNo }}</h4>
<h4 class="quota-kind-line">
<el-tag size="small" :type="certKindTagType(item.kind)">{{
certKindLabel(item.kind)
}}</el-tag>
<span>{{ item.cert.certNo }}</span>
</h4>
<p
>{{ item.cert.buyerName || item.cert.shipperName }} →
{{ item.cert.sellerName || item.cert.consigneeName }};有效期
@@ -393,10 +403,12 @@
:key="row.certType + row.unit"
class="view-section"
>
<strong
>{{ row.certType === 'purchase' ? '购买证' : '运输证' }} · 需求
{{ qtyText(row.demand) }} {{ unitText(row.unit) }}</strong
>
<div class="quota-kind-line">
<el-tag size="small" :type="certKindTagType(row.certType)">{{
certKindLabel(row.certType)
}}</el-tag>
<strong>需求 {{ qtyText(row.demand) }} {{ unitText(row.unit) }}</strong>
</div>
<p v-if="row.availableUpperBound !== null">
适用余额至多 {{ qtyText(row.availableUpperBound) }}
{{ unitText(row.unit) }};缺口至少 {{ qtyText(row.gapLowerBound) }}
@@ -408,7 +420,13 @@
}}</p>
</div>
<el-table :data="allocations" stripe
><el-table-column prop="kind" label="证种" min-width="90" /><el-table-column
><el-table-column label="证种" min-width="110"
><template #default="{ row }"
><el-tag size="small" :type="row.kind === '运输证' ? 'warning' : 'success'">{{
row.kind
}}</el-tag></template
></el-table-column
><el-table-column
prop="certNo"
label="证号"
min-width="160" /><el-table-column
@@ -493,6 +511,7 @@
import { useUserStore } from '@/store/modules/user'
import { useDictStore } from '@/store/modules/dict'
import { getProductUnitLabel } from '@/utils/crm-product-display'
import { certKindLabel, certKindTagType } from '@/views/pl/quota-cert/cert-list-face'
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
@@ -631,7 +650,7 @@
kind,
cert,
certNo: number,
label: `${kind === 'purchase' ? '购买证' : '运输证'} ${number}`
label: `${certKindLabel(kind)} ${number}`
})
}
for (const row of plan.value?.stopProducts || []) {
@@ -1059,6 +1078,14 @@
})
</script>
<style scoped>
.quota-kind-line {
display: inline-flex;
gap: 8px;
align-items: center;
justify-content: flex-start;
max-width: 100%;
}
.quota-dialog :deep(.el-select) {
width: 100%;
}
@@ -2,7 +2,8 @@ import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
canManualToggleStopDibang,
resolveStopDibangDefaults
resolveStopDibangDefaults,
toxicStopCertBindingMessage
} from './pl-plan-stop-form'
import { isPlDibangOptionalVehiclePlate } from './pl-internal-customer'
@@ -23,6 +24,28 @@ describe('isPlDibangOptionalVehiclePlate', () => {
})
})
describe('toxicStopCertBindingMessage', () => {
it('same-city stop only requires the purchase certificate', () => {
assert.equal(
toxicStopCertBindingMessage([
{
contractId: 'c1',
contractNo: 'HT-1',
sameCity: 1,
certificates: [{ certType: 'purchase' }]
}
]),
''
)
assert.equal(
toxicStopCertBindingMessage([
{ contractId: 'c1', contractNo: 'HT-1', sameCity: true, certificates: [] }
]),
'合同HT-1的同市段尚未绑定购买证,请先到合同中绑定后再安排运输'
)
})
})
describe('resolveStopDibangDefaults + canManualToggleStopDibang', () => {
it('internal + normal vehicle: auto-check, not manual', () => {
assert.deepEqual(resolveStopDibangDefaults(INTERNAL, 'load', '皖A11111'), {
+55
View File
@@ -102,6 +102,8 @@ export interface PlanLoadForm {
notes: string
generateDibang: number
dibangDirection: string
/** 1 时本段易制毒只校验购买证 */
sameCity: number
contracts: PlanContractLink[]
products: PlanStopProductFormRow[]
}
@@ -119,6 +121,8 @@ export interface PlanDeliveryForm {
notes: string
generateDibang: number
dibangDirection: string
/** 1 时本段易制毒只校验购买证 */
sameCity: number
contracts: PlanContractLink[]
products: PlanStopProductFormRow[]
}
@@ -146,6 +150,49 @@ export function resolveStopDibangDefaults(
return { generateDibang: 1, dibangDirection: 'inbound' }
}
/** 安排前检查易制毒段的合同证件。同市段只要求购买证。 */
export function toxicStopCertBindingMessage(
rows: Array<{
partyName?: string | null
sameCity?: boolean | number | null
certificates?: { certType?: string | null }[] | null
contractId?: string | null
contractNo?: string | null
productName?: string | null
sourceStopProductId?: string | null
}>
): string {
const missingBoth = new Map<string, string>()
const missingPurchase = new Map<string, string>()
for (const row of rows) {
if (isPlInternalCustomerName(row.partyName)) continue
const certificates = row.certificates || []
const hasPurchase = certificates.some((cert) => cert.certType === 'purchase')
const hasTransport = certificates.some((cert) => cert.certType === 'transport')
const sameCity = row.sameCity === true || Number(row.sameCity) === 1
const key = row.contractId || row.contractNo || row.productName || row.sourceStopProductId || ''
const label = row.contractNo || '未编号合同'
if (sameCity) {
if (!hasPurchase) missingPurchase.set(key, label)
continue
}
if (hasPurchase && hasTransport) continue
missingBoth.set(key, label)
}
const parts: string[] = []
if (missingBoth.size) {
parts.push(
`合同${[...missingBoth.values()].join('、')}尚未绑定购买证和运输证,请先到合同中绑定后再安排运输`
)
}
if (missingPurchase.size) {
parts.push(
`合同${[...missingPurchase.values()].join('、')}的同市段尚未绑定购买证,请先到合同中绑定后再安排运输`
)
}
return parts.join(';')
}
/** 仅「内部客户 + 地磅可选车」允许用户手动勾选地磅单 */
export function canManualToggleStopDibang(
customerName?: string | null,
@@ -433,6 +480,7 @@ export function createEmptyLoadForm(): PlanLoadForm {
notes: '',
generateDibang: 0,
dibangDirection: 'none',
sameCity: 0,
contracts: [],
products: []
}
@@ -451,6 +499,7 @@ export function createEmptyDeliveryForm(seq = 1): PlanDeliveryForm {
notes: '',
generateDibang: 0,
dibangDirection: 'none',
sameCity: 0,
contracts: [],
products: []
}
@@ -540,6 +589,7 @@ export function mapLoadFromApi(
notes: load.notes || '',
generateDibang: dibang.generateDibang,
dibangDirection: dibang.dibangDirection,
sameCity: Number(load.sameCity) === 1 ? 1 : 0,
contracts: (load.contracts || []).map((item) => ({
contractId: String(item.contractId || ''),
contractNo: item.contractNo
@@ -574,6 +624,7 @@ export function mapDeliveriesFromApi(
notes: row.notes || '',
generateDibang: dibang.generateDibang,
dibangDirection: dibang.dibangDirection,
sameCity: Number(row.sameCity) === 1 ? 1 : 0,
contracts: (row.contracts || []).map((item) => ({
contractId: String(item.contractId || ''),
contractNo: item.contractNo
@@ -617,6 +668,7 @@ export function mapLegacyPlanToStops(plan: Plan): {
notes: plan.pickupNotes || '',
generateDibang: loadDefaults.generateDibang,
dibangDirection: loadDefaults.dibangDirection,
sameCity: 0,
contracts: (plan.supplierContracts || []).map((item) => ({
contractId: String(item.contractId || ''),
contractNo: item.contractNo
@@ -636,6 +688,7 @@ export function mapLegacyPlanToStops(plan: Plan): {
notes: plan.destinationNotes || '',
generateDibang: deliveryDefaults.generateDibang,
dibangDirection: deliveryDefaults.dibangDirection,
sameCity: 0,
contracts: (plan.demanderContracts || []).map((item) => ({
contractId: String(item.contractId || ''),
contractNo: item.contractNo
@@ -900,6 +953,7 @@ export function buildLoadPayload(load: PlanLoadForm) {
notes: load.notes || undefined,
generateDibang: load.generateDibang,
dibangDirection: load.dibangDirection,
sameCity: load.sameCity === 1 ? 1 : 0,
contracts: load.contracts,
products: load.products.map(buildStopProductPayload)
}
@@ -919,6 +973,7 @@ export function buildDeliveriesPayload(deliveries: PlanDeliveryForm[]) {
notes: row.notes || undefined,
generateDibang: row.generateDibang,
dibangDirection: row.dibangDirection,
sameCity: row.sameCity === 1 ? 1 : 0,
contracts: row.contracts,
products: row.products.map(buildStopProductPayload)
}))
@@ -16,8 +16,8 @@
<div class="mp-wv-card__no">{{ row.certNo }}</div>
<span
class="status-tag mp-cert-kind-tag"
:class="row.certType === 'transport' ? 'status-tag--warning' : 'status-tag--primary'"
>{{ row.certType === 'transport' ? '运输证' : '购买证' }}</span
:class="`status-tag--${certKindMobileTone(row.certType)}`"
>{{ certKindLabel(row.certType) }}</span
>
</div>
<span class="status-tag" :class="`status-tag--${statusTag(row.status)}`">{{
@@ -109,7 +109,14 @@
import CertConfirmSheet from './CertConfirmSheet.vue'
import { formatDateTime } from '@/utils/format'
import { formatCertQty } from '@/views/pl/quota-cert/model'
import { buyPartyName, expiryWarning, salePartyName } from '@/views/pl/quota-cert/cert-list-face'
import {
buyPartyName,
certKindLabel,
certKindMobileTone,
compareCertUploadTime,
expiryWarning,
salePartyName
} from '@/views/pl/quota-cert/cert-list-face'
import { useDictStore } from '@/store/modules/dict'
import { getProductUnitLabel } from '@/utils/crm-product-display'
import { certStatusLabels } from './face-model'
@@ -209,9 +216,11 @@
const kinds: CertType[] = props.kind ? [props.kind as CertType] : ['purchase', 'transport']
const lists = await Promise.all(kinds.map((item) => quotaCertApi.list(item, params)))
if (current !== epoch) return
rows.value = kinds.flatMap((item, index) =>
lists[index].map((row) => ({ ...row, certType: row.certType || item }))
)
rows.value = kinds
.flatMap((item, index) =>
lists[index].map((row) => ({ ...row, certType: row.certType || item }))
)
.sort(compareCertUploadTime)
} catch {
if (current === epoch) error.value = '证件加载失败,请重试'
} finally {
@@ -41,7 +41,14 @@
计划数量 {{ qtyText(group.source.quantity) }} {{ unitText(group.source.unit) }}
</p>
<div v-for="row in group.rows" :key="conversionKey(row)" class="mp-cert-factor">
<strong>{{ row.cert.certType === 'purchase' ? '购买证' : '运输证' }} {{ row.cert.certNo }}</strong>
<strong class="mp-cert-kind-heading">
<span
class="status-tag mp-cert-kind-tag"
:class="`status-tag--${certKindMobileTone(row.cert.certType)}`"
>{{ certKindLabel(row.cert.certType) }}</span
>
<span>{{ row.cert.certNo }}</span>
</strong>
<van-field
v-model="coefficients[conversionKey(row)]"
label="换算系数"
@@ -91,7 +98,14 @@
<section v-if="isPerStop && faceCards.length" class="mp-wv-section">
<h3>关联证件证面</h3>
<article v-for="face in faceCards" :id="faceDomId(face.key)" :key="face.key" class="mp-cert-face-card">
<h4>{{ face.label }}</h4>
<h4 class="mp-cert-kind-heading">
<span
class="status-tag mp-cert-kind-tag"
:class="`status-tag--${certKindMobileTone(face.kind)}`"
>{{ certKindLabel(face.kind) }}</span
>
<span>{{ face.certNo }}</span>
</h4>
<p v-if="!face.cert" class="plan-empty-hint">没有读到这张证的证面档案。</p>
<template v-else>
<div v-for="(group, groupIndex) in groupsOf(face.cert, face.kind)" :key="groupIndex">
@@ -158,6 +172,7 @@
sourceLabel,
unitConversions
} from '@/components/business/pl-plan-quota-dialog/stop-model'
import { certKindLabel, certKindMobileTone } from '@/views/pl/quota-cert/cert-list-face'
import { formatCertQty } from '@/views/pl/quota-cert/model'
import { useDictStore } from '@/store/modules/dict'
import { getProductUnitLabel } from '@/utils/crm-product-display'
@@ -190,7 +205,13 @@
const sourceOptions = computed(() => plan.value?.sourceOptions || [])
const faceCards = computed(() => {
const seen = new Set<string>()
const tabs: { key: string; label: string; kind: CertType; cert: QuotaCert | null }[] = []
const tabs: {
key: string
label: string
kind: CertType
certNo: string
cert: QuotaCert | null
}[] = []
const push = (kind: string, id: string, certNo: string) => {
if ((kind !== 'purchase' && kind !== 'transport') || !id || seen.has(`${kind}:${id}`)) return
seen.add(`${kind}:${id}`)
@@ -200,7 +221,8 @@
key: `${kind}:${id}`,
kind,
cert,
label: `${kind === 'purchase' ? '购买证' : '运输证'} ${number}`
certNo: number,
label: `${certKindLabel(kind)} ${number}`
})
}
for (const row of plan.value?.stopProducts || []) {
@@ -60,7 +60,14 @@
</button>
</template>
</template>
<p v-else class="mp-cert-link-note">{{ currentLabel }}</p>
<p v-else class="mp-cert-kind-heading">
<span
class="status-tag mp-cert-kind-tag"
:class="`status-tag--${certKindMobileTone(certType)}`"
>{{ certKindLabel(certType) }}</span
>
<span v-if="certNo">{{ certNo }}</span>
</p>
<template v-if="certId">
<h3>适用产品</h3>
@@ -139,6 +146,7 @@
type QuotaCert,
type ScopeProduct
} from '@/api/pl/quota-cert'
import { certKindLabel, certKindMobileTone } from '@/views/pl/quota-cert/cert-list-face'
import { bindingPayload, replaceCertProducts } from '@/views/pl/quota-cert/model'
import { getProductUnitLabel } from '@/utils/crm-product-display'
import { useDictStore } from '@/store/modules/dict'
@@ -178,9 +186,6 @@
let epoch = 0
let searchTimer = 0
const currentLabel = computed(() =>
[certType.value === 'purchase' ? '购买证' : '运输证', certNo.value].filter(Boolean).join(' ')
)
const pickerRows = computed(() => {
const used = new Set(draft.value.map((product) => product.contractProductId))
return lines.value.filter((row) => row.id && !used.has(row.id))
@@ -15,9 +15,13 @@
@click="open(row)"
>
<div class="mp-wv-card__head">
<div>
<div class="mp-cert-card__main">
<div class="mp-wv-card__no">{{ row.certNo }}</div>
<div class="mp-wv-card__sub">{{ row.certType === 'transport' ? '运输证' : '购买证' }}</div>
<span
class="status-tag mp-cert-kind-tag"
:class="`status-tag--${certKindMobileTone(row.certType)}`"
>{{ certKindLabel(row.certType) }}</span
>
</div>
<span class="status-tag" :class="`status-tag--${row.status === 'active' ? 'success' : 'info'}`">
{{ certListStatus(row.status) }}
@@ -61,7 +65,11 @@
import { useDictStore } from '@/store/modules/dict'
import { getProductUnitLabel } from '@/utils/crm-product-display'
import { certListStatus } from './usage-model'
import { expiryWarning } from '@/views/pl/quota-cert/cert-list-face'
import {
certKindLabel,
certKindMobileTone,
expiryWarning
} from '@/views/pl/quota-cert/cert-list-face'
const props = defineProps<{ keyword: string; kind: string }>()
const router = useRouter()
@@ -37,6 +37,24 @@
min-height: calc(100dvh - 132px);
}
.mp-cert-kind-tag {
display: inline-flex;
flex: 0 0 auto;
align-self: flex-start;
width: fit-content;
max-width: 100%;
white-space: nowrap;
vertical-align: middle;
}
.mp-cert-kind-heading {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-start;
}
.mp-plan-cert-list .mp-wv-flat-card {
.mp-cert-card__main {
display: flex;
@@ -46,10 +64,7 @@
}
.mp-cert-kind-tag {
width: fit-content;
max-width: 100%;
margin-top: 4px;
white-space: nowrap;
}
.mp-wv-card__no {
@@ -48,7 +48,11 @@
</div>
</div>
<div class="mp-cert-tags">
<span class="status-tag status-tag--info">{{ cert.certType === 'purchase' ? '购买证' : '运输证' }}</span>
<span
class="status-tag mp-cert-kind-tag"
:class="`status-tag--${certKindMobileTone(cert.certType)}`"
>{{ certKindLabel(cert.certType) }}</span
>
<span class="status-tag" :class="`status-tag--${cert.linkStatus === 'active' ? 'success' : 'info'}`">
{{ linkLabel(cert.linkStatus) }}
</span>
@@ -145,7 +149,14 @@
<div class="mp-wv-card__kv">
<div class="mp-wv-card__kv-row"><span>规格</span><span>{{ row.model }}</span></div>
<div class="mp-wv-card__kv-row"><span>产品单位</span><span>{{ unit(row.unit) || '—' }}</span></div>
<div class="mp-wv-card__kv-row"><span>证件类型</span><span>{{ row.certKind }}</span></div>
<div class="mp-wv-card__kv-row">
<span>证件类型</span>
<span
class="status-tag mp-cert-kind-tag"
:class="`status-tag--${certKindMobileTone(row.certType)}`"
>{{ row.certKind }}</span
>
</div>
<div class="mp-wv-card__kv-row"><span>证件编号</span><span>{{ row.certNo }}</span></div>
<div class="mp-wv-card__kv-row"><span>证面单位</span><span>{{ unit(row.certUnit) || '—' }}</span></div>
<div class="mp-wv-card__kv-row"><span>产品预占</span><span>{{ qty(row.reservedQty) }}</span></div>
@@ -188,7 +199,11 @@
type StatisticsProduct
} from '@/api/pl/quota-cert'
import { bindingPayload, formatCertQty } from '@/views/pl/quota-cert/model'
import { certFaceFields } from '@/views/pl/quota-cert/cert-list-face'
import {
certFaceFields,
certKindLabel,
certKindMobileTone
} from '@/views/pl/quota-cert/cert-list-face'
import { contractTypeLabel } from '@/views/pl/quota-statistics/labels'
import { showCrmConfirmDialog } from '@/utils/mp-crm-confirm'
import CertLinkSheet from './CertLinkSheet.vue'
@@ -1,5 +1,5 @@
import type { ContractStatistics } from '@/api/pl/quota-cert'
import { certFaceFields, expiryWarning } from '@/views/pl/quota-cert/cert-list-face'
import { certFaceFields, certKindLabel, expiryWarning } from '@/views/pl/quota-cert/cert-list-face'
import { statusLabel } from '@/views/pl/quota-statistics/labels'
export function certDetailStatus(status?: string | null) {
@@ -19,7 +19,8 @@ export function contractProductRows(detail: ContractStatistics) {
productName: product.productName || '—',
model: product.model || '—',
unit: product.unit,
certKind: cert.certType === 'purchase' ? '购买证' : '运输证',
certType: cert.certType,
certKind: certKindLabel(cert.certType),
certNo: cert.certNo || '—',
certUnit: cert.quantityUnit,
reservedQty: product.reservedQty,
@@ -9,7 +9,14 @@
<div v-else-if="loading" class="mp-cert-page-loading"><van-loading /></div>
<template v-else-if="detail">
<section class="mp-wv-section">
<h3>{{ detail.certType === 'transport' ? '运输证' : '购买证' }} {{ detail.certNo }}</h3>
<h3 class="mp-cert-kind-heading">
<span
class="status-tag mp-cert-kind-tag"
:class="`status-tag--${certKindMobileTone(detail.certType)}`"
>{{ certKindLabel(detail.certType) }}</span
>
<span>{{ detail.certNo }}</span>
</h3>
<div class="detail-field-grid">
<div class="mp-wv-kv"><span>证面品名</span><span>{{ detail.productNameOnCert || '—' }}</span></div>
<div class="mp-wv-kv">
@@ -101,6 +108,7 @@
import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
import MpBusinessHeader from '@/components/mp/MpBusinessHeader.vue'
import { quotaStatisticsApi, type CertificateStatistics, type CertType } from '@/api/pl/quota-cert'
import { certKindLabel, certKindMobileTone } from '@/views/pl/quota-cert/cert-list-face'
import { formatCertQty } from '@/views/pl/quota-cert/model'
import { useDictStore } from '@/store/modules/dict'
import { getProductUnitLabel } from '@/utils/crm-product-display'
@@ -116,6 +116,13 @@
:readonly="c.props.readonly"
@update:model-value="c.patchDelivery(index, { notes: $event })"
/>
<van-cell class="mp-stop-toggle" title="是否同市" label="同市只需购买证"
><template #right-icon
><van-switch
:model-value="delivery.sameCity === 1"
:disabled="c.props.readonly"
@update:model-value="c.patchDelivery(index, { sameCity: $event ? 1 : 0 })" /></template
></van-cell>
<van-cell
class="mp-stop-toggle"
title="生成地磅单"
@@ -61,6 +61,13 @@
:readonly="c.props.readonly"
@update:model-value="c.patch({ notes: $event })"
/>
<van-cell class="mp-stop-toggle" title="是否同市" label="同市只需购买证"
><template #right-icon
><van-switch
:model-value="c.props.modelValue.sameCity === 1"
:disabled="c.props.readonly"
@update:model-value="c.patch({ sameCity: $event ? 1 : 0 })" /></template
></van-cell>
<van-cell class="mp-stop-toggle" title="生成地磅单" :label="c.dibangDirectionLabel"
><template #right-icon
><van-switch
@@ -34,6 +34,28 @@ export function certFaceFields(row: CertListFace) {
]
}
export function certKindLabel(certType?: string | null) {
return certType === 'transport' ? '运输证' : '购买证'
}
export function certKindTagType(certType?: string | null) {
return certType === 'transport' ? 'warning' : 'success'
}
export function certKindMobileTone(certType?: string | null) {
return certType === 'transport' ? 'warning' : 'primary'
}
export function compareCertUploadTime<T extends { createTime?: string | null; id?: string | null }>(
a: T,
b: T
) {
return (
String(b.createTime || '').localeCompare(String(a.createTime || '')) ||
String(b.id || '').localeCompare(String(a.id || ''))
)
}
export function expiryWarning(validTo?: string | null, today = new Date()) {
const target = calendarDate(validTo)
if (!target) return null
+11 -8
View File
@@ -66,8 +66,8 @@
><el-tag
v-else-if="column.prop === 'certType'"
size="small"
:type="row.certType === 'transport' ? 'warning' : 'success'"
>{{ row.certType === 'transport' ? '运输证' : '购买证' }}</el-tag
:type="certKindTagType(row.certType)"
>{{ certKindLabel(row.certType) }}</el-tag
><el-tag
v-else-if="column.prop === 'status'"
size="small"
@@ -629,7 +629,14 @@
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import CertContractBinder from './CertContractBinder.vue'
import { bindingsFromPurchaseCerts, matchPurchaseCert } from './contract-binder-rules'
import { buyPartyName, expiryWarning, salePartyName } from './cert-list-face'
import {
buyPartyName,
certKindLabel,
certKindTagType,
compareCertUploadTime,
expiryWarning,
salePartyName
} from './cert-list-face'
import ContractScopeDialog from './ContractScopeDialog.vue'
import SystemViewDialog, {
type ViewField,
@@ -1089,11 +1096,7 @@
rows.value = [
...purchases.map((row) => presentCert(row, 'purchase')),
...transports.map((row) => presentCert(row, 'transport'))
].sort(
(a, b) =>
String(b.createTime || '').localeCompare(String(a.createTime || '')) ||
String(b.id).localeCompare(String(a.id))
)
].sort(compareCertUploadTime)
} finally {
if (current === listEpoch && !disposed) loading.value = false
}
@@ -42,7 +42,10 @@
</el-select>
</el-form-item>
</el-form>
<p v-else class="quota-contract-cert-dialog__current">{{ currentCertLabel }}</p>
<p v-else class="quota-contract-cert-dialog__current">
<el-tag size="small" :type="certKindTagType(certType)">{{ certKindLabel(certType) }}</el-tag>
<span v-if="certNo">{{ certNo }}</span>
</p>
<CertContractBinder
v-if="ready && certId && contractId"
:key="binderKey"
@@ -68,6 +71,7 @@
type ScopeProduct
} from '@/api/pl/quota-cert'
import CertContractBinder from '../quota-cert/CertContractBinder.vue'
import { certKindLabel, certKindTagType } from '../quota-cert/cert-list-face'
import { bindingPayload, replaceCertProducts } from '../quota-cert/model'
const emit = defineEmits<{ saved: [] }>()
@@ -91,10 +95,6 @@
() =>
`${mode.value}|${certType.value}|${certId.value}|${savedProducts.value.map((row) => row.contractProductId).join(',')}`
)
const currentCertLabel = computed(() =>
[certType.value === 'purchase' ? '购买证' : '运输证', certNo.value].filter(Boolean).join(' ')
)
function certOptionLabel(item: QuotaCert) {
const name = [item.certNo, item.productNameOnCert].filter(Boolean).join(' · ')
if (item.status === 'void') return `${name}(已作废)`
@@ -209,6 +209,10 @@
<style scoped>
.quota-contract-cert-dialog__current {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
margin: 0 0 12px;
color: #1e293b;
font-weight: 600;
@@ -6,8 +6,15 @@
v-for="cert in contract?.certificates || []"
:key="certKey(cert)"
:name="certKey(cert)"
:label="certTabLabel(cert)"
>
<template #label>
<span class="quota-cert-tab-label">
<el-tag size="small" :type="certKindTagType(cert.certType)">{{
certKindLabel(cert.certType)
}}</el-tag>
<span>{{ cert.certNo }}</span>
</span>
</template>
<section class="quota-cert-summary">
<header class="quota-cert-summary__head">
<strong>{{ cert.certNo }}</strong>
@@ -16,8 +23,8 @@
有效期:{{ cert.validFrom || '—' }} 至 {{ cert.validTo || '—' }}
</span>
<div class="quota-cert-summary__tags">
<el-tag size="small">{{
cert.certType === 'purchase' ? '购买证' : '运输证'
<el-tag size="small" :type="certKindTagType(cert.certType)">{{
certKindLabel(cert.certType)
}}</el-tag>
<el-tag size="small" :type="cert.linkStatus === 'active' ? 'success' : 'info'">{{
linkLabel(cert.linkStatus)
@@ -90,7 +97,13 @@
<el-table-column prop="productName" label="产品" min-width="160" />
<el-table-column prop="model" label="规格" min-width="120" />
<el-table-column prop="unit" label="产品单位" min-width="100" />
<el-table-column prop="certKind" label="证件类型" min-width="100" />
<el-table-column label="证件类型" min-width="110">
<template #default="{ row }">
<el-tag size="small" :type="certKindTagType(row.certType)">{{
row.certKind
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="certNo" label="证件编号" min-width="160" />
<el-table-column prop="certUnit" label="证面单位" min-width="100" />
<el-table-column label="产品预占" min-width="120"
@@ -230,7 +243,12 @@
import { useDictStore } from '@/store/modules/dict'
import { getProductUnitLabel } from '@/utils/crm-product-display'
import { formatCertQty } from '../quota-cert/model'
import { certFaceFields, expiryWarning } from '../quota-cert/cert-list-face'
import {
certFaceFields,
certKindLabel,
certKindTagType,
expiryWarning
} from '../quota-cert/cert-list-face'
import StatisticsProducts from './StatisticsProducts.vue'
import { statusLabel } from './labels'
const props = withDefaults(
@@ -252,10 +270,6 @@
function certKey(cert: { certType: string; certId: string }) {
return `${cert.certType}:${cert.certId}`
}
function certTabLabel(cert: ContractStatistics['certificates'][number]) {
const kind = cert.certType === 'purchase' ? '购买证' : '运输证'
return `${kind} ${cert.certNo || ''}`.trim()
}
watch(
() =>
props.mode === 'contract'
@@ -282,7 +296,8 @@
productName: product.productName || '—',
model: product.model || '—',
unit: unitLabel(product.unit) || '—',
certKind: cert.certType === 'purchase' ? '购买证' : '运输证',
certType: cert.certType,
certKind: certKindLabel(cert.certType),
certNo: cert.certNo || '—',
certUnit: unitLabel(cert.quantityUnit) || '—',
reservedQty: product.reservedQty,
@@ -475,4 +490,10 @@
}
}
}
.quota-cert-tab-label {
display: inline-flex;
gap: 6px;
align-items: center;
}
</style>
@@ -144,9 +144,11 @@
/> </div
></template>
<template #default="{ row }"
><span v-if="column.prop === 'certType'">{{
row.certType === 'transport' ? '运输证' : '购买证'
}}</span
><el-tag
v-if="column.prop === 'certType'"
size="small"
:type="certKindTagType(row.certType)"
>{{ certKindLabel(row.certType) }}</el-tag
><el-tag
v-else-if="column.prop === 'status'"
size="small"
@@ -205,7 +207,7 @@
import StatisticsDetail from './StatisticsDetail.vue'
import { createStatisticsLoader } from './loader'
import { statusLabel, contractTypeLabel } from './labels'
import { expiryWarning } from '../quota-cert/cert-list-face'
import { certKindLabel, certKindTagType, expiryWarning } from '../quota-cert/cert-list-face'
const props = withDefaults(
defineProps<{
mode: 'contract' | 'certificate'