feat(pl): 增加百链充值类型,并保留计划产品与证件上传时间。

充值单号按类型使用 ETC、YK、SX、BL 前缀;更换装货客户清空产品时不抹掉已有计划产品;运输证件列表展示上传时间。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-07 17:55:21 +08:00
1 parent af8d4d90fa
commit cc5cf78bcd
21 files changed
+274 -97

No files matched your search

+4 -4
View File
@@ -1798,7 +1798,7 @@ export interface VehicleRechargeApply {
applyNo: string
licensePlate: string
boundFuelCardNo?: string | null
rechargeType: 'etc' | 'fuel' | 'shanxia'
rechargeType: 'etc' | 'fuel' | 'shanxia' | 'bailian'
applyDate: string
odometerKm?: number | null
attachmentUrls?: string
@@ -1830,14 +1830,14 @@ export interface VehicleRechargeApplyOption {
id: string
applyNo: string
licensePlate: string
rechargeType: 'etc' | 'fuel' | 'shanxia'
rechargeType: 'etc' | 'fuel' | 'shanxia' | 'bailian'
}
export interface VehicleRechargeRecord {
id: string
rechargeOrderNo: string
rechargeDate: string
rechargeType: 'etc' | 'fuel' | 'shanxia'
rechargeType: 'etc' | 'fuel' | 'shanxia' | 'bailian'
cardNo?: string
gasStation?: string
amount: number
@@ -1864,7 +1864,7 @@ export interface VehicleRechargeDisburseBoardItem {
applyNo: string
licensePlate: string
boundFuelCardNo?: string | null
rechargeType: 'etc' | 'fuel' | 'shanxia'
rechargeType: 'etc' | 'fuel' | 'shanxia' | 'bailian'
applyDate: string
odometerKm?: number | null
applicantName?: string
@@ -712,6 +712,7 @@
enrichStopProductsDisplayFromPlan,
mapLoadProductsToPlanFormRows,
mergePlanProductsFromLoadMirror,
shouldRetainPlanProductsWhileLoadEmpty,
resolveStopDibangDefaults,
sumStopProductsPlanQty,
type PlanDeliveryForm,
@@ -1460,6 +1461,7 @@
/** 装货产品变更后:仅镜像计划层产品(不再同步到送货段) */
function syncFromLoadProducts() {
if (suppressStopProductSync.value || detailLoading.value) return
if (shouldRetainPlanProductsWhileLoadEmpty(loadForm.value.products || [], formData.products)) return
mirrorPlanProductsFromLoad()
}
+1 -1
View File
@@ -551,7 +551,7 @@ export function parsePlFeedbackImages(raw?: string | string[] | null): string[]
}
export function getPlRechargeTypeLabel(type: string) {
const labels: Record<string, string> = { etc: 'ETC', fuel: '油卡', shanxia: '山峡' }
const labels: Record<string, string> = { etc: 'ETC', fuel: '油卡', shanxia: '山峡', bailian: '百链' }
return labels[type] || type
}
+11
View File
@@ -768,6 +768,17 @@ export function mapLoadProductsToPlanFormRows(
}))
}
/**
* 更换装货客户会先清空装货产品。这段空窗不要抹掉已有计划产品,
* 重新选回同一产品和包装时才能继承原 id。正式保存仍按当前装货产品镜像。
*/
export function shouldRetainPlanProductsWhileLoadEmpty(
loadProducts: Array<unknown>,
previous: Array<unknown>
): boolean {
return loadProducts.length === 0 && previous.length > 0
}
/** 合并装货映射行与既有计划产品元数据(危化证、税额等) */
export function mergePlanProductsFromLoadMirror(
fromLoad: PlanFormProductRow[],
@@ -14,30 +14,49 @@
<div class="mp-wv-card__head">
<div>
<div class="mp-wv-card__no">{{ row.certNo }}</div>
<div class="mp-wv-card__sub">{{ row.certType === 'transport' ? '运输证' : '购买证' }}</div>
<div class="mp-wv-card__sub">{{
row.certType === 'transport' ? '运输证' : '购买证'
}}</div>
</div>
<span class="status-tag" :class="`status-tag--${statusTag(row.status)}`">{{ statusLabel(row.status) }}</span>
<span class="status-tag" :class="`status-tag--${statusTag(row.status)}`">{{
statusLabel(row.status)
}}</span>
</div>
<div class="mp-wv-card__kv">
<div class="mp-wv-card__kv-row"><span>销售单位</span><span>{{ salePartyName(row) || '—' }}</span></div>
<div class="mp-wv-card__kv-row"><span>购买单位</span><span>{{ buyPartyName(row) || '—' }}</span></div>
<div class="mp-wv-card__kv-row"><span>有效次数</span><span>{{ row.validUseText || '—' }}</span></div>
<div class="mp-wv-card__kv">
<div class="mp-wv-card__kv-row"
><span>销售单位</span><span>{{ salePartyName(row) || '—' }}</span></div
>
<div class="mp-wv-card__kv-row"
><span>购买单位</span><span>{{ buyPartyName(row) || '—' }}</span></div
>
<div class="mp-wv-card__kv-row"
><span>有效次数</span><span>{{ row.validUseText || '—' }}</span></div
>
<div class="mp-wv-card__kv-row">
<span>是否预警</span>
<span v-if="warningText(row)" class="status-tag status-tag--danger">{{ warningText(row) }}</span>
<span v-if="warningText(row)" class="status-tag status-tag--danger">{{
warningText(row)
}}</span>
<span v-else>—</span>
</div>
<div class="mp-wv-card__kv-row"><span>品名</span><span>{{ row.productNameOnCert || '—' }}</span></div>
<div class="mp-wv-card__kv-row"
><span>品名</span><span>{{ row.productNameOnCert || '—' }}</span></div
>
<div class="mp-wv-card__kv-row">
<span>数量</span><span>{{ qty(row.totalQty) }} {{ unit(row.quantityUnit) }}</span>
</div>
<div class="mp-wv-card__kv-row">
<span>可用余额</span><span>{{ qty(row.availableQty) }} {{ unit(row.quantityUnit) }}</span>
</div>
<div class="mp-wv-card__kv-row"
><span>上传时间</span><span>{{ uploadTime(row.createTime) }}</span></div
>
</div>
<div class="mp-wv-card-actions" @click.stop>
<button class="mp-wv-btn" type="button" @click="open(row)">查看</button>
<button v-if="canEdit" class="mp-wv-btn mp-wv-btn--primary" type="button" @click="edit(row)">编辑</button>
<button v-if="canEdit" class="mp-wv-btn mp-wv-btn--primary" type="button" @click="edit(row)"
>编辑</button
>
<button
v-if="canEdit && row.status !== 'void' && row.status !== 'closed'"
class="mp-wv-btn"
@@ -70,9 +89,13 @@
>
新增运输证
</button>
<button class="mp-wv-speed-item primary" type="button" @click="create('purchase')">新增购买证</button>
<button class="mp-wv-speed-item primary" type="button" @click="create('purchase')"
>新增购买证</button
>
</div>
<button class="mp-wv-speed-main" type="button" aria-label="新增证件" @click="onCreate">+</button>
<button class="mp-wv-speed-main" type="button" aria-label="新增证件" @click="onCreate"
>+</button
>
</div>
</template>
</main>
@@ -82,6 +105,7 @@
import { quotaCertApi, type CertType, type QuotaCert } from '@/api/pl/quota-cert'
import ArchiveForm from './archive-form.vue'
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 { useDictStore } from '@/store/modules/dict'
@@ -115,7 +139,9 @@
return !props.expiringSoon || Boolean(expiryWarning(row.validTo))
})
)
const expiringCount = computed(() => rows.value.filter((row) => expiryWarning(row.validTo)).length)
const expiringCount = computed(
() => rows.value.filter((row) => expiryWarning(row.validTo)).length
)
watch(expiringCount, (count) => emit('expiring-count', count), { immediate: true })
let epoch = 0
@@ -134,6 +160,9 @@
function qty(value: unknown) {
return formatCertQty(value)
}
function uploadTime(value?: string | null) {
return formatDateTime(value) || '—'
}
function unit(value?: string | null) {
const label = getProductUnitLabel(useDictStore().getLabel, value)
return !label || label === '—' ? '' : label
+76 -60
View File
@@ -9,8 +9,12 @@
v-model="keyword"
placeholder="编号"
@search="search"
@clear="search" /><label class="cert-expiry-switch" @click.prevent="expiringSoon = !expiringSoon"
><el-switch :model-value="expiringSoon" /><span>近7日到期({{ expiringCount }}个)</span></label
@clear="search" /><label
class="cert-expiry-switch"
@click.prevent="expiringSoon = !expiringSoon"
><el-switch :model-value="expiringSoon" /><span
>近7日到期({{ expiringCount }}个)</span
></label
><span class="list-toolbar-selected">共 {{ filteredRows.length }} 张</span
><el-button v-if="canEdit" class="btn-main" type="primary" @click="edit()"
><el-icon><Plus /></el-icon>新增证件</el-button
@@ -41,7 +45,7 @@
:key="column.prop"
:prop="column.prop"
:label="column.label"
:min-width="column.type === 'text' ? 190 : 140"
:min-width="column.prop === 'createTime' ? 170 : column.type === 'text' ? 190 : 140"
><template #header
><div class="col-header-with-filter"
><span class="col-header-with-filter__label">{{ column.label }}</span>
@@ -70,10 +74,15 @@
><span v-else-if="column.type === 'number'">{{
qtyText(row[column.prop], row.quantityUnit)
}}</span
><el-tag v-else-if="column.prop === 'expiryAlert' && expiryText(row)" type="danger" size="small">{{
expiryText(row)
}}</el-tag
><el-tag
v-else-if="column.prop === 'expiryAlert' && expiryText(row)"
type="danger"
size="small"
>{{ expiryText(row) }}</el-tag
><span v-else-if="column.prop === 'expiryAlert'">—</span
><span v-else-if="column.prop === 'createTime'">{{
formatDateTime(row.createTime) || '—'
}}</span
><span v-else>{{ row[column.prop] || '—' }}</span></template
></el-table-column
><el-table-column label="操作" min-width="320" fixed="right"
@@ -266,7 +275,10 @@
</el-form-item>
</div>
<p
v-if="group.fields.some((field) => field[0] === 'vehicleNumbersJson') && lockedPlates.length"
v-if="
group.fields.some((field) => field[0] === 'vehicleNumbersJson') &&
lockedPlates.length
"
class="form-tip"
>
已有车号不能删除,只能追加。保存后证件仍可安排运输。
@@ -397,49 +409,51 @@
type="danger"
title="移除引用"
aria-label="移除引用"
:disabled="saving || form.referencesEditable === false || referenceRowLocked(row)"
:disabled="
saving || form.referencesEditable === false || referenceRowLocked(row)
"
@click="removeReference(index)"
><el-icon><Delete /></el-icon
></el-button>
</header>
<div class="form-grid-2 cert-reference-fields">
<el-form-item label="引用类型">
<el-select
v-model="row.referenceType"
:disabled="saving || referenceRowLocked(row)"
@change="edited.add('purchaseRefs')"
>
<el-option
v-for="(label, value) in refLabels"
:key="value"
:value="value"
:label="label"
<el-form-item label="引用类型">
<el-select
v-model="row.referenceType"
:disabled="saving || referenceRowLocked(row)"
@change="edited.add('purchaseRefs')"
>
<el-option
v-for="(label, value) in refLabels"
:key="value"
:value="value"
:label="label"
/>
</el-select>
</el-form-item>
<el-form-item :label="purchasePermitLabel">
<el-input
v-model="row.referenceNo"
:disabled="saving || referenceRowLocked(row)"
@input="edited.add('purchaseRefs')"
/>
</el-select>
</el-form-item>
<el-form-item :label="purchasePermitLabel">
<el-input
v-model="row.referenceNo"
:disabled="saving || referenceRowLocked(row)"
@input="edited.add('purchaseRefs')"
/>
</el-form-item>
<el-form-item label="对应购买证档案">
<el-select
v-model="row.purchaseCertId"
:disabled="saving || referenceRowLocked(row)"
filterable
clearable
@change="onPurchaseArchiveChange(row)"
>
<el-option
v-for="c in purchases"
:key="c.id"
:value="c.id"
:label="c.certNo"
/>
</el-select>
</el-form-item>
</el-form-item>
<el-form-item label="对应购买证档案">
<el-select
v-model="row.purchaseCertId"
:disabled="saving || referenceRowLocked(row)"
filterable
clearable
@change="onPurchaseArchiveChange(row)"
>
<el-option
v-for="c in purchases"
:key="c.id"
:value="c.id"
:label="c.certNo"
/>
</el-select>
</el-form-item>
</div>
</article>
</div>
@@ -595,18 +609,11 @@
import QuotaHistoryDialog from './QuotaHistoryDialog.vue'
const historyDialog = ref<InstanceType<typeof QuotaHistoryDialog>>()
import {
Check,
Close,
Connection,
Delete,
Document,
Edit,
Plus
} from '@element-plus/icons-vue'
import { Check, Close, Connection, Delete, Document, Edit, Plus } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/modules/user'
import { useDictStore } from '@/store/modules/dict'
import { getProductUnitLabel } from '@/utils/crm-product-display'
import { formatDateTime } from '@/utils/format'
import { randomOperationId } from '@/utils/random-id'
import { fetchGetMenuCodes } from '@/api/auth'
import {
@@ -1131,9 +1138,7 @@
return row?.certType === 'transport' || row?.certType === 'purchase' ? row.certType : kind.value
}
function plateList(value: unknown) {
return Array.isArray(value)
? value.map((item) => String(item).trim()).filter(Boolean)
: []
return Array.isArray(value) ? value.map((item) => String(item).trim()).filter(Boolean) : []
}
function plateLocked(plate: string) {
return lockedPlates.value.includes(plate)
@@ -1529,7 +1534,9 @@
form.value.contractBindings = next
}
function referenceRowLocked(row: { id?: string; purchaseCertId?: string | null }) {
return form.value.referencesEditable === false && !!row.id && linkedRefIds.value.includes(row.id)
return (
form.value.referencesEditable === false && !!row.id && linkedRefIds.value.includes(row.id)
)
}
function onPurchaseArchiveChange() {
edited.add('purchaseRefs')
@@ -1678,10 +1685,17 @@
enumOptions: Object.entries(statusLabels).map(([value, label]) => ({ value, label }))
},
...qtyColumns.map((c) => ({ prop: c.key, label: c.label, type: 'number' as const })),
{ prop: 'validTo', label: '有效期限止', type: 'datetime' }
{ prop: 'validTo', label: '有效期限止', type: 'datetime' },
{
prop: 'createTime',
label: '上传时间',
type: 'datetime' as const,
getValue: (row: { createTime?: string | null }) =>
String(row.createTime || '').replace('T', ' ')
}
]
const { visibleColumns, checkAll, isIndeterminate, resetToDefault } = usePersistedListColumns(
'pl-quota-cert-columns-v3',
'pl-quota-cert-columns-v4',
listColumnDefs
)
const visibleDefs = computed(() =>
@@ -1704,7 +1718,9 @@
() => columnRows.value.filter((row) => expiryWarning(row.validTo)).length
)
const filteredRows = computed(() =>
expiringSoon.value ? columnRows.value.filter((row) => expiryWarning(row.validTo)) : columnRows.value
expiringSoon.value
? columnRows.value.filter((row) => expiryWarning(row.validTo))
: columnRows.value
)
const pageRows = computed(() => filteredRows.value.slice((page.value - 1) * 20, page.value * 20))
function search() {
@@ -3,7 +3,8 @@ import type { ColumnFilterDef } from '@/composables/useClientColumnFilters'
const RECHARGE_TYPE_OPTIONS = [
{ value: 'etc', label: 'ETC' },
{ value: 'fuel', label: '油卡' },
{ value: 'shanxia', label: '山峡' }
{ value: 'shanxia', label: '山峡' },
{ value: 'bailian', label: '百链' }
]
const RECHARGE_STATUS_OPTIONS = [
@@ -372,6 +372,7 @@
<el-option label="ETC" value="etc" />
<el-option label="油卡" value="fuel" />
<el-option label="山峡" value="shanxia" />
<el-option label="百链" value="bailian" />
</el-select>
</el-form-item>
<el-form-item v-if="formFuelCard.show.value" label="绑定油卡卡号">
@@ -767,7 +768,7 @@ const formData = reactive({
applyNo: '',
applicantId: '',
licensePlate: '',
rechargeType: 'etc' as 'etc' | 'fuel' | 'shanxia',
rechargeType: 'etc' as 'etc' | 'fuel' | 'shanxia' | 'bailian',
applyDate: '',
odometerKm: null as number | null,
attachmentUrls: '',
@@ -463,6 +463,7 @@
<el-option label="ETC" value="etc" />
<el-option label="油卡" value="fuel" />
<el-option label="山峡" value="shanxia" />
<el-option label="百链" value="bailian" />
</el-select>
</el-form-item>
</template>
@@ -779,7 +780,7 @@ const formData = reactive({
rechargeOrderNo: '',
applyId: '',
licensePlate: '',
rechargeType: 'etc' as 'etc' | 'fuel' | 'shanxia',
rechargeType: 'etc' as 'etc' | 'fuel' | 'shanxia' | 'bailian',
rechargeDate: '',
cardNo: '',
gasStation: '',