feat(pl): 装送货段增加同市标记,并同步额度证件与计划表单。
同市易制毒段只校验购买证;同步 Web/H5 表单、额度统计展示与相关回归测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d58f6ea5bd
commit
522b4be705
40 files changed
+576
-107
No files matched your search
@@ -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[]
|
||||
|
||||
@@ -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'), {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user