feat(pl): 增加百链充值类型,并保留计划产品与证件上传时间。
充值单号按类型使用 ETC、YK、SX、BL 前缀;更换装货客户清空产品时不抹掉已有计划产品;运输证件列表展示上传时间。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
af8d4d90fa
commit
cc5cf78bcd
21 files changed
+274
-97
No files matched your search
@@ -36,7 +36,7 @@ from app.services import pl_vehicle_recharge_summary_service as summary_svc
|
||||
apply_router = APIRouter(prefix="/vehicle-recharge-apply", tags=["运输计划-ETC油卡申请"])
|
||||
record_router = APIRouter(prefix="/vehicle-recharge-record", tags=["运输计划-ETC油卡发放"])
|
||||
|
||||
RECHARGE_TYPE_LABELS = {"etc": "ETC", "fuel": "油卡", "shanxia": "山峡"}
|
||||
RECHARGE_TYPE_LABELS = {"etc": "ETC", "fuel": "油卡", "shanxia": "山峡", "bailian": "百链"}
|
||||
RECHARGE_APPLY_STATUS_LABELS = {
|
||||
"draft": "草稿",
|
||||
"pending": "待发放",
|
||||
|
||||
@@ -1352,7 +1352,7 @@ class PlVehicleRechargeApply(Base):
|
||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||
apply_no = Column(String(32), nullable=False, unique=True, comment="申请单号")
|
||||
license_plate = Column(String(20), nullable=False, comment="车牌号")
|
||||
recharge_type = Column(String(20), nullable=False, comment="充值类型 etc/fuel/shanxia")
|
||||
recharge_type = Column(String(20), nullable=False, comment="充值类型 etc/fuel/shanxia/bailian")
|
||||
apply_date = Column(Date, nullable=False, comment="申请日期")
|
||||
odometer_km = Column(DECIMAL(10, 1), nullable=True, comment="期初公里数")
|
||||
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
RECHARGE_TYPES = {"etc", "fuel", "shanxia"}
|
||||
RECHARGE_TYPES = {"etc", "fuel", "shanxia", "bailian"}
|
||||
APPLY_STATUSES = {"draft", "in_approval", "rejected", "disbursed", "cancelled"}
|
||||
NODE_ACTION_TYPES = {"approve", "disburse"}
|
||||
APPROVAL_STATUSES = {"approved", "rejected", "disbursed"}
|
||||
@@ -26,7 +26,7 @@ class VehicleRechargeApplyCreate(BaseModel):
|
||||
@classmethod
|
||||
def validate_recharge_type(cls, v: str) -> str:
|
||||
if v not in RECHARGE_TYPES:
|
||||
raise ValueError("充值类型须为 ETC、油卡或山峡")
|
||||
raise ValueError("充值类型须为 ETC、油卡、山峡或百链")
|
||||
return v
|
||||
|
||||
@field_validator("odometer_km")
|
||||
@@ -181,7 +181,7 @@ class VehicleRechargeRecordCreate(BaseModel):
|
||||
@classmethod
|
||||
def validate_recharge_type(cls, v: str) -> str:
|
||||
if v not in RECHARGE_TYPES:
|
||||
raise ValueError("充值类型须为 ETC、油卡或山峡")
|
||||
raise ValueError("充值类型须为 ETC、油卡、山峡或百链")
|
||||
return v
|
||||
|
||||
@field_validator("amount")
|
||||
|
||||
@@ -41,6 +41,7 @@ from app.services.pl_vehicle_mine_vehicles import (
|
||||
|
||||
_EDITABLE_STATUSES = frozenset({"draft", "rejected"})
|
||||
_BIZ_TYPE_VEHICLE_RECHARGE = "vehicle_recharge"
|
||||
_APPLY_NO_PREFIX = {"etc": "ETC", "fuel": "YK", "shanxia": "SX", "bailian": "BL"}
|
||||
|
||||
|
||||
def _generate_seq_no(
|
||||
@@ -57,12 +58,15 @@ def _generate_seq_no(
|
||||
return f"{prefix}{seq:0{seq_width}d}"
|
||||
|
||||
|
||||
def generate_apply_no(db: Session) -> str:
|
||||
def generate_apply_no(db: Session, recharge_type: str) -> str:
|
||||
prefix = _APPLY_NO_PREFIX.get(recharge_type)
|
||||
if not prefix:
|
||||
raise ValueError(f"未知充值类型: {recharge_type}")
|
||||
return _generate_seq_no(
|
||||
db,
|
||||
PlVehicleRechargeApply,
|
||||
"apply_no",
|
||||
f"ETC{date.today():%Y%m%d}",
|
||||
f"{prefix}{date.today():%Y%m%d}",
|
||||
seq_width=2,
|
||||
)
|
||||
|
||||
@@ -647,7 +651,7 @@ def create_apply(
|
||||
now = datetime.now()
|
||||
row = PlVehicleRechargeApply(
|
||||
id=generate_uuid(),
|
||||
apply_no=generate_apply_no(db),
|
||||
apply_no=generate_apply_no(db, payload.recharge_type),
|
||||
license_plate=license_plate,
|
||||
recharge_type=payload.recharge_type,
|
||||
apply_date=payload.apply_date,
|
||||
|
||||
@@ -110,7 +110,45 @@ def _patch_scope(monkeypatch, *modules) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"])
|
||||
@pytest.mark.parametrize(
|
||||
"recharge_type,prefix",
|
||||
[("etc", "ETC"), ("fuel", "YK"), ("shanxia", "SX"), ("bailian", "BL")],
|
||||
)
|
||||
def test_apply_no_prefix_follows_recharge_type(recharge_type, prefix):
|
||||
from app.services.pl_vehicle_recharge_service import generate_apply_no
|
||||
|
||||
db = _make_session()
|
||||
day = date.today().strftime("%Y%m%d")
|
||||
assert generate_apply_no(db, recharge_type) == f"{prefix}{day}01"
|
||||
|
||||
|
||||
def test_apply_no_sequences_are_independent_per_type():
|
||||
from app.services.pl_vehicle_recharge_service import generate_apply_no
|
||||
|
||||
db = _make_session()
|
||||
day = date.today().strftime("%Y%m%d")
|
||||
db.add(
|
||||
PlVehicleRechargeApply(
|
||||
id="apply-etc-1",
|
||||
apply_no=f"ETC{day}01",
|
||||
license_plate="鄂A00001",
|
||||
recharge_type="etc",
|
||||
apply_date=date.today(),
|
||||
applicant_id="u-applicant",
|
||||
status="draft",
|
||||
tenant_no="t1",
|
||||
create_dept_id="d1",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert generate_apply_no(db, "etc") == f"ETC{day}02"
|
||||
assert generate_apply_no(db, "fuel") == f"YK{day}01"
|
||||
assert generate_apply_no(db, "shanxia") == f"SX{day}01"
|
||||
assert generate_apply_no(db, "bailian") == f"BL{day}01"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia", "bailian"])
|
||||
def test_submit_sets_in_approval_and_first_node(monkeypatch, recharge_type):
|
||||
from app.services import pl_vehicle_recharge_service as svc
|
||||
from app.schemas.pl.vehicle_recharge import VehicleRechargeApplyCreate
|
||||
@@ -136,6 +174,8 @@ def test_submit_sets_in_approval_and_first_node(monkeypatch, recharge_type):
|
||||
),
|
||||
)
|
||||
assert created.recharge_type == recharge_type
|
||||
prefix = {"etc": "ETC", "fuel": "YK", "shanxia": "SX", "bailian": "BL"}[recharge_type]
|
||||
assert created.apply_no.startswith(f"{prefix}{date.today():%Y%m%d}")
|
||||
assert created.status == "draft"
|
||||
assert created.approval_round == 0
|
||||
|
||||
@@ -280,7 +320,7 @@ def test_reject_sets_rejected_and_appends_history(monkeypatch):
|
||||
assert result.approvals[-1].approval_status == "rejected"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"])
|
||||
@pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia", "bailian"])
|
||||
def test_disburse_writes_record_and_disbursed(monkeypatch, recharge_type):
|
||||
from app.services import pl_vehicle_recharge_approval_service as approval_svc
|
||||
from app.services import pl_vehicle_recharge_service as svc
|
||||
|
||||
@@ -14,11 +14,11 @@ from app.schemas.pl.vehicle_recharge import (
|
||||
|
||||
|
||||
def test_recharge_types_include_all_supported_types():
|
||||
assert RECHARGE_TYPES == {"etc", "fuel", "shanxia"}
|
||||
assert RECHARGE_TYPES == {"etc", "fuel", "shanxia", "bailian"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema", [VehicleRechargeApplyCreate, VehicleRechargeApplyUpdate])
|
||||
@pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"])
|
||||
@pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia", "bailian"])
|
||||
def test_apply_accepts_supported_types(schema, recharge_type):
|
||||
obj = schema.model_validate({
|
||||
"licensePlate": "鄂A12345",
|
||||
@@ -50,7 +50,7 @@ def test_recharge_rejects_unknown_type(schema):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("schema", [VehicleRechargeRecordCreate, VehicleRechargeRecordUpdate])
|
||||
@pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"])
|
||||
@pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia", "bailian"])
|
||||
def test_record_accepts_supported_types(schema, recharge_type):
|
||||
obj = schema.model_validate({
|
||||
"rechargeDate": "2026-10-25",
|
||||
@@ -62,7 +62,7 @@ def test_record_accepts_supported_types(schema, recharge_type):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recharge_type,label", [
|
||||
("etc", "ETC"), ("fuel", "油卡"), ("shanxia", "山峡"),
|
||||
("etc", "ETC"), ("fuel", "油卡"), ("shanxia", "山峡"), ("bailian", "百链"),
|
||||
])
|
||||
def test_export_recharge_type_labels(recharge_type, label):
|
||||
from app.api.pl.vehicle_recharge import RECHARGE_TYPE_LABELS
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- ETC/油卡充值申请单号按充值类型校正前缀。
|
||||
-- etc 保持 ETC;fuel 开头的 ETC 改为 YK;shanxia 开头的 ETC 改为 SX。
|
||||
-- 只替换前三个字符,日期和两位序号不变。已是目标前缀的记录不改。
|
||||
-- 含软删除行,避免唯一索引下恢复后仍显示旧前缀。可重复执行。
|
||||
|
||||
UPDATE `pl_vehicle_recharge_apply`
|
||||
SET `apply_no` = CONCAT('YK', SUBSTRING(`apply_no`, 4))
|
||||
WHERE `recharge_type` = 'fuel'
|
||||
AND `apply_no` LIKE 'ETC%';
|
||||
|
||||
UPDATE `pl_vehicle_recharge_apply`
|
||||
SET `apply_no` = CONCAT('SX', SUBSTRING(`apply_no`, 4))
|
||||
WHERE `recharge_type` = 'shanxia'
|
||||
AND `apply_no` LIKE 'ETC%';
|
||||
@@ -0,0 +1,25 @@
|
||||
-- 充值类型新增「百链」;现有 recharge_type VARCHAR(20) 可直接存储 bailian。
|
||||
-- 按已有充值类型字典的租户补充选项,可重复执行,不修改历史充值记录。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
UPDATE `sys_dict_data` AS d
|
||||
INNER JOIN `sys_dict_type` AS t
|
||||
ON t.`tenant_no` = d.`tenant_no` AND t.`dict_type` = d.`dict_type`
|
||||
SET d.`dict_label` = '百链', d.`tag_type` = 'info',
|
||||
d.`del_flag` = 0, d.`update_by` = 'system', d.`update_time` = NOW()
|
||||
WHERE t.`dict_type` = 'pl_recharge_type' AND t.`del_flag` = 0
|
||||
AND d.`dict_value` = 'bailian';
|
||||
|
||||
INSERT INTO `sys_dict_data`
|
||||
(`id`, `dict_label`, `dict_value`, `tag_type`, `dict_type`, `tenant_no`,
|
||||
`order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`,
|
||||
`update_by`, `update_time`, `del_flag`)
|
||||
SELECT UUID(), '百链', 'bailian', 'info', 'pl_recharge_type', t.`tenant_no`,
|
||||
4, NULL, 'system', '', NOW(), 'system', NOW(), 0
|
||||
FROM (SELECT DISTINCT `tenant_no` FROM `sys_dict_type`
|
||||
WHERE `dict_type` = 'pl_recharge_type' AND `del_flag` = 0) AS t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `sys_dict_data` AS d
|
||||
WHERE d.`tenant_no` = t.`tenant_no`
|
||||
AND d.`dict_type` = 'pl_recharge_type' AND d.`dict_value` = 'bailian'
|
||||
);
|
||||
@@ -296,7 +296,8 @@ const MAX_ATTACHMENTS = 5
|
||||
const typeOptions = [
|
||||
{ label: 'ETC', value: 'etc' },
|
||||
{ label: '油卡', value: 'fuel' },
|
||||
{ label: '山峡', value: 'shanxia' }
|
||||
{ label: '山峡', value: 'shanxia' },
|
||||
{ label: '百链', value: 'bailian' }
|
||||
]
|
||||
|
||||
const editId = ref('')
|
||||
|
||||
@@ -171,6 +171,7 @@ function typeLabel(v) {
|
||||
if (v === 'etc') return 'ETC'
|
||||
if (v === 'fuel') return '油卡'
|
||||
if (v === 'shanxia') return '山峡'
|
||||
if (v === 'bailian') return '百链'
|
||||
return v || '—'
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import { countPlAttachmentUrls } from '@/utils/plFuelRegisterCopy'
|
||||
const TYPE_LABELS = {
|
||||
etc: 'ETC',
|
||||
fuel: '油卡',
|
||||
shanxia: '山峡'
|
||||
shanxia: '山峡',
|
||||
bailian: '百链'
|
||||
}
|
||||
|
||||
export function getPlRechargeTypeLabel(type) {
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
buildStopProductPayload,
|
||||
syncDeliveriesProductsFromLoad,
|
||||
createEmptyDeliveryForm,
|
||||
mergePlanProductsFromLoadMirror
|
||||
mergePlanProductsFromLoadMirror,
|
||||
mapLoadProductsToPlanFormRows,
|
||||
shouldRetainPlanProductsWhileLoadEmpty
|
||||
} from '../src/utils/pl-plan-stop-form'
|
||||
|
||||
const product = {
|
||||
@@ -35,6 +37,34 @@ test('refreshing delivery products retains that delivery identity', () => {
|
||||
assert.equal(result.deliveries[0].products[0].id, 'delivery-product')
|
||||
})
|
||||
|
||||
test('clearing load products keeps the plan product until the same product is selected again', () => {
|
||||
const previous = [
|
||||
{
|
||||
id: 'pp',
|
||||
productId: 'sku',
|
||||
packageSpec: '槽罐车',
|
||||
quantity: 33,
|
||||
deliveryMethod: '一票制送到',
|
||||
productName: '32%液碱'
|
||||
}
|
||||
]
|
||||
assert.equal(shouldRetainPlanProductsWhileLoadEmpty([], previous), true)
|
||||
assert.equal(shouldRetainPlanProductsWhileLoadEmpty([], []), false)
|
||||
const reselected = mapLoadProductsToPlanFormRows([
|
||||
{
|
||||
productId: 'sku',
|
||||
productName: '32%液碱',
|
||||
packageSpec: '槽罐车',
|
||||
planQty: 33,
|
||||
deliveryMethod: '自提'
|
||||
}
|
||||
])
|
||||
const merged = mergePlanProductsFromLoadMirror(reselected, previous)
|
||||
assert.equal(merged[0].id, 'pp')
|
||||
assert.equal(merged[0].deliveryMethod, '自提')
|
||||
assert.equal(merged.length, 1)
|
||||
})
|
||||
|
||||
test('desktop mirror consumes previous product identities once for repeated SKUs', () => {
|
||||
const before = [
|
||||
{ ...product, id: 'first', quantity: 1 },
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
Reference in new issue
Block a user